From 2e9c30bccb4b47faecf796d748c462d92137311d Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 2 Sep 2022 17:03:54 +0300 Subject: [PATCH 01/56] update drafts implementation --- FlowCrypt.xcodeproj/project.pbxproj | 12 +- .../Compose/ComposeViewController.swift | 50 ++++--- .../ComposeViewController+Attachment.swift | 49 +------ .../ComposeViewController+Contacts.swift | 47 +++++++ .../ComposeViewController+Drafts.swift | 24 ++-- .../ComposeViewController+MessageSend.swift | 13 +- .../ComposeViewController+Nodes.swift | 4 +- .../ComposeViewController+Picker.swift | 6 +- .../ComposeViewController+Setup.swift | 4 +- .../Inbox/InboxViewController.swift | 29 ++-- .../MsgListViewController.swift | 1 - .../Search/SearchViewController.swift | 9 +- .../Setup/SetupInitialViewController.swift | 2 +- .../Message Gateway/GmailService+draft.swift | 19 +-- .../ComposeMessageContext.swift | 16 ++- .../ComposeMessageService.swift | 31 +++-- .../Models/Inbox Models/InboxViewModel.swift | 8 +- .../Resources/en.lproj/Localizable.strings | 2 +- .../UIViewControllerExtensions.swift | 127 ++++++++++-------- ...xtensions.swift => UIViewExtensions.swift} | 0 .../Cell Nodes/EmptyFolderCellNode.swift | 3 +- 21 files changed, 246 insertions(+), 210 deletions(-) create mode 100644 FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift rename FlowCryptCommon/Extensions/{UIVIewExtensions.swift => UIViewExtensions.swift} (100%) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index a5bbd5a7c..d122fb6ce 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -128,6 +128,7 @@ 51E1675D270F36A400D27C52 /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 51E1675C270F36A400D27C52 /* Realm */; }; 51E1675F270F36A400D27C52 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 51E1675E270F36A400D27C52 /* RealmSwift */; }; 51EBC5702746A06600178DE8 /* TextWithIconNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */; }; + 51FC336128C236770098313D /* ComposeViewController+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FC336028C236770098313D /* ComposeViewController+Contacts.swift */; }; 5298EA408FEC36021F7558BD /* Pods_FlowCrypt.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4753E9A27694B4D34C980FFA /* Pods_FlowCrypt.framework */; }; 5A39F42D239EC321001F4607 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A39F42C239EC321001F4607 /* SettingsViewController.swift */; }; 5A39F430239EC396001F4607 /* SettingsViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A39F42F239EC396001F4607 /* SettingsViewDecorator.swift */; }; @@ -333,7 +334,7 @@ D2531F2F23FEEF52007E5198 /* FlowCryptCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = D2531F2D23FEEF52007E5198 /* FlowCryptCommon.h */; settings = {ATTRIBUTES = (Public, ); }; }; D2531F3423FEEF5F007E5198 /* StyleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56BD3923438D3700A7371A /* StyleExtensions.swift */; }; D2531F3723FFF043007E5198 /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0C3C2723194E8500299985 /* CommonExtensions.swift */; }; - D2531F3D24000E37007E5198 /* UIVIewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA63656CB3323C26BC084 /* UIVIewExtensions.swift */; }; + D2531F3D24000E37007E5198 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA63656CB3323C26BC084 /* UIViewExtensions.swift */; }; D2531F3F24000E57007E5198 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA0E63F2F0473D0A8EDB0 /* StringExtensions.swift */; }; D2531F462402C62D007E5198 /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2531F452402C62D007E5198 /* CollectionExtensions.swift */; }; D2531F472402C9DE007E5198 /* IntExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A37A1CF523C6254F001CF774 /* IntExtensions.swift */; }; @@ -530,7 +531,7 @@ 32DCA38E87F2B7196E0E1F1F /* CodableExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableExtensions.swift; sourceTree = ""; }; 32DCA4B11D4531B3B04D01D1 /* AppErr.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppErr.swift; sourceTree = ""; }; 32DCA55C094E9745AA1FD210 /* Imap+msg.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Imap+msg.swift"; sourceTree = ""; }; - 32DCA63656CB3323C26BC084 /* UIVIewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIVIewExtensions.swift; sourceTree = ""; }; + 32DCA63656CB3323C26BC084 /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; 32DCA7E0AFE19FACB0F233ED /* URLSessionExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionExtensions.swift; sourceTree = ""; }; 32DCA9701B2D5052225A0414 /* SignInViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = ""; }; 32DCAAE9F459F48178CAF8F5 /* ComposeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; @@ -591,6 +592,7 @@ 51E1673C270DAFF900D27C52 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; 51E4F0B427348E310017DABB /* ErrorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorExtensions.swift; sourceTree = ""; }; 51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithIconNode.swift; sourceTree = ""; }; + 51FC336028C236770098313D /* ComposeViewController+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewController+Contacts.swift"; sourceTree = ""; }; 55652F68438D6EDFE71EA13C /* Pods-FlowCryptUIApplication.enterprise.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUIApplication.enterprise.xcconfig"; path = "Target Support Files/Pods-FlowCryptUIApplication/Pods-FlowCryptUIApplication.enterprise.xcconfig"; sourceTree = ""; }; 5A39F42C239EC321001F4607 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 5A39F42F239EC396001F4607 /* SettingsViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewDecorator.swift; sourceTree = ""; }; @@ -938,6 +940,7 @@ 049E606E27FDBBD50089EE2A /* ComposeViewController+Picker.swift */, 049E607027FDBC690089EE2A /* ComposeViewController+Drafts.swift */, 049E607327FDBDBE0089EE2A /* ComposeViewController+TapActions.swift */, + 51FC336028C236770098313D /* ComposeViewController+Contacts.swift */, ); path = Extensions; sourceTree = ""; @@ -2068,7 +2071,7 @@ 9F31AB9D232BF2A600CF87EA /* UIColorExtensions.swift */, 9FD505262785C2CD00FAA82F /* UIDeviceExtensions.swift */, 9FEED1B7230C08D700700F8E /* UIViewControllerExtensions.swift */, - 32DCA63656CB3323C26BC084 /* UIVIewExtensions.swift */, + 32DCA63656CB3323C26BC084 /* UIViewExtensions.swift */, 9F8277952373732000E19C07 /* UIImageExtensions.swift */, 9FD5052A278B2C8600FAA82F /* UIPopoverPresentationControllerExtensions.swift */, 32DCA7E0AFE19FACB0F233ED /* URLSessionExtensions.swift */, @@ -2916,6 +2919,7 @@ 9F82D352256D74FA0069A702 /* InboxViewContainerController.swift in Sources */, D227C0E3250538100070F805 /* LocalFoldersProvider.swift in Sources */, 9FA405C7265AEBA50084D133 /* SetupGenerateKeyViewController.swift in Sources */, + 51FC336128C236770098313D /* ComposeViewController+Contacts.swift in Sources */, 9F6EE1552597399D0059BA51 /* BackupProvider.swift in Sources */, 9FEED1D2230DAD1E00700F8E /* InboxViewModel.swift in Sources */, 32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */, @@ -3064,7 +3068,7 @@ D2CDC3D42402D50A002B045F /* CodableExtensions.swift in Sources */, 9FD505272785C2CD00FAA82F /* UIDeviceExtensions.swift in Sources */, 750A6C3D28244A780048E1CC /* OptionalExtensions.swift in Sources */, - D2531F3D24000E37007E5198 /* UIVIewExtensions.swift in Sources */, + D2531F3D24000E37007E5198 /* UIViewExtensions.swift in Sources */, D2531F3723FFF043007E5198 /* CommonExtensions.swift in Sources */, D2CDC3D32402D4FE002B045F /* DataExtensions.swift in Sources */, 759739BE2833E986004867CD /* Task+Retry.swift in Sources */, diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index ebfd8aa5c..cc0d24ce4 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -21,7 +21,6 @@ final class ComposeViewController: TableNodeViewController { } internal struct ComposedDraft: Equatable { - let email: String let input: ComposeMessageInput let contextToSend: ComposeMessageContext } @@ -58,9 +57,8 @@ final class ComposeViewController: TableNodeViewController { internal let filesManager: FilesManagerType internal let photosManager: PhotosManagerType internal let router: GlobalRouterType - internal let clientConfiguration: ClientConfiguration - internal let sendAsService: SendAsServiceType + private let clientConfiguration: ClientConfiguration internal var isMessagePasswordSupported: Bool { return clientConfiguration.isUsingFes } @@ -69,7 +67,7 @@ final class ComposeViewController: TableNodeViewController { internal var cancellable = Set() internal var input: ComposeMessageInput - internal var contextToSend = ComposeMessageContext() + internal var contextToSend: ComposeMessageContext internal var state: State = .main internal var shouldEvaluateRecipientInput = true @@ -79,8 +77,8 @@ final class ComposeViewController: TableNodeViewController { internal lazy var alertsFactory = AlertsFactory() internal var messagePasswordAlertController: UIAlertController? - internal var didLayoutSubviews = false - internal var topContentInset: CGFloat { + private var didLayoutSubviews = false + private var topContentInset: CGFloat { navigationController?.navigationBar.frame.maxY ?? 0 } @@ -93,7 +91,6 @@ final class ComposeViewController: TableNodeViewController { var composeSubjectNode: ASCellNode! var fromCellNode: RecipientFromCellNode! var sendAsList: [SendAsModel] = [] - var selectedFromEmail = "" init( appContext: AppContextWithUser, @@ -128,13 +125,17 @@ final class ComposeViewController: TableNodeViewController { localContactsProvider: self.localContactsProvider ) self.router = appContext.globalRouter - self.contextToSend.subject = input.subject - self.contextToSend.attachments = input.attachments self.clientConfiguration = clientConfiguration - self.sendAsService = try appContext.getSendAsService() - self.sendAsList = try await sendAsService.fetchList(isForceReload: false, for: appContext.user) - self.sendAsList = self.sendAsList.filter { $0.verificationStatus == .accepted || $0.isDefault } - self.selectedFromEmail = appContext.user.email + + self.sendAsList = try await appContext.getSendAsService() + .fetchList(isForceReload: false, for: appContext.user) + .filter { $0.verificationStatus == .accepted || $0.isDefault } + + self.contextToSend = ComposeMessageContext( + sender: appContext.user.email, + subject: input.subject, + attachments: input.attachments + ) super.init(node: TableNode()) } @@ -192,28 +193,33 @@ final class ComposeViewController: TableNodeViewController { } func update(with message: Message) { - self.contextToSend.subject = message.subject - self.contextToSend.message = message.raw + if let sender = message.sender?.email { + contextToSend.sender = sender + } + + contextToSend.subject = message.subject + contextToSend.message = message.raw + for recipient in message.to { - evaluateMessage(recipient: recipient, type: .to) + add(recipient: recipient, type: .to) } for recipient in message.cc { - evaluateMessage(recipient: recipient, type: .cc) + add(recipient: recipient, type: .cc) } for recipient in message.bcc { - evaluateMessage(recipient: recipient, type: .bcc) + add(recipient: recipient, type: .bcc) } } - func evaluateMessage(recipient: Recipient, type: RecipientType) { - let recipient = ComposeMessageRecipient( + func add(recipient: Recipient, type: RecipientType) { + let composeRecipient = ComposeMessageRecipient( email: recipient.email, name: recipient.name, type: type, state: decorator.recipientIdleState ) - contextToSend.add(recipient: recipient) - evaluate(recipient: recipient) + contextToSend.add(recipient: composeRecipient) + evaluate(recipient: composeRecipient) } private func observeComposeUpdates() { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift index 73933640d..c3e2f6d91 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift @@ -52,19 +52,19 @@ extension ComposeViewController { } } - internal func selectPhoto() { + private func selectPhoto() { Task { await photosManager.selectPhoto(from: self) } } - internal func selectFromFilesApp() { + private func selectFromFilesApp() { Task { await filesManager.selectFromFilesApp(from: self) } } - internal func showNoAccessToCameraAlert() { + private func showNoAccessToCameraAlert() { let alert = UIAlertController( title: "files_picking_no_camera_access_error_title".localized, message: "files_picking_no_camera_access_error_message".localized, @@ -85,47 +85,4 @@ extension ComposeViewController { present(alert, animated: true, completion: nil) } - - internal func askForContactsPermission() { - shouldEvaluateRecipientInput = false - - Task { - do { - try await router.askForContactsPermission(for: .gmailLogin(self), appContext: appContext) - shouldEvaluateRecipientInput = true - reload(sections: [.contacts]) - } catch { - shouldEvaluateRecipientInput = true - handleContactsPermissionError(error) - } - } - } - - internal func handleContactsPermissionError(_ error: Error) { - guard let gmailUserError = error as? GoogleUserServiceError, - case .userNotAllowedAllNeededScopes(let missingScopes, _) = gmailUserError - else { return } - - let scopes = missingScopes.map(\.title).joined(separator: ", ") - - let alert = UIAlertController( - title: "error".localized, - message: "compose_missing_contacts_scopes".localizeWithArguments(scopes), - preferredStyle: .alert - ) - let laterAction = UIAlertAction( - title: "later".localized, - style: .cancel - ) - let allowAction = UIAlertAction( - title: "allow".localized, - style: .default - ) { [weak self] _ in - self?.askForContactsPermission() - } - alert.addAction(laterAction) - alert.addAction(allowAction) - - present(alert, animated: true, completion: nil) - } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift new file mode 100644 index 000000000..7ef73687b --- /dev/null +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift @@ -0,0 +1,47 @@ +// +// ComposeViewController+Contacts.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 02/09/22 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import UIKit + +extension ComposeViewController { + internal func askForContactsPermission() { + shouldEvaluateRecipientInput = false + + Task { + do { + try await router.askForContactsPermission( + for: .gmailLogin(self), + appContext: appContext + ) + shouldEvaluateRecipientInput = true + reload(sections: [.contacts]) + } catch { + shouldEvaluateRecipientInput = true + handleContactsPermissionError(error) + } + } + } + + private func handleContactsPermissionError(_ error: Error) { + guard let gmailUserError = error as? GoogleUserServiceError, + case .userNotAllowedAllNeededScopes(let missingScopes, _) = gmailUserError + else { return } + + let scopes = missingScopes.map(\.title).joined(separator: ", ") + + showAlertWithAction( + title: "error".localized, + message: "compose_missing_contacts_scopes".localizeWithArguments(scopes), + cancelButtonTitle: "later".localized, + actionButtonTitle: "allow".localized, + onAction: { [weak self] _ in + self?.askForContactsPermission() + } + ) + } +} diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 7b2191d44..81b903d79 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -9,7 +9,7 @@ // MARK: - Drafts extension ComposeViewController { @objc internal func startDraftTimer() { - saveDraftTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { [weak self] _ in + saveDraftTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in self?.saveDraftIfNeeded() } saveDraftTimer?.fire() @@ -22,27 +22,31 @@ extension ComposeViewController { } private func shouldSaveDraft() -> Bool { - // https://github.com/FlowCrypt/flowcrypt-ios/issues/975 return false -// let newDraft = ComposedDraft(email: email, input: input, contextToSend: contextToSend) -// guard let oldDraft = composedLatestDraft else { +// let newDraft = ComposedDraft( +// input: input, +// contextToSend: contextToSend +// ) +// +// if let existingDraft = composedLatestDraft { +// let draftHasChanges = newDraft != existingDraft +// self.composedLatestDraft = newDraft +// return draftHasChanges +// } else { // save initial draft // composedLatestDraft = newDraft -// return true +// return false // } -// let result = newDraft != oldDraft -// composedLatestDraft = newDraft -// return result } internal func saveDraftIfNeeded() { guard shouldSaveDraft() else { return } + Task { do { let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg( - senderEmail: selectedFromEmail, input: input, contextToSend: contextToSend, - includeAttachments: false + isDraft: true ) try await composeMessageService.encryptAndSaveDraft(message: sendableMsg, threadId: input.threadId) } catch { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index 5248072e0..bea83fb36 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -29,7 +29,6 @@ extension ComposeViewController { try await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms let sendableMsg = try await self.composeMessageService.validateAndProduceSendableMsg( - senderEmail: selectedFromEmail, input: self.input, contextToSend: self.contextToSend ) @@ -75,9 +74,7 @@ extension ComposeViewController { } internal func handle(error: Error) { - UIApplication.shared.isIdleTimerDisabled = false - hideSpinner() - navigationItem.rightBarButtonItem?.isEnabled = true + reEnableSendButton() if case .promptUserToEnterPassPhraseForSigningKey(let keyPair) = error as? ComposeMessageError { requestMissingPassPhraseWithModal(for: keyPair) @@ -106,10 +103,14 @@ extension ComposeViewController { } private func handleSuccessfullySentMessage() { + reEnableSendButton() + showToast(input.successfullySentToast) + navigationController?.popViewController(animated: true) + } + + private func reEnableSendButton() { UIApplication.shared.isIdleTimerDisabled = false hideSpinner() navigationItem.rightBarButtonItem?.isEnabled = true - showToast(input.successfullySentToast) - navigationController?.popViewController(animated: true) } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index 167633d67..55890571e 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -81,7 +81,7 @@ extension ComposeViewController { self.presentSendAsActionSheet() } ) - fromCellNode.fromEmail = selectedFromEmail + fromCellNode.fromEmail = contextToSend.sender } private func presentSendAsActionSheet() { @@ -118,7 +118,7 @@ extension ComposeViewController { return } fromCell.fromEmail = email - self.selectedFromEmail = email + contextToSend.sender = email } internal func messagePasswordNode() -> ASCellNode { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Picker.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Picker.swift index 4ef66b2eb..7dc7624af 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Picker.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Picker.swift @@ -34,7 +34,7 @@ extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationCo reload(sections: [.attachments]) } - internal func appendAttachmentIfAllowed(_ attachment: MessageAttachment) { + private func appendAttachmentIfAllowed(_ attachment: MessageAttachment) { let totalSize = contextToSend.attachments.map(\.size).reduce(0, +) + attachment.size if totalSize > GeneralConstants.Global.attachmentSizeLimit { showToast("files_picking_size_error_message".localized) @@ -53,7 +53,7 @@ extension ComposeViewController: PHPickerViewControllerDelegate { } } - internal func handleResults(_ results: [PHPickerResult]) { + private func handleResults(_ results: [PHPickerResult]) { guard let itemProvider = results.first?.itemProvider else { return } enum MediaType: String { @@ -79,7 +79,7 @@ extension ComposeViewController: PHPickerViewControllerDelegate { ) } - internal func handleRepresentation(url: URL?, error: Error?, isVideo: Bool) { + private func handleRepresentation(url: URL?, error: Error?, isVideo: Bool) { guard let url = url, let composeMessageAttachment = MessageAttachment(fileURL: url) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 71564aeb8..ce105f85c 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -53,11 +53,11 @@ extension ComposeViewController { guard input.isQuote else { return } for recipient in input.quoteRecipients { - evaluateMessage(recipient: recipient, type: .to) + add(recipient: recipient, type: .to) } for recipient in input.quoteCCRecipients { - evaluateMessage(recipient: recipient, type: .cc) + add(recipient: recipient, type: .cc) } if input.quoteCCRecipients.isNotEmpty { diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 57b944b9e..b2679fa28 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -25,7 +25,7 @@ class InboxViewController: ViewController { private let inboxDataProvider: InboxDataProvider private let viewModel: InboxViewModel - internal var inboxInput: [InboxRenderable] = [] + private var inboxInput: [InboxRenderable] = [] internal var state: InboxViewController.State = .idle private var inboxTitle: String { viewModel.folderName.isEmpty ? "Inbox" : viewModel.folderName @@ -34,12 +34,12 @@ class InboxViewController: ViewController { inboxInput.isNotEmpty && (viewModel.path == "SPAM" || viewModel.path == "TRASH") } - var path: String { viewModel.path } + internal var path: String { viewModel.path } // Search related varaibles - internal var isSearch = false + private var isSearch = false + private var shouldBeginFetch = true internal var searchedExpression = "" - var shouldBeginFetch = true private var isVisible = false private var didLayoutSubviews = false @@ -505,15 +505,16 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { var rowNumber = indexPath.row if self.shouldShowEmptyView { if indexPath.row == 0 { - return EmptyFolderCellNode(path: self.viewModel.path, emptyFolder: { - self.showConfirmAlert( - message: "folder_empty_confirm".localized, - onConfirm: { [weak self] _ in - self?.emptyInboxFolder() - } - ) - - }) + return EmptyFolderCellNode( + path: self.viewModel.path, + emptyFolder: { + self.showConfirmAlert( + message: "folder_empty_confirm".localized, + onConfirm: { [weak self] _ in + self?.emptyInboxFolder() + } + ) + }) } rowNumber -= 1 } @@ -529,7 +530,7 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { return InboxCellNode(input: .init(input)) case let .error(message): return TextCellNode( - input: TextCellNode.Input( + input: .init( backgroundColor: .backgroundColor, title: message, withSpinner: false, diff --git a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift b/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift index 38575db08..21014a474 100644 --- a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift +++ b/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift @@ -32,7 +32,6 @@ extension MsgListViewController where Self: UIViewController { } } - // TODO: uncomment in "sent message from draft" feature private func open(draft: Message, appContext: AppContextWithUser) { Task { do { diff --git a/FlowCrypt/Controllers/Search/SearchViewController.swift b/FlowCrypt/Controllers/Search/SearchViewController.swift index 6edf21249..241a0b19c 100644 --- a/FlowCrypt/Controllers/Search/SearchViewController.swift +++ b/FlowCrypt/Controllers/Search/SearchViewController.swift @@ -86,14 +86,13 @@ extension SearchViewController: UISearchControllerDelegate, UISearchBarDelegate // MARK: - UISearchResultsUpdating extension SearchViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { - guard searchController.isActive else { - searchTask?.cancel() - return - } - guard let searchText = searchText(for: searchController.searchBar) else { + guard searchController.isActive, + let searchText = searchText(for: searchController.searchBar) + else { searchTask?.cancel() return } + guard searchedExpression != searchText else { return } diff --git a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift index b31cf31fa..1113f149f 100644 --- a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift @@ -177,7 +177,7 @@ extension SetupInitialViewController { func showRetryAlert(for errorMessage: String) { showRetryAlert( message: errorMessage, - cancelActionTitle: "log_out".localized, + cancelButtonTitle: "log_out".localized, onRetry: { [weak self] _ in self?.state = .fetchingKeysFromEKM }, diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 5ff4b689c..3ec761cce 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -11,14 +11,15 @@ import GoogleAPIClientForREST_Gmail extension GmailService: DraftGateway { func saveDraft(input: MessageGatewayInput, draft: GTLRGmail_Draft?) async throws -> GTLRGmail_Draft { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - guard let raw = GTLREncodeBase64(input.mime) else { return continuation.resume(throwing: GmailServiceError.messageEncode) } + let draftQuery = createQueryForDraftAction( raw: raw, threadId: input.threadId, - draft: draft) + draft: draft + ) gmailService.executeQuery(draftQuery) { _, object, error in if let error = error { @@ -43,8 +44,8 @@ extension GmailService: DraftGateway { private func createQueryForDraftAction(raw: String, threadId: String?, draft: GTLRGmail_Draft?) -> GTLRGmailQuery { guard - let createdDraft = draft, - let draftIdentifier = createdDraft.identifier + let existingDraft = draft, + let draftIdentifier = existingDraft.identifier else { // draft is not created yet. creating draft let newDraft = GTLRGmail_Draft() @@ -56,19 +57,21 @@ extension GmailService: DraftGateway { return GTLRGmailQuery_UsersDraftsCreate.query( withObject: newDraft, userId: "me", - uploadParameters: nil) + uploadParameters: nil + ) } // updating existing draft with new data let gtlMessage = GTLRGmail_Message() gtlMessage.raw = raw gtlMessage.threadId = threadId - createdDraft.message = gtlMessage + existingDraft.message = gtlMessage return GTLRGmailQuery_UsersDraftsUpdate.query( - withObject: createdDraft, + withObject: existingDraft, userId: "me", identifier: draftIdentifier, - uploadParameters: nil) + uploadParameters: nil + ) } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index c28984f7b..7015da2dd 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -9,13 +9,14 @@ import Foundation struct ComposeMessageContext: Equatable { + var sender: String var message: String? var recipients: [ComposeMessageRecipient] var subject: String? var attachments: [MessageAttachment] var messagePassword: String? { get { - (_messagePassword ?? "").isNotEmpty ? _messagePassword : nil + _messagePassword.isEmptyOrNil ? nil : _messagePassword } set { _messagePassword = newValue } } @@ -24,12 +25,15 @@ struct ComposeMessageContext: Equatable { } extension ComposeMessageContext { - init(message: String? = nil, - recipients: [ComposeMessageRecipient] = [], - subject: String? = nil, - attachments: [MessageAttachment] = [], - messagePassword: String? = nil + init( + sender: String, + message: String? = nil, + recipients: [ComposeMessageRecipient] = [], + subject: String? = nil, + attachments: [MessageAttachment] = [], + messagePassword: String? = nil ) { + self.sender = sender self.message = message self.recipients = recipients self.subject = subject diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index a70ea30f8..ead4d9786 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -72,7 +72,7 @@ final class ComposeMessageService { func handlePassPhraseEntry(_ passPhrase: String, for signingKey: Keypair) async throws -> Bool { // since pass phrase was entered (an inconvenient thing for user to do), - // let's find all keys that match and save the pass phrase for all + // let's find all keys that match and save the pass phrase for all let allKeys = try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: sender) guard allKeys.isNotEmpty else { throw KeypairError.noAccountKeysAvailable @@ -91,14 +91,14 @@ final class ComposeMessageService { // MARK: - Validation func validateAndProduceSendableMsg( - senderEmail: String, input: ComposeMessageInput, contextToSend: ComposeMessageContext, - includeAttachments: Bool = true + isDraft: Bool = false ) async throws -> SendableMsg { - onStateChanged?(.validatingMessage) + if !isDraft { onStateChanged?(.validatingMessage) } let recipients = contextToSend.recipients + guard recipients.isNotEmpty else { throw MessageValidationError.emptyRecipient } @@ -127,14 +127,14 @@ final class ComposeMessageService { let senderKeys = try await keyMethods.chooseSenderKeys( for: .encryption, keys: try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: sender), - senderEmail: senderEmail + senderEmail: contextToSend.sender ) guard senderKeys.isNotEmpty else { throw MessageValidationError.noUsableAccountKeys } - let sendableAttachments: [SendableMsg.Attachment] = includeAttachments + let sendableAttachments: [SendableMsg.Attachment] = !isDraft ? contextToSend.attachments.map { $0.toSendableMsgAttachment() } : [] @@ -155,7 +155,7 @@ final class ComposeMessageService { } } - let signingPrv = try await prepareSigningKey(senderEmail: senderEmail) + let signingPrv = try await prepareSigningKey(senderEmail: contextToSend.sender) return SendableMsg( text: text, @@ -163,7 +163,7 @@ final class ComposeMessageService { to: contextToSend.recipientEmails(type: .to), cc: contextToSend.recipientEmails(type: .cc), bcc: contextToSend.recipientEmails(type: .bcc), - from: senderEmail, + from: contextToSend.sender, subject: subject, replyToMsgId: input.replyToMsgId, inReplyTo: input.inReplyTo, @@ -190,8 +190,10 @@ final class ComposeMessageService { return recipientsWithKeys } - private func validate(recipients: [RecipientWithSortedPubKeys], - hasMessagePassword: Bool) throws -> [String] { + private func validate( + recipients: [RecipientWithSortedPubKeys], + hasMessagePassword: Bool + ) throws -> [String] { func contains(keyState: PubKeyState) -> Bool { recipients.first(where: { $0.keyState == keyState }) != nil } @@ -221,7 +223,11 @@ final class ComposeMessageService { msg: message, fmt: .encryptInline ) - draft = try await draftGateway?.saveDraft(input: MessageGatewayInput(mime: r.mimeEncoded, threadId: threadId), draft: draft) + draft = try await draftGateway?.saveDraft( + input: MessageGatewayInput( + mime: r.mimeEncoded, + threadId: threadId + ), draft: draft) } catch { throw ComposeMessageError.gatewayError(error) } @@ -232,7 +238,7 @@ final class ComposeMessageService { do { onStateChanged?(.startComposing) - let hasPassword = (message.password ?? "").isNotEmpty + let hasPassword = !message.password.isEmptyOrNil let composedEmail: CoreRes.ComposeEmail if hasPassword { @@ -376,7 +382,6 @@ extension ComposeMessageService { return "
" } - // TODO: - Anton - compose_password_link private func createMessageBodyWithPasswordLink(sender: String, url: String) -> SendableMsgBody { let text = "compose_password_link".localizeWithArguments(sender, url) let aStyle = "padding: 2px 6px; background: #2199e8; color: #fff; display: inline-block; text-decoration: none;" diff --git a/FlowCrypt/Models/Inbox Models/InboxViewModel.swift b/FlowCrypt/Models/Inbox Models/InboxViewModel.swift index 1e154c1a2..e331330a5 100644 --- a/FlowCrypt/Models/Inbox Models/InboxViewModel.swift +++ b/FlowCrypt/Models/Inbox Models/InboxViewModel.swift @@ -14,15 +14,11 @@ struct InboxViewModel { init(folderName: String, path: String) { self.folderName = folderName - if folderName.isEmpty { - self.path = "Inbox" - } else { - self.path = path - } + self.path = folderName.isEmpty ? "Inbox" : path } var isDrafts: Bool { - return folderName == "Draft" + return folderName == "Drafts" } } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 8e6f69438..748da7f17 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -162,7 +162,7 @@ "folder_SENT" = "Sent"; "folder_IMPORTANT" = "Important"; "folder_TRASH" = "Trash"; -"folder_DRAFT" = "Draft"; +"folder_DRAFT" = "Drafts"; "folder_SPAM" = "Spam"; "folder_STARRED" = "Starred"; "folder_UNREAD" = "Unread"; diff --git a/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift b/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift index 17eb02034..e87b8f518 100644 --- a/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift +++ b/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift @@ -10,8 +10,8 @@ import Toast import UIKit import MBProgressHUD +// MARK: - Toast public typealias ShowToastCompletion = (Bool) -> Void - public extension UIViewController { /// Showing toast on root controller /// @@ -20,13 +20,15 @@ public extension UIViewController { /// - title: Title for the toast /// - duration: Toast presented duration. Default is 3.0 /// - position: Bottom by default. Can be top, center, bottom. - /// - completion: Notify when toast dissapeared + /// - shouldHideKeyboard: True by default. Hide keyboard when toast is presented + /// - completion: Notify when toast dissappeared @MainActor func showToast( _ message: String, title: String? = nil, duration: TimeInterval = 3.0, position: ToastPosition = .bottom, + shouldHideKeyboard: Bool = true, completion: ShowToastCompletion? = nil ) { guard let view = UIApplication.shared.keyWindow?.rootViewController?.view else { @@ -34,7 +36,10 @@ public extension UIViewController { return } view.hideAllToasts() - view.endEditing(true) + + if shouldHideKeyboard { + view.endEditing(true) + } view.makeToast( message, @@ -48,10 +53,11 @@ public extension UIViewController { } } +// MARK: - Alerts public extension UIViewController { @MainActor func showAlert(title: String? = "error".localized, message: String, onOk: (() -> Void)? = nil) { - self.view.hideAllToasts() + view.hideAllToasts() hideSpinner() let alert = UIAlertController( title: title, @@ -63,76 +69,78 @@ public extension UIViewController { style: .destructive ) { _ in onOk?() } alert.addAction(ok) - self.present(alert, animated: true, completion: nil) + present(alert, animated: true, completion: nil) } @MainActor func showAsyncAlert(title: String? = "error".localized, message: String) async throws { - return try await withCheckedThrowingContinuation { (continuation) in - showAlert(title: title, message: message, onOk: { + try await withCheckedThrowingContinuation { continuation in + showAlert(title: title, message: message) { return continuation.resume() - }) + } } } @MainActor - func showRetryAlert( - title: String? = "error".localized, + func showAlertWithAction( + title: String?, message: String, - cancelActionTitle: String = "cancel".localized, - onRetry: ((UIAlertAction) -> Void)?, + cancelButtonTitle: String = "cancel".localized, + actionButtonTitle: String, + actionAccessibilityIdentifier: String? = nil, + onAction: ((UIAlertAction) -> Void)?, onCancel: ((UIAlertAction) -> Void)? = nil ) { - self.view.hideAllToasts() + view.hideAllToasts() hideSpinner() let alert = UIAlertController( title: title, message: message, preferredStyle: .alert ) - let retry = UIAlertAction( - title: "retry_title".localized, - style: .cancel, - handler: onRetry + let action = UIAlertAction( + title: actionButtonTitle, + style: .default, + handler: onAction ) + action.accessibilityIdentifier = actionAccessibilityIdentifier let cancel = UIAlertAction( - title: cancelActionTitle, - style: .default, - handler: onCancel) - alert.addAction(retry) + title: cancelButtonTitle, + style: .cancel, + handler: onCancel + ) + alert.addAction(action) alert.addAction(cancel) present(alert, animated: true, completion: nil) } @MainActor - func showConfirmAlert( - title: String? = "warning".localized, + func showRetryAlert( + title: String? = "error".localized, message: String, - confirmActionTitle: String = "confirm".localized, - cancelActionTitle: String = "cancel".localized, - onConfirm: ((UIAlertAction) -> Void)?, + cancelButtonTitle: String = "cancel".localized, + onRetry: ((UIAlertAction) -> Void)?, onCancel: ((UIAlertAction) -> Void)? = nil ) { - self.view.hideAllToasts() - hideSpinner() - let alert = UIAlertController( + showAlertWithAction( title: title, message: message, - preferredStyle: .alert + cancelButtonTitle: cancelButtonTitle, + actionButtonTitle: "retry_title".localized, + onAction: onRetry, + onCancel: onCancel ) - let confirm = UIAlertAction( - title: confirmActionTitle, - style: .cancel, - handler: onConfirm + } + + @MainActor + func showConfirmAlert(message: String, onConfirm: ((UIAlertAction) -> Void)?) { + showAlertWithAction( + title: "warning".localized, + message: message, + actionButtonTitle: "confirm".localized, + actionAccessibilityIdentifier: "aid-confirm-button", + onAction: onConfirm ) - let cancel = UIAlertAction( - title: cancelActionTitle, - style: .default, - handler: onCancel) - confirm.accessibilityIdentifier = "aid-confirm-button" - alert.addAction(confirm) - alert.addAction(cancel) - present(alert, animated: true, completion: nil) } func keyboardHeight(from notification: Notification) -> CGFloat { @@ -140,6 +148,7 @@ public extension UIViewController { } } +// MARK: - Navigation public extension UINavigationController { func pushViewController(viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) { pushViewController(viewController, animated: animated) @@ -174,13 +183,13 @@ public extension UIViewController { @MainActor func showSpinner(_ message: String = "loading_title".localized, isUserInteractionEnabled: Bool = false) { - guard self.view.subviews.first(where: { $0 is MBProgressHUD }) == nil else { + guard view.subviews.first(where: { $0 is MBProgressHUD }) == nil else { // hud is already shown return } - self.view.isUserInteractionEnabled = isUserInteractionEnabled + view.isUserInteractionEnabled = isUserInteractionEnabled - let spinner = MBProgressHUD.showAdded(to: self.view, animated: true) + let spinner = MBProgressHUD.showAdded(to: view, animated: true) spinner.label.text = message spinner.isUserInteractionEnabled = isUserInteractionEnabled spinner.accessibilityIdentifier = "loadingSpinner" @@ -192,26 +201,28 @@ public extension UIViewController { progress: Float? = nil, systemImageName: String? = nil ) { - if let progress = progress { - if progress >= 1, let imageName = systemImageName { - self.updateSpinner( - label: "compose_sent".localized, - systemImageName: imageName) - } else { - self.showProgressHUD(progress: progress, label: label) - } - } else { + guard let progress = progress else { showIndeterminateHUD(with: label) + return + } + + if progress >= 1, let imageName = systemImageName { + updateSpinner( + label: "compose_sent".localized, + systemImageName: imageName + ) + } else { + showProgressHUD(progress: progress, label: label) } } @MainActor func hideSpinner() { - let subviews = self.view.subviews.compactMap { $0 as? MBProgressHUD } + let subviews = view.subviews.compactMap { $0 as? MBProgressHUD } for subview in subviews { subview.hide(animated: true) } - self.view.isUserInteractionEnabled = true + view.isUserInteractionEnabled = true } @MainActor @@ -234,7 +245,7 @@ public extension UIViewController { @MainActor func showIndeterminateHUD(with title: String) { - self.currentProgressHUD.mode = .indeterminate - self.currentProgressHUD.label.text = title + currentProgressHUD.mode = .indeterminate + currentProgressHUD.label.text = title } } diff --git a/FlowCryptCommon/Extensions/UIVIewExtensions.swift b/FlowCryptCommon/Extensions/UIViewExtensions.swift similarity index 100% rename from FlowCryptCommon/Extensions/UIVIewExtensions.swift rename to FlowCryptCommon/Extensions/UIViewExtensions.swift diff --git a/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift b/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift index 618b4a2d5..4dcd46886 100644 --- a/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift @@ -35,8 +35,7 @@ public final class EmptyFolderCellNode: CellNode { return buttonNode }() - public init(path: String, - emptyFolder: (() -> Void)?) { + public init(path: String, emptyFolder: (() -> Void)?) { self.path = path self.emptyFolder = emptyFolder super.init() From d79b82956b2c384914fbed5ef166ae0b9d0f3820 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 5 Sep 2022 11:13:02 +0300 Subject: [PATCH 02/56] update swiftlint config --- .swiftlint.yml | 89 ++++++++++++++++++- FlowCrypt.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- FlowCrypt/App/AppContext.swift | 2 +- FlowCrypt/App/AppStartup.swift | 6 +- FlowCrypt/App/GlobalRouter.swift | 4 +- .../View Controllers/BlurViewController.swift | 1 + .../Compose/ComposeViewController.swift | 2 +- .../Compose/ComposeViewDecorator.swift | 4 +- ...ComposeViewController+ActionHandling.swift | 2 +- .../ComposeViewController+Drafts.swift | 33 +++---- .../ComposeViewController+Nodes.swift | 9 +- ...ComposeViewController+RecipientInput.swift | 9 +- .../ComposeViewController+TableView.swift | 4 +- .../Controllers/Inbox/InboxProviders.swift | 10 +-- .../MsgListViewController.swift | 2 +- .../Search/SearchViewController.swift | 1 + .../BackupSelectKeyViewController.swift | 6 +- .../Key Details/KeyDetailViewDecorator.swift | 2 +- .../SettingsViewController.swift | 2 +- .../SetupGenerateKeyViewController.swift | 2 +- .../SetupImap/SetupImapViewController.swift | 16 ++-- .../SideMenu/Menu/MyMenuViewController.swift | 5 +- .../SideMenu/Menu/MyMenuViewDecorator.swift | 2 +- .../Threads/ThreadDetailsViewController.swift | 31 +++---- FlowCrypt/Extensions/UIColorExtensions.swift | 6 +- .../Api/Account Server Apis/BackendApi.swift | 2 +- .../EmailKeyManagerApi.swift | 3 +- .../Api/Remote Pub Key Apis/WkdApi.swift | 2 +- .../Encrypted Storage/EncryptedStorage.swift | 2 +- .../Encrypted Storage/KeyChainService.swift | 4 +- .../Version6SchemaMigration.swift | 4 +- .../Mail Provider/Imap/Imap+retry.swift | 2 +- .../Mail Provider/Imap/Imap+session.swift | 4 +- .../Message Provider/Gmail+Message.swift | 2 +- .../Gmail+MessageOperations.swift | 2 +- .../MessagesList Provider/Model/Message.swift | 10 +-- .../Threads/MessagesThreadProvider.swift | 6 +- FlowCrypt/Functionality/Pgp/KeyMethods.swift | 2 +- .../ComposeMessageContext.swift | 4 +- .../ComposeMessageError.swift | 2 +- .../ComposeMessageService.swift | 83 ++++++++--------- .../Imap+folders.swift | 2 +- .../GoogleUserService.swift | 4 +- .../InMemoryPassPhraseStorage.swift | 4 +- .../KeyAndPassPhraseStorage.swift | 3 +- .../SendAs Services/Models/SendAsModel.swift | 4 +- .../RemoteSendAsProvider.swift | 2 +- FlowCrypt/Models/Common/Keypair.swift | 4 +- FlowCrypt/Models/Common/RecipientBase.swift | 2 +- FlowCrypt/Models/Contact Models/PubKey.swift | 4 +- .../Extensions/LocalizationExtensions.swift | 12 ++- .../Extensions/OptionalExtensions.swift | 8 +- .../Extensions/StringExtensions.swift | 2 +- FlowCryptCommon/Extensions/Task+Retry.swift | 4 +- .../Extensions/UIDeviceExtensions.swift | 6 +- .../UIViewControllerExtensions.swift | 2 +- FlowCryptUI/Cell Nodes/BackupCellNode.swift | 2 +- FlowCryptUI/Cell Nodes/ButtonCellNode.swift | 4 +- FlowCryptUI/Cell Nodes/CellNode.swift | 2 +- FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift | 2 +- .../Cell Nodes/ComposeRecipientCellNode.swift | 6 +- .../ComposeRecipientPopupNameNode.swift | 2 +- FlowCryptUI/Cell Nodes/ContactCellNode.swift | 2 +- .../Cell Nodes/ContactKeyCellNode.swift | 2 +- .../Cell Nodes/ContactUserCellNode.swift | 2 +- FlowCryptUI/Cell Nodes/DividerCellNode.swift | 2 +- FlowCryptUI/Cell Nodes/EmptyCellNode.swift | 2 +- .../Cell Nodes/EmptyFolderCellNode.swift | 2 +- FlowCryptUI/Cell Nodes/InboxCellNode.swift | 2 +- FlowCryptUI/Cell Nodes/InfoCellNode.swift | 2 +- FlowCryptUI/Cell Nodes/LabelCellNode.swift | 2 +- .../Cell Nodes/MessagePasswordCellNode.swift | 2 +- .../Cell Nodes/MessageSubjectNode.swift | 2 +- .../Cell Nodes/MessageTextSubjectNode.swift | 2 +- .../Cell Nodes/RecipientEmailNode.swift | 4 +- .../RecipientEmailTextFieldNode.swift | 6 +- .../Cell Nodes/RecipientEmailsCellNode.swift | 10 +-- .../RecipientEmailsCellNodeInput.swift | 18 ++-- .../Cell Nodes/RecipientFromCellNode.swift | 4 +- FlowCryptUI/Cell Nodes/SetupTitleNode.swift | 4 +- FlowCryptUI/Cell Nodes/SwitchCellNode.swift | 2 +- FlowCryptUI/Cell Nodes/TextCellNode.swift | 2 +- .../Cell Nodes/TextFieldCellNode.swift | 4 +- FlowCryptUI/Cell Nodes/TextViewCellNode.swift | 4 +- .../ThreadMessageInfoCellNode.swift | 2 +- FlowCryptUI/Cell Nodes/TitleCellNode.swift | 2 +- FlowCryptUI/Nodes/AttachmentNode.swift | 2 +- FlowCryptUI/Nodes/BadgeNode.swift | 2 +- FlowCryptUI/Nodes/ButtonNode.swift | 2 +- FlowCryptUI/Nodes/ButtonWithPaddingNode.swift | 2 +- FlowCryptUI/Nodes/CheckBoxNode.swift | 2 +- FlowCryptUI/Nodes/KeySettingCellNode.swift | 2 +- FlowCryptUI/Nodes/KeyTextCellNode.swift | 2 +- FlowCryptUI/Nodes/LinkButtonNode.swift | 2 +- FlowCryptUI/Nodes/MessageRecipientsNode.swift | 2 +- FlowCryptUI/Nodes/SignInDescriptionNode.swift | 2 +- FlowCryptUI/Nodes/SignInImageNode.swift | 2 +- FlowCryptUI/Nodes/SigninButtonNode.swift | 2 +- FlowCryptUI/Nodes/TableNode.swift | 8 +- FlowCryptUI/Nodes/TableViewController.swift | 6 +- FlowCryptUI/Nodes/TextFieldNode.swift | 10 +-- FlowCryptUI/Nodes/TextImageNode.swift | 2 +- FlowCryptUI/Nodes/TextWithIconNode.swift | 2 +- FlowCryptUI/Nodes/ViewController.swift | 2 +- .../Views/NavigationBarItemsView.swift | 2 +- 106 files changed, 357 insertions(+), 258 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 6500b6193..d7a2c5b84 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -18,6 +18,80 @@ disabled_rules: # - class_delegate_protocol #disabled this rule since it cannot track inheritance of protocol if protocols declared in different files # - operator_whitespace +analyzer_rules: + - unused_declaration + - unused_import +opt_in_rules: + - anyobject_protocol + - array_init + - attributes + - closure_end_indentation + - closure_spacing + - collection_alignment + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + # - discouraged_none_name + - discouraged_object_literal + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - explicit_init + - extension_access_modifier + - fallthrough + - fatal_error_message + - file_header + - file_name + - first_where + - flatmap_over_map_reduce + - identical_operands + - joined_default_parameter + - last_where + - legacy_multiple + - literal_expression_end_indentation + - lower_acl_than_parent + - modifier_order + - nimble_operator + # - nslocalizedstring_key + - number_separator + # - object_literal + - operator_usage_whitespace + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_in_static_references + - prefer_self_type_over_type_of_self + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - return_value_from_void_function + - single_test_class + - sorted_first_last + # - sorted_imports + - static_operator + - strong_iboutlet + - test_case_accessibility + - toggle_bool + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + # - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition + included: - FlowCrypt - FlowCryptUI @@ -25,12 +99,21 @@ included: excluded: - FlowCrypt/Core - FlowCrypt/Functionality/Imap + + +attributes: + always_on_same_line: + ["@objc"] line_length: 140 function_body_length: 50 -# type_body_length: -# - 400 # Warning -# - 550 # Error +type_body_length: + - 500 # Warning + - 600 # Error + +number_separator: + minimum_length: 5 + minimum_fraction_length: 7 # warning_threshold: 1 diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index d122fb6ce..6fdcbb65a 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 55; objects = { /* Begin PBXBuildFile section */ @@ -2460,7 +2460,7 @@ }; }; buildConfigurationList = C132B9AB1EC2DBD800763715 /* Build configuration list for PBXProject "FlowCrypt" */; - compatibilityVersion = "Xcode 12.0"; + compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index f0003337e..e1bc202b4 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-cocoa", "state" : { - "revision" : "5629961d905387b40fb2e1e9d8594c87a2ab8811", - "version" : "10.28.6" + "revision" : "ae0ceea258fd8f58392881cc4b2228bff62f74f5", + "version" : "10.28.7" } }, { diff --git a/FlowCrypt/App/AppContext.swift b/FlowCrypt/App/AppContext.swift index c1ef970e1..7191b55d7 100644 --- a/FlowCrypt/App/AppContext.swift +++ b/FlowCrypt/App/AppContext.swift @@ -3,7 +3,7 @@ // FlowCrypt // // Created by Tom on 30.11.2021 -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// Copyright © 2017-present FlowCrypt a.s. All rights reserved. // import Foundation diff --git a/FlowCrypt/App/AppStartup.swift b/FlowCrypt/App/AppStartup.swift index e41075586..0a1bd9a05 100644 --- a/FlowCrypt/App/AppStartup.swift +++ b/FlowCrypt/App/AppStartup.swift @@ -134,7 +134,7 @@ struct AppStartup { guard let mailProvider = try await appContext.getOptionalMailProvider() else { return } - return try await mailProvider.sessionProvider.renewSession() + try await mailProvider.sessionProvider.renewSession() } @MainActor @@ -247,8 +247,8 @@ struct AppStartup { Task { do { try await appContext.globalRouter.signOut(appContext: appContext) - } catch let logoutError { - Logger.logError("Logout failed due to \(logoutError.localizedDescription)") + } catch { + Logger.logError("Logout failed due to \(error.errorMessage)") } } } diff --git a/FlowCrypt/App/GlobalRouter.swift b/FlowCrypt/App/GlobalRouter.swift index 1ff283393..3bbff6f20 100644 --- a/FlowCrypt/App/GlobalRouter.swift +++ b/FlowCrypt/App/GlobalRouter.swift @@ -2,7 +2,7 @@ // GlobalRouter.swift // FlowCrypt // -// Created by Anton Kharchevskyi on 9/13/19. +// Created by Anton Kharchevskyi on 9/13/19 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // @@ -75,7 +75,7 @@ extension GlobalRouter: GlobalRouterType { keyWindow.rootViewController?.showAlert( title: "error".localized, message: message, - onOk: { fatalError() } + onOk: { fatalError(message) } ) return diff --git a/FlowCrypt/Common UI/View Controllers/BlurViewController.swift b/FlowCrypt/Common UI/View Controllers/BlurViewController.swift index 852290867..f63a57c1b 100644 --- a/FlowCrypt/Common UI/View Controllers/BlurViewController.swift +++ b/FlowCrypt/Common UI/View Controllers/BlurViewController.swift @@ -16,6 +16,7 @@ final class BlurViewController: UIViewController { self.blurView = UIVisualEffectView(effect: blurEffect) super.init(nibName: nil, bundle: nil) } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index cc0d24ce4..ad8e5a405 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -33,7 +33,7 @@ final class ComposeViewController: TableNodeViewController { case recipientsLabel, recipients(RecipientType), password, compose, attachments, searchResults, contacts static var recipientsSections: [Section] { - RecipientType.allCases.map { Section.recipients($0) } + RecipientType.allCases.map { Self.recipients($0) } } } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 4faf8a72f..0eb89cb51 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -244,7 +244,7 @@ extension ComposeViewDecorator { backgroundColor: .titleNodeBackgroundColor, borderColor: .borderColor, textColor: .mainTextColor, - image: #imageLiteral(resourceName: "retry"), + image: UIImage(named: "retry"), accessibilityIdentifier: "gray" ) } @@ -314,7 +314,7 @@ extension ComposeViewDecorator { backgroundColor: .red, borderColor: .borderColor, textColor: .white, - image: #imageLiteral(resourceName: "retry"), + image: UIImage(named: "retry"), accessibilityIdentifier: "red" ) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift index 8fc7a7b6d..58b7c4a6b 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift @@ -117,7 +117,7 @@ extension ComposeViewController { } var displayName = name - if let name = name, let address = MCOAddress.init(nonEncodedRFC822String: name), address.displayName != nil { + if let name = name, let address = MCOAddress(nonEncodedRFC822String: name), address.displayName != nil { displayName = address.displayName } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 81b903d79..ef6329579 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -22,25 +22,26 @@ extension ComposeViewController { } private func shouldSaveDraft() -> Bool { - return false -// let newDraft = ComposedDraft( -// input: input, -// contextToSend: contextToSend -// ) -// -// if let existingDraft = composedLatestDraft { -// let draftHasChanges = newDraft != existingDraft -// self.composedLatestDraft = newDraft -// return draftHasChanges -// } else { // save initial draft -// composedLatestDraft = newDraft -// return false -// } + let newDraft = ComposedDraft( + input: input, + contextToSend: contextToSend + ) + + if let existingDraft = composedLatestDraft { + let draftHasChanges = newDraft != existingDraft + self.composedLatestDraft = newDraft + return draftHasChanges + } else { // save initial draft + composedLatestDraft = newDraft + return false + } } internal func saveDraftIfNeeded() { guard shouldSaveDraft() else { return } - + + print("saving draft") + Task { do { let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg( @@ -50,6 +51,8 @@ extension ComposeViewController { ) try await composeMessageService.encryptAndSaveDraft(message: sendableMsg, threadId: input.threadId) } catch { + print("got error") + print(error) if case .promptUserToEnterPassPhraseForSigningKey(let keyPair) = error as? ComposeMessageError { requestMissingPassPhraseWithModal(for: keyPair, isDraft: true) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index 55890571e..d8d7e7569 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -96,10 +96,11 @@ extension ComposeViewController { for aliasEmail in sendAsList { let action = UIAlertAction( - title: aliasEmail.descriptoin, - style: .default) { [weak self] _ in - self?.changeSendAs(to: aliasEmail.sendAsEmail) - } + title: aliasEmail.description, + style: .default + ) { [weak self] _ in + self?.changeSendAs(to: aliasEmail.sendAsEmail) + } // Remove @, . in email part as appium throws error for identifiers which contain @, . let emailIentifier = aliasEmail.sendAsEmail .replacingOccurrences(of: "@", with: "-") diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift index 5e64ee8f9..215353994 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift @@ -76,7 +76,7 @@ extension ComposeViewController { state: decorator.recipientIdleState ) - if idleRecipients.firstIndex(where: { $0.email == newRecipient.email }) == nil { + if !idleRecipients.contains(where: { $0.email == newRecipient.email }) { // add new recipient contextToSend.add(recipient: newRecipient) @@ -145,7 +145,7 @@ extension ComposeViewController { } internal func handleBackspaceAction(with textField: UITextField, for recipientType: RecipientType) { - guard textField.text == "" else { return } + guard textField.text != "" else { return } var recipients = contextToSend.recipients(type: recipientType) @@ -177,8 +177,9 @@ extension ComposeViewController { } internal func handleEditingChanged(with text: String?) { - shouldDisplaySearchResult = text != "" - search.send(text ?? "") + let inputText = text ?? "" + shouldDisplaySearchResult = !inputText.isEmpty + search.send(inputText) } internal func handleDidBeginEditing(recipientType: RecipientType) { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift index 8de5a1f1b..9de6d5dd1 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift @@ -77,7 +77,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case let (.searchEmails(recipients), .searchResults): guard indexPath.row > 0 else { return DividerCellNode() } guard recipients.isNotEmpty else { return self.noSearchResultsNode() } - guard let recipient = recipients[safe: indexPath.row-1] else { return ASCellNode() } + guard let recipient = recipients[safe: indexPath.row - 1] else { return ASCellNode() } if let name = recipient.name { let input = self.decorator.styledRecipientInfo( @@ -103,7 +103,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { switch section { case .searchResults: - let recipient = recipients[safe: indexPath.row-1] + let recipient = recipients[safe: indexPath.row - 1] handleEndEditingAction(with: recipient?.email, name: recipient?.name, for: recipientType) case .contacts: askForContactsPermission() diff --git a/FlowCrypt/Controllers/Inbox/InboxProviders.swift b/FlowCrypt/Controllers/Inbox/InboxProviders.swift index 2349286cb..6a3f0aa1e 100644 --- a/FlowCrypt/Controllers/Inbox/InboxProviders.swift +++ b/FlowCrypt/Controllers/Inbox/InboxProviders.swift @@ -13,10 +13,8 @@ struct InboxContext { let pagination: MessagesListPagination } -class InboxDataProvider { - func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { - fatalError("Should be implemented") - } +protocol InboxDataProvider { + func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext } // used when displaying conversations (threads) in inbox (Gmail API default) @@ -27,7 +25,7 @@ class InboxMessageThreadsProvider: InboxDataProvider { self.provider = provider } - override func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { + func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { let result = try await provider.fetchThreads(using: context) let inboxData = result.threads.map { @@ -54,7 +52,7 @@ class InboxMessageListProvider: InboxDataProvider { self.provider = provider } - override func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { + func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { let result = try await provider.fetchMessages(using: context) let inboxData = result.messages.map(InboxRenderable.init) diff --git a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift b/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift index 21014a474..3a6bb1a92 100644 --- a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift +++ b/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift @@ -60,7 +60,7 @@ extension MsgListViewController where Self: UIViewController { let viewController = try await ThreadDetailsViewController( appContext: appContext, thread: thread - ) { [weak self] (action, message) in + ) { [weak self] action, message in self?.handleMessageOperation(message: message, action: action) } navigationController?.pushViewController(viewController, animated: true) diff --git a/FlowCrypt/Controllers/Search/SearchViewController.swift b/FlowCrypt/Controllers/Search/SearchViewController.swift index 241a0b19c..eab306ba4 100644 --- a/FlowCrypt/Controllers/Search/SearchViewController.swift +++ b/FlowCrypt/Controllers/Search/SearchViewController.swift @@ -15,6 +15,7 @@ class SearchViewController: InboxViewController { private let searchController = UISearchController(searchResultsController: nil) override func viewDidLoad() { + super.viewDidLoad() self.setupSearchUI() self.setupSearch() } diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift index 096812000..37c61c12f 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift @@ -63,10 +63,10 @@ extension BackupSelectKeyViewController { // MARK: - Actions extension BackupSelectKeyViewController { @objc private func handleSave() { - if backupsContext.filter({ $0.1 == true }).isEmpty { - showAlert(message: "backup_select_key_screen_no_selection".localized) - } else { + if backupsContext.contains(where: { $0.1 == true }) { makeBackup() + } else { + showAlert(message: "backup_select_key_screen_no_selection".localized) } } diff --git a/FlowCrypt/Controllers/Settings/KeySettings/Key Details/KeyDetailViewDecorator.swift b/FlowCrypt/Controllers/Settings/KeySettings/Key Details/KeyDetailViewDecorator.swift index 4824f715a..2f46465c7 100644 --- a/FlowCrypt/Controllers/Settings/KeySettings/Key Details/KeyDetailViewDecorator.swift +++ b/FlowCrypt/Controllers/Settings/KeySettings/Key Details/KeyDetailViewDecorator.swift @@ -2,7 +2,7 @@ // KeySettingsItemDecorator.swift // FlowCrypt // -// Created by Anton Kharchevskyi on 12/13/19. +// Created by Anton Kharchevskyi on 12/13/19 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // diff --git a/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift b/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift index 44936b3a1..95a7a9963 100644 --- a/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift +++ b/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift @@ -33,7 +33,7 @@ final class SettingsViewController: TableNodeViewController { } static func filtered(with rules: ClientConfiguration) -> [SettingsMenuItem] { - var cases = SettingsMenuItem.allCases + var cases = Self.allCases if !rules.canBackupKeys { cases.removeAll(where: { $0 == .backups }) diff --git a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift index 3a0d0e5c0..f12b5187a 100644 --- a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift @@ -97,7 +97,7 @@ final class SetupGenerateKeyViewController: SetupCreatePassphraseAbstractViewCon ) throws { try appContext.encryptedStorage.putKeypairs( keyDetails: [encryptedPrv.key], - passPhrase: storageMethod == .persistent ? passPhrase: nil, + passPhrase: storageMethod == .persistent ? passPhrase : nil, source: .generated, for: appContext.user.email ) diff --git a/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift b/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift index b8ddb4d6e..93379a29c 100644 --- a/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift +++ b/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift @@ -162,17 +162,19 @@ extension SetupImapViewController { NotificationCenter.default.addObserver( forName: UIResponder.keyboardWillShowNotification, object: nil, - queue: .main) { [weak self] notification in - guard let self = self else { return } - self.adjustForKeyboard(height: self.keyboardHeight(from: notification)) - } + queue: .main + ) { [weak self] notification in + guard let self = self else { return } + self.adjustForKeyboard(height: self.keyboardHeight(from: notification)) + } NotificationCenter.default.addObserver( forName: UIResponder.keyboardWillHideNotification, object: nil, - queue: .main) { [weak self] _ in - self?.adjustForKeyboard(height: 0) - } + queue: .main + ) { [weak self] _ in + self?.adjustForKeyboard(height: 0) + } } private func adjustForKeyboard(height: CGFloat) { diff --git a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift index f661d916e..98ffdf335 100644 --- a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift +++ b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import ENSwiftSideMenu import FlowCryptUI +import UIKit /** * Menu view controller @@ -30,8 +31,8 @@ final class MyMenuViewController: ViewController { var arrowImage: UIImage? { switch self { - case .folders: return #imageLiteral(resourceName: "arrow_down").tinted(.white) - case .accountAdding: return #imageLiteral(resourceName: "arrow_up").tinted(.white) + case .folders: return UIImage(named: "arrow_down")?.tinted(.white) + case .accountAdding: return UIImage(named: "arrow_up")?.tinted(.white) } } } diff --git a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewDecorator.swift b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewDecorator.swift index 9d55f9b56..170bbd905 100644 --- a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewDecorator.swift +++ b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewDecorator.swift @@ -72,7 +72,7 @@ extension InfoCellNode.Input { attributedText: "folder_add_account" .localized .attributed(.regular(17), color: .mainTextColor), - image: #imageLiteral(resourceName: "plus").tinted(.mainTextColor), + image: UIImage(named: "plus")?.tinted(.mainTextColor), insets: .side(16), backgroundColor: .backgroundColor, accessibilityIdentifier: "aid-add-account-btn" diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 9ce207587..9f3977892 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -118,7 +118,7 @@ extension ThreadDetailsViewController { input[indexPath.section - 1].isExpanded.toggle() - if input[indexPath.section-1].isExpanded { + if input[indexPath.section - 1].isExpanded { UIView.animate( withDuration: 0.3, animations: { @@ -127,7 +127,7 @@ extension ThreadDetailsViewController { completion: { [weak self] _ in guard let self = self else { return } - if let processedMessage = self.input[indexPath.section-1].processedMessage { + if let processedMessage = self.input[indexPath.section - 1].processedMessage { self.handleReceived(message: processedMessage, at: indexPath) } else { self.fetchDecryptAndRenderMsg(at: indexPath) @@ -179,9 +179,10 @@ extension ThreadDetailsViewController { private func createComposeNewMessageAlertAction(at indexPath: IndexPath, type: MessageQuoteType) -> UIAlertAction { let action = UIAlertAction( title: type.actionLabel, - style: .default) { [weak self] _ in - self?.composeNewMessage(at: indexPath, quoteType: type) - } + style: .default + ) { [weak self] _ in + self?.composeNewMessage(at: indexPath, quoteType: type) + } action.accessibilityIdentifier = type.accessibilityIdentifier return action } @@ -202,7 +203,7 @@ extension ThreadDetailsViewController { defer { node.reloadRows(at: [indexPath], with: .automatic) } let trace = Trace(id: "Attachment") - let section = input[indexPath.section-1] + let section = input[indexPath.section - 1] let attachmentIndex = indexPath.row - 2 guard var attachment = section.processedMessage?.attachments[attachmentIndex] else { @@ -229,11 +230,11 @@ extension ThreadDetailsViewController { ) logger.logInfo("Got encrypted attachment - \(trace.finish())") - input[indexPath.section-1].processedMessage?.attachments[attachmentIndex] = decryptedAttachment + input[indexPath.section - 1].processedMessage?.attachments[attachmentIndex] = decryptedAttachment return decryptedAttachment } else { logger.logInfo("Got not encrypted attachment - \(trace.finish())") - input[indexPath.section-1].processedMessage?.attachments[attachmentIndex] = attachment + input[indexPath.section - 1].processedMessage?.attachments[attachmentIndex] = attachment return attachment } } @@ -244,7 +245,7 @@ extension ThreadDetailsViewController { } private func composeNewMessage(at indexPath: IndexPath, quoteType: MessageQuoteType) { - guard let input = input[safe: indexPath.section-1], + guard let input = input[safe: indexPath.section - 1], let processedMessage = input.processedMessage else { return } @@ -321,7 +322,7 @@ extension ThreadDetailsViewController { extension ThreadDetailsViewController { private func fetchDecryptAndRenderMsg(at indexPath: IndexPath) { - let message = input[indexPath.section-1].rawMessage + let message = input[indexPath.section - 1].rawMessage logger.logInfo("Start loading message") handleFetchProgress(state: .fetch) @@ -410,7 +411,7 @@ extension ThreadDetailsViewController { ) let downloadAction = UIAlertAction(title: "download".localized, style: .default) { [weak self] _ in - guard let attachment = self?.input[indexPath.section-1].processedMessage?.attachments[indexPath.row-2] else { + guard let attachment = self?.input[indexPath.section - 1].processedMessage?.attachments[indexPath.row - 2] else { return } self?.show(attachment: attachment) @@ -455,7 +456,7 @@ extension ThreadDetailsViewController { userEmail: appContext.user.email ) if matched { - let sender = input[indexPath.section-1].rawMessage.sender + let sender = input[indexPath.section - 1].rawMessage.sender let processedMessage = try await messageService.decryptAndProcess( message: message, sender: sender, @@ -490,7 +491,7 @@ extension ThreadDetailsViewController { handleReceived(message: processedMessage, at: indexPath) } catch { let message = "message_signature_fail_reason".localizeWithArguments(error.errorMessage) - input[indexPath.section-1].processedMessage?.signature = .error(message) + input[indexPath.section - 1].processedMessage?.signature = .error(message) } } } @@ -583,9 +584,9 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { - guard section > 0, input[section-1].isExpanded else { return 1 } + guard section > 0, input[section - 1].isExpanded else { return 1 } - let attachmentsCount = input[section-1].processedMessage?.attachments.count ?? 0 + let attachmentsCount = input[section - 1].processedMessage?.attachments.count ?? 0 return Parts.allCases.count + attachmentsCount } diff --git a/FlowCrypt/Extensions/UIColorExtensions.swift b/FlowCrypt/Extensions/UIColorExtensions.swift index 7d3f5ffb7..3d3f6b894 100644 --- a/FlowCrypt/Extensions/UIColorExtensions.swift +++ b/FlowCrypt/Extensions/UIColorExtensions.swift @@ -72,9 +72,9 @@ public extension UIColor { extension UIColor { convenience init(r: Int, g: Int, b: Int, alpha: CGFloat = 1) { self.init( - red: CGFloat(r)/CGFloat(255.0), - green: CGFloat(g)/CGFloat(255.0), - blue: CGFloat(b)/CGFloat(255.0), + red: CGFloat(r) / CGFloat(255.0), + green: CGFloat(g) / CGFloat(255.0), + blue: CGFloat(b) / CGFloat(255.0), alpha: alpha ) } diff --git a/FlowCrypt/Functionality/Api/Account Server Apis/BackendApi.swift b/FlowCrypt/Functionality/Api/Account Server Apis/BackendApi.swift index 7f1dc4d35..fcf5c3086 100644 --- a/FlowCrypt/Functionality/Api/Account Server Apis/BackendApi.swift +++ b/FlowCrypt/Functionality/Api/Account Server Apis/BackendApi.swift @@ -7,7 +7,7 @@ import Foundation /// Backend API for regular consumers and small businesses /// (not implemented on iOS yet) final class BackendApi { - static let shared: BackendApi = BackendApi() + static let shared = BackendApi() private init() {} diff --git a/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift b/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift index 085555366..7c56c6bb8 100644 --- a/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift +++ b/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift @@ -59,7 +59,8 @@ actor EmailKeyManagerApi: EmailKeyManagerApiType { URLHeader( value: "Bearer \(idToken)", httpHeaderField: "Authorization" - )] + ) + ] let request = ApiCall.Request( apiName: Constants.apiName, url: urlString, diff --git a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift index aca8f59e6..9a4f67b60 100644 --- a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift +++ b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift @@ -77,7 +77,7 @@ class WkdApi: WkdApiType { private func parseAndFilter(keysData: Data, email: String) async throws -> [KeyDetails] { return try await core.parseKeys(armoredOrBinary: keysData).keyDetails - .filter { !$0.users.filter { $0.contains(email) }.isEmpty } + .filter { $0.users.contains { user in user.contains(email) } } } private func urlLookup(_ urls: WkdUrls) async throws -> InternalResult { diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift index 4512c3d21..99478578f 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift @@ -99,7 +99,7 @@ final class EncryptedStorage: EncryptedStorageType { return Realm.Configuration(inMemoryIdentifier: UUID().uuidString) } - let path = try EncryptedStorage.path + let path = try Self.path let latestSchemaVersion = currentSchema.version.dbSchemaVersion return Realm.Configuration( diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift index 7c9250143..c2deac4f6 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift @@ -2,8 +2,8 @@ // KeyChainService.swift // FlowCrypt // -// Created by Anton Kharchevskyi on 25.11.2019. -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// Created by Anton Kharchevskyi on 25.11.2019 +// Copyright © 2017-present FlowCrypt a.s. All rights reserved. // import FlowCryptCommon diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/Version6SchemaMigration.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/Version6SchemaMigration.swift index 3654cd05f..fbd97d959 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/Version6SchemaMigration.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/Version6SchemaMigration.swift @@ -2,8 +2,8 @@ // Version6SchemaMigration.swift // FlowCrypt // -// Created by  Ivan Ushakov on 07.12.2021 -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// Created by Ivan Ushakov on 07.12.2021 +// Copyright © 2017-present FlowCrypt a.s. All rights reserved. // import FlowCryptCommon diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift index 7c067edf0..145ff9959 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift @@ -85,7 +85,7 @@ extension Imap { _ imapSess: MCOIMAPSession, _ executor: @escaping (MCOIMAPSession, @escaping (Error?) -> Void) -> Void ) async throws { - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in executor(imapSess) { error in if let error = error { return continuation.resume(throwing: error) diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift index a1f6daaef..ab1f007dd 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift @@ -18,7 +18,7 @@ extension Imap { } func connectSmtp(session: SMTPSession) async throws { - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in MCOSMTPSession(session: session) .startLogging() .loginOperation()? @@ -33,7 +33,7 @@ extension Imap { } func connectImap(session: IMAPSession) async throws { - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in MCOIMAPSession(session: session) .startLogging() .connectOperation()? diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift index 55dbd6777..bb3b97ebd 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift @@ -85,7 +85,7 @@ extension GmailService: MessageProvider { let fetcher = createAttachmentFetcher(identifier: identifier, messageId: messageIdentifier) if let estimatedSize = estimatedSize { fetcher.receivedProgressBlock = { _, received in - let progress = min(Float(received)/estimatedSize, 1) + let progress = min(Float(received) / estimatedSize, 1) progressHandler?(progress) } } diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift index 0d4ee0985..e026b7339 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift @@ -104,7 +104,7 @@ extension GmailService: MessageOperationsProvider { userId: .me ) - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in self.gmailService.executeQuery(query) { _, _, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index e2a316e6b..d9b69fce1 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -46,7 +46,7 @@ struct Message: Hashable { } var hasSignatureAttachment: Bool { - attachments.first(where: { $0.type == "application/pgp-signature" }) != nil + attachments.contains(where: { $0.type == "application/pgp-signature" }) } init( @@ -80,10 +80,10 @@ struct Message: Hashable { self.threadId = threadId self.draftIdentifier = draftIdentifier self.raw = raw - self.to = Message.parseRecipients(to) - self.cc = Message.parseRecipients(cc) - self.bcc = Message.parseRecipients(bcc) - self.replyTo = Message.parseRecipients(replyTo) + self.to = Self.parseRecipients(to) + self.cc = Self.parseRecipients(cc) + self.bcc = Self.parseRecipients(bcc) + self.replyTo = Self.parseRecipients(replyTo) self.inReplyTo = inReplyTo } } diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift index 4b19dbb37..c1f6f4d92 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift @@ -17,14 +17,14 @@ extension GmailService: MessagesThreadProvider { func fetchThreads(using context: FetchMessageContext) async throws -> MessageThreadContext { let threadsList = try await getThreadsList(using: context) let requests = threadsList.threads? - .compactMap { (thread) -> (String, String?)? in + .compactMap { thread -> (String, String?)? in guard let id = thread.identifier else { return nil } return (id, thread.snippet) } ?? [] - return try await withThrowingTaskGroup(of: MessageThread.self) { (taskGroup) in + return try await withThrowingTaskGroup(of: MessageThread.self) { taskGroup in var messageThreadsById: [String: MessageThread] = [:] for request in requests { taskGroup.addTask { @@ -68,7 +68,7 @@ extension GmailService: MessagesThreadProvider { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in self.gmailService.executeQuery( GTLRGmailQuery_UsersThreadsGet.query(withUserId: .me, identifier: identifier) - ) { (_, data, error) in + ) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } diff --git a/FlowCrypt/Functionality/Pgp/KeyMethods.swift b/FlowCrypt/Functionality/Pgp/KeyMethods.swift index 96ce4ce6e..cd3ff8518 100644 --- a/FlowCrypt/Functionality/Pgp/KeyMethods.swift +++ b/FlowCrypt/Functionality/Pgp/KeyMethods.swift @@ -56,7 +56,7 @@ final class KeyMethods: KeyMethodsType { guard parsed.isNotEmpty else { throw KeypairError.noAccountKeysAvailable } - let usable = parsed.filter { $0.isKeyUsable } + let usable = parsed.filter(\.isKeyUsable) guard usable.isNotEmpty else { throw MessageValidationError.noUsableAccountKeys } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index 7015da2dd..d233ee2f9 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -48,11 +48,11 @@ extension ComposeMessageContext { } var hasCcOrBccRecipients: Bool { - recipients.first(where: { $0.type == .cc || $0.type == .bcc }) != nil + recipients.contains(where: { $0.type == .cc || $0.type == .bcc }) } var hasRecipientsWithoutPubKey: Bool { - recipients.first { $0.keyState == .empty } != nil + recipients.contains(where: { $0.keyState == .empty }) } var hasMessagePasswordIfNeeded: Bool { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift index 5091c8330..b2206fa5f 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift @@ -68,7 +68,7 @@ enum ComposeMessageError: Error, CustomStringConvertible, Equatable { return "compose_sign_passphrase_required".localized case .passPhraseNoMatch: return "compose_sign_passphrase_no_match".localized - case .noKeysFoundForSign(let count, let sender): + case let .noKeysFoundForSign(count, sender): return "compose_sign_no_keys".localizeWithArguments("\(count)", sender) case .gatewayError(let error): return error.localizedDescription diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index ead4d9786..4cf424f70 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -95,43 +95,55 @@ final class ComposeMessageService { contextToSend: ComposeMessageContext, isDraft: Bool = false ) async throws -> SendableMsg { - if !isDraft { onStateChanged?(.validatingMessage) } - let recipients = contextToSend.recipients + let subject = contextToSend.subject ?? "(no subject)" - guard recipients.isNotEmpty else { - throw MessageValidationError.emptyRecipient - } + let senderKeys = try await keyMethods.chooseSenderKeys( + for: .encryption, + keys: try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: sender), + senderEmail: contextToSend.sender + ) - let emails = recipients.map(\.email) - let emptyEmails = emails.filter { !$0.hasContent } + if !isDraft { + onStateChanged?(.validatingMessage) - guard emails.isNotEmpty, emptyEmails.isEmpty else { - throw MessageValidationError.emptyRecipient - } + guard recipients.isNotEmpty else { + throw MessageValidationError.emptyRecipient + } - guard emails.filter({ !$0.isValidEmail }).isEmpty else { - throw MessageValidationError.invalidEmailRecipient - } + let emails = recipients.map(\.email) + let emptyEmails = emails.filter { !$0.hasContent } - guard input.isQuote || contextToSend.subject?.hasContent ?? false else { - throw MessageValidationError.emptySubject - } + guard emails.isNotEmpty, emptyEmails.isEmpty else { + throw MessageValidationError.emptyRecipient + } - guard let text = contextToSend.message, text.hasContent else { - throw MessageValidationError.emptyMessage - } + guard !emails.contains(where: { !$0.isValidEmail }) else { + throw MessageValidationError.invalidEmailRecipient + } - let subject = contextToSend.subject ?? "(no subject)" + guard input.isQuote || contextToSend.subject?.hasContent ?? false else { + throw MessageValidationError.emptySubject + } - let senderKeys = try await keyMethods.chooseSenderKeys( - for: .encryption, - keys: try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: sender), - senderEmail: contextToSend.sender - ) + guard let text = contextToSend.message, text.hasContent else { + throw MessageValidationError.emptyMessage + } - guard senderKeys.isNotEmpty else { - throw MessageValidationError.noUsableAccountKeys + if let password = contextToSend.messagePassword, password.isNotEmpty { + if subject.lowercased().contains(password.lowercased()) { + throw MessageValidationError.subjectContainsPassword + } + + let allAvailablePassPhrases = try appContext.combinedPassPhraseStorage.getPassPhrases(for: sender).map(\.value) + if allAvailablePassPhrases.contains(password) { + throw MessageValidationError.notUniquePassword + } + } + + guard senderKeys.isNotEmpty else { + throw MessageValidationError.noUsableAccountKeys + } } let sendableAttachments: [SendableMsg.Attachment] = !isDraft @@ -144,21 +156,12 @@ final class ComposeMessageService { hasMessagePassword: contextToSend.hasMessagePassword ) - if let password = contextToSend.messagePassword, password.isNotEmpty { - if subject.lowercased().contains(password.lowercased()) { - throw MessageValidationError.subjectContainsPassword - } - - let allAvailablePassPhrases = try appContext.combinedPassPhraseStorage.getPassPhrases(for: sender).map(\.value) - if allAvailablePassPhrases.contains(password) { - throw MessageValidationError.notUniquePassword - } - } - let signingPrv = try await prepareSigningKey(senderEmail: contextToSend.sender) + print("create sendable") + return SendableMsg( - text: text, + text: contextToSend.message ?? "", html: nil, to: contextToSend.recipientEmails(type: .to), cc: contextToSend.recipientEmails(type: .cc), @@ -195,7 +198,7 @@ final class ComposeMessageService { hasMessagePassword: Bool ) throws -> [String] { func contains(keyState: PubKeyState) -> Bool { - recipients.first(where: { $0.keyState == keyState }) != nil + recipients.contains(where: { $0.keyState == keyState }) } logger.logDebug("validate recipients: \(recipients)") diff --git a/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift b/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift index 225816d11..0ee03d089 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift @@ -2,7 +2,7 @@ // Imap+folders.swift // FlowCrypt // -// Created by Anton Kharchevskyi on 9/11/19. +// Created by Anton Kharchevskyi on 9/11/19 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // diff --git a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift index d4447d41b..50c9ca3d3 100644 --- a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift @@ -30,7 +30,7 @@ enum GoogleUserServiceError: Error, CustomStringConvertible { switch self { case .cancelledAuthorization: return "google_user_service_error_auth_cancelled".localized - case .wrongAccount(let signedAccount, let currentAccount): + case let .wrongAccount(signedAccount, currentAccount): return "google_user_service_error_wrong_account".localizeWithArguments( signedAccount, currentAccount, currentAccount ) @@ -326,7 +326,7 @@ extension GoogleUserService { .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") - while decodedString.utf16.count % 4 != 0 { + while decodedString.utf16.count.isMultiple(of: 4) { decodedString += "=" } diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/InMemoryPassPhraseStorage.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/InMemoryPassPhraseStorage.swift index aa5b318e5..e945b3f3d 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/InMemoryPassPhraseStorage.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/InMemoryPassPhraseStorage.swift @@ -18,7 +18,7 @@ final class InMemoryPassPhraseStorage: PassPhraseStorageType { init( passPhraseProvider: InMemoryPassPhraseProviderType = InMemoryPassPhraseProvider.shared, - timeoutInSeconds: Int = 4*60*60 // 4 hours + timeoutInSeconds: Int = 4 * 60 * 60 // 4 hours ) { self.passPhraseProvider = passPhraseProvider self.timeoutInSeconds = timeoutInSeconds @@ -76,7 +76,7 @@ protocol InMemoryPassPhraseProviderType { /// - Warning: - should be shared instance final class InMemoryPassPhraseProvider: InMemoryPassPhraseProviderType { - static let shared: InMemoryPassPhraseProvider = InMemoryPassPhraseProvider() + static let shared = InMemoryPassPhraseProvider() private(set) var passPhrases: Set = [] diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift index 7a3aafe0b..224ca83e4 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift @@ -31,8 +31,7 @@ final class KeyAndPassPhraseStorage: KeyAndPassPhraseStorageType { var keypairs = try encryptedStorage.getKeypairs(by: email) for i in keypairs.indices { keypairs[i].passphrase = keypairs[i].passphrase ?? storedPassPhrases - .filter { $0.value.isNotEmpty } - .first(where: { $0.primaryFingerprintOfAssociatedKey == keypairs[i].primaryFingerprint })? + .first(where: { $0.value.isNotEmpty && $0.primaryFingerprintOfAssociatedKey == keypairs[i].primaryFingerprint })? .value } return keypairs diff --git a/FlowCrypt/Functionality/Services/SendAs Services/Models/SendAsModel.swift b/FlowCrypt/Functionality/Services/SendAs Services/Models/SendAsModel.swift index 48a2b707e..d7887705d 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/Models/SendAsModel.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/Models/SendAsModel.swift @@ -14,8 +14,8 @@ struct SendAsModel { let isDefault: Bool let verificationStatus: SendAsVerificationStatus - var descriptoin: String { - if displayName == "" { + var description: String { + if displayName.isEmpty { return sendAsEmail } return "\(sendAsEmail) (\(displayName))" diff --git a/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/RemoteSendAsProvider.swift b/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/RemoteSendAsProvider.swift index 556b967e7..00f3641ac 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/RemoteSendAsProvider.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/RemoteSendAsProvider.swift @@ -2,7 +2,7 @@ // RemoteSendAsProviderType.swift // FlowCrypt // -// Created by Ioan Moldovan on 06/13/22. +// Created by Ioan Moldovan on 06/13/22 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // diff --git a/FlowCrypt/Models/Common/Keypair.swift b/FlowCrypt/Models/Common/Keypair.swift index 3baf2cfeb..83865cc85 100644 --- a/FlowCrypt/Models/Common/Keypair.swift +++ b/FlowCrypt/Models/Common/Keypair.swift @@ -58,8 +58,8 @@ extension Keypair { self.public = object.public self.passphrase = object.passphrase self.source = object.source - self.allFingerprints = object.allFingerprints.map { $0 } - self.allLongids = object.allLongids.map { $0 } + self.allFingerprints = Array(object.allFingerprints) + self.allLongids = Array(object.allLongids) self.lastModified = object.lastModified self.isRevoked = object.isRevoked } diff --git a/FlowCrypt/Models/Common/RecipientBase.swift b/FlowCrypt/Models/Common/RecipientBase.swift index 5cd823b7f..81558a52c 100644 --- a/FlowCrypt/Models/Common/RecipientBase.swift +++ b/FlowCrypt/Models/Common/RecipientBase.swift @@ -26,7 +26,7 @@ extension RecipientBase { } var displayName: String { - if let name = name, name != "" { + if let name = name, !name.isEmpty { return name } return email diff --git a/FlowCrypt/Models/Contact Models/PubKey.swift b/FlowCrypt/Models/Contact Models/PubKey.swift index 79de16a69..8eac2c752 100644 --- a/FlowCrypt/Models/Contact Models/PubKey.swift +++ b/FlowCrypt/Models/Contact Models/PubKey.swift @@ -77,8 +77,8 @@ extension PubKey { self.lastSig = object.lastSig self.lastChecked = object.lastChecked self.expiresOn = object.expiresOn - self.longids = object.longids.map { $0 } - self.fingerprints = object.fingerprints.map { $0 } + self.longids = Array(object.longids) + self.fingerprints = Array(object.fingerprints) self.created = object.created self.algo = nil diff --git a/FlowCryptCommon/Extensions/LocalizationExtensions.swift b/FlowCryptCommon/Extensions/LocalizationExtensions.swift index 7f2ad8c64..fd3168082 100644 --- a/FlowCryptCommon/Extensions/LocalizationExtensions.swift +++ b/FlowCryptCommon/Extensions/LocalizationExtensions.swift @@ -8,11 +8,13 @@ import Foundation -@inline(__always) private func localize(_ key: String) -> String { +@inline(__always) +private func localize(_ key: String) -> String { return NSLocalizedString(key, comment: "") } -@inline(__always) private func LocalizedString(_ key: String) -> String { +@inline(__always) +private func LocalizedString(_ key: String) -> String { return localize(key) } @@ -21,12 +23,14 @@ public extension String { return LocalizedString(self) } - @inline(__always) func localizeWithArguments(_ arguments: String...) -> String { + @inline(__always) + func localizeWithArguments(_ arguments: String...) -> String { String(format: localize(self), arguments: arguments) } /// use to localize plurals with Localizable.stringsdict - @inline(__always) func localizePluralsWithArguments(_ arguments: Int...) -> String { + @inline(__always) + func localizePluralsWithArguments(_ arguments: Int...) -> String { String(format: localize(self), arguments: arguments) } } diff --git a/FlowCryptCommon/Extensions/OptionalExtensions.swift b/FlowCryptCommon/Extensions/OptionalExtensions.swift index c31159abf..37253e9ac 100644 --- a/FlowCryptCommon/Extensions/OptionalExtensions.swift +++ b/FlowCryptCommon/Extensions/OptionalExtensions.swift @@ -8,8 +8,8 @@ import Foundation -extension Optional { - public func ifNotNil(_ transform: (Wrapped) throws -> U) rethrows -> U? { +public extension Optional { + func ifNotNil(_ transform: (Wrapped) throws -> U) rethrows -> U? { switch self { case .some(let value): return .some(try transform(value)) @@ -19,8 +19,8 @@ extension Optional { } } -extension Optional where Wrapped: Collection { - public var isEmptyOrNil: Bool { +public extension Optional where Wrapped: Collection { + var isEmptyOrNil: Bool { self?.isEmpty ?? true } } diff --git a/FlowCryptCommon/Extensions/StringExtensions.swift b/FlowCryptCommon/Extensions/StringExtensions.swift index cfd253fa2..7ad1df893 100644 --- a/FlowCryptCommon/Extensions/StringExtensions.swift +++ b/FlowCryptCommon/Extensions/StringExtensions.swift @@ -33,7 +33,7 @@ public extension String { ) -> String { String( self.enumerated() - .map { $0 > 0 && $0 % stride == 0 ? [separator, $1] : [$1] } + .map { $0 > 0 && $0.isMultiple(of: stride) ? [separator, $1] : [$1] } .joined() ) } diff --git a/FlowCryptCommon/Extensions/Task+Retry.swift b/FlowCryptCommon/Extensions/Task+Retry.swift index b555fc48b..a6c7101f6 100644 --- a/FlowCryptCommon/Extensions/Task+Retry.swift +++ b/FlowCryptCommon/Extensions/Task+Retry.swift @@ -11,9 +11,9 @@ enum GmailServiceError: Error { case invalidGrant(Error) } -extension Task where Failure == Error { +public extension Task where Failure == Error { @discardableResult - static public func retrying( + static func retrying( priority: TaskPriority? = nil, maxRetryCount: Int = 2, retryDelayMs: UInt64 = 1000, diff --git a/FlowCryptCommon/Extensions/UIDeviceExtensions.swift b/FlowCryptCommon/Extensions/UIDeviceExtensions.swift index 1b464e3f2..79e3d6aa2 100644 --- a/FlowCryptCommon/Extensions/UIDeviceExtensions.swift +++ b/FlowCryptCommon/Extensions/UIDeviceExtensions.swift @@ -8,12 +8,12 @@ import UIKit -extension UIDevice { - public static var isIpad: Bool { +public extension UIDevice { + static var isIpad: Bool { UIDevice.current.userInterfaceIdiom == .pad } - public static var isIphone: Bool { + static var isIphone: Bool { UIDevice.current.userInterfaceIdiom == .phone } } diff --git a/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift b/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift index e87b8f518..7355affdb 100644 --- a/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift +++ b/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift @@ -183,7 +183,7 @@ public extension UIViewController { @MainActor func showSpinner(_ message: String = "loading_title".localized, isUserInteractionEnabled: Bool = false) { - guard view.subviews.first(where: { $0 is MBProgressHUD }) == nil else { + guard !view.subviews.contains(where: { $0 is MBProgressHUD }) else { // hud is already shown return } diff --git a/FlowCryptUI/Cell Nodes/BackupCellNode.swift b/FlowCryptUI/Cell Nodes/BackupCellNode.swift index cf52b94b5..446bbe3b4 100644 --- a/FlowCryptUI/Cell Nodes/BackupCellNode.swift +++ b/FlowCryptUI/Cell Nodes/BackupCellNode.swift @@ -17,7 +17,7 @@ public final class BackupCellNode: CellNode { self.insets = insets } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { return ASCenterLayoutSpec( centeringOptions: .XY, sizingOptions: .minimumXY, diff --git a/FlowCryptUI/Cell Nodes/ButtonCellNode.swift b/FlowCryptUI/Cell Nodes/ButtonCellNode.swift index 794fefdad..ac90bc811 100644 --- a/FlowCryptUI/Cell Nodes/ButtonCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ButtonCellNode.swift @@ -33,7 +33,7 @@ public final class ButtonCellNode: CellNode { private let insets: UIEdgeInsets private let buttonColor: UIColor? - public var isButtonEnabled: Bool = true { + public var isButtonEnabled = true { didSet { button.isEnabled = isButtonEnabled let alpha: CGFloat = isButtonEnabled ? 1 : 0.5 @@ -53,7 +53,7 @@ public final class ButtonCellNode: CellNode { button.setAttributedTitle(input.title, for: .normal) } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: insets, child: button diff --git a/FlowCryptUI/Cell Nodes/CellNode.swift b/FlowCryptUI/Cell Nodes/CellNode.swift index 682c6fd71..ec62c033e 100644 --- a/FlowCryptUI/Cell Nodes/CellNode.swift +++ b/FlowCryptUI/Cell Nodes/CellNode.swift @@ -9,7 +9,7 @@ import AsyncDisplayKit open class CellNode: ASCellNode { - public override init() { + override public init() { super.init() automaticallyManagesSubnodes = true selectionStyle = .none diff --git a/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift b/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift index ec82fa089..632ebf154 100644 --- a/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift +++ b/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift @@ -51,7 +51,7 @@ public final class CheckBoxTextNode: CellNode { } } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { checkBox.style.preferredSize = input.preferredSize if input.subtitle != nil { diff --git a/FlowCryptUI/Cell Nodes/ComposeRecipientCellNode.swift b/FlowCryptUI/Cell Nodes/ComposeRecipientCellNode.swift index 4f832f612..47c1e88fc 100644 --- a/FlowCryptUI/Cell Nodes/ComposeRecipientCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ComposeRecipientCellNode.swift @@ -38,14 +38,14 @@ public final class ComposeRecipientCellNode: CellNode { darkStyle: .white, lightStyle: .black ) - recipientNode.attributedText = input.recipients.map { (recipient) -> NSAttributedString in + recipientNode.attributedText = input.recipients.map { recipient -> NSAttributedString in // Use black text color for gray bubbles var textColor = recipient.state.backgroundColor if textColor == titleNodeBackgroundColorSelected { textColor = grayBubbleTextColor } return recipient.email.string.attributed(.regular(17), color: textColor, alignment: .left) - }.reduce(NSMutableAttributedString()) { (r, e) in + }.reduce(NSMutableAttributedString()) { r, e in if r.length > 0 { r.append(", ".attributed(color: grayBubbleTextColor)) } @@ -54,7 +54,7 @@ public final class ComposeRecipientCellNode: CellNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { return ASInsetLayoutSpec( insets: .deviceSpecificTextInsets(top: 8, bottom: 8), child: recipientNode diff --git a/FlowCryptUI/Cell Nodes/ComposeRecipientPopupNameNode.swift b/FlowCryptUI/Cell Nodes/ComposeRecipientPopupNameNode.swift index 5f10daa99..ae814f19a 100644 --- a/FlowCryptUI/Cell Nodes/ComposeRecipientPopupNameNode.swift +++ b/FlowCryptUI/Cell Nodes/ComposeRecipientPopupNameNode.swift @@ -37,7 +37,7 @@ public final class ComposeRecipientPopupNameNode: CellNode { super.init() } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let stack = ASStackLayoutSpec.vertical() if name != nil { stack.children = [nameNode, emailNode] diff --git a/FlowCryptUI/Cell Nodes/ContactCellNode.swift b/FlowCryptUI/Cell Nodes/ContactCellNode.swift index 8487fe2d9..8501e6adb 100644 --- a/FlowCryptUI/Cell Nodes/ContactCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ContactCellNode.swift @@ -54,7 +54,7 @@ public final class ContactCellNode: CellNode { action?() } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let children: [ASLayoutElement] if input.name == nil { emailNode.style.flexGrow = 1 diff --git a/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift b/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift index 2e0d31973..13de93abe 100644 --- a/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift @@ -68,7 +68,7 @@ public final class ContactKeyCellNode: CellNode { borderNode.isUserInteractionEnabled = false } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let specs = [ [fingerprintTitleNode, fingerprintNode], [createdAtTitleNode, createdAtNode], diff --git a/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift b/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift index 19b54c104..c99fc6d07 100644 --- a/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift @@ -31,7 +31,7 @@ public final class ContactUserCellNode: CellNode { userNode.accessibilityIdentifier = "aid-user-email" } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: .deviceSpecificTextInsets(top: 8, bottom: 8), child: ASStackLayoutSpec( diff --git a/FlowCryptUI/Cell Nodes/DividerCellNode.swift b/FlowCryptUI/Cell Nodes/DividerCellNode.swift index 57c247a8f..28e22f68c 100644 --- a/FlowCryptUI/Cell Nodes/DividerCellNode.swift +++ b/FlowCryptUI/Cell Nodes/DividerCellNode.swift @@ -24,7 +24,7 @@ public final class DividerCellNode: CellNode { backgroundColor = .clear } - public override func layoutSpecThatFits(_ range: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ range: ASSizeRange) -> ASLayoutSpec { let expectedWidth = range.max.width - inset.width line.style.preferredSize.width = expectedWidth > 0 ? expectedWidth : range.max.width return ASInsetLayoutSpec(insets: inset, child: line) diff --git a/FlowCryptUI/Cell Nodes/EmptyCellNode.swift b/FlowCryptUI/Cell Nodes/EmptyCellNode.swift index 4fb6e4b65..3e5c1dbb7 100644 --- a/FlowCryptUI/Cell Nodes/EmptyCellNode.swift +++ b/FlowCryptUI/Cell Nodes/EmptyCellNode.swift @@ -60,7 +60,7 @@ public final class EmptyCellNode: CellNode { accessibilityIdentifier = input.accessibilityIdentifier } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let spec = ASStackLayoutSpec( direction: .vertical, spacing: 16, diff --git a/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift b/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift index 4dcd46886..7e23d6dc5 100644 --- a/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/EmptyFolderCellNode.swift @@ -45,7 +45,7 @@ public final class EmptyFolderCellNode: CellNode { emptyFolder?() } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let stack = ASStackLayoutSpec.horizontal() textNode.style.maxWidth = ASDimensionMake(constrainedSize.max.width - 60) stack.children = [ diff --git a/FlowCryptUI/Cell Nodes/InboxCellNode.swift b/FlowCryptUI/Cell Nodes/InboxCellNode.swift index 8e431f275..0c65dd027 100644 --- a/FlowCryptUI/Cell Nodes/InboxCellNode.swift +++ b/FlowCryptUI/Cell Nodes/InboxCellNode.swift @@ -76,7 +76,7 @@ public final class InboxCellNode: CellNode { accessibilityIdentifier = "aid-inbox-item" } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let emailElement: ASLayoutElement = { guard let countNode = countNode else { return emailNode } emailNode.style.flexShrink = 1.0 diff --git a/FlowCryptUI/Cell Nodes/InfoCellNode.swift b/FlowCryptUI/Cell Nodes/InfoCellNode.swift index b2a6ae33c..b7fbd3adf 100644 --- a/FlowCryptUI/Cell Nodes/InfoCellNode.swift +++ b/FlowCryptUI/Cell Nodes/InfoCellNode.swift @@ -52,7 +52,7 @@ public final class InfoCellNode: CellNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { guard imageNode.image != nil else { return ASInsetLayoutSpec( insets: input?.insets ?? .zero, diff --git a/FlowCryptUI/Cell Nodes/LabelCellNode.swift b/FlowCryptUI/Cell Nodes/LabelCellNode.swift index b9d4bc914..142fa5f75 100644 --- a/FlowCryptUI/Cell Nodes/LabelCellNode.swift +++ b/FlowCryptUI/Cell Nodes/LabelCellNode.swift @@ -49,7 +49,7 @@ public final class LabelCellNode: CellNode { textNode.accessibilityIdentifier = input.accessibilityIdentifier } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: input.insets, child: ASStackLayoutSpec( diff --git a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift index 599eec670..94a317f92 100644 --- a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift +++ b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift @@ -54,7 +54,7 @@ public final class MessagePasswordCellNode: CellNode { buttonNode.addTarget(self, action: #selector(onButtonTap), forControlEvents: .touchUpInside) } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { buttonNode.style.flexShrink = 1.0 let spacer = ASLayoutSpec() diff --git a/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift b/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift index ee8397adf..d957f49a0 100644 --- a/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift @@ -21,7 +21,7 @@ public final class MessageSubjectNode: CellNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { subjectNode.style.flexGrow = 1.0 return ASInsetLayoutSpec( insets: .deviceSpecificTextInsets(top: 16, bottom: 4), diff --git a/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift b/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift index f74ff8c6a..3c38f0fd5 100644 --- a/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift @@ -24,7 +24,7 @@ public final class MessageTextSubjectNode: CellNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { textNode.style.flexGrow = 1.0 return ASInsetLayoutSpec( insets: .deviceSpecificTextInsets(top: 8, bottom: 8), diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift index 7f52e8c0e..1299a22f8 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift @@ -29,7 +29,7 @@ final class RecipientEmailNode: CellNode { let input: Input let imageNode = ASImageNode() - public var onTap: ((Tap) -> Void)? + var onTap: ((Tap) -> Void)? init(input: Input, index: Int) { self.input = input @@ -47,7 +47,7 @@ final class RecipientEmailNode: CellNode { titleNode.clipsToBounds = true titleNode.borderWidth = 1 titleNode.borderColor = input.recipient.state.borderColor.cgColor - titleNode.textContainerInset = RecipientEmailNode.Constants.titleInsets + titleNode.textContainerInset = Self.Constants.titleInsets imageNode.image = input.recipient.state.stateImage imageNode.alpha = 0 diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift index 9a3d20b7d..dbbf89db4 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift @@ -10,7 +10,7 @@ import AsyncDisplayKit public final class RecipientEmailTextFieldNode: TextFieldCellNode { - public override init( + override public init( input: TextFieldCellNode.Input, action: TextFieldAction? = nil ) { @@ -20,12 +20,12 @@ public final class RecipientEmailTextFieldNode: TextFieldCellNode { } @discardableResult - public override func becomeFirstResponder() -> Bool { + override public func becomeFirstResponder() -> Bool { textField.becomeFirstResponder() return true } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { textField.style.preferredSize.height = input.height return ASInsetLayoutSpec(insets: input.insets, child: textField) diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index a324b1985..11c414f5a 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -9,7 +9,7 @@ import AsyncDisplayKit import FlowCryptCommon -final public class RecipientEmailsCellNode: CellNode { +public final class RecipientEmailsCellNode: CellNode { public typealias RecipientTap = (RecipientEmailTapAction) -> Void public enum RecipientEmailTapAction { @@ -91,7 +91,7 @@ final public class RecipientEmailsCellNode: CellNode { self.toggleButtonAction = toggleButtonAction } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let collectionNodeSize = CGSize(width: constrainedSize.max.width, height: collectionLayoutHeight) let buttonSize = CGSize(width: 40, height: 32) @@ -118,13 +118,13 @@ final public class RecipientEmailsCellNode: CellNode { } } -extension RecipientEmailsCellNode { - public func onItemSelect(_ action: RecipientTap?) -> Self { +public extension RecipientEmailsCellNode { + func onItemSelect(_ action: RecipientTap?) -> Self { self.onAction = action return self } - public func onLayoutHeightChanged(_ completion: @escaping (CGFloat) -> Void) -> Self { + func onLayoutHeightChanged(_ completion: @escaping (CGFloat) -> Void) -> Self { self.layout.onHeightChanged = completion return self } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift index 1a594dfb9..b4f877aa2 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift @@ -9,8 +9,8 @@ import UIKit // MARK: Input -extension RecipientEmailsCellNode { - public struct Input { +public extension RecipientEmailsCellNode { + struct Input { public struct StateContext: Equatable { let backgroundColor, borderColor, textColor: UIColor let image: UIImage? @@ -55,11 +55,11 @@ extension RecipientEmailsCellNode { } } - public var backgroundColor: UIColor { + var backgroundColor: UIColor { stateContext.backgroundColor } - public var borderColor: UIColor { + var borderColor: UIColor { stateContext.borderColor } @@ -67,11 +67,11 @@ extension RecipientEmailsCellNode { stateContext.textColor } - public var stateImage: UIImage? { + var stateImage: UIImage? { stateContext.image } - public var accessibilityIdentifier: String? { + var accessibilityIdentifier: String? { stateContext.accessibilityIdentifier } @@ -96,9 +96,9 @@ extension RecipientEmailsCellNode { } } - public let email: NSAttributedString - public let type: String - public var state: State + let email: NSAttributedString + let type: String + var state: State public init( email: NSAttributedString, diff --git a/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift index 42bcfccab..adce6a01f 100644 --- a/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift @@ -9,7 +9,7 @@ import AsyncDisplayKit import FlowCryptCommon -final public class RecipientFromCellNode: CellNode { +public final class RecipientFromCellNode: CellNode { private enum Constants { static let sectionInset = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0) static let minimumLineSpacing: CGFloat = 4 @@ -54,7 +54,7 @@ final public class RecipientFromCellNode: CellNode { self.toggleButtonAction = toggleButtonAction } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let insets = UIEdgeInsets.deviceSpecificTextInsets(top: 0, bottom: 0) toggleButtonNode.style.preferredSize = CGSize(width: 40, height: 28) diff --git a/FlowCryptUI/Cell Nodes/SetupTitleNode.swift b/FlowCryptUI/Cell Nodes/SetupTitleNode.swift index 3c45c14ed..575bf83e2 100644 --- a/FlowCryptUI/Cell Nodes/SetupTitleNode.swift +++ b/FlowCryptUI/Cell Nodes/SetupTitleNode.swift @@ -43,7 +43,7 @@ public final class SetupTitleNode: CellNode { backgroundColor = input.backgroundColor } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let layout = ASInsetLayoutSpec( insets: input.insets, child: ASCenterLayoutSpec( @@ -67,7 +67,7 @@ public final class SetupTitleNode: CellNode { } } - public override var isSelected: Bool { + override public var isSelected: Bool { didSet { selectedNode.backgroundColor = isSelected ? input.selectedLineColor : diff --git a/FlowCryptUI/Cell Nodes/SwitchCellNode.swift b/FlowCryptUI/Cell Nodes/SwitchCellNode.swift index 5476636cf..91ae04191 100644 --- a/FlowCryptUI/Cell Nodes/SwitchCellNode.swift +++ b/FlowCryptUI/Cell Nodes/SwitchCellNode.swift @@ -60,7 +60,7 @@ public final class SwitchCellNode: CellNode { onAction(sender.isOn) } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { switchNode.style.preferredSize = CGSize(width: 100, height: 30) return ASStackLayoutSpec( direction: .horizontal, diff --git a/FlowCryptUI/Cell Nodes/TextCellNode.swift b/FlowCryptUI/Cell Nodes/TextCellNode.swift index 4d6d1338f..94557dfd3 100644 --- a/FlowCryptUI/Cell Nodes/TextCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextCellNode.swift @@ -65,7 +65,7 @@ public final class TextCellNode: CellNode { backgroundColor = input.backgroundColor } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let spec = ASStackLayoutSpec( direction: .vertical, spacing: 16, diff --git a/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift b/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift index d6c6d1e72..6f782157d 100644 --- a/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift @@ -88,7 +88,7 @@ public class TextFieldCellNode: CellNode { } } - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let preferredWidth = input.width ?? (constrainedSize.max.width - input.insets.width) textField.style.preferredSize = CGSize( width: max(0, preferredWidth), @@ -98,7 +98,7 @@ public class TextFieldCellNode: CellNode { } @discardableResult - public override func becomeFirstResponder() -> Bool { + override public func becomeFirstResponder() -> Bool { textField.becomeFirstResponder() return true } diff --git a/FlowCryptUI/Cell Nodes/TextViewCellNode.swift b/FlowCryptUI/Cell Nodes/TextViewCellNode.swift index 7579e4c77..a5b82222a 100644 --- a/FlowCryptUI/Cell Nodes/TextViewCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextViewCellNode.swift @@ -71,7 +71,7 @@ public final class TextViewCellNode: CellNode { if shouldAnimate { action?(.heightChanged(textView.textView)) } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { textView.style.preferredSize.height = height return ASInsetLayoutSpec( @@ -81,7 +81,7 @@ public final class TextViewCellNode: CellNode { } @discardableResult - public override func becomeFirstResponder() -> Bool { + override public func becomeFirstResponder() -> Bool { DispatchQueue.main.async { _ = self.textView.textView.becomeFirstResponder() } diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift index 44df1d049..237c9b0f8 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift @@ -289,7 +289,7 @@ public final class ThreadMessageInfoCellNode: CellNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { replyNode.style.preferredSize = CGSize(width: 44, height: 44) menuNode.style.preferredSize = CGSize(width: 36, height: 44) expandNode.style.preferredSize = CGSize(width: 36, height: 44) diff --git a/FlowCryptUI/Cell Nodes/TitleCellNode.swift b/FlowCryptUI/Cell Nodes/TitleCellNode.swift index 953439ddf..4bdd89591 100644 --- a/FlowCryptUI/Cell Nodes/TitleCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TitleCellNode.swift @@ -18,7 +18,7 @@ public final class TitleCellNode: CellNode { textNode.attributedText = title } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: insets, child: textNode diff --git a/FlowCryptUI/Nodes/AttachmentNode.swift b/FlowCryptUI/Nodes/AttachmentNode.swift index 1509964d5..4f83a5211 100644 --- a/FlowCryptUI/Nodes/AttachmentNode.swift +++ b/FlowCryptUI/Nodes/AttachmentNode.swift @@ -68,7 +68,7 @@ public final class AttachmentNode: CellNode { onDeleteTap?() } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let verticalStack = ASStackLayoutSpec.vertical() verticalStack.spacing = 3 verticalStack.style.flexShrink = 1.0 diff --git a/FlowCryptUI/Nodes/BadgeNode.swift b/FlowCryptUI/Nodes/BadgeNode.swift index 70996428b..be0b06b6e 100644 --- a/FlowCryptUI/Nodes/BadgeNode.swift +++ b/FlowCryptUI/Nodes/BadgeNode.swift @@ -50,7 +50,7 @@ public final class BadgeNode: ASDisplayNode { cornerRadius = 4 } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let contentSpec = ASStackLayoutSpec( direction: .horizontal, spacing: 2, diff --git a/FlowCryptUI/Nodes/ButtonNode.swift b/FlowCryptUI/Nodes/ButtonNode.swift index baeec6255..3ffbcab89 100644 --- a/FlowCryptUI/Nodes/ButtonNode.swift +++ b/FlowCryptUI/Nodes/ButtonNode.swift @@ -2,7 +2,7 @@ // ButtonNode.swift // FlowCrypt // -// Created by Anton Kharchevskyi on 29.10.2019. +// Created by Anton Kharchevskyi on 29.10.2019 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // diff --git a/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift b/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift index 43e90966a..3c53048ef 100644 --- a/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift +++ b/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift @@ -30,7 +30,7 @@ public final class ButtonWithPaddingNode: ASDisplayNode { self.cornerRadius = cornerRadius } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: insets, child: buttonNode diff --git a/FlowCryptUI/Nodes/CheckBoxNode.swift b/FlowCryptUI/Nodes/CheckBoxNode.swift index 47822ecb9..920114001 100644 --- a/FlowCryptUI/Nodes/CheckBoxNode.swift +++ b/FlowCryptUI/Nodes/CheckBoxNode.swift @@ -8,7 +8,7 @@ import AsyncDisplayKit -final public class CheckBoxNode: ASDisplayNode { +public final class CheckBoxNode: ASDisplayNode { public struct Input { let color: UIColor let strokeWidth: CGFloat diff --git a/FlowCryptUI/Nodes/KeySettingCellNode.swift b/FlowCryptUI/Nodes/KeySettingCellNode.swift index 6d6bc0ece..68480b7a8 100644 --- a/FlowCryptUI/Nodes/KeySettingCellNode.swift +++ b/FlowCryptUI/Nodes/KeySettingCellNode.swift @@ -45,7 +45,7 @@ public final class KeySettingCellNode: CellNode { separatorNode.backgroundColor = .lightGray } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let nameLocationStack = ASStackLayoutSpec.vertical() nameLocationStack.spacing = 6 nameLocationStack.style.flexShrink = 1.0 diff --git a/FlowCryptUI/Nodes/KeyTextCellNode.swift b/FlowCryptUI/Nodes/KeyTextCellNode.swift index 92ca1e76a..a91bc8547 100644 --- a/FlowCryptUI/Nodes/KeyTextCellNode.swift +++ b/FlowCryptUI/Nodes/KeyTextCellNode.swift @@ -22,7 +22,7 @@ public final class KeyTextCellNode: CellNode { textNode.attributedText = title } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: insets, child: textNode diff --git a/FlowCryptUI/Nodes/LinkButtonNode.swift b/FlowCryptUI/Nodes/LinkButtonNode.swift index 49d976e02..f4073b299 100644 --- a/FlowCryptUI/Nodes/LinkButtonNode.swift +++ b/FlowCryptUI/Nodes/LinkButtonNode.swift @@ -43,7 +43,7 @@ public final class LinkButtonNode: CellNode { tapAction?(identifier) } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { return ASInsetLayoutSpec( insets: UIEdgeInsets(top: 30, left: 16, bottom: 8, right: 18), child: ASCenterLayoutSpec( diff --git a/FlowCryptUI/Nodes/MessageRecipientsNode.swift b/FlowCryptUI/Nodes/MessageRecipientsNode.swift index c16431621..f59d19bd9 100644 --- a/FlowCryptUI/Nodes/MessageRecipientsNode.swift +++ b/FlowCryptUI/Nodes/MessageRecipientsNode.swift @@ -95,7 +95,7 @@ public final class MessageRecipientsNode: ASDisplayNode { return node } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let recipientsNodes: [ASStackLayoutSpec] = RecipientType.allCases.compactMap { type in let recipients: [MessageRecipient] switch type { diff --git a/FlowCryptUI/Nodes/SignInDescriptionNode.swift b/FlowCryptUI/Nodes/SignInDescriptionNode.swift index 69491d641..1d2b71d9d 100644 --- a/FlowCryptUI/Nodes/SignInDescriptionNode.swift +++ b/FlowCryptUI/Nodes/SignInDescriptionNode.swift @@ -19,7 +19,7 @@ public final class SignInDescriptionNode: CellNode { selectionStyle = .none } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { return ASInsetLayoutSpec( insets: UIEdgeInsets(top: 30, left: 16, bottom: 55, right: 16), child: ASCenterLayoutSpec( diff --git a/FlowCryptUI/Nodes/SignInImageNode.swift b/FlowCryptUI/Nodes/SignInImageNode.swift index 33d613691..1bfe9e9e3 100644 --- a/FlowCryptUI/Nodes/SignInImageNode.swift +++ b/FlowCryptUI/Nodes/SignInImageNode.swift @@ -23,7 +23,7 @@ public final class SignInImageNode: CellNode { setNeedsLayout() } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { imageNode.style.preferredSize.height = imageHeight return ASInsetLayoutSpec( diff --git a/FlowCryptUI/Nodes/SigninButtonNode.swift b/FlowCryptUI/Nodes/SigninButtonNode.swift index 04e56fe83..93a52c69c 100644 --- a/FlowCryptUI/Nodes/SigninButtonNode.swift +++ b/FlowCryptUI/Nodes/SigninButtonNode.swift @@ -45,7 +45,7 @@ public final class SigninButtonNode: CellNode { onTap?() } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: .deviceSpecificInsets( top: 16, diff --git a/FlowCryptUI/Nodes/TableNode.swift b/FlowCryptUI/Nodes/TableNode.swift index dd837ec38..7e373e59a 100644 --- a/FlowCryptUI/Nodes/TableNode.swift +++ b/FlowCryptUI/Nodes/TableNode.swift @@ -9,7 +9,7 @@ import AsyncDisplayKit public final class TableNode: ASTableNode { - public override init(style: UITableView.Style) { + override public init(style: UITableView.Style) { super.init(style: style) view.showsVerticalScrollIndicator = false view.separatorStyle = .none @@ -17,7 +17,7 @@ public final class TableNode: ASTableNode { backgroundColor = .backgroundColor } - public var bounces: Bool = true { + public var bounces = true { didSet { DispatchQueue.main.async { self.view.bounces = self.bounces @@ -25,14 +25,14 @@ public final class TableNode: ASTableNode { } } - public override func asyncTraitCollectionDidChange( + override public func asyncTraitCollectionDidChange( withPreviousTraitCollection previousTraitCollection: ASPrimitiveTraitCollection ) { super.asyncTraitCollectionDidChange(withPreviousTraitCollection: previousTraitCollection) backgroundColor = .backgroundColor } - public override func reloadData() { + override public func reloadData() { DispatchQueue.main.async { super.reloadData() } diff --git a/FlowCryptUI/Nodes/TableViewController.swift b/FlowCryptUI/Nodes/TableViewController.swift index d4a20ccb6..6378a5375 100644 --- a/FlowCryptUI/Nodes/TableViewController.swift +++ b/FlowCryptUI/Nodes/TableViewController.swift @@ -11,18 +11,18 @@ import FlowCryptCommon @MainActor open class TableNodeViewController: ASDKViewController { - public override var title: String? { + override public var title: String? { didSet { navigationItem.setAccessibility(id: title) } } - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) node.reloadData() } - open override func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() Logger.nested(Self.self).logDebug("View did load") } diff --git a/FlowCryptUI/Nodes/TextFieldNode.swift b/FlowCryptUI/Nodes/TextFieldNode.swift index 4641aa3bb..52c1259c9 100644 --- a/FlowCryptUI/Nodes/TextFieldNode.swift +++ b/FlowCryptUI/Nodes/TextFieldNode.swift @@ -57,7 +57,7 @@ public final class TextFieldNode: ASDisplayNode { } } - public var isSecureTextEntry: Bool = false { + public var isSecureTextEntry = false { didSet { DispatchQueue.main.async { self.textField.isSecureTextEntry = self.isSecureTextEntry @@ -159,12 +159,12 @@ public final class TextFieldNode: ASDisplayNode { } } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec(insets: .zero, child: node) } @discardableResult - public override func becomeFirstResponder() -> Bool { + override public func becomeFirstResponder() -> Bool { DispatchQueue.main.async { super.becomeFirstResponder() _ = self.textField.becomeFirstResponder() @@ -173,8 +173,8 @@ public final class TextFieldNode: ASDisplayNode { } } -extension TextFieldNode { - public func reset() { +public extension TextFieldNode { + func reset() { (node.view as? TextField)?.text = nil } } diff --git a/FlowCryptUI/Nodes/TextImageNode.swift b/FlowCryptUI/Nodes/TextImageNode.swift index b013d3e9d..ddf51e403 100644 --- a/FlowCryptUI/Nodes/TextImageNode.swift +++ b/FlowCryptUI/Nodes/TextImageNode.swift @@ -63,7 +63,7 @@ public final class TextImageNode: CellNode { onTap?(self) } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { imageNode.style.preferredSize = input.imageSize subTitleNode.style.flexGrow = 1 subTitleNode.style.flexShrink = 1 diff --git a/FlowCryptUI/Nodes/TextWithIconNode.swift b/FlowCryptUI/Nodes/TextWithIconNode.swift index d023a5395..a71b4814d 100644 --- a/FlowCryptUI/Nodes/TextWithIconNode.swift +++ b/FlowCryptUI/Nodes/TextWithIconNode.swift @@ -42,7 +42,7 @@ public final class TextWithIconNode: CellNode { imageNode.image = input.image } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { imageNode.style.preferredSize = input.imageSize return ASInsetLayoutSpec( diff --git a/FlowCryptUI/Nodes/ViewController.swift b/FlowCryptUI/Nodes/ViewController.swift index f71c2bd9a..794b372c9 100644 --- a/FlowCryptUI/Nodes/ViewController.swift +++ b/FlowCryptUI/Nodes/ViewController.swift @@ -11,7 +11,7 @@ import FlowCryptCommon @MainActor open class ViewController: ASDKViewController { - open override func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() Logger.nested(Self.self).logDebug("View did load") } diff --git a/FlowCryptUI/Views/NavigationBarItemsView.swift b/FlowCryptUI/Views/NavigationBarItemsView.swift index e981ba3aa..3f3e29d21 100644 --- a/FlowCryptUI/Views/NavigationBarItemsView.swift +++ b/FlowCryptUI/Views/NavigationBarItemsView.swift @@ -69,7 +69,7 @@ public final class NavigationBarItemsView: UIBarButtonItem { fatalError("init(coder:) has not been implemented") } - public override var isEnabled: Bool { + override public var isEnabled: Bool { didSet { customView?.alpha = isEnabled ? 1 : 0.5 } From b879bd56d334099b73efdf2b603d6e5f2b8dbbf4 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 6 Sep 2022 10:48:40 +0300 Subject: [PATCH 03/56] add missing passphrase button --- .swiftlint.yml | 4 +- FlowCrypt.xcodeproj/project.pbxproj | 8 +- .../ComposeRecipientPopupViewController.swift | 6 +- .../Compose/ComposeViewController.swift | 69 +++++++++--------- .../Compose/ComposeViewDecorator.swift | 20 +++-- ...ComposeViewController+ActionHandling.swift | 29 ++++---- .../ComposeViewController+Attachment.swift | 4 +- .../ComposeViewController+Contacts.swift | 2 +- .../ComposeViewController+Drafts.swift | 16 ++-- .../ComposeViewController+Keyboard.swift | 6 +- .../ComposeViewController+MessageSend.swift | 6 +- .../ComposeViewController+Nodes.swift | 73 +++++++++++-------- ...ComposeViewController+RecipientInput.swift | 22 +++--- ...ComposeViewController+RecipientPopup.swift | 4 +- .../ComposeViewController+Setup.swift | 11 ++- .../ComposeViewController+State.swift | 8 +- .../ComposeViewController+TableView.swift | 10 ++- .../ComposeViewController+TapActions.swift | 8 +- .../Inbox/InboxViewController.swift | 12 +-- .../Encrypted Storage/KeyChainService.swift | 2 +- .../DataManager/SessionService.swift | 17 ----- .../ComposeMessageService.swift | 7 +- .../GoogleUserService+Contacts.swift | 2 +- .../GoogleUserService.swift | 4 +- .../Resources/en.lproj/Localizable.strings | 1 + ...Node.swift => MessageActionCellNode.swift} | 13 ++-- .../Cell Nodes/RecipientFromCellNode.swift | 26 +++---- .../Views/NavigationBarActionButton.swift | 13 +--- 28 files changed, 193 insertions(+), 210 deletions(-) rename FlowCryptUI/Cell Nodes/{MessagePasswordCellNode.swift => MessageActionCellNode.swift} (86%) diff --git a/.swiftlint.yml b/.swiftlint.yml index d7a2c5b84..c625f64a6 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -43,8 +43,8 @@ opt_in_rules: - extension_access_modifier - fallthrough - fatal_error_message - - file_header - - file_name + # - file_header + # - file_name - first_where - flatmap_over_map_reduce - identical_operands diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 6fdcbb65a..3d5950ea8 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -87,7 +87,7 @@ 51114DB6280EB699006C252A /* gmp-interface.m in Sources */ = {isa = PBXBuildFile; fileRef = 51114DB5280EB699006C252A /* gmp-interface.m */; }; 5113D22D28C0C43700A131E0 /* Gmail+MessageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5113D22C28C0C43700A131E0 /* Gmail+MessageExtension.swift */; }; 511737682851F31700337B9F /* GoogleScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511737672851F31700337B9F /* GoogleScope.swift */; }; - 511D07E12769FBBA0050417B /* MessagePasswordCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */; }; + 511D07E12769FBBA0050417B /* MessageActionCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E02769FBBA0050417B /* MessageActionCellNode.swift */; }; 511D07E3276A2DF80050417B /* ButtonWithPaddingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E2276A2DF80050417B /* ButtonWithPaddingNode.swift */; }; 512C1414271077F8002DE13F /* GoogleAPIClientForREST_PeopleService in Frameworks */ = {isa = PBXBuildFile; productRef = 512C1413271077F8002DE13F /* GoogleAPIClientForREST_PeopleService */; }; 5133B6702716320F00C95463 /* ContactKeyDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133B66F2716320F00C95463 /* ContactKeyDetailViewController.swift */; }; @@ -557,7 +557,7 @@ 51114DBA280EBB78006C252A /* gmp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = gmp.h; sourceTree = ""; }; 5113D22C28C0C43700A131E0 /* Gmail+MessageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Gmail+MessageExtension.swift"; sourceTree = ""; }; 511737672851F31700337B9F /* GoogleScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleScope.swift; sourceTree = ""; }; - 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePasswordCellNode.swift; sourceTree = ""; }; + 511D07E02769FBBA0050417B /* MessageActionCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActionCellNode.swift; sourceTree = ""; }; 511D07E2276A2DF80050417B /* ButtonWithPaddingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonWithPaddingNode.swift; sourceTree = ""; }; 5133B66F2716320F00C95463 /* ContactKeyDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyDetailViewController.swift; sourceTree = ""; }; 5133B6712716321F00C95463 /* ContactKeyDetailDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyDetailDecorator.swift; sourceTree = ""; }; @@ -2188,7 +2188,7 @@ 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */, 04CF208127F5ABE100BC4E3D /* ComposeRecipientPopupNameNode.swift */, 9F82779D23737E3800E19C07 /* MessageTextSubjectNode.swift */, - 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */, + 511D07E02769FBBA0050417B /* MessageActionCellNode.swift */, 5180CB96273724E9001FC7EF /* ThreadMessageInfoCellNode.swift */, 9F56BD3123438B5B00A7371A /* InboxCellNode.swift */, 9F56BD3523438B9D00A7371A /* TextCellNode.swift */, @@ -3001,7 +3001,7 @@ D2CDC3D72404704D002B045F /* RecipientEmailsCellNode.swift in Sources */, 5165ABCC27B526D100CCC379 /* RecipientEmailTextFieldNode.swift in Sources */, D2717752242567EB00BDA9A9 /* KeyTextCellNode.swift in Sources */, - 511D07E12769FBBA0050417B /* MessagePasswordCellNode.swift in Sources */, + 511D07E12769FBBA0050417B /* MessageActionCellNode.swift in Sources */, D211CE7B23FC59ED00D1CE38 /* InfoCellNode.swift in Sources */, D2E26F7224F26FFF00612AF1 /* ContactUserCellNode.swift in Sources */, D211CE6F23FC358000D1CE38 /* ButtonNode.swift in Sources */, diff --git a/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift b/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift index ef4fd6190..82589fcc0 100644 --- a/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift @@ -25,12 +25,12 @@ final class ComposeRecipientPopupViewController: TableNodeViewController { case nameEmail, divider, copy, edit, remove } - var parts: [Parts] { - return [.nameEmail, .divider, .copy, .edit, .divider, .remove] + private var parts: [Parts] { + [.nameEmail, .divider, .copy, .edit, .divider, .remove] } private let recipient: ComposeMessageRecipient - internal let type: RecipientType + let type: RecipientType var delegate: ComposeRecipientPopupViewControllerProtocol? init( diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index ad8e5a405..59b59ef35 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -15,22 +15,22 @@ import Foundation **/ final class ComposeViewController: TableNodeViewController { - internal enum Constants { + enum Constants { static let endTypingCharacters = [",", "\n", ";"] static let minRecipientsPartHeight: CGFloat = 32 } - internal struct ComposedDraft: Equatable { + struct ComposedDraft: Equatable { let input: ComposeMessageInput let contextToSend: ComposeMessageContext } - internal enum State { + enum State { case main, searchEmails([Recipient]) } enum Section: Hashable { - case recipientsLabel, recipients(RecipientType), password, compose, attachments, searchResults, contacts + case passphrase, recipientsLabel, recipients(RecipientType), password, compose, attachments, searchResults, contacts static var recipientsSections: [Section] { RecipientType.allCases.map { Self.recipients($0) } @@ -41,55 +41,54 @@ final class ComposeViewController: TableNodeViewController { case delete, reload, add, scrollToBottom } - internal enum ComposePart: Int, CaseIterable { + enum ComposePart: Int, CaseIterable { case topDivider, subject, subjectDivider, text } - internal var shouldDisplaySearchResult = false - internal var userTappedOutSideRecipientsArea = false - internal var shouldShowEmailRecipientsLabel = false - internal let appContext: AppContextWithUser - internal let composeMessageService: ComposeMessageService - internal var decorator: ComposeViewDecorator - internal let localContactsProvider: LocalContactsProviderType - internal let pubLookup: PubLookupType - internal let googleUserService: GoogleUserServiceType - internal let filesManager: FilesManagerType - internal let photosManager: PhotosManagerType - internal let router: GlobalRouterType + var shouldDisplaySearchResult = false + var userTappedOutSideRecipientsArea = false + var shouldShowEmailRecipientsLabel = false + let appContext: AppContextWithUser + let composeMessageService: ComposeMessageService + var decorator: ComposeViewDecorator + let localContactsProvider: LocalContactsProviderType + let pubLookup: PubLookupType + let googleUserService: GoogleUserServiceType + let filesManager: FilesManagerType + let photosManager: PhotosManagerType + let router: GlobalRouterType private let clientConfiguration: ClientConfiguration - internal var isMessagePasswordSupported: Bool { - return clientConfiguration.isUsingFes - } + var isMessagePasswordSupported: Bool { clientConfiguration.isUsingFes } + + let search = PassthroughSubject() + var cancellable = Set() - internal let search = PassthroughSubject() - internal var cancellable = Set() + var input: ComposeMessageInput + var contextToSend: ComposeMessageContext - internal var input: ComposeMessageInput - internal var contextToSend: ComposeMessageContext + var state: State = .main + var shouldEvaluateRecipientInput = true - internal var state: State = .main - internal var shouldEvaluateRecipientInput = true + weak var saveDraftTimer: Timer? + var composedLatestDraft: ComposedDraft? - internal weak var saveDraftTimer: Timer? - internal var composedLatestDraft: ComposedDraft? + var signingKeyWithMissingPassphrase: Keypair? + var messagePasswordAlertController: UIAlertController? + lazy var alertsFactory = AlertsFactory() - internal lazy var alertsFactory = AlertsFactory() - internal var messagePasswordAlertController: UIAlertController? private var didLayoutSubviews = false private var topContentInset: CGFloat { navigationController?.navigationBar.frame.maxY ?? 0 } - internal var selectedRecipientType: RecipientType? = .to - internal var shouldShowAllRecipientTypes = false - internal var popoverVC: ComposeRecipientPopupViewController! + var selectedRecipientType: RecipientType? = .to + var shouldShowAllRecipientTypes = false + var popoverVC: ComposeRecipientPopupViewController! - internal var sectionsList: [Section] = [] + var sectionsList: [Section] = [] var composeTextNode: ASCellNode! var composeSubjectNode: ASCellNode! - var fromCellNode: RecipientFromCellNode! var sendAsList: [SendAsModel] = [] init( diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 0eb89cb51..45ebfd638 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -127,16 +127,24 @@ struct ComposeViewDecorator { return (text + message).attributed(.regular(17)) } - func styledEmptyMessagePasswordInput() -> MessagePasswordCellNode.Input { - messagePasswordInput( + func styledMessagePassPhraseInput() -> MessageActionCellNode.Input { + messageActionInput( + text: "compose_passphrase_placeholder".localized, + color: .warningColor, + imageName: "lock" + ) + } + + func styledEmptyMessagePasswordInput() -> MessageActionCellNode.Input { + messageActionInput( text: "compose_password_placeholder".localized, color: .warningColor, imageName: "lock" ) } - func styledFilledMessagePasswordInput() -> MessagePasswordCellNode.Input { - messagePasswordInput( + func styledFilledMessagePasswordInput() -> MessageActionCellNode.Input { + messageActionInput( text: "compose_password_set_message".localized, color: .main, imageName: "checkmark.circle" @@ -180,11 +188,11 @@ struct ComposeViewDecorator { completion?() } - private func messagePasswordInput( + private func messageActionInput( text: String, color: UIColor, imageName: String - ) -> MessagePasswordCellNode.Input { + ) -> MessageActionCellNode.Input { .init( text: text.attributed(.regular(14), color: color), color: color, diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift index 58b7c4a6b..b7a0d4343 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift @@ -12,24 +12,21 @@ import UIKit // MARK: - Action Handling extension ComposeViewController { - internal func searchEmail(with query: String) { + func searchEmail(with query: String) { Task { do { let cloudRecipients = try await googleUserService.searchContacts(query: query) let localRecipients = try localContactsProvider.searchRecipients(query: query) - let recipients = (cloudRecipients + localRecipients) - .unique() - .sorted() - + let recipients = (cloudRecipients + localRecipients).unique().sorted() updateView(newState: .searchEmails(recipients)) } catch { - showAlert(message: error.localizedDescription) + showAlert(message: error.errorMessage) } } } - internal func evaluate(recipient: ComposeMessageRecipient) { + func evaluate(recipient: ComposeMessageRecipient) { guard recipient.email.isValidEmail else { updateRecipient( email: recipient.email, @@ -62,7 +59,7 @@ extension ComposeViewController { } } - internal func handleEvaluation(for recipient: RecipientWithSortedPubKeys) { + func handleEvaluation(for recipient: RecipientWithSortedPubKeys) { let state = getRecipientState(from: recipient) updateRecipient( @@ -73,7 +70,7 @@ extension ComposeViewController { ) } - internal func getRecipientState(from recipient: RecipientWithSortedPubKeys) -> RecipientState { + func getRecipientState(from recipient: RecipientWithSortedPubKeys) -> RecipientState { switch recipient.keyState { case .active: return decorator.recipientKeyFoundState @@ -86,7 +83,7 @@ extension ComposeViewController { } } - internal func handleEvaluation(error: Error, with email: String, contact: RecipientWithSortedPubKeys?) { + func handleEvaluation(error: Error, with email: String, contact: RecipientWithSortedPubKeys?) { let recipientState: RecipientState = { if let contact = contact, contact.keyState == .active { return getRecipientState(from: contact) @@ -106,7 +103,7 @@ extension ComposeViewController { ) } - internal func updateRecipient( + func updateRecipient( email: String, name: String? = nil, state: RecipientState, @@ -138,7 +135,7 @@ extension ComposeViewController { } } - internal func handleRecipientSelection(with indexPath: IndexPath, type: RecipientType) { + func handleRecipientSelection(with indexPath: IndexPath, type: RecipientType) { guard let recipient = contextToSend.recipient(at: indexPath.row, type: type) else { return } let isSelected = recipient.state.isSelected @@ -158,7 +155,7 @@ extension ComposeViewController { textField?.reset() } - internal func handleRecipientAction(with indexPath: IndexPath, type: RecipientType) { + func handleRecipientAction(with indexPath: IndexPath, type: RecipientType) { guard let recipient = contextToSend.recipient(at: indexPath.row, type: type) else { return } switch recipient.state { @@ -185,15 +182,15 @@ extension ComposeViewController { } // MARK: - Message password - internal func setMessagePassword() { + func setMessagePassword() { Task { contextToSend.messagePassword = await enterMessagePassword() reload(sections: [.password]) } } - internal func enterMessagePassword() async -> String? { - return await withCheckedContinuation { (continuation: CheckedContinuation) in + func enterMessagePassword() async -> String? { + return await withCheckedContinuation { continuation in self.messagePasswordAlertController = createMessagePasswordAlert(continuation: continuation) self.present(self.messagePasswordAlertController!, animated: true, completion: nil) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift index c3e2f6d91..eacc3b7ed 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift @@ -10,7 +10,7 @@ import UIKit // MARK: - Attachments sheet handling extension ComposeViewController { - internal func openAttachmentsInputSourcesSheet() { + func openAttachmentsInputSourcesSheet() { let alert = UIAlertController( title: "files_picking_select_input_source_title".localized, message: nil, @@ -42,7 +42,7 @@ extension ComposeViewController { present(alert, animated: true, completion: nil) } - internal func takePhoto() { + func takePhoto() { Task { do { try await photosManager.takePhoto(from: self) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift index 7ef73687b..9c41fb42b 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift @@ -9,7 +9,7 @@ import UIKit extension ComposeViewController { - internal func askForContactsPermission() { + func askForContactsPermission() { shouldEvaluateRecipientInput = false Task { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index ef6329579..714a1dd60 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -8,14 +8,14 @@ // MARK: - Drafts extension ComposeViewController { - @objc internal func startDraftTimer() { + @objc func startDraftTimer() { saveDraftTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in self?.saveDraftIfNeeded() } saveDraftTimer?.fire() } - @objc internal func stopDraftTimer() { + @objc func stopDraftTimer() { saveDraftTimer?.invalidate() saveDraftTimer = nil saveDraftIfNeeded() @@ -37,11 +37,9 @@ extension ComposeViewController { } } - internal func saveDraftIfNeeded() { + func saveDraftIfNeeded() { guard shouldSaveDraft() else { return } - print("saving draft") - Task { do { let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg( @@ -51,12 +49,10 @@ extension ComposeViewController { ) try await composeMessageService.encryptAndSaveDraft(message: sendableMsg, threadId: input.threadId) } catch { - print("got error") - print(error) if case .promptUserToEnterPassPhraseForSigningKey(let keyPair) = error as? ComposeMessageError { - requestMissingPassPhraseWithModal(for: keyPair, isDraft: true) - } - if !(error is MessageValidationError) { + signingKeyWithMissingPassphrase = keyPair + reload(sections: [.passphrase]) + } else if !(error is MessageValidationError) { // no need to save or notify user if validation error // for other errors show toast // todo - should make sure that the toast doesn't hide the keyboard. Also should be toasted on top when keyboard open? diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift index 837a0839c..3c37547c3 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift @@ -11,7 +11,7 @@ import FlowCryptUI // MARK: - Keyboard extension ComposeViewController { - internal func observeKeyboardNotifications() { + func observeKeyboardNotifications() { // swiftlint:disable discarded_notification_center_observer NotificationCenter.default.addObserver( forName: UIResponder.keyboardWillShowNotification, @@ -31,7 +31,7 @@ extension ComposeViewController { } } - internal func observerAppStates() { + func observerAppStates() { NotificationCenter.default.addObserver( self, selector: #selector(startDraftTimer), @@ -45,7 +45,7 @@ extension ComposeViewController { object: nil) } - internal func adjustForKeyboard(height: CGFloat) { + func adjustForKeyboard(height: CGFloat) { node.contentInset.bottom = height + 8 guard let textView = node.visibleNodes.compactMap({ $0 as? TextViewCellNode }).first?.textView.textView, diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index bea83fb36..ae7e7ee06 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -12,7 +12,7 @@ import FlowCryptUI // MARK: - Message Sending extension ComposeViewController { - internal func sendMessage() async throws { + func sendMessage() async throws { view.endEditing(true) navigationItem.rightBarButtonItem?.isEnabled = false @@ -40,7 +40,7 @@ extension ComposeViewController { handleSuccessfullySentMessage() } - internal func requestMissingPassPhraseWithModal(for signingKey: Keypair, isDraft: Bool = false) { + func requestMissingPassPhraseWithModal(for signingKey: Keypair, isDraft: Bool = false) { let alert = alertsFactory.makePassPhraseAlert( onCancel: { self.handle(error: ComposeMessageError.passPhraseRequired) @@ -73,7 +73,7 @@ extension ComposeViewController { present(alert, animated: true, completion: nil) } - internal func handle(error: Error) { + func handle(error: Error) { reEnableSendButton() if case .promptUserToEnterPassPhraseForSigningKey(let keyPair) = error as? ComposeMessageError { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index d8d7e7569..c2c46dd37 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -12,7 +12,7 @@ import FlowCryptUI // MARK: - Nodes extension ComposeViewController { - internal func recipientTextNode() -> ComposeRecipientCellNode { + func recipientTextNode() -> ComposeRecipientCellNode { let recipients = contextToSend.recipients.map(RecipientEmailsCellNode.Input.init) let textNode = ComposeRecipientCellNode( input: ComposeRecipientCellNode.Input(recipients: recipients), @@ -25,7 +25,7 @@ extension ComposeViewController { return textNode } - internal func showRecipientLabelIfNecessary() { + func showRecipientLabelIfNecessary() { let isRecipientLoading = self.contextToSend.recipients.filter { $0.state == decorator.recipientIdleState }.isNotEmpty guard !isRecipientLoading, self.contextToSend.recipients.isNotEmpty, @@ -39,12 +39,12 @@ extension ComposeViewController { } } - internal func hideRecipientLabel() { + func hideRecipientLabel() { self.shouldShowEmailRecipientsLabel = false self.reload(sections: [.recipientsLabel, .recipients(.from), .recipients(.to), .recipients(.cc), .recipients(.bcc)]) } - internal func setupSubjectNode() { + func setupSubjectNode() { composeSubjectNode = TextFieldCellNode( input: decorator.styledTextFieldInput( with: "compose_subject".localized, @@ -75,13 +75,13 @@ extension ComposeViewController { } } - internal func setupFromNode() { - fromCellNode = RecipientFromCellNode( - toggleButtonAction: { - self.presentSendAsActionSheet() + func fromCellNode() -> RecipientFromCellNode { + RecipientFromCellNode( + fromEmail: contextToSend.sender, + toggleButtonAction: { [weak self] in + self?.presentSendAsActionSheet() } ) - fromCellNode.fromEmail = contextToSend.sender } private func presentSendAsActionSheet() { @@ -102,10 +102,10 @@ extension ComposeViewController { self?.changeSendAs(to: aliasEmail.sendAsEmail) } // Remove @, . in email part as appium throws error for identifiers which contain @, . - let emailIentifier = aliasEmail.sendAsEmail + let emailIdentifier = aliasEmail.sendAsEmail .replacingOccurrences(of: "@", with: "-") .replacingOccurrences(of: ".", with: "-") - action.accessibilityIdentifier = "aid-send-as-\(emailIentifier)" + action.accessibilityIdentifier = "aid-send-as-\(emailIdentifier)" alert.addAction(action) } alert.addAction(cancelAction) @@ -114,26 +114,35 @@ extension ComposeViewController { } private func changeSendAs(to email: String) { - guard let section = sectionsList.firstIndex(of: .recipients(.from)), - let fromCell = node.nodeForRow(at: IndexPath(row: 0, section: section)) as? RecipientFromCellNode else { - return - } - fromCell.fromEmail = email contextToSend.sender = email + reload(sections: [.recipients(.from)]) + } + + func messagePassPhraseNode() -> ASCellNode { + MessageActionCellNode( + input: decorator.styledMessagePassPhraseInput(), + action: { [weak self] in + guard let self = self, + let keyPair = self.signingKeyWithMissingPassphrase + else { return } + + self.requestMissingPassPhraseWithModal(for: keyPair, isDraft: true) + } + ) } - internal func messagePasswordNode() -> ASCellNode { + func messagePasswordNode() -> ASCellNode { let input = contextToSend.hasMessagePassword ? decorator.styledFilledMessagePasswordInput() : decorator.styledEmptyMessagePasswordInput() - return MessagePasswordCellNode( + return MessageActionCellNode( input: input, - setMessagePassword: { [weak self] in self?.setMessagePassword() } + action: { [weak self] in self?.setMessagePassword() } ) } - internal func setupTextNode() { + func setupTextNode() { let styledQuote = decorator.styledQuote(with: input) let height = max(decorator.frame(for: styledQuote).height, 40) composeTextNode = TextViewCellNode( @@ -178,7 +187,7 @@ extension ComposeViewController { } } - internal func ensureCursorVisible(textView: UITextView) { + func ensureCursorVisible(textView: UITextView) { guard let range = textView.selectedTextRange else { return } let cursorRect = textView.caretRect(for: range.start) @@ -192,7 +201,7 @@ extension ComposeViewController { } } - internal func recipientsNode(type: RecipientType) -> ASCellNode { + func recipientsNode(type: RecipientType) -> ASCellNode { let recipients = contextToSend.recipients(type: type) let shouldShowToggleButton = type == .to @@ -222,7 +231,7 @@ extension ComposeViewController { } ) } - .onItemSelect { [weak self] (action: RecipientEmailsCellNode.RecipientEmailTapAction) in + .onItemSelect { [weak self] action in switch action { case let .imageTap(indexPath): self?.handleRecipientAction(with: indexPath, type: type) @@ -233,32 +242,32 @@ extension ComposeViewController { } } - internal func recipientInput(type: RecipientType) -> RecipientEmailTextFieldNode { + func recipientInput(type: RecipientType) -> RecipientEmailTextFieldNode { return RecipientEmailTextFieldNode( input: decorator.styledTextFieldInput( with: "", keyboardType: .emailAddress, accessibilityIdentifier: "aid-recipients-text-field-\(type.rawValue)", - insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + insets: .zero ), action: { [weak self] action in self?.handle(textFieldAction: action, for: type) } ) - .onShouldReturn { [weak self] textField -> (Bool) in + .onShouldReturn { [weak self] textField in if let isValid = self?.showAlertIfTextFieldNotValidEmail(textField: textField), isValid { textField.resignFirstResponder() return true } return false } - .onShouldEndEditing { [weak self] textField -> (Bool) in + .onShouldEndEditing { [weak self] textField in if let isValid = self?.showAlertIfTextFieldNotValidEmail(textField: textField), isValid { return true } return false } - .onShouldChangeCharacters { [weak self] textField, character -> (Bool) in + .onShouldChangeCharacters { [weak self] textField, character in self?.shouldChange(with: textField, and: character, for: type) ?? true } .then { @@ -268,7 +277,7 @@ extension ComposeViewController { } } - internal func showAlertIfTextFieldNotValidEmail(textField: UITextField) -> Bool { + func showAlertIfTextFieldNotValidEmail(textField: UITextField) -> Bool { if let text = textField.text, text.isEmpty || text.isValidEmail { return true } @@ -276,7 +285,7 @@ extension ComposeViewController { return false } - internal func attachmentNode(for index: Int) -> ASCellNode { + func attachmentNode(for index: Int) -> ASCellNode { AttachmentNode( input: .init( attachment: contextToSend.attachments[index], @@ -289,7 +298,7 @@ extension ComposeViewController { ) } - internal func noSearchResultsNode() -> ASCellNode { + func noSearchResultsNode() -> ASCellNode { TextCellNode(input: .init( backgroundColor: .clear, title: "compose_no_contacts_found".localized, @@ -300,7 +309,7 @@ extension ComposeViewController { ) } - internal func enableGoogleContactsNode() -> ASCellNode { + func enableGoogleContactsNode() -> ASCellNode { TextWithIconNode(input: .init( title: "compose_enable_google_contacts_search" .localized diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift index 215353994..45ea70150 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift @@ -11,7 +11,7 @@ import FlowCryptUI // MARK: - Recipients Input extension ComposeViewController { - internal func shouldChange(with textField: UITextField, and character: String, for recipientType: RecipientType) -> Bool { + func shouldChange(with textField: UITextField, and character: String, for recipientType: RecipientType) -> Bool { func nextResponder() { guard let node = node.visibleNodes[safe: ComposePart.subject.rawValue] as? TextFieldCellNode else { return } node.becomeFirstResponder() @@ -38,7 +38,7 @@ extension ComposeViewController { return true } - internal func handle(textFieldAction: TextFieldActionType, for recipientType: RecipientType) { + func handle(textFieldAction: TextFieldActionType, for recipientType: RecipientType) { switch textFieldAction { case let .deleteBackward(textField): handleBackspaceAction(with: textField, for: recipientType) case let .didEndEditing(text): handleEndEditingAction(with: text, for: recipientType) @@ -48,7 +48,7 @@ extension ComposeViewController { } } - internal func handleEndEditingAction(with email: String?, name: String? = nil, for recipientType: RecipientType) { + func handleEndEditingAction(with email: String?, name: String? = nil, for recipientType: RecipientType) { guard shouldEvaluateRecipientInput, let email = email, email.isNotEmpty else { return } @@ -101,7 +101,7 @@ extension ComposeViewController { /// - Parameter type: Recipient type. /// - Parameter refreshType: Refresh type (delete/add/reload/scrollToBottom). /// - Parameter TempRecipients: Temp recipients (Optional). Used to get deleted recipient index - internal func refreshRecipient( + func refreshRecipient( for email: String, type: RecipientType, refreshType: RefreshType, @@ -134,18 +134,18 @@ extension ComposeViewController { } } - internal func recipientsIndexPath(type: RecipientType) -> IndexPath? { + func recipientsIndexPath(type: RecipientType) -> IndexPath? { guard let section = sectionsList.firstIndex(of: .recipients(type)) else { return nil } return IndexPath(row: 0, section: section) } - internal func recipientsTextField(type: RecipientType) -> TextFieldNode? { + func recipientsTextField(type: RecipientType) -> TextFieldNode? { guard let indexPath = recipientsIndexPath(type: type) else { return nil } return (node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode)?.recipientInput.textField } - internal func handleBackspaceAction(with textField: UITextField, for recipientType: RecipientType) { - guard textField.text != "" else { return } + func handleBackspaceAction(with textField: UITextField, for recipientType: RecipientType) { + guard let text = textField.text, !text.isEmpty else { return } var recipients = contextToSend.recipients(type: recipientType) @@ -176,18 +176,18 @@ extension ComposeViewController { } } - internal func handleEditingChanged(with text: String?) { + func handleEditingChanged(with text: String?) { let inputText = text ?? "" shouldDisplaySearchResult = !inputText.isEmpty search.send(inputText) } - internal func handleDidBeginEditing(recipientType: RecipientType) { + func handleDidBeginEditing(recipientType: RecipientType) { selectedRecipientType = recipientType node.view.keyboardDismissMode = .none } - internal func toggleRecipientsList() { + func toggleRecipientsList() { shouldShowAllRecipientTypes.toggle() reload(sections: [.recipients(.cc), .recipients(.bcc)]) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift index ff3ce24f1..29008fb60 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift @@ -10,7 +10,7 @@ import FlowCryptUI import UIKit extension ComposeViewController { - internal func displayRecipientPopOver(with indexPath: IndexPath, type: RecipientType, sender: CellNode) { + func displayRecipientPopOver(with indexPath: IndexPath, type: RecipientType, sender: CellNode) { guard let recipient = contextToSend.recipient(at: indexPath.row, type: type) else { return } popoverVC = ComposeRecipientPopupViewController( @@ -25,7 +25,7 @@ extension ComposeViewController { self.present(popoverVC, animated: true, completion: nil) } - internal func hideRecipientPopOver() { + func hideRecipientPopOver() { if popoverVC != nil { popoverVC.dismiss(animated: true, completion: nil) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index ce105f85c..d731e0b82 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -11,7 +11,7 @@ import FlowCryptUI // MARK: - Setup UI extension ComposeViewController { - internal func setupNavigationBar() { + func setupNavigationBar() { navigationItem.rightBarButtonItem = NavigationBarItemsView( with: [ NavigationBarItemsView.Input( @@ -34,7 +34,7 @@ extension ComposeViewController { ) } - internal func setupUI() { + func setupUI() { let tap = UITapGestureRecognizer(target: self, action: #selector(handleTableTap)) node.do { @@ -49,7 +49,7 @@ extension ComposeViewController { updateView(newState: .main) } - internal func setupQuote() { + func setupQuote() { guard input.isQuote else { return } for recipient in input.quoteRecipients { @@ -65,16 +65,15 @@ extension ComposeViewController { } } - internal func setupNodes() { + func setupNodes() { setupTextNode() setupSubjectNode() - setupFromNode() } } // MARK: - Search extension ComposeViewController { - internal func setupSearch() { + func setupSearch() { search .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift index ae4eb7a72..93bc41eb3 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift @@ -10,8 +10,8 @@ import UIKit // MARK: - State Handling extension ComposeViewController { - internal func updateView(newState: State) { - if case .searchEmails = newState, !self.shouldDisplaySearchResult { + func updateView(newState: State) { + if case .searchEmails = newState, !shouldDisplaySearchResult { return } @@ -20,11 +20,11 @@ extension ComposeViewController { switch state { case .main: - sectionsList = Section.recipientsSections + [.recipientsLabel, .password, .compose, .attachments] + sectionsList = [.passphrase] + Section.recipientsSections + [.recipientsLabel, .password, .compose, .attachments] node.reloadData() case .searchEmails: let previousSectionsCount = sectionsList.count - sectionsList = Section.recipientsSections + [.searchResults, .contacts] + sectionsList = [.passphrase] + Section.recipientsSections + [.searchResults, .contacts] let deletedSectionsCount = previousSectionsCount - sectionsList.count diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift index 9de6d5dd1..d089d91f6 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift @@ -20,6 +20,8 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { guard let sectionItem = sectionsList[safe: section] else { return 0 } switch (state, sectionItem) { + case (.main, .passphrase): + return signingKeyWithMissingPassphrase != nil ? 1 : 0 case (.main, .recipientsLabel): return shouldShowEmailRecipientsLabel ? 1 : 0 case (.main, .recipients(.to)): @@ -54,12 +56,14 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { switch (self.state, section) { case (_, .recipients(.from)): - return self.fromCellNode + return self.fromCellNode() case (_, .recipients(.to)), (_, .recipients(.cc)), (_, .recipients(.bcc)): - let recipientType = RecipientType.allCases[indexPath.section] + let recipientType = RecipientType.allCases[indexPath.section - 1] return self.recipientsNode(type: recipientType) case (.main, .recipientsLabel): return self.recipientTextNode() + case (.main, .passphrase): + return self.messagePassPhraseNode() case (.main, .password): return self.messagePasswordNode() case (.main, .compose): @@ -119,7 +123,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { } } - internal func reload(sections: [Section]) { + func reload(sections: [Section]) { let indexes = sectionsList.enumerated().compactMap { index, section in sections.contains(section) ? index : nil } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index 9ee5db2b6..252831f6f 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -8,15 +8,15 @@ // MARK: - Handle actions extension ComposeViewController { - internal func handleInfoTap() { + func handleInfoTap() { showToast("Please email us at human@flowcrypt.com for help") } - internal func handleAttachTap() { + func handleAttachTap() { openAttachmentsInputSourcesSheet() } - internal func handleSendTap() { + func handleSendTap() { Task { do { guard contextToSend.hasMessagePasswordIfNeeded else { @@ -30,7 +30,7 @@ extension ComposeViewController { } } - @objc internal func handleTableTap() { + @objc func handleTableTap() { if case .searchEmails = state, let selectedRecipientType = selectedRecipientType, let textField = recipientsTextField(type: selectedRecipientType), diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index b2679fa28..1e8794e0f 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -18,7 +18,7 @@ class InboxViewController: ViewController { private let draftsListProvider: DraftsListProvider? private let messageOperationsProvider: MessageOperationsProvider private let refreshControl = UIRefreshControl() - internal let tableNode: ASTableNode + let tableNode: ASTableNode private lazy var composeButton = ComposeButtonNode { [weak self] in self?.btnComposeTap() } @@ -26,7 +26,7 @@ class InboxViewController: ViewController { private let inboxDataProvider: InboxDataProvider private let viewModel: InboxViewModel private var inboxInput: [InboxRenderable] = [] - internal var state: InboxViewController.State = .idle + var state: InboxViewController.State = .idle private var inboxTitle: String { viewModel.folderName.isEmpty ? "Inbox" : viewModel.folderName } @@ -34,12 +34,12 @@ class InboxViewController: ViewController { inboxInput.isNotEmpty && (viewModel.path == "SPAM" || viewModel.path == "TRASH") } - internal var path: String { viewModel.path } + var path: String { viewModel.path } // Search related varaibles private var isSearch = false private var shouldBeginFetch = true - internal var searchedExpression = "" + var searchedExpression = "" private var isVisible = false private var didLayoutSubviews = false @@ -124,7 +124,7 @@ extension InboxViewController { refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) } - internal func setupTableNode() { + func setupTableNode() { tableNode.do { $0.delegate = self $0.dataSource = self @@ -231,7 +231,7 @@ extension InboxViewController { return "\(searchedExpression) OR subject:\(searchedExpression)" } - internal func fetchAndRenderEmailsOnly(_ batchContext: ASBatchContext?) { + func fetchAndRenderEmailsOnly(_ batchContext: ASBatchContext?) { Task { do { if isSearch { diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift index c2deac4f6..1cc4e9ba2 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift @@ -3,7 +3,7 @@ // FlowCrypt // // Created by Anton Kharchevskyi on 25.11.2019 -// Copyright © 2017-present FlowCrypt a.s. All rights reserved. +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. // import FlowCryptCommon diff --git a/FlowCrypt/Functionality/DataManager/SessionService.swift b/FlowCrypt/Functionality/DataManager/SessionService.swift index cee9e88b8..7b6ef92c5 100644 --- a/FlowCrypt/Functionality/DataManager/SessionService.swift +++ b/FlowCrypt/Functionality/DataManager/SessionService.swift @@ -36,7 +36,6 @@ protocol SessionServiceType { func startSessionFor(session: SessionType) throws func switchActiveSessionFor(user: User) throws -> SessionType? func startActiveSessionForNextUser() throws -> SessionType? - func logOutUsersThatDontHaveAnyKeysSetUp() throws func cleanup() throws } @@ -112,22 +111,6 @@ extension SessionService: SessionServiceType { return try switchActiveSession(for: currentUser) } - func logOutUsersThatDontHaveAnyKeysSetUp() throws { - logger.logInfo("Clean up sessions") - for user in try encryptedStorage.getAllUsers() { - if try !encryptedStorage.doesAnyKeypairExist(for: user.email) { - logger.logInfo("User session to clean up \(user.email)") - try logOut(user: user) - } - } - - let users = try encryptedStorage.getAllUsers() - if !users.contains(where: { $0.isActive }), - let user = try users.first(where: { try encryptedStorage.doesAnyKeypairExist(for: $0.email) }) { - try switchActiveSession(for: user) - } - } - @discardableResult private func switchActiveSession(for user: User) throws -> SessionType? { logger.logInfo("Try to switch session for \(user.email)") diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 4cf424f70..5f01e2517 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -50,7 +50,6 @@ final class ComposeMessageService { self.draftGateway = draftGateway self.core = core self.localContactsProvider = localContactsProvider ?? LocalContactsProvider(encryptedStorage: appContext.encryptedStorage) - self.logger = Logger.nested(in: Self.self, with: "ComposeMessageService") } private var onStateChanged: ((State) -> Void)? @@ -158,8 +157,6 @@ final class ComposeMessageService { let signingPrv = try await prepareSigningKey(senderEmail: contextToSend.sender) - print("create sendable") - return SendableMsg( text: contextToSend.message ?? "", html: nil, @@ -230,7 +227,9 @@ final class ComposeMessageService { input: MessageGatewayInput( mime: r.mimeEncoded, threadId: threadId - ), draft: draft) + ), + draft: draft + ) } catch { throw ComposeMessageError.gatewayError(error) } diff --git a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService+Contacts.swift b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService+Contacts.swift index 51a0e84c7..0ee635578 100644 --- a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService+Contacts.swift +++ b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService+Contacts.swift @@ -64,7 +64,7 @@ extension GoogleUserService { return contactsScope.allSatisfy(currentScope.contains) } - internal func runWarmupQuery() { + func runWarmupQuery() { Task { // Warmup query for google contacts cache _ = await searchContacts(query: "") diff --git a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift index 50c9ca3d3..0797f7070 100644 --- a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift @@ -105,7 +105,7 @@ final class GoogleUserService: NSObject, GoogleUserServiceType { static let index = "GTMAppAuthAuthorizerIndex" } - internal lazy var logger = Logger.nested(in: Self.self, with: .userAppStart) + lazy var logger = Logger.nested(in: Self.self, with: .userAppStart) private var tokenResponse: OIDTokenResponse? { authorization?.authState.lastTokenResponse @@ -326,7 +326,7 @@ extension GoogleUserService { .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") - while decodedString.utf16.count.isMultiple(of: 4) { + while !decodedString.utf16.count.isMultiple(of: 4) { decodedString += "=" } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 748da7f17..5dd1bfc99 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -123,6 +123,7 @@ "compose_sign_passphrase_required" = "Passphrase is required for message signing."; "compose_sign_passphrase_no_match" = "This pass phrase did not match your signing private key."; "compose_sign_no_keys" = "Cannot sign message: none of your %@ account keys can be used for sending address %@"; +"compose_passphrase_placeholder" = "Draft not saved - tap to add pass phrase"; "compose_password_placeholder" = "Tap to add password for recipients who don't have encryption set up."; "compose_password_set_message" = "Web portal password added"; "compose_password_modal_title" = "Set web portal password"; diff --git a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift b/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift similarity index 86% rename from FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift rename to FlowCryptUI/Cell Nodes/MessageActionCellNode.swift index 94a317f92..eb9dde190 100644 --- a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift @@ -1,5 +1,5 @@ // -// MessagePasswordCellNode.swift +// MessageActionCellNode.swift // FlowCryptUI // // Created by Roma Sosnovsky on 15/12/21 @@ -9,7 +9,7 @@ import AsyncDisplayKit import UIKit -public final class MessagePasswordCellNode: CellNode { +public final class MessageActionCellNode: CellNode { public struct Input { let text: NSAttributedString? let color: UIColor @@ -27,12 +27,11 @@ public final class MessagePasswordCellNode: CellNode { private let input: Input private let buttonNode = ASButtonNode() - private let setMessagePassword: (() -> Void)? + private let action: (() -> Void)? - public init(input: Input, - setMessagePassword: (() -> Void)?) { + public init(input: Input, action: (() -> Void)?) { self.input = input - self.setMessagePassword = setMessagePassword + self.action = action super.init() @@ -75,6 +74,6 @@ public final class MessagePasswordCellNode: CellNode { } @objc private func onButtonTap() { - setMessagePassword?() + action?() } } diff --git a/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift index adce6a01f..651d20726 100644 --- a/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift @@ -15,7 +15,7 @@ public final class RecipientFromCellNode: CellNode { static let minimumLineSpacing: CGFloat = 4 } - lazy var labelTextNode: ASTextNode2 = { + private lazy var labelTextNode: ASTextNode2 = { let textNode = ASTextNode2() let textTitle = "compose_recipient_from".localized textNode.attributedText = textTitle.attributed(.regular(17), color: .lightGray, alignment: .left) @@ -23,19 +23,15 @@ public final class RecipientFromCellNode: CellNode { return textNode }() - lazy var valueTextNode: ASTextNode2 = { + private lazy var valueTextNode: ASTextNode2 = { let textNode = ASTextNode2() textNode.accessibilityIdentifier = "aid-from-value-node" return textNode }() - public var fromEmail: String? { - didSet { - self.valueTextNode.attributedText = fromEmail?.attributed(.regular(17)) - } - } + private let fromEmail: String - lazy var toggleButtonNode: ASButtonNode = { + private lazy var toggleButtonNode: ASButtonNode = { let configuration = UIImage.SymbolConfiguration(pointSize: 14, weight: .light) let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) let button = ASButtonNode() @@ -43,15 +39,16 @@ public final class RecipientFromCellNode: CellNode { button.setImage(image, for: .normal) button.contentEdgeInsets = UIEdgeInsets(top: 3, left: 0, bottom: 0, right: 0) button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) - button.addTarget(self, action: #selector(self.onToggleButtonTap), forControlEvents: .touchUpInside) + button.addTarget(self, action: #selector(onToggleButtonTap), forControlEvents: .touchUpInside) return button }() - var toggleButtonAction: (() -> Void)? + private var toggleButtonAction: (() -> Void)? - public init(toggleButtonAction: (() -> Void)?) { - super.init() + public init(fromEmail: String, toggleButtonAction: (() -> Void)?) { + self.fromEmail = fromEmail self.toggleButtonAction = toggleButtonAction + super.init() } override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { @@ -62,14 +59,15 @@ public final class RecipientFromCellNode: CellNode { let stack = ASStackLayoutSpec.horizontal() stack.verticalAlignment = .center valueTextNode.style.flexGrow = 1 + valueTextNode.attributedText = fromEmail.attributed(.regular(17)) - let textNodeStack = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0), child: labelTextNode) + let textNodeStack = ASInsetLayoutSpec(insets: .zero, child: labelTextNode) stack.children = [textNodeStack, valueTextNode, toggleButtonNode] return ASInsetLayoutSpec(insets: insets, child: stack) } - @objc func onToggleButtonTap() { + @objc private func onToggleButtonTap() { toggleButtonAction?() } } diff --git a/FlowCryptUI/Views/NavigationBarActionButton.swift b/FlowCryptUI/Views/NavigationBarActionButton.swift index d923efe6c..4ad781e96 100644 --- a/FlowCryptUI/Views/NavigationBarActionButton.swift +++ b/FlowCryptUI/Views/NavigationBarActionButton.swift @@ -18,7 +18,8 @@ public final class NavigationBarActionButton: UIBarButtonItem { public convenience init(imageSystemName: String, action: (() -> Void)?, accessibilityIdentifier: String? = nil) { self.init() onAction = action - customView = LeftAlignedIconButton(type: .system).with { + customView = UIButton(type: .system).with { + $0.contentHorizontalAlignment = .left $0.setImage(UIImage(systemName: imageSystemName), for: .normal) $0.frame.size = Constants.buttonSize $0.addTarget(self, action: #selector(tap), for: .touchUpInside) @@ -31,13 +32,3 @@ public final class NavigationBarActionButton: UIBarButtonItem { onAction?() } } - -private final class LeftAlignedIconButton: UIButton { - override func layoutSubviews() { - super.layoutSubviews() - contentHorizontalAlignment = .left - let availableSpace = bounds.inset(by: contentEdgeInsets) - let availableWidth = availableSpace.width - imageEdgeInsets.right - (imageView?.frame.width ?? 0) - (titleLabel?.frame.width ?? 0) - titleEdgeInsets = UIEdgeInsets(top: 0, left: availableWidth / 2, bottom: 0, right: 0) - } -} From 8752c9e5559c08d7546f3b0a88db10b699e58977 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 6 Sep 2022 16:57:23 +0300 Subject: [PATCH 04/56] update drafts subtitle format --- .../ComposeViewController+Drafts.swift | 5 ++-- .../ComposeViewController+MessageSend.swift | 11 +++++---- ...ComposeViewController+RecipientInput.swift | 2 +- .../Controllers/Inbox/InboxRenderable.swift | 24 ++++++++++++++----- .../Gmail+MessagesList.swift | 9 ++++--- .../ComposeMessageService.swift | 4 ++-- .../Resources/en.lproj/Localizable.strings | 2 +- package-lock.json | 24 +++++++++---------- 8 files changed, 49 insertions(+), 32 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 714a1dd60..8bb8a4530 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -37,8 +37,9 @@ extension ComposeViewController { } } - func saveDraftIfNeeded() { - guard shouldSaveDraft() else { return } + // TODO: Better naming + func saveDraftIfNeeded(isForceSave: Bool = false) { + guard isForceSave || shouldSaveDraft() else { return } Task { do { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index ae7e7ee06..c01289195 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -28,7 +28,7 @@ extension ComposeViewController { // https://github.com/FlowCrypt/flowcrypt-ios/issues/291 try await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms - let sendableMsg = try await self.composeMessageService.validateAndProduceSendableMsg( + let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg( input: self.input, contextToSend: self.contextToSend ) @@ -46,9 +46,8 @@ extension ComposeViewController { self.handle(error: ComposeMessageError.passPhraseRequired) }, onCompletion: { [weak self] passPhrase in - guard let self = self else { - return - } + guard let self = self else { return } + Task { do { let matched = try await self.composeMessageService.handlePassPhraseEntry( @@ -56,11 +55,13 @@ extension ComposeViewController { for: signingKey ) if matched { + self.signingKeyWithMissingPassphrase = nil if isDraft { - self.saveDraftIfNeeded() + self.saveDraftIfNeeded(isForceSave: true) } else { self.handleSendTap() } + self.reload(sections: [.passphrase]) } else { self.handle(error: ComposeMessageError.passPhraseNoMatch) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift index 45ea70150..d0407aac5 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientInput.swift @@ -145,7 +145,7 @@ extension ComposeViewController { } func handleBackspaceAction(with textField: UITextField, for recipientType: RecipientType) { - guard let text = textField.text, !text.isEmpty else { return } + guard let text = textField.text, text.isEmpty else { return } var recipients = contextToSend.recipients(type: recipientType) diff --git a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift index 6e3575e2d..269f639a2 100644 --- a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift +++ b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift @@ -46,9 +46,14 @@ extension InboxRenderable { extension InboxRenderable { init(message: Message) { - self.title = message.sender?.shortName ?? "message_unknown_sender".localized + self.title = Self.messageTitle(for: message) self.messageCount = 1 - self.subtitle = message.subject ?? "message_missing_subject".localized + if let subject = message.subject, subject.hasContent { + self.subtitle = subject + } else { + self.subtitle = "message_missing_subject".localized + } + self.dateString = DateFormatter().formatDate(message.date) self.isRead = message.isMessageRead self.date = message.date @@ -59,7 +64,7 @@ extension InboxRenderable { init(thread: MessageThread, folderPath: String?) { - self.title = InboxRenderable.messageTitle(for: thread, folderPath: folderPath) + self.title = Self.messageTitle(for: thread, folderPath: folderPath) self.messageCount = thread.messages.count self.subtitle = thread.subject ?? "message_missing_subject".localized @@ -78,9 +83,7 @@ extension InboxRenderable { } private static func messageTitle(for thread: MessageThread, folderPath: String?) -> String { - // for now its not exactly clear how titles on other folders should look like - // so in scope of this PR we are applying this title presentation only for "sent" folder - if folderPath == MessageLabel.sent.value { + if folderPath == MessageLabel.sent.value || folderPath == MessageLabel.draft.value { let recipients = thread.messages .flatMap(\.allRecipients) .map(\.shortName) @@ -95,6 +98,15 @@ extension InboxRenderable { } } + private static func messageTitle(for message: Message) -> String { + if message.labels.contains(.draft) { + let recipients = message.allRecipients.map(\.shortName).joined(separator: ", ") + return recipients.isEmpty ? "" : "To: \(recipients)" + } else { + return message.sender?.shortName ?? "message_unknown_sender".localized + } + } + mutating func updateMessage(labelsToAdd: [MessageLabel], labelsToRemove: [MessageLabel]) { switch wrappedType { case .thread(var thread): diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift index a0cb933fe..1b14a6253 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift @@ -13,7 +13,7 @@ import GoogleAPIClientForREST_Gmail // TODO: - https://github.com/FlowCrypt/flowcrypt-ios/issues/669 Remove in scope of the ticket extension GmailService: MessagesListProvider { func fetchMessages(using context: FetchMessageContext) async throws -> MessageContext { - return try await withThrowingTaskGroup(of: Message.self) { [weak self] taskGroup -> MessageContext in + return try await withThrowingTaskGroup(of: Message.self) { [weak self] taskGroup in let list = try await fetchMessagesList(using: context) let messageIdentifiers = list.messages?.compactMap(\.identifier) ?? [] @@ -41,20 +41,23 @@ extension GmailService: MessagesListProvider { extension GmailService: DraftsListProvider { func fetchDrafts(using context: FetchMessageContext) async throws -> MessageContext { - return try await withThrowingTaskGroup(of: Message.self) { taskGroup -> MessageContext in + return try await withThrowingTaskGroup(of: Message.self) { taskGroup in let list = try await fetchDraftsList(using: context) for draft in list.drafts ?? [] { taskGroup.addTask { try await self.fetchFullMessage( with: draft.message?.identifier ?? "", - draftIdentifier: draft.identifier) + draftIdentifier: draft.identifier + ) } } + var messages: [Message] = [] for try await result in taskGroup { messages.append(result) } + messages.sort(by: { $0.date > $1.date }) return MessageContext( messages: messages, diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 5f01e2517..4dee48ca4 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -95,7 +95,7 @@ final class ComposeMessageService { isDraft: Bool = false ) async throws -> SendableMsg { let recipients = contextToSend.recipients - let subject = contextToSend.subject ?? "(no subject)" + let subject = contextToSend.subject ?? "" let senderKeys = try await keyMethods.chooseSenderKeys( for: .encryption, @@ -121,7 +121,7 @@ final class ComposeMessageService { throw MessageValidationError.invalidEmailRecipient } - guard input.isQuote || contextToSend.subject?.hasContent ?? false else { + guard input.isQuote || subject.hasContent else { throw MessageValidationError.emptySubject } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 5dd1bfc99..c1b31ccbb 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -49,7 +49,7 @@ "message_compose_secure" = "Compose Secure Message"; "message_unknown_sender" = "(unknown sender)"; "message_no_recipients" = "(no recipients)"; -"message_missing_subject" = "No subject"; +"message_missing_subject" = "(no subject)"; "message_attachment_saved_successfully_title" = "Attachment Saved"; "message_attachment_saved_successfully_message" = "Your attachment was saved in Files. Would you like to open it?"; "message_attachment_saved_with_error" = "Attachment could not be saved. %@"; diff --git a/package-lock.json b/package-lock.json index ea25daa31..c9b25dd20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -736,16 +736,16 @@ "dev": true }, "node_modules/es-abstract": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", - "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.2.tgz", + "integrity": "sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", + "get-intrinsic": "^1.1.2", "get-symbol-description": "^1.0.0", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", @@ -757,9 +757,9 @@ "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", + "object-inspect": "^1.12.2", "object-keys": "^1.1.1", - "object.assign": "^4.1.2", + "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", "string.prototype.trimend": "^1.0.5", "string.prototype.trimstart": "^1.0.5", @@ -3652,16 +3652,16 @@ "dev": true }, "es-abstract": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", - "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.2.tgz", + "integrity": "sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==", "dev": true, "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", + "get-intrinsic": "^1.1.2", "get-symbol-description": "^1.0.0", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", @@ -3673,9 +3673,9 @@ "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", + "object-inspect": "^1.12.2", "object-keys": "^1.1.1", - "object.assign": "^4.1.2", + "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", "string.prototype.trimend": "^1.0.5", "string.prototype.trimstart": "^1.0.5", From a6d1894bc432c0a0b97dad4325ad2c59ffcfa9b9 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 7 Sep 2022 22:35:33 +0300 Subject: [PATCH 05/56] save and use existing drafts --- FlowCrypt/App/AppContext.swift | 10 +-- .../Compose/ComposeViewController.swift | 25 ++----- .../Compose/ComposeViewControllerInput.swift | 55 +++++++++------ .../ComposeViewController+Nodes.swift | 9 +-- .../ComposeViewController+Setup.swift | 30 +++++--- .../Controllers/Inbox/InboxRenderable.swift | 2 +- .../Inbox/InboxViewController.swift | 31 +++++---- .../MsgListViewController.swift | 24 +++++-- .../Controllers/Threads/MessageThread.swift | 2 +- .../Threads/ThreadDetailsDecorator.swift | 2 +- .../Threads/ThreadDetailsViewController.swift | 69 ++++++++++++++----- .../Mail Provider/MailProvider.swift | 8 ++- .../Message Gateway/GmailService+draft.swift | 2 +- .../MessagesList Provider/Model/Message.swift | 15 +++- .../ComposeMessageService.swift | 7 +- .../Resources/en.lproj/Localizable.strings | 1 + 16 files changed, 185 insertions(+), 107 deletions(-) diff --git a/FlowCrypt/App/AppContext.swift b/FlowCrypt/App/AppContext.swift index 7191b55d7..572167e6d 100644 --- a/FlowCrypt/App/AppContext.swift +++ b/FlowCrypt/App/AppContext.swift @@ -105,7 +105,7 @@ class AppContext { @MainActor func getBackupService() throws -> BackupService { - let mailProvider = try self.getRequiredMailProvider() + let mailProvider = try getRequiredMailProvider() return BackupService( backupProvider: try mailProvider.backupProvider, messageSender: try mailProvider.messageSender @@ -115,16 +115,16 @@ class AppContext { @MainActor func getFoldersService() throws -> FoldersService { return FoldersService( - encryptedStorage: self.encryptedStorage, - remoteFoldersProvider: try self.getRequiredMailProvider().remoteFoldersProvider + encryptedStorage: encryptedStorage, + remoteFoldersProvider: try getRequiredMailProvider().remoteFoldersProvider ) } @MainActor func getSendAsService() throws -> SendAsService { return SendAsService( - encryptedStorage: self.encryptedStorage, - remoteSendAsProvider: try self.getRequiredMailProvider().remoteSendAsProvider + encryptedStorage: encryptedStorage, + remoteSendAsProvider: try getRequiredMailProvider().remoteSendAsProvider ) } } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 59b59ef35..80d231a91 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -113,9 +113,11 @@ final class ComposeViewController: TableNodeViewController { appDelegateGoogleSessionContainer: UIApplication.shared.delegate as? AppDelegate, shouldRunWarmupQuery: true ) + let draftGateway = try appContext.getRequiredMailProvider().draftGateway self.composeMessageService = composeMessageService ?? ComposeMessageService( appContext: appContext, - keyMethods: keyMethods + keyMethods: keyMethods, + draftGateway: draftGateway ) self.filesManager = filesManager self.photosManager = photosManager @@ -152,7 +154,7 @@ final class ComposeViewController: TableNodeViewController { observeKeyboardNotifications() observerAppStates() observeComposeUpdates() - setupQuote() + fillDataFromInput() } override func viewWillDisappear(_ animated: Bool) { @@ -191,25 +193,6 @@ final class ComposeViewController: TableNodeViewController { NotificationCenter.default.removeObserver(self) } - func update(with message: Message) { - if let sender = message.sender?.email { - contextToSend.sender = sender - } - - contextToSend.subject = message.subject - contextToSend.message = message.raw - - for recipient in message.to { - add(recipient: recipient, type: .to) - } - for recipient in message.cc { - add(recipient: recipient, type: .cc) - } - for recipient in message.bcc { - add(recipient: recipient, type: .bcc) - } - } - func add(recipient: Recipient, type: RecipientType) { let composeRecipient = ComposeMessageRecipient( email: recipient.email, diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index 4a88f9a3a..68452ff8e 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -14,6 +14,7 @@ struct ComposeMessageInput: Equatable { struct MessageQuoteInfo: Equatable { let recipients: [Recipient] let ccRecipients: [Recipient] + let bccRecipients: [Recipient] let sender: Recipient? let subject: String? let sentDate: Date @@ -28,28 +29,19 @@ struct ComposeMessageInput: Equatable { case idle case reply(MessageQuoteInfo) case forward(MessageQuoteInfo) + case draft(MessageQuoteInfo) } let type: InputType - var quoteRecipients: [Recipient] { - guard case .reply(let info) = type else { - return [] - } - return info.recipients - } - - var quoteCCRecipients: [Recipient] { - guard case .reply(let info) = type else { - return [] - } - return info.ccRecipients - } - var subject: String? { type.info?.subject } + var message: String? { + type.info?.message + } + var replyToMsgId: String? { type.info?.replyToMsgId } @@ -70,7 +62,7 @@ struct ComposeMessageInput: Equatable { extension ComposeMessageInput { var successfullySentToast: String { switch type { - case .idle: + case .idle, .draft: return "compose_encrypted_sent".localized case .forward: return "compose_forward_successful".localized @@ -82,14 +74,21 @@ extension ComposeMessageInput { extension ComposeMessageInput { var isQuote: Bool { - type != .idle + switch type { + case .reply, .forward: + return true + case .idle, .draft: + return false + } } - var isReply: Bool { - guard case .reply = type else { + var shouldFocusTextNode: Bool { + switch type { + case .reply, .draft: + return true + case .idle, .forward: return false } - return true } } @@ -98,8 +97,24 @@ extension ComposeMessageInput.InputType { switch self { case .idle: return nil - case .reply(let info), .forward(let info): + case .reply(let info), .forward(let info), .draft(let info): return info } } } + +extension ComposeMessageInput.MessageQuoteInfo { + init(message: Message, processed: ProcessedMessage?) { + self.recipients = message.to + self.ccRecipients = message.cc + self.bccRecipients = message.bcc + self.sender = message.sender + self.subject = message.subject + self.sentDate = message.date + self.message = processed?.text ?? message.body.text + self.threadId = message.threadId + self.replyToMsgId = nil // TODO: draft.rawMessage.replyToMsgId, + self.inReplyTo = message.inReplyTo + self.attachments = processed?.attachments ?? message.attachments + } +} diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index c2c46dd37..e4ed7632d 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -163,11 +163,12 @@ extension ComposeViewController { } } .then { - let messageText = decorator.styledMessage(with: contextToSend.message ?? "") - let mutableString = NSMutableAttributedString(attributedString: messageText) + let message = contextToSend.message ?? input.message ?? "" + let attributedString = decorator.styledMessage(with: message) + let mutableString = NSMutableAttributedString(attributedString: attributedString) let textNode = $0 - if input.isQuote && !messageText.string.contains(styledQuote.string) { + if input.isQuote && !mutableString.string.contains(styledQuote.string) { mutableString.append(styledQuote) } @@ -178,7 +179,7 @@ extension ComposeViewController { from: textNode.textView.textView.beginningOfDocument, to: textNode.textView.textView.beginningOfDocument ) - if self.input.isReply { + if self.input.shouldFocusTextNode { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { textNode.becomeFirstResponder() }) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index d731e0b82..dce3dcb72 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -49,19 +49,29 @@ extension ComposeViewController { updateView(newState: .main) } - func setupQuote() { - guard input.isQuote else { return } + func fillDataFromInput() { + switch input.type { + case .draft(let info), .reply(let info), .forward(let info): + contextToSend.subject = info.subject + contextToSend.message = info.message - for recipient in input.quoteRecipients { - add(recipient: recipient, type: .to) - } + for recipient in info.recipients { + add(recipient: recipient, type: .to) + } - for recipient in input.quoteCCRecipients { - add(recipient: recipient, type: .cc) - } + for recipient in info.ccRecipients { + add(recipient: recipient, type: .cc) + } - if input.quoteCCRecipients.isNotEmpty { - shouldShowAllRecipientTypes.toggle() + for recipient in info.bccRecipients { + add(recipient: recipient, type: .bcc) + } + + if info.ccRecipients.isNotEmpty || info.bccRecipients.isNotEmpty { + shouldShowAllRecipientTypes.toggle() + } + case .idle: + return } } diff --git a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift index 269f639a2..eac1c38b3 100644 --- a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift +++ b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift @@ -55,7 +55,7 @@ extension InboxRenderable { } self.dateString = DateFormatter().formatDate(message.date) - self.isRead = message.isMessageRead + self.isRead = message.isRead self.date = message.date self.wrappedType = .message(message) self.badge = nil diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 1e8794e0f..469ba0549 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -13,12 +13,13 @@ class InboxViewController: ViewController { private let numberOfInboxItemsToLoad: Int - private let appContext: AppContextWithUser + let appContext: AppContextWithUser + let tableNode: ASTableNode + private let decorator: InboxViewDecorator private let draftsListProvider: DraftsListProvider? private let messageOperationsProvider: MessageOperationsProvider private let refreshControl = UIRefreshControl() - let tableNode: ASTableNode private lazy var composeButton = ComposeButtonNode { [weak self] in self?.btnComposeTap() } @@ -475,7 +476,7 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { return } tableNode.deselectRow(at: indexPath, animated: true) - open(message: message, path: viewModel.path, appContext: appContext) + open(message: message, path: viewModel.path) } private func cellNode(for indexPath: IndexPath, and size: CGSize) -> ASCellNodeBlock { @@ -505,16 +506,7 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { var rowNumber = indexPath.row if self.shouldShowEmptyView { if indexPath.row == 0 { - return EmptyFolderCellNode( - path: self.viewModel.path, - emptyFolder: { - self.showConfirmAlert( - message: "folder_empty_confirm".localized, - onConfirm: { [weak self] _ in - self?.emptyInboxFolder() - } - ) - }) + return self.emptyFolderNode() } rowNumber -= 1 } @@ -541,6 +533,19 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { } } + private func emptyFolderNode() -> ASCellNode { + return EmptyFolderCellNode( + path: viewModel.path, + emptyFolder: { [weak self] in + self?.showConfirmAlert( + message: "folder_empty_confirm".localized, + onConfirm: { [weak self] _ in + self?.emptyInboxFolder() + } + ) + }) + } + private func emptyInboxFolder() { Task { do { diff --git a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift b/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift index 3a6bb1a92..a891bf61e 100644 --- a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift +++ b/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift @@ -11,8 +11,9 @@ import UIKit @MainActor protocol MsgListViewController { var path: String { get } + var appContext: AppContextWithUser { get } - func open(message: InboxRenderable, path: String, appContext: AppContextWithUser) + func open(message: InboxRenderable, path: String) func getUpdatedIndex(for message: InboxRenderable) -> Int? func updateMessage(isRead: Bool, at index: Int) @@ -21,12 +22,14 @@ protocol MsgListViewController { } extension MsgListViewController where Self: UIViewController { - - // todo - tom - don't know how to add AppContext into init of protocol/extension - func open(message: InboxRenderable, path: String, appContext: AppContextWithUser) { + func open(message: InboxRenderable, path: String) { switch message.wrappedType { case .message(let message): - open(message: message, path: path, appContext: appContext) + if message.isDraft { + open(draft: message, appContext: appContext) + } else { + open(message: message, path: path, appContext: appContext) + } case .thread(let thread): open(thread: thread, appContext: appContext) } @@ -35,8 +38,15 @@ extension MsgListViewController where Self: UIViewController { private func open(draft: Message, appContext: AppContextWithUser) { Task { do { - let controller = try await ComposeViewController(appContext: appContext) - controller.update(with: draft) + let draftInfo = ComposeMessageInput.MessageQuoteInfo( + message: draft, + processed: nil + ) + + let controller = try await ComposeViewController( + appContext: appContext, + input: .init(type: .draft(draftInfo)) + ) navigationController?.pushViewController(controller, animated: true) } catch { showAlert(message: error.localizedDescription) diff --git a/FlowCrypt/Controllers/Threads/MessageThread.swift b/FlowCrypt/Controllers/Threads/MessageThread.swift index 9efc1808b..d202ac19d 100644 --- a/FlowCrypt/Controllers/Threads/MessageThread.swift +++ b/FlowCrypt/Controllers/Threads/MessageThread.swift @@ -43,7 +43,7 @@ struct MessageThread: Equatable { } var isRead: Bool { - !messages.contains(where: { !$0.isMessageRead }) + !messages.contains(where: { !$0.isRead }) } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift index 499b00888..6ddf2151c 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift @@ -19,7 +19,7 @@ extension ThreadMessageInfoCellNode.Input { .joined(separator: ", ") let recipientLabel = [recipientPrefix, recipientsList].joined(separator: " ") let date = DateFormatter().formatDate(threadMessage.rawMessage.date) - let isMessageRead = threadMessage.rawMessage.isMessageRead + let isMessageRead = threadMessage.rawMessage.isRead let style: NSAttributedString.Style = isMessageRead ? .regular(16) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 9f3977892..e0a83f1a1 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -102,7 +102,7 @@ final class ThreadDetailsViewController: TableNodeViewController { Task { try await threadOperationsProvider.mark(thread: thread, asRead: true, in: currentFolderPath) } - let indexOfSectionToExpand = input.firstIndex(where: { $0.rawMessage.isMessageRead == false }) ?? input.count - 1 + let indexOfSectionToExpand = input.firstIndex(where: { !$0.rawMessage.isRead }) ?? input.count - 1 let indexPath = IndexPath(row: 0, section: indexOfSectionToExpand + 1) handleExpandTap(at: indexPath) } @@ -111,18 +111,15 @@ final class ThreadDetailsViewController: TableNodeViewController { extension ThreadDetailsViewController { private func handleExpandTap(at indexPath: IndexPath) { - guard let threadNode = node.nodeForRow(at: indexPath) as? ThreadMessageInfoCellNode else { - logger.logError("Fail to handle tap at \(indexPath)") - return - } - input[indexPath.section - 1].isExpanded.toggle() if input[indexPath.section - 1].isExpanded { UIView.animate( withDuration: 0.3, animations: { - threadNode.expandNode.view.alpha = 0 + if let threadNode = self.node.nodeForRow(at: indexPath) as? ThreadMessageInfoCellNode { + threadNode.expandNode.view.alpha = 0 + } }, completion: { [weak self] _ in guard let self = self else { return } @@ -153,6 +150,27 @@ extension ThreadDetailsViewController { composeNewMessage(at: indexPath, quoteType: .reply) } + private func handleDraftTap(at indexPath: IndexPath) { + Task { + do { + let draft = input[indexPath.section - 1] + + let draftInfo = ComposeMessageInput.MessageQuoteInfo( + message: draft.rawMessage, + processed: draft.processedMessage + ) + + let controller = try await ComposeViewController( + appContext: appContext, + input: .init(type: .draft(draftInfo)) + ) + navigationController?.pushViewController(controller, animated: true) + } catch { + showAlert(message: error.localizedDescription) + } + } + } + private func handleMenuTap(at indexPath: IndexPath) { let alert = UIAlertController( title: nil, @@ -286,6 +304,7 @@ extension ThreadDetailsViewController { let replyInfo = ComposeMessageInput.MessageQuoteInfo( recipients: recipients, ccRecipients: ccRecipients, + bccRecipients: [], sender: input.rawMessage.sender, subject: [quoteType.subjectPrefix, subject].joined(), sentDate: input.rawMessage.date, @@ -305,14 +324,13 @@ extension ThreadDetailsViewController { } }() - let composeInput = ComposeMessageInput(type: composeType) - Task { do { - navigationController?.pushViewController( - try await ComposeViewController(appContext: appContext, input: composeInput), - animated: true + let composeVC = try await ComposeViewController( + appContext: appContext, + input: ComposeMessageInput(type: composeType) ) + navigationController?.pushViewController(composeVC, animated: true) } catch { showAlert(message: error.localizedDescription) } @@ -365,14 +383,14 @@ extension ThreadDetailsViewController { UIView.animate( withDuration: 0.2, animations: { - self.node.reloadSections(IndexSet(integer: indexPath.section), with: .fade) + self.node.reloadSections(IndexSet(integer: indexPath.section), with: .automatic) }, completion: { [weak self] _ in self?.node.scrollToRow(at: indexPath, at: .middle, animated: true) }) } else { input[messageIndex].processedMessage?.signature = processedMessage.signature - node.reloadSections(IndexSet(integer: indexPath.section), with: .fade) + node.reloadSections(IndexSet(integer: indexPath.section), with: .automatic) } } @@ -584,7 +602,9 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { - guard section > 0, input[section - 1].isExpanded else { return 1 } + guard section > 0, input[section - 1].isExpanded, + !input[section - 1].rawMessage.isDraft + else { return 1 } let attachmentsCount = input[section - 1].processedMessage?.attachments.count ?? 0 return Parts.allCases.count + attachmentsCount @@ -602,7 +622,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { let messageIndex = indexPath.section - 1 let message = self.input[messageIndex] - if indexPath.row == 0 { + if !message.rawMessage.isDraft && indexPath.row == 0 { return ThreadMessageInfoCellNode( input: .init(threadMessage: message, index: messageIndex), onReplyTap: { [weak self] _ in self?.handleReplyTap(at: indexPath) }, @@ -615,6 +635,16 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { return ASCellNode() } + if message.rawMessage.isDraft { + let draft = processedMessage.message.body.textWithoutThreadQuote + return LabelCellNode( + input: .init( + title: "compose_draft".localized.attributed(color: .red), + text: draft.attributed(color: .secondaryLabel) + ) + ) + } + guard indexPath.row > 1 else { return MessageTextSubjectNode(processedMessage.attributedMessage, index: messageIndex) } @@ -636,7 +666,12 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { handleExpandTap(at: indexPath) case is AttachmentNode: handleAttachmentTap(at: indexPath) - default: return + default: + let message = input[indexPath.section - 1] + + if message.rawMessage.isDraft { + handleDraftTap(at: indexPath) + } } } diff --git a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift index df18a1e6f..1e2f90167 100644 --- a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift @@ -117,14 +117,18 @@ final class MailProvider { } private func resolveService(of type: T.Type) throws -> T { - guard let service = services.first(where: { $0.mailServiceProviderType == authType.mailServiceProviderType }) as? T else { + guard let service = services.first(where: { + $0.mailServiceProviderType == authType.mailServiceProviderType + }) as? T else { throw AppErr.general("Email Provider should support this functionality. Can't resolve dependency for \(type)") } return service } private func resolveOptionalService(of type: T.Type) -> T? { - guard let service = services.first(where: { $0.mailServiceProviderType == authType.mailServiceProviderType }) as? T else { + guard let service = services.first(where: { + $0.mailServiceProviderType == authType.mailServiceProviderType + }) as? T else { return nil } return service diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 3ec761cce..57ccaf14a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -25,7 +25,7 @@ extension GmailService: DraftGateway { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } else if let draft = object as? GTLRGmail_Draft { - return continuation.resume(returning: (draft)) + return continuation.resume(returning: draft) } else { return continuation.resume(throwing: GmailServiceError.failedToParseData(nil)) } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index d9b69fce1..03468c42e 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -29,7 +29,7 @@ struct Message: Hashable { let inReplyTo: String? private(set) var labels: [MessageLabel] - var isMessageRead: Bool { + var isRead: Bool { // imap if labels.contains(.none) { return false @@ -41,6 +41,8 @@ struct Message: Hashable { return true } + var isDraft: Bool { labels.contains(.draft) } + var isPgp: Bool { (body.text.contains("-----BEGIN PGP ") && body.text.contains("-----END PGP ")) || hasSignatureAttachment } @@ -145,3 +147,14 @@ struct MessageBody: Hashable { let text: String let html: String? } + +extension MessageBody { + var textWithoutThreadQuote: String { + guard let range = text.range( + of: "On [a-zA-Z]*, [a-zA-Z0-9 ]* at [0-9:]*, .*", + options: [.regularExpression] + ) else { return text } + + return text[text.startIndex.. Date: Fri, 9 Sep 2022 21:21:12 +0300 Subject: [PATCH 06/56] use existing draft for new messages, add delete draft button --- .husky/pre-commit | 1 + FlowCrypt.xcodeproj/project.pbxproj | 21 +------ .../Compose/ComposeViewController.swift | 16 ++++++ .../Compose/ComposeViewControllerInput.swift | 24 ++++++-- .../Compose/ComposeViewDecorator.swift | 2 +- .../ComposeViewController+Drafts.swift | 6 +- .../ComposeViewController+Nodes.swift | 2 +- .../ComposeViewController+Setup.swift | 55 +++++++++++++------ .../Controllers/Inbox/InboxRenderable.swift | 40 +++++++++++--- .../Inbox/InboxViewController.swift | 29 +++++----- .../Inbox/InboxViewDecorator.swift | 2 +- .../Threads/ThreadDetailsDecorator.swift | 7 ++- .../Threads/ThreadDetailsViewController.swift | 43 +++++++++------ FlowCrypt/Core/Core.swift | 2 +- .../Message Gateway/GmailService+draft.swift | 50 ++++++++--------- .../Message Gateway/MessageGateway.swift | 2 +- .../Message Provider/MessageService.swift | 17 +++--- .../Threads/MessagesThreadProvider.swift | 12 +--- .../ComposeMessageService.swift | 13 ++--- FlowCryptUI/Cell Nodes/LabelCellNode.swift | 55 +++++++++++++++---- .../ThreadMessageInfoCellNode.swift | 6 +- Gemfile.lock | 4 +- package-lock.json | 16 ++++++ package.json | 3 +- 24 files changed, 268 insertions(+), 160 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index d24fdfc60..677ad2724 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,3 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" npx lint-staged +npx git-format-staged --formatter "swiftformat stdin --stdinpath '{}'" "*.swift" "!Pods/*" \ No newline at end of file diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index c815409d1..49802c283 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -2338,7 +2338,6 @@ buildConfigurationList = C132B9C21EC2DBD800763715 /* Build configuration list for PBXNativeTarget "FlowCrypt" */; buildPhases = ( 85C230714DC341D246864AD0 /* [CP] Check Pods Manifest.lock */, - D2324CAA2471D47B00FD1C6F /* Format code */, D2324CA92471CED200FD1C6F /* SwiftLint */, C132B9AC1EC2DBD800763715 /* Sources */, C132B9AD1EC2DBD800763715 /* Frameworks */, @@ -2657,24 +2656,6 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; - D2324CAA2471D47B00FD1C6F /* Format code */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Format code"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "cd BuildTools\nSDKROOT=(xcrun --sdk macosx --show-sdk-path)\n# swift package update\n# Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\nswift run -c release swiftformat \"$SRCROOT\" --exclude appium,fastlane,Pods,vendor\n"; - }; E531C3B50A9C90454C72F878 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -3591,7 +3572,7 @@ CODE_SIGN_ENTITLEMENTS = FlowCrypt/FlowCrypt.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 6DZ6CC3YMY; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 80d231a91..8193cefc6 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -52,6 +52,7 @@ final class ComposeViewController: TableNodeViewController { let composeMessageService: ComposeMessageService var decorator: ComposeViewDecorator let localContactsProvider: LocalContactsProviderType + let messageService: MessageService let pubLookup: PubLookupType let googleUserService: GoogleUserServiceType let filesManager: FilesManagerType @@ -96,6 +97,7 @@ final class ComposeViewController: TableNodeViewController { decorator: ComposeViewDecorator = ComposeViewDecorator(), input: ComposeMessageInput = .empty, composeMessageService: ComposeMessageService? = nil, + messageService: MessageService? = nil, filesManager: FilesManagerType = FilesManager(), photosManager: PhotosManagerType = PhotosManager(), keyMethods: KeyMethodsType = KeyMethods() @@ -128,6 +130,15 @@ final class ComposeViewController: TableNodeViewController { self.router = appContext.globalRouter self.clientConfiguration = clientConfiguration + let mailProvider = try appContext.getRequiredMailProvider() + self.messageService = try messageService ?? MessageService( + localContactsProvider: localContactsProvider, + pubLookup: PubLookup(clientConfiguration: clientConfiguration, localContactsProvider: localContactsProvider), + keyAndPassPhraseStorage: appContext.keyAndPassPhraseStorage, + messageProvider: try mailProvider.messageProvider, + combinedPassPhraseStorage: appContext.combinedPassPhraseStorage + ) + self.sendAsList = try await appContext.getSendAsService() .fetchList(isForceReload: false, for: appContext.user) .filter { $0.verificationStatus == .accepted || $0.isDefault } @@ -232,3 +243,8 @@ final class ComposeViewController: TableNodeViewController { } extension ComposeViewController: FilesManagerPresenter {} + +// update draft, don't create a new one each time +// decrypt draft body +// add delete button for drafts in thread view +// add delete button for drafts in compose view diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index 68452ff8e..4cc0aec18 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -12,13 +12,14 @@ struct ComposeMessageInput: Equatable { static let empty = ComposeMessageInput(type: .idle) struct MessageQuoteInfo: Equatable { + let id: String? let recipients: [Recipient] let ccRecipients: [Recipient] let bccRecipients: [Recipient] let sender: Recipient? let subject: String? let sentDate: Date - let message: String + let text: String let threadId: String? let replyToMsgId: String? let inReplyTo: String? @@ -34,12 +35,26 @@ struct ComposeMessageInput: Equatable { let type: InputType + var draftId: String? { + switch type { + case .draft(let info): + return info.id + case .forward, .idle, .reply: + return nil + } + } + var subject: String? { type.info?.subject } - var message: String? { - type.info?.message + var text: String? { + type.info?.text + } + + var isPgp: Bool { + guard let text = text else { return false } + return text.contains("-----BEGIN PGP ") && text.contains("-----END PGP ") } var replyToMsgId: String? { @@ -105,13 +120,14 @@ extension ComposeMessageInput.InputType { extension ComposeMessageInput.MessageQuoteInfo { init(message: Message, processed: ProcessedMessage?) { + self.id = message.identifier.stringId self.recipients = message.to self.ccRecipients = message.cc self.bccRecipients = message.bcc self.sender = message.sender self.subject = message.subject self.sentDate = message.date - self.message = processed?.text ?? message.body.text + self.text = processed?.text ?? message.body.text self.threadId = message.threadId self.replyToMsgId = nil // TODO: draft.rawMessage.replyToMsgId, self.inReplyTo = message.inReplyTo diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 45ebfd638..364be462f 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -122,7 +122,7 @@ struct ComposeViewDecorator { + "compose_quote_from".localizeWithArguments(date, time, from) + "\n" - let message = " > " + info.message.replacingOccurrences(of: "\n", with: "\n > ") + let message = " > " + info.text.replacingOccurrences(of: "\n", with: "\n > ") return (text + message).attributed(.regular(17)) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 8bb8a4530..872bd51af 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -48,7 +48,11 @@ extension ComposeViewController { contextToSend: contextToSend, isDraft: true ) - try await composeMessageService.encryptAndSaveDraft(message: sendableMsg, threadId: input.threadId) + try await composeMessageService.encryptAndSaveDraft( + message: sendableMsg, + threadId: input.threadId, + draftId: input.draftId + ) } catch { if case .promptUserToEnterPassPhraseForSigningKey(let keyPair) = error as? ComposeMessageError { signingKeyWithMissingPassphrase = keyPair diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index e4ed7632d..1a650bb61 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -163,7 +163,7 @@ extension ComposeViewController { } } .then { - let message = contextToSend.message ?? input.message ?? "" + let message = contextToSend.message ?? input.text ?? "" let attributedString = decorator.styledMessage(with: message) let mutableString = NSMutableAttributedString(attributedString: attributedString) let textNode = $0 diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index dce3dcb72..dd3b413b2 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -50,28 +50,49 @@ extension ComposeViewController { } func fillDataFromInput() { - switch input.type { - case .draft(let info), .reply(let info), .forward(let info): - contextToSend.subject = info.subject - contextToSend.message = info.message + guard let info = input.type.info else { return } - for recipient in info.recipients { - add(recipient: recipient, type: .to) - } + contextToSend.subject = info.subject - for recipient in info.ccRecipients { - add(recipient: recipient, type: .cc) - } + for recipient in info.recipients { + add(recipient: recipient, type: .to) + } - for recipient in info.bccRecipients { - add(recipient: recipient, type: .bcc) - } + for recipient in info.ccRecipients { + add(recipient: recipient, type: .cc) + } + + for recipient in info.bccRecipients { + add(recipient: recipient, type: .bcc) + } + + if info.ccRecipients.isNotEmpty || info.bccRecipients.isNotEmpty { + shouldShowAllRecipientTypes.toggle() + } - if info.ccRecipients.isNotEmpty || info.bccRecipients.isNotEmpty { - shouldShowAllRecipientTypes.toggle() + if input.isPgp { + let message = Message( + identifier: .random, + date: info.sentDate, + sender: info.sender, + subject: info.subject, + size: nil, + labels: [], + attachmentIds: [], + body: .init(text: info.text, html: nil) + ) + Task { + let processedMessage = try await messageService.decryptAndProcess( + message: message, + onlyLocalKeys: false, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + contextToSend.message = processedMessage.text + reload(sections: [.compose]) } - case .idle: - return + } else { + contextToSend.message = info.text } } diff --git a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift index eac1c38b3..e96ad7729 100644 --- a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift +++ b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift @@ -6,7 +6,7 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation +import UIKit struct InboxRenderable: Equatable { enum WrappedType: Equatable { @@ -14,7 +14,7 @@ struct InboxRenderable: Equatable { case thread(MessageThread) } - let title: String + let title: NSAttributedString let messageCount: Int let subtitle: String let dateString: String @@ -64,7 +64,7 @@ extension InboxRenderable { init(thread: MessageThread, folderPath: String?) { - self.title = Self.messageTitle(for: thread, folderPath: folderPath) + self.title = Self.messageTitle(for: thread, folderPath: folderPath, isRead: thread.isRead) self.messageCount = thread.messages.count self.subtitle = thread.subject ?? "message_missing_subject".localized @@ -82,28 +82,50 @@ extension InboxRenderable { self.updateBadge() } - private static func messageTitle(for thread: MessageThread, folderPath: String?) -> String { + private static func messageTitle(for thread: MessageThread, folderPath: String?, isRead: Bool) -> NSAttributedString { + let style: NSAttributedString.Style = isRead + ? .regular(17) + : .bold(17) + + let textColor: UIColor = isRead + ? .lightGray + : .mainTextUnreadColor + if folderPath == MessageLabel.sent.value || folderPath == MessageLabel.draft.value { let recipients = thread.messages .flatMap(\.allRecipients) .map(\.shortName) .unique() .joined(separator: ", ") - return "To: \(recipients)" + return "To: \(recipients)".attributed(style, color: textColor) } else { - return thread.messages + let hasDrafts = thread.messages.contains(where: { $0.isDraft }) + let senderNames = thread.messages .compactMap(\.sender?.shortName) .unique() .joined(separator: ",") + .attributed(style, color: textColor) + + if hasDrafts { + let draftLabel = "compose_draft".localized.attributed(style, color: .red.withAlphaComponent(0.65)) + let title = senderNames.mutable() + title.append(",".attributed(style, color: textColor)) + title.append(draftLabel) + return title + } else { + return senderNames + } } } - private static func messageTitle(for message: Message) -> String { + private static func messageTitle(for message: Message) -> NSAttributedString { if message.labels.contains(.draft) { let recipients = message.allRecipients.map(\.shortName).joined(separator: ", ") - return recipients.isEmpty ? "" : "To: \(recipients)" + let title = recipients.isEmpty ? "" : "To: \(recipients)" + return title.attributed() } else { - return message.sender?.shortName ?? "message_unknown_sender".localized + let title = message.sender?.shortName ?? "message_unknown_sender".localized + return title.attributed() } } diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 469ba0549..cb2d28b42 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -196,17 +196,19 @@ extension InboxViewController { // MARK: - Functionality extension InboxViewController { private func fetchAndRenderEmails(_ batchContext: ASBatchContext?) { - if let provider = draftsListProvider, viewModel.isDrafts { - fetchAndRenderDrafts(batchContext, draftsProvider: provider) + if viewModel.isDrafts { + fetchAndRenderDrafts(batchContext) } else { fetchAndRenderEmailsOnly(batchContext) } } - private func fetchAndRenderDrafts(_ batchContext: ASBatchContext?, draftsProvider: DraftsListProvider) { + private func fetchAndRenderDrafts(_ batchContext: ASBatchContext?) { + guard let draftsListProvider = draftsListProvider else { return } + Task { do { - let context = try await draftsProvider.fetchDrafts( + let context = try await draftsListProvider.fetchDrafts( using: FetchMessageContext( folderPath: viewModel.path, count: numberOfInboxItemsToLoad, @@ -237,7 +239,7 @@ extension InboxViewController { do { if isSearch { state = .searching - await self.tableNode.reloadData() + await tableNode.reloadData() } else { state = .fetching } @@ -248,7 +250,8 @@ extension InboxViewController { count: numberOfInboxItemsToLoad, searchQuery: getSearchQuery(), pagination: currentMessagesListPagination() - ), userEmail: appContext.user.email + ), + userEmail: appContext.user.email ) state = .refresh handleEndFetching(with: context, context: batchContext) @@ -351,11 +354,7 @@ extension InboxViewController { shouldBeginFetch = false inboxInput = input.data if inboxInput.isEmpty { - if isSearch { - state = .searchEmpty - } else { - state = .empty - } + state = isSearch ? .searchEmpty : .empty } else { state = .fetched(input.pagination) } @@ -416,7 +415,7 @@ extension InboxViewController { let viewController = try SearchViewController( appContext: appContext, viewModel: viewModel, - provider: self.inboxDataProvider, + provider: inboxDataProvider, isSearch: true ) navigationController?.pushViewController(viewController, animated: false) @@ -549,14 +548,14 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { private func emptyInboxFolder() { Task { do { - self.showSpinner() + showSpinner() try await self.messageOperationsProvider.emptyFolder(path: viewModel.path) self.state = .empty self.inboxInput = [] await tableNode.reloadData() - self.hideSpinner() + hideSpinner() } catch { - self.showAlert(message: error.errorMessage) + showAlert(message: error.errorMessage) } } } diff --git a/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift b/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift index cbe024fdc..6ca316d3c 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift @@ -30,7 +30,7 @@ extension InboxCellNode.Input { : .mainTextUnreadColor self.init( - emailText: NSAttributedString.text(from: email, style: style, color: textColor), + emailText: email, countText: { guard element.messageCount > 1 else { return nil } let count = element.messageCount > 99 ? "99+" : String(element.messageCount) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift index 6ddf2151c..488566680 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift @@ -74,11 +74,14 @@ extension AttachmentNode.Input { } } -private func makeEncryptionBadge(_ input: ThreadDetailsViewController.Input) -> BadgeNode.Input { +private func makeEncryptionBadge(_ input: ThreadDetailsViewController.Input) -> BadgeNode.Input? { + guard let type = input.processedMessage?.type else { return nil } + let icon: String let text: String let color: UIColor - switch input.processedMessage?.type { + + switch type { case .error: icon = "lock.open" text = "message_decrypt_error".localized diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index e0a83f1a1..5b9cc3b72 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -34,6 +34,7 @@ final class ThreadDetailsViewController: TableNodeViewController { } private let appContext: AppContextWithUser + private let draftGateway: DraftGateway? private let messageService: MessageService private let messageOperationsProvider: MessageOperationsProvider private let threadOperationsProvider: MessagesThreadOperationsProvider @@ -58,6 +59,7 @@ final class ThreadDetailsViewController: TableNodeViewController { encryptedStorage: appContext.encryptedStorage ) let mailProvider = try appContext.getRequiredMailProvider() + self.draftGateway = try mailProvider.draftGateway self.messageService = try messageService ?? MessageService( localContactsProvider: localContactsProvider, pubLookup: PubLookup(clientConfiguration: clientConfiguration, localContactsProvider: localContactsProvider), @@ -65,8 +67,7 @@ final class ThreadDetailsViewController: TableNodeViewController { messageProvider: try mailProvider.messageProvider, combinedPassPhraseStorage: appContext.combinedPassPhraseStorage ) - let threadOperationsProvider = try mailProvider.threadOperationsProvider - self.threadOperationsProvider = threadOperationsProvider + self.threadOperationsProvider = try mailProvider.threadOperationsProvider self.messageOperationsProvider = try mailProvider.messageOperationsProvider self.trashFolderProvider = TrashFolderProvider( user: appContext.user, @@ -302,13 +303,14 @@ extension ThreadDetailsViewController { let replyToMsgId = input.rawMessage.identifier.stringId let replyInfo = ComposeMessageInput.MessageQuoteInfo( + id: nil, recipients: recipients, ccRecipients: ccRecipients, bccRecipients: [], sender: input.rawMessage.sender, subject: [quoteType.subjectPrefix, subject].joined(), sentDate: input.rawMessage.date, - message: processedMessage.text, + text: processedMessage.text, threadId: threadId, replyToMsgId: replyToMsgId, inReplyTo: input.rawMessage.inReplyTo, @@ -399,8 +401,8 @@ extension ThreadDetailsViewController { hideSpinner() switch error as? MessageServiceError { - case let .missingPassPhrase(message): - handleWrongPassPhrase(for: message, at: indexPath) + case .missingPassPhrase: + handleWrongPassPhrase(indexPath: indexPath) default: // TODO: - Ticket - Improve error handling for ThreadDetailsViewController if let someError = error as NSError?, someError.code == Imap.Err.fetch.rawValue { @@ -444,7 +446,7 @@ extension ThreadDetailsViewController { present(alertController, animated: true) } - private func handleWrongPassPhrase(_ passPhrase: String? = nil, for message: Message, at indexPath: IndexPath) { + private func handleWrongPassPhrase(_ passPhrase: String? = nil, indexPath: IndexPath) { let title = passPhrase == nil ? "setup_enter_pass_phrase".localized : "setup_wrong_pass_phrase_retry".localized @@ -455,14 +457,14 @@ extension ThreadDetailsViewController { self?.navigationController?.popViewController(animated: true) }, onCompletion: { [weak self] passPhrase in - self?.handlePassPhraseEntry(message: message, with: passPhrase, at: indexPath) + self?.handlePassPhraseEntry(passPhrase, indexPath: indexPath) } ) present(alert, animated: true, completion: nil) } - private func handlePassPhraseEntry(message: Message, with passPhrase: String, at indexPath: IndexPath) { + private func handlePassPhraseEntry(_ passPhrase: String, indexPath: IndexPath) { presentedViewController?.dismiss(animated: true) handleFetchProgress(state: .decrypt) @@ -473,18 +475,17 @@ extension ThreadDetailsViewController { passPhrase, userEmail: appContext.user.email ) + let message = input[indexPath.section - 1].rawMessage if matched { - let sender = input[indexPath.section - 1].rawMessage.sender let processedMessage = try await messageService.decryptAndProcess( message: message, - sender: sender, onlyLocalKeys: false, userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) handleReceived(message: processedMessage, at: indexPath) } else { - handleWrongPassPhrase(passPhrase, for: message, at: indexPath) + handleWrongPassPhrase(passPhrase, indexPath: indexPath) } } catch { handleError(error, at: indexPath) @@ -631,20 +632,28 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { ) } - guard let processedMessage = message.processedMessage else { - return ASCellNode() - } - if message.rawMessage.isDraft { - let draft = processedMessage.message.body.textWithoutThreadQuote + let messageData = message.processedMessage?.message ?? message.rawMessage return LabelCellNode( input: .init( title: "compose_draft".localized.attributed(color: .red), - text: draft.attributed(color: .secondaryLabel) + text: messageData.body.textWithoutThreadQuote.attributed(color: .secondaryLabel), + actionButtonImageName: "trash", + action: { [weak self] in + guard let id = messageData.draftIdentifier else { return } + + Task { + await self?.draftGateway?.deleteDraft(with: id) + } + } ) ) } + guard let processedMessage = message.processedMessage else { + return ASCellNode() + } + guard indexPath.row > 1 else { return MessageTextSubjectNode(processedMessage.attributedMessage, index: messageIndex) } diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index fb5bb8a76..4e9abd7a0 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -183,7 +183,7 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { let blocks = parsed.data .split(separator: 10) // newline separated block jsons, one json per line - .map { data -> MsgBlock in + .map { data in guard let block = try? data.decodeJson(as: MsgBlock.self) else { let content = String(data: data, encoding: .utf8) ?? "(utf err)" return MsgBlock.blockParseErr(with: content) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 57ccaf14a..ce109244a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -9,7 +9,7 @@ import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: DraftGateway { - func saveDraft(input: MessageGatewayInput, draft: GTLRGmail_Draft?) async throws -> GTLRGmail_Draft { + func saveDraft(input: MessageGatewayInput, draftId: String?) async throws -> GTLRGmail_Draft { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in guard let raw = GTLREncodeBase64(input.mime) else { return continuation.resume(throwing: GmailServiceError.messageEncode) @@ -18,7 +18,7 @@ extension GmailService: DraftGateway { let draftQuery = createQueryForDraftAction( raw: raw, threadId: input.threadId, - draft: draft + draftId: draftId ) gmailService.executeQuery(draftQuery) { _, object, error in @@ -36,42 +36,36 @@ extension GmailService: DraftGateway { func deleteDraft(with identifier: String) async { await withCheckedContinuation { (continuation: CheckedContinuation) in let query = GTLRGmailQuery_UsersDraftsDelete.query(withUserId: .me, identifier: identifier) - gmailService.executeQuery(query) { _, _, _ in + gmailService.executeQuery(query) { response, object, error in return continuation.resume() } } } - private func createQueryForDraftAction(raw: String, threadId: String?, draft: GTLRGmail_Draft?) -> GTLRGmailQuery { - guard - let existingDraft = draft, - let draftIdentifier = existingDraft.identifier - else { - // draft is not created yet. creating draft - let newDraft = GTLRGmail_Draft() - let gtlMessage = GTLRGmail_Message() - gtlMessage.raw = raw - gtlMessage.threadId = threadId - newDraft.message = gtlMessage + private func createQueryForDraftAction(raw: String, threadId: String?, draftId: String?) -> GTLRGmailQuery { + let draft = GTLRGmail_Draft() + let message = GTLRGmail_Message() + message.raw = raw + message.threadId = threadId + + draft.message = message + + if let draftId = draftId { + draft.identifier = draftId + + return GTLRGmailQuery_UsersDraftsUpdate.query( + withObject: draft, + userId: "me", + identifier: draftId, + uploadParameters: nil + ) + } else { return GTLRGmailQuery_UsersDraftsCreate.query( - withObject: newDraft, + withObject: draft, userId: "me", uploadParameters: nil ) } - - // updating existing draft with new data - let gtlMessage = GTLRGmail_Message() - gtlMessage.raw = raw - gtlMessage.threadId = threadId - existingDraft.message = gtlMessage - - return GTLRGmailQuery_UsersDraftsUpdate.query( - withObject: existingDraft, - userId: "me", - identifier: draftIdentifier, - uploadParameters: nil - ) } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift index 902a722b3..0f8f4245a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift @@ -19,6 +19,6 @@ protocol MessageGateway { } protocol DraftGateway { - func saveDraft(input: MessageGatewayInput, draft: GTLRGmail_Draft?) async throws -> GTLRGmail_Draft + func saveDraft(input: MessageGatewayInput, draftId: String?) async throws -> GTLRGmail_Draft func deleteDraft(with identifier: String) async } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 8b9544bd3..25efe15ef 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -17,7 +17,7 @@ enum MessageFetchState { // MARK: - MessageServiceError enum MessageServiceError: Error, CustomStringConvertible { - case missingPassPhrase(_ message: Message) + case missingPassPhrase case emptyKeys case emptyKeysForEKM case attachmentNotFound @@ -101,7 +101,6 @@ final class MessageService { if message.isPgp { return try await decryptAndProcess( message: message, - sender: message.sender, onlyLocalKeys: onlyLocalKeys, userEmail: userEmail, isUsingKeyManager: isUsingKeyManager @@ -113,7 +112,6 @@ final class MessageService { func decryptAndProcess( message: Message, - sender: Recipient?, onlyLocalKeys: Bool, userEmail: String, isUsingKeyManager: Bool @@ -125,7 +123,10 @@ final class MessageService { } throw MessageServiceError.emptyKeys } - let verificationPubKeys = try await fetchVerificationPubKeys(for: sender, onlyLocal: onlyLocalKeys) + let verificationPubKeys = try await fetchVerificationPubKeys( + for: message.sender, + onlyLocal: onlyLocalKeys + ) var message = message if message.hasSignatureAttachment { @@ -142,8 +143,8 @@ final class MessageService { verificationPubKeys: verificationPubKeys ) - guard !self.hasMsgBlockThatNeedsPassPhrase(decrypted) else { - throw MessageServiceError.missingPassPhrase(message) + guard !hasMsgBlockThatNeedsPassPhrase(decrypted) else { + throw MessageServiceError.missingPassPhrase } return try await process( @@ -174,8 +175,8 @@ final class MessageService { let err = decryptErrBlock.decryptErr?.error let hideContent = err?.type == .badMdc || err?.type == .noMdc let rawMsg = hideContent - ? "content_hidden".localized - : decryptErrBlock.content + ? "content_hidden".localized + : decryptErrBlock.content text = "error_decrypt".localized + "\n\(err?.type.rawValue ?? "unknown".localized): \(err?.message ?? "??")\n\n\n\(rawMsg)" diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift index c1f6f4d92..2dc14f789 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift @@ -77,17 +77,7 @@ extension GmailService: MessagesThreadProvider { return continuation.resume(throwing: AppErr.cast("GTLRGmail_Thread")) } - guard let threadMsg = thread.messages else { - let empty = MessageThread( - identifier: identifier, - snippet: snippet, - path: path, - messages: [] - ) - return continuation.resume(returning: empty) - } - - let messages = threadMsg.compactMap { try? Message(gmailMessage: $0) } + let messages = thread.messages?.compactMap { try? Message(gmailMessage: $0) } ?? [] let result = MessageThread( identifier: thread.identifier, diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 88c0755e8..ce1726b46 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -8,7 +8,6 @@ import FlowCryptUI import Foundation -import GoogleAPIClientForREST_Gmail import FlowCryptCommon import UIKit @@ -216,21 +215,21 @@ final class ComposeMessageService { } // MARK: - Drafts - private var draft: GTLRGmail_Draft? - func encryptAndSaveDraft(message: SendableMsg, threadId: String?) async throws { + private var draftId: String? + func encryptAndSaveDraft(message: SendableMsg, threadId: String?, draftId: String?) async throws { do { let mime = try await core.composeEmail( msg: message, fmt: .encryptInline ).mimeEncoded - draft = try await draftGateway?.saveDraft( + self.draftId = try await draftGateway?.saveDraft( input: MessageGatewayInput( mime: mime, threadId: threadId ), - draft: draft - ) + draftId: draftId + ).identifier } catch { throw ComposeMessageError.gatewayError(error) } @@ -267,7 +266,7 @@ final class ComposeMessageService { ) // cleaning any draft saved/created/fetched during editing - if let draftId = draft?.identifier { + if let draftId = draftId { await draftGateway?.deleteDraft(with: draftId) } diff --git a/FlowCryptUI/Cell Nodes/LabelCellNode.swift b/FlowCryptUI/Cell Nodes/LabelCellNode.swift index 142fa5f75..0e38eb524 100644 --- a/FlowCryptUI/Cell Nodes/LabelCellNode.swift +++ b/FlowCryptUI/Cell Nodes/LabelCellNode.swift @@ -7,7 +7,6 @@ // import AsyncDisplayKit -import Foundation public final class LabelCellNode: CellNode { public struct Input { @@ -17,6 +16,8 @@ public final class LabelCellNode: CellNode { let spacing: CGFloat let accessibilityIdentifier: String? let labelAccessibilityIdentifier: String? + let actionButtonImageName: String? + let action: (() -> Void)? public init( title: NSAttributedString, @@ -24,7 +25,9 @@ public final class LabelCellNode: CellNode { insets: UIEdgeInsets = .deviceSpecificTextInsets(top: 8, bottom: 8), spacing: CGFloat = 4, accessibilityIdentifier: String? = nil, - labelAccessibilityIdentifier: String? = nil + labelAccessibilityIdentifier: String? = nil, + actionButtonImageName: String? = nil, + action: (() -> Void)? = nil ) { self.title = title self.text = text @@ -32,12 +35,16 @@ public final class LabelCellNode: CellNode { self.spacing = spacing self.accessibilityIdentifier = accessibilityIdentifier self.labelAccessibilityIdentifier = labelAccessibilityIdentifier + self.actionButtonImageName = actionButtonImageName + self.action = action } } private let titleNode = ASTextNode2() private let textNode = ASTextNode2() + private let actionButtonNode = ASButtonNode() private let input: Input + private var action: (() -> Void)? public init(input: Input) { self.input = input @@ -47,18 +54,46 @@ public final class LabelCellNode: CellNode { titleNode.accessibilityIdentifier = input.labelAccessibilityIdentifier textNode.attributedText = input.text textNode.accessibilityIdentifier = input.accessibilityIdentifier + + action = input.action + actionButtonNode.addTarget(self, action: #selector(onActionButtonTap), forControlEvents: .touchUpInside) + + if let imageName = input.actionButtonImageName { + actionButtonNode.setImage(UIImage(systemName: imageName)?.tinted(.secondaryLabel), for: .normal) + } + } + + @objc private func onActionButtonTap() { + action?() } override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { - ASInsetLayoutSpec( - insets: input.insets, - child: ASStackLayoutSpec( - direction: .vertical, + let labelSpec = ASStackLayoutSpec( + direction: .vertical, + spacing: input.spacing, + justifyContent: .start, + alignItems: .start, + children: [titleNode, textNode] + ) + if action != nil { + actionButtonNode.style.preferredSize = CGSize(width: 36, height: 44) + + let spec = ASStackLayoutSpec( + direction: .horizontal, spacing: input.spacing, - justifyContent: .start, - alignItems: .start, - children: [titleNode, textNode] + justifyContent: .spaceBetween, + alignItems: .stretch, + children: [labelSpec, actionButtonNode] ) - ) + return ASInsetLayoutSpec( + insets: input.insets, + child: spec + ) + } else { + return ASInsetLayoutSpec( + insets: input.insets, + child: labelSpec + ) + } } } diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift index 237c9b0f8..ca537b0e1 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift @@ -12,7 +12,7 @@ import UIKit public final class ThreadMessageInfoCellNode: CellNode { // MARK: - Input public struct Input { - public let encryptionBadge: BadgeNode.Input + public let encryptionBadge: BadgeNode.Input? public let signatureBadge: BadgeNode.Input? public let sender: NSAttributedString public let recipientLabel: NSAttributedString @@ -26,7 +26,7 @@ public final class ThreadMessageInfoCellNode: CellNode { public let index: Int public init( - encryptionBadge: BadgeNode.Input, + encryptionBadge: BadgeNode.Input?, signatureBadge: BadgeNode.Input?, sender: NSAttributedString, recipientLabel: NSAttributedString, @@ -148,7 +148,7 @@ public final class ThreadMessageInfoCellNode: CellNode { bccRecipients: input.bccRecipients ) ) - private lazy var encryptionNode = BadgeNode(input: input.encryptionBadge) + private lazy var encryptionNode: BadgeNode? = input.encryptionBadge.map(BadgeNode.init) private lazy var signatureNode: BadgeNode? = input.signatureBadge.map(BadgeNode.init) // MARK: - Properties diff --git a/Gemfile.lock b/Gemfile.lock index 0ee916247..b996bd04e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,8 +17,8 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.624.0) - aws-sdk-core (3.138.0) + aws-partitions (1.626.0) + aws-sdk-core (3.142.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) diff --git a/package-lock.json b/package-lock.json index c9b25dd20..a95c14781 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "eslint-plugin-jsdoc": "^39.3.6", "eslint-plugin-no-only-tests": "3.0.0", "eslint-plugin-prefer-arrow": "^1.2.3", + "git-format-staged": "^3.0.0", "husky": "^8.0.1", "lint-staged": "^13.0.0" } @@ -1385,6 +1386,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/git-format-staged": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/git-format-staged/-/git-format-staged-3.0.0.tgz", + "integrity": "sha512-cdDJxV06qY8ucBsW/uIFR4PYN/kDHl43nG8yg+VPPaDeLAf8hEPhEIJTeJ+yRClxcDSpoTmDPiFZUoxFx1wPCg==", + "dev": true, + "bin": { + "git-format-staged": "git-format-staged" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4161,6 +4171,12 @@ "get-intrinsic": "^1.1.1" } }, + "git-format-staged": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/git-format-staged/-/git-format-staged-3.0.0.tgz", + "integrity": "sha512-cdDJxV06qY8ucBsW/uIFR4PYN/kDHl43nG8yg+VPPaDeLAf8hEPhEIJTeJ+yRClxcDSpoTmDPiFZUoxFx1wPCg==", + "dev": true + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", diff --git a/package.json b/package.json index ee8cdd882..3a68b6fb6 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "eslint-plugin-jsdoc": "^39.3.6", "eslint-plugin-no-only-tests": "3.0.0", "eslint-plugin-prefer-arrow": "^1.2.3", + "git-format-staged": "^3.0.0", "husky": "^8.0.1", "lint-staged": "^13.0.0" }, @@ -35,4 +36,4 @@ "npx eslint" ] } -} \ No newline at end of file +} From 8562389bda3a829e542040bc000c5805250db4f5 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 12 Sep 2022 13:55:29 +0300 Subject: [PATCH 07/56] delete drafts from thread screen --- .../xcshareddata/swiftpm/Package.resolved | 8 +-- .../Threads/ThreadDetailsViewController.swift | 7 ++- .../Message Gateway/GmailService+draft.swift | 4 +- .../Message Gateway/GmailService+send.swift | 2 +- .../Message Gateway/Imap+send.swift | 2 +- .../Gmail+MessageOperations.swift | 57 +++++++++++-------- .../Imap+MessageOperations.swift | 42 ++++++-------- .../MessageOperationsProvider.swift | 12 ++-- .../MessagesThreadOperationsProvider.swift | 8 +-- .../SendAs Services/SendAsService.swift | 4 +- 10 files changed, 73 insertions(+), 73 deletions(-) diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index e1bc202b4..35eb47d51 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-cocoa", "state" : { - "revision" : "ae0ceea258fd8f58392881cc4b2228bff62f74f5", - "version" : "10.28.7" + "revision" : "28c488974f544c9affc4eacdd5b9dfc6785ebbc0", + "version" : "10.29.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-core", "state" : { - "revision" : "6f6a0f415bd33cf2ced4467e36a47f7c84f0a1d7", - "version" : "12.5.1" + "revision" : "5da7744b4056ad185c025bccf0924f17f73f7a91", + "version" : "12.6.0" } }, { diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 5b9cc3b72..f36be02f5 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -640,10 +640,11 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { text: messageData.body.textWithoutThreadQuote.attributed(color: .secondaryLabel), actionButtonImageName: "trash", action: { [weak self] in - guard let id = messageData.draftIdentifier else { return } - Task { - await self?.draftGateway?.deleteDraft(with: id) + try await self?.messageOperationsProvider.deleteMessage( + id: messageData.identifier, + from: nil + ) } } ) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index ce109244a..14c93bed6 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -34,9 +34,9 @@ extension GmailService: DraftGateway { } func deleteDraft(with identifier: String) async { - await withCheckedContinuation { (continuation: CheckedContinuation) in + await withCheckedContinuation { continuation in let query = GTLRGmailQuery_UsersDraftsDelete.query(withUserId: .me, identifier: identifier) - gmailService.executeQuery(query) { response, object, error in + gmailService.executeQuery(query) { _, _, _ in return continuation.resume() } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift index ea28a723a..91c09c59d 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift @@ -33,7 +33,7 @@ extension GmailService: MessageGateway { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift index f2ae9f8d9..52f281a0f 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift @@ -14,7 +14,7 @@ extension Imap: MessageGateway { if let error = error { return continuation.resume(throwing: error) } - return continuation.resume(returning: ()) + return continuation.resume() } } catch { return continuation.resume(throwing: ImapError.noSession) diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift index e026b7339..cd9bf6f17 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift @@ -10,25 +10,25 @@ import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: MessageOperationsProvider { - func markAsUnread(message: Message, folder: String) async throws { - try await update(message: message, labelsToAdd: [.unread]) + func markAsUnread(id: Identifier, folder: String) async throws { + try await updateMessage(id: id, labelsToAdd: [.unread]) } - func markAsRead(message: Message, folder: String) async throws { - try await update(message: message, labelsToRemove: [.unread]) + func markAsRead(id: Identifier, folder: String) async throws { + try await updateMessage(id: id, labelsToRemove: [.unread]) } - func moveMessageToInbox(message: Message, folderPath: String) async throws { - try await update(message: message, labelsToAdd: [.inbox]) + func moveMessageToInbox(id: Identifier, folderPath: String) async throws { + try await updateMessage(id: id, labelsToAdd: [.inbox]) } - func moveMessageToTrash(message: Message, trashPath: String?, from folder: String) async throws { - try await update(message: message, labelsToAdd: [.trash]) + func moveMessageToTrash(id: Identifier, trashPath: String?, from folder: String) async throws { + try await updateMessage(id: id, labelsToAdd: [.trash]) } - func delete(message: Message, from folderPath: String?) async throws { + func deleteMessage(id: Identifier, from folderPath: String?) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - guard let identifier = message.identifier.stringId else { + guard let identifier = id.stringId else { return continuation.resume(throwing: GmailServiceError.missingMessageInfo("id")) } @@ -41,24 +41,31 @@ extension GmailService: MessageOperationsProvider { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } func emptyFolder(path: String) async throws { - let messageIdentifiers = try await fetchAllMessageIdentifers(for: path) + let messageIdentifiers = try await fetchAllMessageIdentifiers(for: path) try await batchDeleteMessages(identifiers: messageIdentifiers, from: path) } - private func fetchAllMessageIdentifers(for path: String, token: String? = nil, result: [String] = []) async throws -> [String] { + private func fetchAllMessageIdentifiers( + for path: String, + token: String? = nil, + result: [String] = [] + ) async throws -> [String] { let context = FetchMessageContext(folderPath: path, count: 500, pagination: .byNextPage(token: token)) let list = try await fetchMessagesList(using: context) - var newResult = (list.messages?.compactMap(\.identifier) ?? []) + result + + let newResult = (list.messages?.compactMap(\.identifier) ?? []) + result + if let nextPageToken = list.nextPageToken { - newResult = try await fetchAllMessageIdentifers(for: path, token: nextPageToken, result: newResult) + return try await fetchAllMessageIdentifiers(for: path, token: nextPageToken, result: newResult) + } else { + return newResult } - return newResult } func batchDeleteMessages(identifiers: [String], from folderPath: String?) async throws { @@ -71,14 +78,14 @@ extension GmailService: MessageOperationsProvider { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } - func archiveMessage(message: Message, folderPath: String) async throws { - try await update( - message: message, + func archiveMessage(id: Identifier, folderPath: String) async throws { + try await updateMessage( + id: id, labelsToRemove: [.inbox] ) } @@ -109,18 +116,18 @@ extension GmailService: MessageOperationsProvider { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } - private func update( - message: Message, + private func updateMessage( + id: Identifier, labelsToAdd: [MessageLabel] = [], labelsToRemove: [MessageLabel] = [] ) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - guard let identifier = message.identifier.stringId else { + guard let identifier = id.stringId else { return continuation.resume(throwing: GmailServiceError.missingMessageInfo("id")) } let request = GTLRGmail_ModifyMessageRequest() @@ -136,7 +143,7 @@ extension GmailService: MessageOperationsProvider { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift index dc13e5158..828730344 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift @@ -11,51 +11,43 @@ import MailCore extension Imap: MessageOperationsProvider { - func markAsUnread(message: Message, folder: String) async throws { - guard let identifier = message.identifier.intId else { + func markAsUnread(id: Identifier, folder: String) async throws { + guard let identifier = id.intId else { throw ImapError.missingMessageInfo("intId") } try await executeVoid("markAsUnread", { sess, respond in sess.storeFlagsOperation( withFolder: folder, uids: MCOIndexSet(index: UInt64(identifier)), - kind: MCOIMAPStoreFlagsRequestKind.remove, - flags: [MCOMessageFlag.seen] + kind: .remove, + flags: [.seen] ).start { error in respond(error) } }) } - func markAsRead(message: Message, folder: String) async throws { - guard let identifier = message.identifier.intId else { + func markAsRead(id: Identifier, folder: String) async throws { + guard let identifier = id.intId else { throw ImapError.missingMessageInfo("intId") } - var flags: MCOMessageFlag = [] - let imapFlagValues = message.labels.map(\.imapFlagValue) - // keep previous flags - for value in imapFlagValues { - flags.insert(MCOMessageFlag(rawValue: value)) - } - // add seen flag - flags.insert(MCOMessageFlag.seen) try await executeVoid("markAsRead", { sess, respond in sess.storeFlagsOperation( withFolder: folder, uids: MCOIndexSet(index: UInt64(identifier)), - kind: MCOIMAPStoreFlagsRequestKind.add, - flags: flags + kind: .add, + flags: [.seen] ).start { error in respond(error) } }) } - func moveMessageToInbox(message: Message, folderPath: String) async throws { + func moveMessageToInbox(id: Identifier, folderPath: String) async throws { // should be implemented later - guard message.identifier.intId != nil else { + guard id.intId != nil else { throw ImapError.missingMessageInfo("intId") } } - func moveMessageToTrash(message: Message, trashPath: String?, from folder: String) async throws { - guard let identifier = message.identifier.intId else { + func moveMessageToTrash(id: Identifier, trashPath: String?, from folder: String) async throws { + guard let identifier = id.intId else { throw ImapError.missingMessageInfo("intId") } guard let trashPath = trashPath else { @@ -74,8 +66,8 @@ extension Imap: MessageOperationsProvider { }) } - func delete(message: Message, from folderPath: String?) async throws { - guard let identifier = message.identifier.intId else { + func deleteMessage(id: Identifier, from folderPath: String?) async throws { + guard let identifier = id.intId else { throw ImapError.missingMessageInfo("intId") } guard let folderPath = folderPath else { @@ -101,7 +93,7 @@ extension Imap: MessageOperationsProvider { sess.storeFlagsOperation( withFolder: folder, uids: MCOIndexSet(index: UInt64(identifier)), - kind: MCOIMAPStoreFlagsRequestKind.set, + kind: .set, flags: flags ).start { error in respond(error) } }) @@ -115,8 +107,8 @@ extension Imap: MessageOperationsProvider { }) } - func archiveMessage(message: Message, folderPath: String) async throws { - guard let identifier = message.identifier.intId else { + func archiveMessage(id: Identifier, folderPath: String) async throws { + guard let identifier = id.intId else { throw ImapError.missingMessageInfo("intId") } try await pushUpdatedMsgFlags(with: identifier, folder: folderPath, flags: MCOMessageFlag.deleted) diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift index 40e13b7c6..f327a2407 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift @@ -10,12 +10,12 @@ import Foundation import FlowCryptCommon protocol MessageOperationsProvider { - func moveMessageToTrash(message: Message, trashPath: String?, from folder: String) async throws - func delete(message: Message, from folderPath: String?) async throws - func moveMessageToInbox(message: Message, folderPath: String) async throws - func archiveMessage(message: Message, folderPath: String) async throws - func markAsUnread(message: Message, folder: String) async throws - func markAsRead(message: Message, folder: String) async throws + func moveMessageToTrash(id: Identifier, trashPath: String?, from folder: String) async throws + func deleteMessage(id: Identifier, from folderPath: String?) async throws + func moveMessageToInbox(id: Identifier, folderPath: String) async throws + func archiveMessage(id: Identifier, folderPath: String) async throws + func markAsUnread(id: Identifier, folder: String) async throws + func markAsRead(id: Identifier, folder: String) async throws func emptyFolder(path: String) async throws func batchDeleteMessages(identifiers: [String], from folderPath: String?) async throws } diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift index 3dd74a7d0..0b9946c80 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift @@ -35,7 +35,7 @@ extension GmailService: MessagesThreadOperationsProvider { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - continuation.resume(returning: ()) + return continuation.resume() } } } @@ -61,8 +61,8 @@ extension GmailService: MessagesThreadOperationsProvider { for message in thread.messages { taskGroup.addTask { asRead - ? try await self.markAsRead(message: message, folder: folder) - : try await self.markAsUnread(message: message, folder: folder) + ? try await self.markAsRead(id: message.identifier, folder: folder) + : try await self.markAsUnread(id: message.identifier, folder: folder) } } @@ -100,7 +100,7 @@ extension GmailService: MessagesThreadOperationsProvider { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume(returning: ()) + return continuation.resume() } } } diff --git a/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift b/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift index ccb1d9ff7..e375cfdb4 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift @@ -49,9 +49,9 @@ final class SendAsService: SendAsServiceType { try self.localSendAsProvider.save(list: fetchedList, for: user) // return list - continuation.resume(returning: fetchedList) + return continuation.resume(returning: fetchedList) } catch { - continuation.resume(throwing: error) + return continuation.resume(throwing: error) } } } From 90e429081ceff4c7480085e26b8b11d2de020f87 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 12 Sep 2022 16:54:48 +0300 Subject: [PATCH 08/56] improve draft deletion --- FlowCrypt/Common UI/AttachmentManager.swift | 16 +--- .../Compose/ComposeViewController.swift | 1 - .../ComposeViewController+Attachment.swift | 21 ++--- .../ComposeViewController+Drafts.swift | 2 +- .../ComposeViewController+Nodes.swift | 18 ++--- .../Threads/MessageActionsHandler.swift | 30 +++---- .../Threads/ThreadDetailsViewController.swift | 78 +++++++++++-------- .../MessagesThreadOperationsProvider.swift | 2 +- .../ComposeMessageService.swift | 24 +++--- .../Resources/en.lproj/Localizable.strings | 2 + .../Mocks/DraftGatewayMock.swift | 3 +- .../UIViewControllerExtensions.swift | 6 +- 12 files changed, 95 insertions(+), 108 deletions(-) diff --git a/FlowCrypt/Common UI/AttachmentManager.swift b/FlowCrypt/Common UI/AttachmentManager.swift index c4bcaddad..7471efd05 100644 --- a/FlowCrypt/Common UI/AttachmentManager.swift +++ b/FlowCrypt/Common UI/AttachmentManager.swift @@ -25,22 +25,14 @@ final class AttachmentManager: NSObject { self.filesManager = filesManager } + @MainActor private func showFileSharedAlert(with url: URL) { - let alert = UIAlertController( + controller?.showAlertWithAction( title: "message_attachment_saved_successfully_title".localized, message: "message_attachment_saved_successfully_message".localized, - preferredStyle: .alert + actionButtonTitle: "open".localized, + onAction: { _ in UIApplication.shared.open(url) } ) - - let cancel = UIAlertAction(title: "cancel".localized, style: .cancel) { _ in } - let open = UIAlertAction(title: "open".localized, style: .default) { _ in - UIApplication.shared.open(url) - } - - alert.addAction(cancel) - alert.addAction(open) - - controller?.present(alert, animated: true) } @MainActor diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 8193cefc6..1d6b0bae1 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -246,5 +246,4 @@ extension ComposeViewController: FilesManagerPresenter {} // update draft, don't create a new one each time // decrypt draft body -// add delete button for drafts in thread view // add delete button for drafts in compose view diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift index eacc3b7ed..6b6012db1 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Attachment.swift @@ -65,24 +65,13 @@ extension ComposeViewController { } private func showNoAccessToCameraAlert() { - let alert = UIAlertController( + showAlertWithAction( title: "files_picking_no_camera_access_error_title".localized, message: "files_picking_no_camera_access_error_message".localized, - preferredStyle: .alert + actionButtonTitle: "settings".localized, + onAction: { _ in + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + } ) - let okAction = UIAlertAction( - title: "ok".localized, - style: .cancel - ) { _ in } - let settingsAction = UIAlertAction( - title: "settings".localized, - style: .default - ) { _ in - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) - } - alert.addAction(okAction) - alert.addAction(settingsAction) - - present(alert, animated: true, completion: nil) } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 872bd51af..dc2dc2e81 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -37,7 +37,6 @@ extension ComposeViewController { } } - // TODO: Better naming func saveDraftIfNeeded(isForceSave: Bool = false) { guard isForceSave || shouldSaveDraft() else { return } @@ -48,6 +47,7 @@ extension ComposeViewController { contextToSend: contextToSend, isDraft: true ) + try await composeMessageService.encryptAndSaveDraft( message: sendableMsg, threadId: input.threadId, diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index 1a650bb61..dffa600d2 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -28,20 +28,20 @@ extension ComposeViewController { func showRecipientLabelIfNecessary() { let isRecipientLoading = self.contextToSend.recipients.filter { $0.state == decorator.recipientIdleState }.isNotEmpty guard !isRecipientLoading, - self.contextToSend.recipients.isNotEmpty, - self.userTappedOutSideRecipientsArea else { + contextToSend.recipients.isNotEmpty, + userTappedOutSideRecipientsArea else { return } - if !self.shouldShowEmailRecipientsLabel { - self.shouldShowEmailRecipientsLabel = true - self.userTappedOutSideRecipientsArea = false - self.reload(sections: [.recipientsLabel, .recipients(.from), .recipients(.to), .recipients(.cc), .recipients(.bcc)]) + if !shouldShowEmailRecipientsLabel { + shouldShowEmailRecipientsLabel = true + userTappedOutSideRecipientsArea = false + reload(sections: [.recipientsLabel, .recipients(.from), .recipients(.to), .recipients(.cc), .recipients(.bcc)]) } } func hideRecipientLabel() { - self.shouldShowEmailRecipientsLabel = false - self.reload(sections: [.recipientsLabel, .recipients(.from), .recipients(.to), .recipients(.cc), .recipients(.bcc)]) + shouldShowEmailRecipientsLabel = false + reload(sections: [.recipientsLabel, .recipients(.from), .recipients(.to), .recipients(.cc), .recipients(.bcc)]) } func setupSubjectNode() { @@ -163,7 +163,7 @@ extension ComposeViewController { } } .then { - let message = contextToSend.message ?? input.text ?? "" + let message = contextToSend.message ?? "" let attributedString = decorator.styledMessage(with: message) let mutableString = NSMutableAttributedString(attributedString: attributedString) let textNode = $0 diff --git a/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift b/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift index bb717bb8f..983a3bb10 100644 --- a/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift +++ b/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift @@ -126,29 +126,19 @@ extension MessageActionsHandler where Self: UIViewController { private func deleteMessage(trashPath: String) { guard currentFolderPath.caseInsensitiveCompare(trashPath) != .orderedSame else { - awaitUserDeleteConfirmation { [weak self] in - self?.permanentlyDelete() - } + showAlertWithAction( + title: "message_permanently_delete_title".localized, + message: "message_permanently_delete".localized, + actionButtonTitle: "delete".localized, + actionStyle: .destructive, + onAction: { [weak self] _ in + self?.permanentlyDelete() + } + ) + return } moveToTrash(with: trashPath) } - - private func awaitUserDeleteConfirmation(_ completion: @escaping () -> Void) { - let alert = UIAlertController( - title: "message_permanently_delete_title".localized, - message: "message_permanently_delete".localized, - preferredStyle: .alert - ) - alert.addAction( - UIAlertAction(title: "cancel".localized, style: .default) - ) - alert.addAction( - UIAlertAction(title: "delete".localized, style: .destructive) { _ in - completion() - } - ) - present(alert, animated: true, completion: nil) - } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index f36be02f5..6fd48a081 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -421,29 +421,20 @@ extension ThreadDetailsViewController { } private func handleAttachmentDecryptError(_ error: Error, at indexPath: IndexPath) { - hideSpinner() let message = "message_attachment_corrupted_file".localized - let alertController = UIAlertController( + showAlertWithAction( title: "message_attachment_decrypt_error".localized, message: "\n\(error.errorMessage)\n\n\(message)", - preferredStyle: .alert - ) - - let downloadAction = UIAlertAction(title: "download".localized, style: .default) { [weak self] _ in - guard let attachment = self?.input[indexPath.section - 1].processedMessage?.attachments[indexPath.row - 2] else { - return + actionButtonTitle: "download".localized, + actionAccessibilityIdentifier: "aid-download-button", + onAction: { [weak self] _ in + guard let attachment = self?.input[indexPath.section - 1].processedMessage?.attachments[indexPath.row - 2] else { + return + } + self?.show(attachment: attachment) } - self?.show(attachment: attachment) - } - downloadAction.accessibilityIdentifier = "aid-download-button" - let cancelAction = UIAlertAction(title: "cancel".localized, style: .cancel) - cancelAction.accessibilityIdentifier = "aid-cancel-button" - - alertController.addAction(downloadAction) - alertController.addAction(cancelAction) - - present(alertController, animated: true) + ) } private func handleWrongPassPhrase(_ passPhrase: String? = nil, indexPath: IndexPath) { @@ -633,22 +624,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } if message.rawMessage.isDraft { - let messageData = message.processedMessage?.message ?? message.rawMessage - return LabelCellNode( - input: .init( - title: "compose_draft".localized.attributed(color: .red), - text: messageData.body.textWithoutThreadQuote.attributed(color: .secondaryLabel), - actionButtonImageName: "trash", - action: { [weak self] in - Task { - try await self?.messageOperationsProvider.deleteMessage( - id: messageData.identifier, - from: nil - ) - } - } - ) - ) + return self.draftNode(messageIndex: messageIndex) } guard let processedMessage = message.processedMessage else { @@ -693,6 +669,40 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { section > 0 && section < input.count ? 1 / UIScreen.main.nativeScale : 0 } + private func draftNode(messageIndex: Int) -> ASCellNode { + let message = input[messageIndex] + let messageData = message.processedMessage?.message ?? message.rawMessage + return LabelCellNode( + input: .init( + title: "compose_draft".localized.attributed(color: .red), + text: messageData.body.textWithoutThreadQuote.attributed(color: .secondaryLabel), + actionButtonImageName: "trash", + action: { [weak self] in + self?.deleteDraft(id: messageData.identifier, at: messageIndex) + } + ) + ) + } + + private func deleteDraft(id: Identifier, at index: Int) { + showAlertWithAction( + title: "draft_delete_confirmation".localized, + message: nil, + actionButtonTitle: "delete".localized, + actionStyle: .destructive, + onAction: { [weak self] _ in + Task { + try await self?.messageOperationsProvider.deleteMessage( + id: id, + from: nil + ) + self?.input.remove(at: index) + self?.node.deleteSections([index + 1], with: .automatic) + } + } + ) + } + private func dividerView() -> UIView { UIView().then { let frame = CGRect(x: 8, y: 0, width: view.frame.width - 16, height: 1 / UIScreen.main.nativeScale) diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift index 0b9946c80..00321b917 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift @@ -73,7 +73,7 @@ extension GmailService: MessagesThreadOperationsProvider { func archive(thread: MessageThread, in folder: String) async throws { // manually updated each message rather than using update(thread:...) method // https://github.com/FlowCrypt/flowcrypt-ios/pull/1769#discussion_r932964129 - try await self.archiveBatchMessages(messages: thread.messages) + try await archiveBatchMessages(messages: thread.messages) } private func update( diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index ce1726b46..4b2d4fea8 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -151,7 +151,8 @@ final class ComposeMessageService { let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) let validPubKeys = try validate( recipients: recipientsWithPubKeys, - hasMessagePassword: contextToSend.hasMessagePassword + hasMessagePassword: contextToSend.hasMessagePassword, + ignoreErrors: isDraft ) let signingPrv = try await prepareSigningKey(senderEmail: contextToSend.sender) @@ -191,7 +192,8 @@ final class ComposeMessageService { private func validate( recipients: [RecipientWithSortedPubKeys], - hasMessagePassword: Bool + hasMessagePassword: Bool, + ignoreErrors: Bool = false ) throws -> [String] { func contains(keyState: PubKeyState) -> Bool { recipients.contains(where: { $0.keyState == keyState }) @@ -200,15 +202,17 @@ final class ComposeMessageService { logger.logDebug("validate recipients: \(recipients)") logger.logDebug("validate recipient keyStates: \(recipients.map(\.keyState))") - guard hasMessagePassword || !contains(keyState: .empty) else { - throw MessageValidationError.noPubRecipients - } + if !ignoreErrors { + guard hasMessagePassword || !contains(keyState: .empty) else { + throw MessageValidationError.noPubRecipients + } - guard !contains(keyState: .expired) else { - throw MessageValidationError.expiredKeyRecipients - } - guard !contains(keyState: .revoked) else { - throw MessageValidationError.revokedKeyRecipients + guard !contains(keyState: .expired) else { + throw MessageValidationError.expiredKeyRecipients + } + guard !contains(keyState: .revoked) else { + throw MessageValidationError.revokedKeyRecipients + } } return recipients.flatMap(\.activePubKeys).map(\.armored) diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index eb35bdcd9..d4cc0f9a5 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -404,6 +404,8 @@ "message_permanently_delete" = "You're about to permanently delete a message"; "message_permanently_delete_title" = "Are you sure?"; +"draft_delete_confirmation" = "Delete draft?"; + "contacts_fingerprint" = "Fingerprint:"; "contacts_created" = "Created:"; diff --git a/FlowCryptAppTests/Mocks/DraftGatewayMock.swift b/FlowCryptAppTests/Mocks/DraftGatewayMock.swift index 6bdd985a0..50151a6cc 100644 --- a/FlowCryptAppTests/Mocks/DraftGatewayMock.swift +++ b/FlowCryptAppTests/Mocks/DraftGatewayMock.swift @@ -7,11 +7,10 @@ // @testable import FlowCrypt -import Foundation import GoogleAPIClientForREST_Gmail class DraftGatewayMock: DraftGateway { - func saveDraft(input: MessageGatewayInput, draft: GTLRGmail_Draft?) async throws -> GTLRGmail_Draft { + func saveDraft(input: MessageGatewayInput, draftId: String?) async throws -> GTLRGmail_Draft { return GTLRGmail_Draft() } diff --git a/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift b/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift index 7355affdb..de98d4121 100644 --- a/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift +++ b/FlowCryptCommon/Extensions/UIViewControllerExtensions.swift @@ -84,10 +84,11 @@ public extension UIViewController { @MainActor func showAlertWithAction( title: String?, - message: String, + message: String?, cancelButtonTitle: String = "cancel".localized, actionButtonTitle: String, actionAccessibilityIdentifier: String? = nil, + actionStyle: UIAlertAction.Style = .default, onAction: ((UIAlertAction) -> Void)?, onCancel: ((UIAlertAction) -> Void)? = nil ) { @@ -100,7 +101,7 @@ public extension UIViewController { ) let action = UIAlertAction( title: actionButtonTitle, - style: .default, + style: actionStyle, handler: onAction ) action.accessibilityIdentifier = actionAccessibilityIdentifier @@ -109,6 +110,7 @@ public extension UIViewController { style: .cancel, handler: onCancel ) + cancel.accessibilityIdentifier = "aid-cancel-button" alert.addAction(action) alert.addAction(cancel) present(alert, animated: true, completion: nil) From 61d101436a23c553fab667bf6d3964a3a5d439bc Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 12 Sep 2022 21:01:40 +0300 Subject: [PATCH 09/56] fix semaphore build --- .../Compose/ComposeViewController.swift | 1 - .../ComposeViewController+Setup.swift | 40 ++++++++++--------- FlowCrypt/Core/Core.swift | 2 +- .../Message Gateway/GmailService+draft.swift | 2 +- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 1d6b0bae1..52eb8ab52 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -246,4 +246,3 @@ extension ComposeViewController: FilesManagerPresenter {} // update draft, don't create a new one each time // decrypt draft body -// add delete button for drafts in compose view diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index dd3b413b2..9bef2f40d 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -12,25 +12,29 @@ import FlowCryptUI // MARK: - Setup UI extension ComposeViewController { func setupNavigationBar() { + let deleteButton = NavigationBarItemsView.Input( + image: UIImage(systemName: "trash") + ) { [weak self] in + // TODO: + } + let helpButton = NavigationBarItemsView.Input( + image: UIImage(systemName: "questionmark.circle") + ) { [weak self] in + self?.handleInfoTap() + } + let attachmentButton = NavigationBarItemsView.Input( + image: UIImage(systemName: "paperclip") + ) { [weak self] in + self?.handleAttachTap() + } + let sendButton = NavigationBarItemsView.Input( + image: UIImage(systemName: "paperplane"), + accessibilityId: "aid-compose-send" + ) { [weak self] in + self?.handleSendTap() + } navigationItem.rightBarButtonItem = NavigationBarItemsView( - with: [ - NavigationBarItemsView.Input( - image: UIImage(systemName: "questionmark.circle") - ) { [weak self] in - self?.handleInfoTap() - }, - NavigationBarItemsView.Input( - image: UIImage(systemName: "paperclip") - ) { [weak self] in - self?.handleAttachTap() - }, - NavigationBarItemsView.Input( - image: UIImage(systemName: "paperplane"), - accessibilityId: "aid-compose-send" - ) { [weak self] in - self?.handleSendTap() - } - ] + with: [deleteButton, helpButton, attachmentButton, sendButton] ) } diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index 4e9abd7a0..fb5bb8a76 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -183,7 +183,7 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { let blocks = parsed.data .split(separator: 10) // newline separated block jsons, one json per line - .map { data in + .map { data -> MsgBlock in guard let block = try? data.decodeJson(as: MsgBlock.self) else { let content = String(data: data, encoding: .utf8) ?? "(utf err)" return MsgBlock.blockParseErr(with: content) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 14c93bed6..853ba54a6 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -34,7 +34,7 @@ extension GmailService: DraftGateway { } func deleteDraft(with identifier: String) async { - await withCheckedContinuation { continuation in + await withCheckedContinuation { (continuation: CheckedContinuation) in let query = GTLRGmailQuery_UsersDraftsDelete.query(withUserId: .me, identifier: identifier) gmailService.executeQuery(query) { _, _, _ in return continuation.resume() From b492cf2b5471b5107b10d4fc8a0ac3987594483d Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 13 Sep 2022 14:02:12 +0300 Subject: [PATCH 10/56] delete drafts from compose screen --- FlowCrypt.xcodeproj/project.pbxproj | 12 -- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Compose/ComposeViewController.swift | 22 +++- .../ComposeViewController+Setup.swift | 2 +- .../ComposeViewController+TapActions.swift | 26 +++++ .../Controllers/Inbox/InboxRenderable.swift | 2 +- .../Inbox/InboxViewController.swift | 90 ++++++++++++++- .../MsgListViewController.swift | 106 ------------------ .../Threads/ThreadDetailsViewController.swift | 18 ++- .../Message Gateway/GmailService+draft.swift | 9 +- .../Message Gateway/MessageGateway.swift | 2 +- .../MessagesThreadOperationsProvider.swift | 3 +- .../ComposeMessageService.swift | 15 ++- Gemfile.lock | 10 +- 14 files changed, 175 insertions(+), 146 deletions(-) delete mode 100644 FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 49802c283..f136bf6f9 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -179,7 +179,6 @@ 9F3861A227A18AAF00851419 /* .swiftformat in Resources */ = {isa = PBXBuildFile; fileRef = 9F3861A127A18AAF00851419 /* .swiftformat */; }; 9F3861A427A18ABA00851419 /* .swiftformat in Resources */ = {isa = PBXBuildFile; fileRef = 9F3861A327A18ABA00851419 /* .swiftformat */; }; 9F3EF32523B15C1400FA0CEF /* ImapHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EF32423B15C1400FA0CEF /* ImapHelper.swift */; }; - 9F3EF33123B1785600FA0CEF /* MsgListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EF33023B1785600FA0CEF /* MsgListViewController.swift */; }; 9F4163B8265ED61C00106194 /* SetupInitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4163B7265ED61C00106194 /* SetupInitialViewController.swift */; }; 9F4163E6266520B600106194 /* CommonNodesInputs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4163E5266520B600106194 /* CommonNodesInputs.swift */; }; 9F416428266575DC00106194 /* BackupServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F416427266575DC00106194 /* BackupServiceType.swift */; }; @@ -655,7 +654,6 @@ 9F3861A327A18ABA00851419 /* .swiftformat */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftformat; sourceTree = ""; }; 9F3EF32423B15C1400FA0CEF /* ImapHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImapHelper.swift; sourceTree = ""; }; 9F3EF32923B15C9500FA0CEF /* ImapHelperTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImapHelperTest.swift; sourceTree = ""; }; - 9F3EF33023B1785600FA0CEF /* MsgListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgListViewController.swift; sourceTree = ""; }; 9F4163B7265ED61C00106194 /* SetupInitialViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupInitialViewController.swift; sourceTree = ""; }; 9F4163E5266520B600106194 /* CommonNodesInputs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonNodesInputs.swift; sourceTree = ""; }; 9F416427266575DC00106194 /* BackupServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupServiceType.swift; sourceTree = ""; }; @@ -1918,7 +1916,6 @@ 04B4728F1ECE29F600B8266F /* Compose */, D29AFFF02409300600C1387D /* Search */, 5A39F42E239EC32B001F4607 /* Settings */, - D29A0001240C137700C1387D /* MessageList Extension */, 9F778E7F271620AF001D4B21 /* Threads */, ); path = Controllers; @@ -2104,14 +2101,6 @@ path = Views; sourceTree = ""; }; - D29A0001240C137700C1387D /* MessageList Extension */ = { - isa = PBXGroup; - children = ( - 9F3EF33023B1785600FA0CEF /* MsgListViewController.swift */, - ); - path = "MessageList Extension"; - sourceTree = ""; - }; D29A0002240C140500C1387D /* Key Detail Info */ = { isa = PBXGroup; children = ( @@ -2831,7 +2820,6 @@ 04722B45281ABD2C00D0242F /* EKMVcHelper.swift in Sources */, 9F6F3BEF26ADF5DE005BD9C6 /* ComposeMessageError.swift in Sources */, 5ADEDCAF23A3EA9E00EC495E /* KeySettingsViewDecorator.swift in Sources */, - 9F3EF33123B1785600FA0CEF /* MsgListViewController.swift in Sources */, 9F31ABA0232C071700CF87EA /* GlobalRouter.swift in Sources */, 9F5C2A92257E94DF00DE9B4B /* Imap+MessageOperations.swift in Sources */, D27B911F24EFE828002DF0A1 /* RecipientWithSortedPubKeys.swift in Sources */, diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 35eb47d51..d4809eee3 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/openid/AppAuth-iOS.git", "state" : { - "revision" : "33660c271c961f8ce1084cc13f2ea8195e864f7d", - "version" : "1.5.0" + "revision" : "3d36a58a2b736f7bc499453e996a704929b25080", + "version" : "1.6.0" } }, { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 52eb8ab52..7eb00c24a 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -92,6 +92,8 @@ final class ComposeViewController: TableNodeViewController { var composeSubjectNode: ASCellNode! var sendAsList: [SendAsModel] = [] + let onDelete: ((Identifier) -> Void)? + init( appContext: AppContextWithUser, decorator: ComposeViewDecorator = ComposeViewDecorator(), @@ -100,7 +102,8 @@ final class ComposeViewController: TableNodeViewController { messageService: MessageService? = nil, filesManager: FilesManagerType = FilesManager(), photosManager: PhotosManagerType = PhotosManager(), - keyMethods: KeyMethodsType = KeyMethods() + keyMethods: KeyMethodsType = KeyMethods(), + onDelete: ((Identifier) -> Void)? = nil ) async throws { self.appContext = appContext self.input = input @@ -116,11 +119,17 @@ final class ComposeViewController: TableNodeViewController { shouldRunWarmupQuery: true ) let draftGateway = try appContext.getRequiredMailProvider().draftGateway - self.composeMessageService = composeMessageService ?? ComposeMessageService( - appContext: appContext, - keyMethods: keyMethods, - draftGateway: draftGateway - ) + + if let composeMessageService = composeMessageService { + self.composeMessageService = composeMessageService + } else { + self.composeMessageService = try await ComposeMessageService( + appContext: appContext, + keyMethods: keyMethods, + draftGateway: draftGateway + ) + } + self.filesManager = filesManager self.photosManager = photosManager self.pubLookup = PubLookup( @@ -148,6 +157,7 @@ final class ComposeViewController: TableNodeViewController { subject: input.subject, attachments: input.attachments ) + self.onDelete = onDelete super.init(node: TableNode()) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 9bef2f40d..1f6acbe52 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -15,7 +15,7 @@ extension ComposeViewController { let deleteButton = NavigationBarItemsView.Input( image: UIImage(systemName: "trash") ) { [weak self] in - // TODO: + self?.handleTrashTap() } let helpButton = NavigationBarItemsView.Input( image: UIImage(systemName: "questionmark.circle") diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index 252831f6f..6b795cde7 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -30,6 +30,32 @@ extension ComposeViewController { } } + func handleTrashTap() { + showAlertWithAction( + title: "draft_delete_confirmation".localized, + message: nil, + actionButtonTitle: "delete".localized, + actionStyle: .destructive, + onAction: { [weak self] _ in + guard let self = self else { return } + Task { + do { + let messageId = self.input.type.info?.id + try await self.composeMessageService.deleteDraft(messageId: messageId) + + if let messageId = messageId { + self.onDelete?(Identifier(stringId: messageId)) + } + + self.navigationController?.popViewController(animated: true) + } catch { + self.handle(error: error) + } + } + } + ) + } + @objc func handleTableTap() { if case .searchEmails = state, let selectedRecipientType = selectedRecipientType, diff --git a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift index e96ad7729..3fa2889d7 100644 --- a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift +++ b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift @@ -122,7 +122,7 @@ extension InboxRenderable { if message.labels.contains(.draft) { let recipients = message.allRecipients.map(\.shortName).joined(separator: ", ") let title = recipients.isEmpty ? "" : "To: \(recipients)" - return title.attributed() + return title.attributed(.regular(17), color: .lightGray) } else { let title = message.sender?.shortName ?? "message_unknown_sender".localized return title.attributed() diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index cb2d28b42..0bc6fbfce 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -562,7 +562,7 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { } // MARK: - MsgListViewController -extension InboxViewController: MsgListViewController { +extension InboxViewController { func getUpdatedIndex(for message: InboxRenderable) -> Int? { let index = inboxInput.firstIndex(where: { $0.title == message.title && $0.subtitle == message.subtitle && $0.wrappedType == message.wrappedType @@ -642,4 +642,92 @@ extension InboxViewController: MsgListViewController { } } } + + func open(message: InboxRenderable, path: String) { + switch message.wrappedType { + case .message(let message): + if message.isDraft { + open(draft: message, appContext: appContext) + } else { + open(message: message, path: path, appContext: appContext) + } + case .thread(let thread): + open(thread: thread, appContext: appContext) + } + } + + private func open(draft: Message, appContext: AppContextWithUser) { + Task { + do { + let draftInfo = ComposeMessageInput.MessageQuoteInfo( + message: draft, + processed: nil + ) + + let controller = try await ComposeViewController( + appContext: appContext, + input: .init(type: .draft(draftInfo)), + onDelete: { [weak self] identifier in + guard let self = self, + let index = self.inboxInput.firstIndex(where: { $0.wrappedMessage?.identifier == identifier }) else { return } + self.inboxInput.remove(at: index) + self.tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + } + ) + navigationController?.pushViewController(controller, animated: true) + } catch { + showAlert(message: error.localizedDescription) + } + } + } + + private func open(message: Message, path: String, appContext: AppContextWithUser) { + let thread = MessageThread( + identifier: message.threadId, + snippet: nil, + path: path, + messages: [message] + ) + open(thread: thread, appContext: appContext) + } + + private func open(thread: MessageThread, appContext: AppContextWithUser) { + Task { + do { + let viewController = try await ThreadDetailsViewController( + appContext: appContext, + thread: thread + ) { [weak self] action, message in + self?.handleMessageOperation(message: message, action: action) + } + navigationController?.pushViewController(viewController, animated: true) + } catch { + showAlert(message: error.localizedDescription) + } + } + } + + // MARK: Operation + private func handleMessageOperation(message: InboxRenderable, action: MessageAction) { + guard let indexToUpdate = getUpdatedIndex(for: message) else { + return + } + + switch action { + case .markAsRead(let isRead): + updateMessage(isRead: isRead, at: indexToUpdate) + case .moveToTrash, .permanentlyDelete: + removeMessage(at: indexToUpdate) + case .archive, .moveToInbox: + if path.isEmpty { // no need to remove in 'All Mail' folder + updateMessage( + labelsToAdd: action == .moveToInbox ? [.inbox] : [], + labelsToRemove: action == .archive ? [.inbox] : [], + at: indexToUpdate + ) + } else { + removeMessage(at: indexToUpdate) + } + } + } } diff --git a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift b/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift deleted file mode 100644 index a891bf61e..000000000 --- a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// MsgListViewController.swift -// FlowCrypt -// -// Created by Anton Kharchevskyi on 23/12/2019. -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import UIKit - -@MainActor -protocol MsgListViewController { - var path: String { get } - var appContext: AppContextWithUser { get } - - func open(message: InboxRenderable, path: String) - - func getUpdatedIndex(for message: InboxRenderable) -> Int? - func updateMessage(isRead: Bool, at index: Int) - func updateMessage(labelsToAdd: [MessageLabel], labelsToRemove: [MessageLabel], at index: Int) - func removeMessage(at index: Int) -} - -extension MsgListViewController where Self: UIViewController { - func open(message: InboxRenderable, path: String) { - switch message.wrappedType { - case .message(let message): - if message.isDraft { - open(draft: message, appContext: appContext) - } else { - open(message: message, path: path, appContext: appContext) - } - case .thread(let thread): - open(thread: thread, appContext: appContext) - } - } - - private func open(draft: Message, appContext: AppContextWithUser) { - Task { - do { - let draftInfo = ComposeMessageInput.MessageQuoteInfo( - message: draft, - processed: nil - ) - - let controller = try await ComposeViewController( - appContext: appContext, - input: .init(type: .draft(draftInfo)) - ) - navigationController?.pushViewController(controller, animated: true) - } catch { - showAlert(message: error.localizedDescription) - } - } - } - - private func open(message: Message, path: String, appContext: AppContextWithUser) { - let thread = MessageThread( - identifier: message.threadId, - snippet: nil, - path: path, - messages: [message] - ) - open(thread: thread, appContext: appContext) - } - - private func open(thread: MessageThread, appContext: AppContextWithUser) { - Task { - do { - let viewController = try await ThreadDetailsViewController( - appContext: appContext, - thread: thread - ) { [weak self] action, message in - self?.handleMessageOperation(message: message, action: action) - } - navigationController?.pushViewController(viewController, animated: true) - } catch { - showAlert(message: error.localizedDescription) - } - } - } - - // MARK: Operation - private func handleMessageOperation(message: InboxRenderable, action: MessageAction) { - guard let indexToUpdate = getUpdatedIndex(for: message) else { - return - } - - switch action { - case .markAsRead(let isRead): - updateMessage(isRead: isRead, at: indexToUpdate) - case .moveToTrash, .permanentlyDelete: - removeMessage(at: indexToUpdate) - case .archive, .moveToInbox: - if path.isEmpty { // no need to remove in 'All Mail' folder - updateMessage( - labelsToAdd: action == .moveToInbox ? [.inbox] : [], - labelsToRemove: action == .archive ? [.inbox] : [], - at: indexToUpdate - ) - } else { - removeMessage(at: indexToUpdate) - } - } - } -} diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 6fd48a081..85070dbe6 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -163,7 +163,15 @@ extension ThreadDetailsViewController { let controller = try await ComposeViewController( appContext: appContext, - input: .init(type: .draft(draftInfo)) + input: .init(type: .draft(draftInfo)), + onDelete: { [weak self] identifier in + guard let self = self, + let index = self.input.firstIndex(where: { $0.rawMessage.identifier == identifier }) + else { return } + + self.input.remove(at: index) + self.node.deleteSections([index + 1], with: .automatic) + } ) navigationController?.pushViewController(controller, animated: true) } catch { @@ -528,8 +536,8 @@ extension ThreadDetailsViewController: MessageActionsHandler { navigationController?.popViewController(animated: true) } - private func handleMessageAction(error: Error) { - logger.logError("Error mark as read \(error)") + private func handleMessageAction(error: Error, action: MessageAction) { + logger.logError("\(action.error ?? "Error: ") \(error)") hideSpinner() } @@ -582,7 +590,7 @@ extension ThreadDetailsViewController: MessageActionsHandler { handleSuccessfulMessage(action: action) } catch { - handleMessageAction(error: error) + handleMessageAction(error: error, action: action) } } } @@ -718,7 +726,7 @@ extension ThreadDetailsViewController: NavigationChildController { func handleBackButtonTap() { logger.logInfo("Back button. Messages are all read") onComplete( - MessageAction.markAsRead(true), + .markAsRead(true), .init(thread: thread, folderPath: currentFolderPath) ) navigationController?.popViewController(animated: true) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 853ba54a6..2e87ccd8d 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -33,10 +33,13 @@ extension GmailService: DraftGateway { } } - func deleteDraft(with identifier: String) async { - await withCheckedContinuation { (continuation: CheckedContinuation) in + func deleteDraft(with identifier: String) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in let query = GTLRGmailQuery_UsersDraftsDelete.query(withUserId: .me, identifier: identifier) - gmailService.executeQuery(query) { _, _, _ in + gmailService.executeQuery(query) { _, _, error in + if let error = error { + return continuation.resume(throwing: GmailServiceError.providerError(error)) + } return continuation.resume() } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift index 0f8f4245a..22fc389bd 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift @@ -20,5 +20,5 @@ protocol MessageGateway { protocol DraftGateway { func saveDraft(input: MessageGatewayInput, draftId: String?) async throws -> GTLRGmail_Draft - func deleteDraft(with identifier: String) async + func deleteDraft(with identifier: String) async throws } diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift index 00321b917..5ffa3a38e 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift @@ -41,7 +41,8 @@ extension GmailService: MessagesThreadOperationsProvider { } func moveThreadToTrash(thread: MessageThread) async throws { - try await update(thread: thread, labelsToAdd: [.trash], labelsToRemove: [.inbox, .sent]) + let labelsToRemove = [MessageLabel.inbox, MessageLabel.sent].filter { thread.labels.contains($0) } + try await update(thread: thread, labelsToAdd: [.trash], labelsToRemove: labelsToRemove) } func moveThreadToInbox(thread: MessageThread) async throws { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 4b2d4fea8..c1d9ea3c9 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -26,6 +26,7 @@ final class ComposeMessageService { private let localContactsProvider: LocalContactsProviderType private let core: CoreComposeMessageType & KeyParser private let draftGateway: DraftGateway? + private let messageOperationsProvider: MessageOperationsProvider private lazy var logger = Logger.nested(Self.self) private struct ReplyInfo: Encodable { @@ -43,12 +44,13 @@ final class ComposeMessageService { draftGateway: DraftGateway? = nil, core: CoreComposeMessageType & KeyParser = Core.shared, localContactsProvider: LocalContactsProviderType? = nil - ) { + ) async throws { self.appContext = appContext self.keyMethods = keyMethods self.draftGateway = draftGateway self.core = core self.localContactsProvider = localContactsProvider ?? LocalContactsProvider(encryptedStorage: appContext.encryptedStorage) + self.messageOperationsProvider = try await appContext.getRequiredMailProvider().messageOperationsProvider } private var onStateChanged: ((State) -> Void)? @@ -239,6 +241,15 @@ final class ComposeMessageService { } } + func deleteDraft(messageId: String?) async throws { + if let draftId = draftId { + try await draftGateway?.deleteDraft(with: draftId) + } else if let messageId = messageId { + let id = Identifier(stringId: messageId) + try await messageOperationsProvider.deleteMessage(id: id, from: nil) + } + } + // MARK: - Encrypt and Send func encryptAndSend(message: SendableMsg, threadId: String?) async throws { do { @@ -271,7 +282,7 @@ final class ComposeMessageService { // cleaning any draft saved/created/fetched during editing if let draftId = draftId { - await draftGateway?.deleteDraft(with: draftId) + try await draftGateway?.deleteDraft(with: draftId) } onStateChanged?(.messageSent) diff --git a/Gemfile.lock b/Gemfile.lock index b996bd04e..7ea8df387 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: CFPropertyList (3.0.5) rexml - activesupport (6.1.6.1) + activesupport (6.1.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -17,8 +17,8 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.626.0) - aws-sdk-core (3.142.0) + aws-partitions (1.628.0) + aws-sdk-core (3.145.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) @@ -160,7 +160,7 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.25.0) + google-apis-androidpublisher_v3 (0.26.0) google-apis-core (>= 0.7, < 2.a) google-apis-core (0.7.0) addressable (~> 2.5, >= 2.5.1) @@ -182,7 +182,7 @@ GEM google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.2.0) + google-cloud-errors (1.3.0) google-cloud-storage (1.39.0) addressable (~> 2.8) digest-crc (~> 0.4) From 95672a4837d9800eecc7e651bc46073e1753e9f1 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 14 Sep 2022 21:56:06 +0300 Subject: [PATCH 11/56] fill draft data on compose screen --- FlowCrypt.xcodeproj/project.pbxproj | 24 ++--- .../xcshareddata/swiftpm/Package.resolved | 4 +- FlowCrypt/App/AppDelegate.swift | 2 - .../CheckMailAuthViewController.swift | 1 - .../Compose/ComposeViewController.swift | 13 +-- .../Compose/ComposeViewControllerInput.swift | 11 +-- .../Compose/ComposeViewDecorator.swift | 2 +- .../ComposeViewController+Drafts.swift | 29 +++--- .../ComposeViewController+ErrorHandling.swift | 90 +++++++++++++++++++ .../ComposeViewController+Keyboard.swift | 2 +- .../ComposeViewController+MessageSend.swift | 71 --------------- .../ComposeViewController+Nodes.swift | 4 +- .../ComposeViewController+Setup.swift | 57 +++++++++--- .../ComposeViewController+TableView.swift | 4 +- .../Threads/ThreadDetailsViewController.swift | 1 + .../Message Provider/MessageService.swift | 7 +- .../ComposeMessageError.swift | 2 +- .../ComposeMessageService.swift | 2 +- .../Resources/en.lproj/Localizable.strings | 4 +- .../Cell Nodes/RecipientEmailsCellNode.swift | 1 - .../Cell Nodes/RecipientFromCellNode.swift | 1 - FlowCryptUI/Cell Nodes/TextCellNode.swift | 1 - 22 files changed, 196 insertions(+), 137 deletions(-) create mode 100644 FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index f136bf6f9..8b97f9a76 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ 51B7421B27F318D300E702C8 /* XCTestCaseExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B7421A27F318D300E702C8 /* XCTestCaseExtension.swift */; }; 51B9EE6F27567B520080B2D5 /* MessageRecipientsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */; }; 51C0C1EF271982A1000C9738 /* MailCore in Frameworks */ = {isa = PBXBuildFile; productRef = 51C0C1EE271982A1000C9738 /* MailCore */; }; + 51C0C63828D1E42A003C540E /* ComposeViewController+ErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0C63728D1E42A003C540E /* ComposeViewController+ErrorHandling.swift */; }; 51DA5BD62721AB07001C4359 /* PubKeyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DA5BD52721AB07001C4359 /* PubKeyState.swift */; }; 51DA5BDA2722C82E001C4359 /* RecipientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DA5BD92722C82E001C4359 /* RecipientTests.swift */; }; 51DAD9BD273E7DD20076CBA7 /* BadgeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */; }; @@ -583,6 +584,7 @@ 51B4AE5227144E590001F33B /* PubKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKey.swift; sourceTree = ""; }; 51B7421A27F318D300E702C8 /* XCTestCaseExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtension.swift; sourceTree = ""; }; 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRecipientsNode.swift; sourceTree = ""; }; + 51C0C63728D1E42A003C540E /* ComposeViewController+ErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewController+ErrorHandling.swift"; sourceTree = ""; }; 51DA5BD52721AB07001C4359 /* PubKeyState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKeyState.swift; sourceTree = ""; }; 51DA5BD92722C82E001C4359 /* RecipientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientTests.swift; sourceTree = ""; }; 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeNode.swift; sourceTree = ""; }; @@ -925,20 +927,21 @@ 049E607227FDBCC30089EE2A /* Extensions */ = { isa = PBXGroup; children = ( + 049E606C27FDBB5B0089EE2A /* ComposeViewController+ActionHandling.swift */, 049E605827FDB6310089EE2A /* ComposeViewController+Attachment.swift */, - 049E605A27FDB6BE0089EE2A /* ComposeViewController+TableView.swift */, - 049E605C27FDB7D10089EE2A /* ComposeViewController+Nodes.swift */, - 049E605E27FDB8500089EE2A /* ComposeViewController+MessageSend.swift */, - 049E606027FDB9370089EE2A /* ComposeViewController+Setup.swift */, + 51FC336028C236770098313D /* ComposeViewController+Contacts.swift */, + 049E607027FDBC690089EE2A /* ComposeViewController+Drafts.swift */, + 51C0C63728D1E42A003C540E /* ComposeViewController+ErrorHandling.swift */, 049E606227FDB9C70089EE2A /* ComposeViewController+Keyboard.swift */, - 049E606427FDBA9F0089EE2A /* ComposeViewController+State.swift */, + 049E605E27FDB8500089EE2A /* ComposeViewController+MessageSend.swift */, + 049E605C27FDB7D10089EE2A /* ComposeViewController+Nodes.swift */, + 049E606E27FDBBD50089EE2A /* ComposeViewController+Picker.swift */, 049E606627FDBAEF0089EE2A /* ComposeViewController+RecipientInput.swift */, 049E606827FDBB2D0089EE2A /* ComposeViewController+RecipientPopup.swift */, - 049E606C27FDBB5B0089EE2A /* ComposeViewController+ActionHandling.swift */, - 049E606E27FDBBD50089EE2A /* ComposeViewController+Picker.swift */, - 049E607027FDBC690089EE2A /* ComposeViewController+Drafts.swift */, + 049E606027FDB9370089EE2A /* ComposeViewController+Setup.swift */, + 049E606427FDBA9F0089EE2A /* ComposeViewController+State.swift */, + 049E605A27FDB6BE0089EE2A /* ComposeViewController+TableView.swift */, 049E607327FDBDBE0089EE2A /* ComposeViewController+TapActions.swift */, - 51FC336028C236770098313D /* ComposeViewController+Contacts.swift */, ); path = Extensions; sourceTree = ""; @@ -959,10 +962,10 @@ isa = PBXGroup; children = ( 32DCAAE9F459F48178CAF8F5 /* ComposeViewController.swift */, - 049E607227FDBCC30089EE2A /* Extensions */, D269E02624103A20000495C3 /* ComposeViewControllerInput.swift */, 9F23EA4F237217140017DFED /* ComposeViewDecorator.swift */, 042B140127F596C70018BDC4 /* ComposeRecipientPopupViewController.swift */, + 049E607227FDBCC30089EE2A /* Extensions */, ); path = Compose; sourceTree = ""; @@ -2838,6 +2841,7 @@ 9FC4112E2595EA8B001180A8 /* Gmail+Search.swift in Sources */, 049E606327FDB9C70089EE2A /* ComposeViewController+Keyboard.swift in Sources */, 5A948DC5239EF2F4006284D7 /* LegalViewController.swift in Sources */, + 51C0C63828D1E42A003C540E /* ComposeViewController+ErrorHandling.swift in Sources */, 5133B6722716321F00C95463 /* ContactKeyDetailDecorator.swift in Sources */, A3B7C31923F576BA0022D628 /* AppStartup.swift in Sources */, 9F31AB8E23298BCF00CF87EA /* Imap+folders.swift in Sources */, diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index d4809eee3..687542a79 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GTMAppAuth.git", "state" : { - "revision" : "b9d1683be336ba8c8d1c6867bafeb056a5399700", - "version" : "1.3.0" + "revision" : "6dee0cde8a1b223737a5159e55e6b4ec16bbbdd9", + "version" : "1.3.1" } }, { diff --git a/FlowCrypt/App/AppDelegate.swift b/FlowCrypt/App/AppDelegate.swift index ff9d63133..064c3e5fc 100644 --- a/FlowCrypt/App/AppDelegate.swift +++ b/FlowCrypt/App/AppDelegate.swift @@ -4,9 +4,7 @@ // import AppAuth -import UIKit import GTMAppAuth -import FlowCryptCommon import Combine @main diff --git a/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift index 67b028654..6cac1b56f 100644 --- a/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift +++ b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift @@ -7,7 +7,6 @@ // import AsyncDisplayKit -import FlowCryptCommon import FlowCryptUI import UIKit diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 7eb00c24a..595908d6e 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -78,6 +78,11 @@ final class ComposeViewController: TableNodeViewController { var messagePasswordAlertController: UIAlertController? lazy var alertsFactory = AlertsFactory() + var didFinishSetup = false { + didSet { + if didFinishSetup { setupTextNode() } + } + } private var didLayoutSubviews = false private var topContentInset: CGFloat { navigationController?.navigationBar.frame.maxY ?? 0 @@ -88,8 +93,8 @@ final class ComposeViewController: TableNodeViewController { var popoverVC: ComposeRecipientPopupViewController! var sectionsList: [Section] = [] - var composeTextNode: ASCellNode! - var composeSubjectNode: ASCellNode! + var composeTextNode: ASCellNode? + var composeSubjectNode: ASCellNode? var sendAsList: [SendAsModel] = [] let onDelete: ((Identifier) -> Void)? @@ -182,6 +187,7 @@ final class ComposeViewController: TableNodeViewController { super.viewWillDisappear(animated) node.view.endEditing(true) stopDraftTimer() + navigationController?.interactivePopGestureRecognizer?.isEnabled = true } override func viewDidAppear(_ animated: Bool) { @@ -253,6 +259,3 @@ final class ComposeViewController: TableNodeViewController { } extension ComposeViewController: FilesManagerPresenter {} - -// update draft, don't create a new one each time -// decrypt draft body diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index 4cc0aec18..d88298e57 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -23,6 +23,7 @@ struct ComposeMessageInput: Equatable { let threadId: String? let replyToMsgId: String? let inReplyTo: String? + let draftIdentifier: String? let attachments: [MessageAttachment] } @@ -35,15 +36,6 @@ struct ComposeMessageInput: Equatable { let type: InputType - var draftId: String? { - switch type { - case .draft(let info): - return info.id - case .forward, .idle, .reply: - return nil - } - } - var subject: String? { type.info?.subject } @@ -131,6 +123,7 @@ extension ComposeMessageInput.MessageQuoteInfo { self.threadId = message.threadId self.replyToMsgId = nil // TODO: draft.rawMessage.replyToMsgId, self.inReplyTo = message.inReplyTo + self.draftIdentifier = message.draftIdentifier self.attachments = processed?.attachments ?? message.attachments } } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 364be462f..ff44c1e9f 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -129,7 +129,7 @@ struct ComposeViewDecorator { func styledMessagePassPhraseInput() -> MessageActionCellNode.Input { messageActionInput( - text: "compose_passphrase_placeholder".localized, + text: "compose_draft_passphrase_placeholder".localized, color: .warningColor, imageName: "lock" ) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index dc2dc2e81..7c742faed 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -21,40 +21,44 @@ extension ComposeViewController { saveDraftIfNeeded() } - private func shouldSaveDraft() -> Bool { + private func createDraft() -> ComposedDraft? { let newDraft = ComposedDraft( input: input, contextToSend: contextToSend ) if let existingDraft = composedLatestDraft { - let draftHasChanges = newDraft != existingDraft - self.composedLatestDraft = newDraft - return draftHasChanges + return newDraft != existingDraft ? newDraft : nil } else { // save initial draft composedLatestDraft = newDraft - return false + return nil } } - func saveDraftIfNeeded(isForceSave: Bool = false) { - guard isForceSave || shouldSaveDraft() else { return } + func saveDraftIfNeeded(withAlert: Bool = false, completion: ((Error?) -> Void)? = nil) { + guard let draft = createDraft() else { + completion?(nil) + return + } Task { do { let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg( - input: input, - contextToSend: contextToSend, + input: draft.input, + contextToSend: draft.contextToSend, isDraft: true ) try await composeMessageService.encryptAndSaveDraft( message: sendableMsg, - threadId: input.threadId, - draftId: input.draftId + threadId: draft.input.threadId, + draftId: draft.input.type.info?.draftIdentifier ) + + composedLatestDraft = draft + completion?(nil) } catch { - if case .promptUserToEnterPassPhraseForSigningKey(let keyPair) = error as? ComposeMessageError { + if case .missingPassPhrase(let keyPair) = error as? ComposeMessageError { signingKeyWithMissingPassphrase = keyPair reload(sections: [.passphrase]) } else if !(error is MessageValidationError) { @@ -63,6 +67,7 @@ extension ComposeViewController { // todo - should make sure that the toast doesn't hide the keyboard. Also should be toasted on top when keyboard open? showToast("Error saving draft: \(error.errorMessage)") } + completion?(error) } } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift new file mode 100644 index 000000000..c82f59eb9 --- /dev/null +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift @@ -0,0 +1,90 @@ +// +// ComposeViewController+ErrorHandling.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 14/09/22 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import UIKit + +// MARK: - Error handling +extension ComposeViewController { + func requestMissingPassPhraseWithModal(for signingKey: Keypair, isDraft: Bool = false, withDiscard: Bool = false) { + let alert = alertsFactory.makePassPhraseAlert( + onCancel: { [weak self] in + guard let self = self else { return } + if !withDiscard { + self.handle(error: ComposeMessageError.passPhraseRequired) + } else { + self.navigationController?.popViewController(animated: true) + } + }, + onCompletion: { [weak self] passPhrase in + guard let self = self else { return } + + Task { + do { + let matched = try await self.composeMessageService.handlePassPhraseEntry( + passPhrase, + for: signingKey + ) + if matched { + self.signingKeyWithMissingPassphrase = nil + if isDraft { + if self.didFinishSetup { + self.saveDraftIfNeeded() + } else { + self.fillDataFromInput() + } + } else { + self.handleSendTap() + } + self.reload(sections: [.passphrase]) + } else { + self.handle(error: ComposeMessageError.passPhraseNoMatch) + } + } catch { + self.handle(error: error) + } + } + } + ) + present(alert, animated: true, completion: nil) + } + + func handle(error: Error) { + reEnableSendButton() + + if case .missingPassPhrase(let keyPair) = error as? ComposeMessageError { + requestMissingPassPhraseWithModal(for: keyPair) + return + } + + let hideSpinnerAnimationDuration: TimeInterval = 1 + DispatchQueue.main.asyncAfter(deadline: .now() + hideSpinnerAnimationDuration) { [weak self] in + guard let self = self else { return } + + if self.isMessagePasswordSupported { + switch error { + case MessageValidationError.noPubRecipients: + self.setMessagePassword() + case MessageValidationError.notUniquePassword, + MessageValidationError.subjectContainsPassword, + MessageValidationError.weakPassword: + self.showAlert(message: error.errorMessage) + default: + self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + } + } else { + self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + } + } + } + + private func reEnableSendButton() { + UIApplication.shared.isIdleTimerDisabled = false + hideSpinner() + navigationItem.rightBarButtonItem?.isEnabled = true + } +} diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift index 3c37547c3..214a73c18 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Keyboard.swift @@ -45,7 +45,7 @@ extension ComposeViewController { object: nil) } - func adjustForKeyboard(height: CGFloat) { + private func adjustForKeyboard(height: CGFloat) { node.contentInset.bottom = height + 8 guard let textView = node.visibleNodes.compactMap({ $0 as? TextViewCellNode }).first?.textView.textView, diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index c01289195..1d5fe2d91 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -7,7 +7,6 @@ // import UIKit -import FlowCryptCommon import FlowCryptUI // MARK: - Message Sending @@ -40,78 +39,8 @@ extension ComposeViewController { handleSuccessfullySentMessage() } - func requestMissingPassPhraseWithModal(for signingKey: Keypair, isDraft: Bool = false) { - let alert = alertsFactory.makePassPhraseAlert( - onCancel: { - self.handle(error: ComposeMessageError.passPhraseRequired) - }, - onCompletion: { [weak self] passPhrase in - guard let self = self else { return } - - Task { - do { - let matched = try await self.composeMessageService.handlePassPhraseEntry( - passPhrase, - for: signingKey - ) - if matched { - self.signingKeyWithMissingPassphrase = nil - if isDraft { - self.saveDraftIfNeeded(isForceSave: true) - } else { - self.handleSendTap() - } - self.reload(sections: [.passphrase]) - } else { - self.handle(error: ComposeMessageError.passPhraseNoMatch) - } - } catch { - self.handle(error: error) - } - } - } - ) - present(alert, animated: true, completion: nil) - } - - func handle(error: Error) { - reEnableSendButton() - - if case .promptUserToEnterPassPhraseForSigningKey(let keyPair) = error as? ComposeMessageError { - requestMissingPassPhraseWithModal(for: keyPair) - return - } - - let hideSpinnerAnimationDuration: TimeInterval = 1 - DispatchQueue.main.asyncAfter(deadline: .now() + hideSpinnerAnimationDuration) { [weak self] in - guard let self = self else { return } - - if self.isMessagePasswordSupported { - switch error { - case MessageValidationError.noPubRecipients: - self.setMessagePassword() - case MessageValidationError.notUniquePassword, - MessageValidationError.subjectContainsPassword, - MessageValidationError.weakPassword: - self.showAlert(message: error.errorMessage) - default: - self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) - } - } else { - self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) - } - } - } - private func handleSuccessfullySentMessage() { - reEnableSendButton() showToast(input.successfullySentToast) navigationController?.popViewController(animated: true) } - - private func reEnableSendButton() { - UIApplication.shared.isIdleTimerDisabled = false - hideSpinner() - navigationItem.rightBarButtonItem?.isEnabled = true - } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index dffa600d2..fd41a2146 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -35,13 +35,13 @@ extension ComposeViewController { if !shouldShowEmailRecipientsLabel { shouldShowEmailRecipientsLabel = true userTappedOutSideRecipientsArea = false - reload(sections: [.recipientsLabel, .recipients(.from), .recipients(.to), .recipients(.cc), .recipients(.bcc)]) + reload(sections: Section.recipientsSections + [.recipientsLabel]) } } func hideRecipientLabel() { shouldShowEmailRecipientsLabel = false - reload(sections: [.recipientsLabel, .recipients(.from), .recipients(.to), .recipients(.cc), .recipients(.bcc)]) + reload(sections: Section.recipientsSections + [.recipientsLabel]) } func setupSubjectNode() { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 1f6acbe52..75a80d57b 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -54,7 +54,10 @@ extension ComposeViewController { } func fillDataFromInput() { - guard let info = input.type.info else { return } + guard let info = input.type.info else { + didFinishSetup = true + return + } contextToSend.subject = info.subject @@ -75,6 +78,8 @@ extension ComposeViewController { } if input.isPgp { + // showSpinner("processing_title".localized) + let message = Message( identifier: .random, date: info.sentDate, @@ -86,22 +91,34 @@ extension ComposeViewController { body: .init(text: info.text, html: nil) ) Task { - let processedMessage = try await messageService.decryptAndProcess( - message: message, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager - ) - contextToSend.message = processedMessage.text - reload(sections: [.compose]) + do { + let processedMessage = try await messageService.decryptAndProcess( + message: message, + onlyLocalKeys: false, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + contextToSend.message = processedMessage.text + setupTextNode() + reload(sections: [.compose]) + didFinishSetup = true + } catch { + if case .missingPassPhrase(let keyPair) = error as? MessageServiceError, let keyPair = keyPair { + requestMissingPassPhraseWithModal(for: keyPair, isDraft: true) + return + } else { + handle(error: error) + } + } } } else { contextToSend.message = info.text + reload(sections: Section.recipientsSections) + didFinishSetup = true } } func setupNodes() { - setupTextNode() setupSubjectNode() } } @@ -125,3 +142,23 @@ extension ComposeViewController { .store(in: &cancellable) } } + +// MARK: - NavigationChildController +extension ComposeViewController: NavigationChildController { + func handleBackButtonTap() { + // TODO: + navigationController?.popViewController(animated: true) +// if let keyPair = signingKeyWithMissingPassphrase { +// requestMissingPassPhraseWithModal(for: keyPair, isDraft: true, withDiscard: true) +// } else { +// saveDraftIfNeeded(withAlert: true) { [weak self] error in +// guard let self = self else { return } +// if case .missingPassphrase(let keyPair) = error as? ComposeMessageError { +// self.requestMissingPassPhraseWithModal(for: keyPair) +// } else { +// self.navigationController?.popViewController(animated: true) +// } +// } +// } + } +} diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift index d089d91f6..ff2f7a024 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift @@ -69,8 +69,8 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case (.main, .compose): guard let part = ComposePart(rawValue: indexPath.row) else { return ASCellNode() } switch part { - case .subject: return self.composeSubjectNode - case .text: return self.composeTextNode + case .subject: return self.composeSubjectNode ?? ASCellNode() + case .text: return self.composeTextNode ?? ASCellNode() case .topDivider, .subjectDivider: return DividerCellNode() } case (.main, .attachments): diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 85070dbe6..15164a67a 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -322,6 +322,7 @@ extension ThreadDetailsViewController { threadId: threadId, replyToMsgId: replyToMsgId, inReplyTo: input.rawMessage.inReplyTo, + draftIdentifier: nil, attachments: attachments ) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 25efe15ef..953b311a7 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -17,11 +17,11 @@ enum MessageFetchState { // MARK: - MessageServiceError enum MessageServiceError: Error, CustomStringConvertible { - case missingPassPhrase + case missingPassPhrase(Keypair?) case emptyKeys case emptyKeysForEKM case attachmentNotFound - case attachmentDecryptFailed(_ message: String) + case attachmentDecryptFailed(String) } extension MessageServiceError { @@ -144,7 +144,8 @@ final class MessageService { ) guard !hasMsgBlockThatNeedsPassPhrase(decrypted) else { - throw MessageServiceError.missingPassPhrase + let keyPair = keys.first(where: { $0.passphrase == nil }) + throw MessageServiceError.missingPassPhrase(keyPair) } return try await process( diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift index b2206fa5f..22b3b4cd6 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift @@ -57,7 +57,7 @@ enum ComposeMessageError: Error, CustomStringConvertible, Equatable { case passPhraseRequired case passPhraseNoMatch case gatewayError(Error) - case promptUserToEnterPassPhraseForSigningKey(Keypair) + case missingPassPhrase(Keypair) case noKeysFoundForSign(Int, String) var description: String { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index c1d9ea3c9..fb11dcfe5 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -65,7 +65,7 @@ final class ComposeMessageService { throw ComposeMessageError.noKeysFoundForSign(keys.count, senderEmail) } if signingKey.passphrase == nil { - throw ComposeMessageError.promptUserToEnterPassPhraseForSigningKey(signingKey) + throw ComposeMessageError.missingPassPhrase(signingKey) } return signingKey } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index d4cc0f9a5..68202d6a2 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -123,7 +123,9 @@ "compose_sign_passphrase_required" = "Passphrase is required for message signing."; "compose_sign_passphrase_no_match" = "This pass phrase did not match your signing private key."; "compose_sign_no_keys" = "Cannot sign message: none of your %@ account keys can be used for sending address %@"; -"compose_passphrase_placeholder" = "Draft not saved - tap to add pass phrase"; +"compose_draft_passphrase_alert" = "Enter passphrase to save draft"; +"compose_draft_discard" = "Discard draft"; +"compose_draft_passphrase_placeholder" = "Draft not saved - tap to add pass phrase"; "compose_password_placeholder" = "Tap to add password for recipients who don't have encryption set up."; "compose_password_set_message" = "Web portal password added"; "compose_password_modal_title" = "Set web portal password"; diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index 11c414f5a..f5810c140 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -7,7 +7,6 @@ // import AsyncDisplayKit -import FlowCryptCommon public final class RecipientEmailsCellNode: CellNode { public typealias RecipientTap = (RecipientEmailTapAction) -> Void diff --git a/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift index 651d20726..eab515499 100644 --- a/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientFromCellNode.swift @@ -7,7 +7,6 @@ // import AsyncDisplayKit -import FlowCryptCommon public final class RecipientFromCellNode: CellNode { private enum Constants { diff --git a/FlowCryptUI/Cell Nodes/TextCellNode.swift b/FlowCryptUI/Cell Nodes/TextCellNode.swift index 94557dfd3..3fde502b8 100644 --- a/FlowCryptUI/Cell Nodes/TextCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextCellNode.swift @@ -7,7 +7,6 @@ // import AsyncDisplayKit -import FlowCryptCommon import UIKit public final class TextCellNode: CellNode { From 9eca1985c1af186d6c8923c82adc22bfab68df5f Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 15 Sep 2022 23:29:55 +0300 Subject: [PATCH 12/56] improve draft id fetch --- FlowCrypt.xcodeproj/project.pbxproj | 12 ---- .../xcshareddata/swiftpm/Package.resolved | 8 +-- .../Compose/ComposeViewControllerInput.swift | 10 +-- .../ComposeViewController+Drafts.swift | 3 +- .../ComposeViewController+Setup.swift | 6 ++ .../Inbox/InboxViewController.swift | 47 +++----------- .../Search/SearchViewController.swift | 2 +- .../Controllers/Threads/MessageThread.swift | 2 +- .../Threads/ThreadDetailsViewController.swift | 2 +- .../DraftsListProvider.swift | 13 ---- .../Mail Provider/Gmail/GmailService.swift | 2 +- .../Mail Provider/MailProvider.swift | 6 -- .../Message Gateway/GmailService+draft.swift | 21 ++++++ .../Message Gateway/MessageGateway.swift | 1 + .../Gmail+MessageExtension.swift | 6 +- .../Gmail+MessagesList.swift | 64 +------------------ .../MessagesList Provider/Model/Message.swift | 6 +- .../Threads/MessagesThreadProvider.swift | 19 ++---- .../ComposeMessageService.swift | 9 ++- 19 files changed, 73 insertions(+), 166 deletions(-) delete mode 100644 FlowCrypt/Functionality/Mail Provider/DraftsListProvider/DraftsListProvider.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 8b97f9a76..4fc9fa948 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -406,7 +406,6 @@ D2FF6966243115EC007182F0 /* SetupImapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FF6965243115EC007182F0 /* SetupImapViewController.swift */; }; D2FF6968243115F9007182F0 /* SetupImapViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FF6967243115F9007182F0 /* SetupImapViewDecorator.swift */; }; F191F621272511790053833E /* BlurViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F191F620272511790053833E /* BlurViewController.swift */; }; - F80E95362720B6640093F243 /* DraftsListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80E95352720B6640093F243 /* DraftsListProvider.swift */; }; F8678DCC2722143300BB1710 /* GmailService+draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8678DCB2722143300BB1710 /* GmailService+draft.swift */; }; F8A72FA12729F82800E4BCAB /* DraftGatewayMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A72FA02729F82800E4BCAB /* DraftGatewayMock.swift */; }; /* End PBXBuildFile section */ @@ -861,7 +860,6 @@ D9381A4EE6BAAD97294A7F9A /* Pods-FlowCryptUI.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUI.release.xcconfig"; path = "Target Support Files/Pods-FlowCryptUI/Pods-FlowCryptUI.release.xcconfig"; sourceTree = ""; }; E26D5E20275AA417007B8802 /* BundleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = ""; }; F191F620272511790053833E /* BlurViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurViewController.swift; sourceTree = ""; }; - F80E95352720B6640093F243 /* DraftsListProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsListProvider.swift; sourceTree = ""; }; F8678DCB2722143300BB1710 /* GmailService+draft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GmailService+draft.swift"; sourceTree = ""; }; F8A72FA02729F82800E4BCAB /* DraftGatewayMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftGatewayMock.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1697,7 +1695,6 @@ 9FC4114A25961CD6001180A8 /* Mail Provider */ = { isa = PBXGroup; children = ( - F80E95342720B64A0093F243 /* DraftsListProvider */, 9FF0671B25520D9D00FCC9E6 /* MailProvider.swift */, 9FC4114B25961CEA001180A8 /* MailServiceProviderType.swift */, 9FF0670E25520D4B00FCC9E6 /* Gmail */, @@ -2277,14 +2274,6 @@ path = SetupImap; sourceTree = ""; }; - F80E95342720B64A0093F243 /* DraftsListProvider */ = { - isa = PBXGroup; - children = ( - F80E95352720B6640093F243 /* DraftsListProvider.swift */, - ); - path = DraftsListProvider; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -2764,7 +2753,6 @@ 2CC50FB12744167A0051629A /* Folder.swift in Sources */, 514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */, D2F41373243CC7990066AFB5 /* UserRealmObject.swift in Sources */, - F80E95362720B6640093F243 /* DraftsListProvider.swift in Sources */, 5A39F42D239EC321001F4607 /* SettingsViewController.swift in Sources */, 5ADEDCBC23A4329000EC495E /* PublicKeyDetailViewController.swift in Sources */, 21489B80267CC39E00BDE4AC /* ClientConfigurationService.swift in Sources */, diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 687542a79..f0536fe66 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleSignIn-iOS", "state" : { - "revision" : "5ce850448e89500aca5b095af7247eb46dc0ca18", - "version" : "6.2.3" + "revision" : "9c9b36af86a4dd3da16048a36cf37351e63ccfe1", + "version" : "6.2.4" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "4e9bbf2808b8fee444e84a48f5f3c12641987d3e", - "version" : "1.7.2" + "revision" : "d4289da23e978f37c344ea6a386e5546e2466294", + "version" : "2.1.0" } }, { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index d88298e57..cf23df4b4 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -23,7 +23,7 @@ struct ComposeMessageInput: Equatable { let threadId: String? let replyToMsgId: String? let inReplyTo: String? - let draftIdentifier: String? + let rfc822MsgId: String? let attachments: [MessageAttachment] } @@ -91,9 +91,9 @@ extension ComposeMessageInput { var shouldFocusTextNode: Bool { switch type { - case .reply, .draft: + case .reply: return true - case .idle, .forward: + case .idle, .forward, .draft: return false } } @@ -111,7 +111,7 @@ extension ComposeMessageInput.InputType { } extension ComposeMessageInput.MessageQuoteInfo { - init(message: Message, processed: ProcessedMessage?) { + init(message: Message, processed: ProcessedMessage? = nil) { self.id = message.identifier.stringId self.recipients = message.to self.ccRecipients = message.cc @@ -121,9 +121,9 @@ extension ComposeMessageInput.MessageQuoteInfo { self.sentDate = message.date self.text = processed?.text ?? message.body.text self.threadId = message.threadId + self.rfc822MsgId = message.rfc822MsgId self.replyToMsgId = nil // TODO: draft.rawMessage.replyToMsgId, self.inReplyTo = message.inReplyTo - self.draftIdentifier = message.draftIdentifier self.attachments = processed?.attachments ?? message.attachments } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 7c742faed..6c109eaf7 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -51,8 +51,7 @@ extension ComposeViewController { try await composeMessageService.encryptAndSaveDraft( message: sendableMsg, - threadId: draft.input.threadId, - draftId: draft.input.type.info?.draftIdentifier + threadId: draft.input.threadId ) composedLatestDraft = draft diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 75a80d57b..bea5a38bc 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -59,6 +59,12 @@ extension ComposeViewController { return } + if case .draft = input.type, let messageId = input.type.info?.rfc822MsgId { + Task { + try await composeMessageService.fetchDraftId(messageId: messageId) + } + } + contextToSend.subject = info.subject for recipient in info.recipients { diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 0bc6fbfce..82cfad6cf 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -17,7 +17,6 @@ class InboxViewController: ViewController { let tableNode: ASTableNode private let decorator: InboxViewDecorator - private let draftsListProvider: DraftsListProvider? private let messageOperationsProvider: MessageOperationsProvider private let refreshControl = UIRefreshControl() private lazy var composeButton = ComposeButtonNode { [weak self] in @@ -50,7 +49,6 @@ class InboxViewController: ViewController { viewModel: InboxViewModel, numberOfInboxItemsToLoad: Int = 50, provider: InboxDataProvider, - draftsListProvider: DraftsListProvider? = nil, decorator: InboxViewDecorator = InboxViewDecorator(), isSearch: Bool = false ) throws { @@ -60,7 +58,6 @@ class InboxViewController: ViewController { self.inboxDataProvider = provider let mailProvider = try appContext.getRequiredMailProvider() - self.draftsListProvider = try draftsListProvider ?? mailProvider.draftsProvider self.messageOperationsProvider = try mailProvider.messageOperationsProvider self.decorator = decorator self.tableNode = TableNode() @@ -179,8 +176,6 @@ extension InboxViewController { private func messagesToLoad() -> Int { switch state { - case .fetched(.byNextPage): - return numberOfInboxItemsToLoad case .fetched(.byNumber(let totalNumberOfMessages)): guard let total = totalNumberOfMessages else { return numberOfInboxItemsToLoad @@ -195,37 +190,6 @@ extension InboxViewController { // MARK: - Functionality extension InboxViewController { - private func fetchAndRenderEmails(_ batchContext: ASBatchContext?) { - if viewModel.isDrafts { - fetchAndRenderDrafts(batchContext) - } else { - fetchAndRenderEmailsOnly(batchContext) - } - } - - private func fetchAndRenderDrafts(_ batchContext: ASBatchContext?) { - guard let draftsListProvider = draftsListProvider else { return } - - Task { - do { - let context = try await draftsListProvider.fetchDrafts( - using: FetchMessageContext( - folderPath: viewModel.path, - count: numberOfInboxItemsToLoad, - pagination: currentMessagesListPagination() - ) - ) - let inboxContext = InboxContext( - data: context.messages.map(InboxRenderable.init), - pagination: context.pagination - ) - handleEndFetching(with: inboxContext, context: batchContext) - } catch { - handle(error: error) - } - } - } - private func getSearchQuery() -> String? { guard searchedExpression.isNotEmpty else { return nil } @@ -234,7 +198,7 @@ extension InboxViewController { return "\(searchedExpression) OR subject:\(searchedExpression)" } - func fetchAndRenderEmailsOnly(_ batchContext: ASBatchContext?) { + func fetchAndRenderEmails(_ batchContext: ASBatchContext?) { Task { do { if isSearch { @@ -277,7 +241,8 @@ extension InboxViewController { folderPath: viewModel.path, count: messagesToLoad(), pagination: pagination - ), userEmail: appContext.user.email + ), + userEmail: appContext.user.email ) state = .fetched(context.pagination) handleEndFetching(with: context, context: batchContext) @@ -652,7 +617,11 @@ extension InboxViewController { open(message: message, path: path, appContext: appContext) } case .thread(let thread): - open(thread: thread, appContext: appContext) + if let message = thread.messages.first, thread.messages.count == 1, message.isDraft { + open(draft: message, appContext: appContext) + } else { + open(thread: thread, appContext: appContext) + } } } diff --git a/FlowCrypt/Controllers/Search/SearchViewController.swift b/FlowCrypt/Controllers/Search/SearchViewController.swift index eab306ba4..f05cf4372 100644 --- a/FlowCrypt/Controllers/Search/SearchViewController.swift +++ b/FlowCrypt/Controllers/Search/SearchViewController.swift @@ -123,6 +123,6 @@ extension SearchViewController: UISearchResultsUpdating { private func search(for searchText: String) { searchedExpression = searchText - fetchAndRenderEmailsOnly(nil) + fetchAndRenderEmails(nil) } } diff --git a/FlowCrypt/Controllers/Threads/MessageThread.swift b/FlowCrypt/Controllers/Threads/MessageThread.swift index d202ac19d..60427cbf4 100644 --- a/FlowCrypt/Controllers/Threads/MessageThread.swift +++ b/FlowCrypt/Controllers/Threads/MessageThread.swift @@ -37,7 +37,7 @@ struct MessageThread: Equatable { guard let firstMessageLabels = messages.first?.labels else { return false } - // Thread is treaded as archived when labels don't contain `inbox` and first message label doesn't contain sent label + // Thread is treated as archived when labels don't contain `inbox` and first message label doesn't contain sent label // https://github.com/FlowCrypt/flowcrypt-ios/pull/1769#discussion_r931874353 return !isInbox && !firstMessageLabels.contains(.sent) } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 15164a67a..625cc6f17 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -322,7 +322,7 @@ extension ThreadDetailsViewController { threadId: threadId, replyToMsgId: replyToMsgId, inReplyTo: input.rawMessage.inReplyTo, - draftIdentifier: nil, + rfc822MsgId: input.rawMessage.rfc822MsgId, attachments: attachments ) diff --git a/FlowCrypt/Functionality/Mail Provider/DraftsListProvider/DraftsListProvider.swift b/FlowCrypt/Functionality/Mail Provider/DraftsListProvider/DraftsListProvider.swift deleted file mode 100644 index e55efc276..000000000 --- a/FlowCrypt/Functionality/Mail Provider/DraftsListProvider/DraftsListProvider.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// DraftsListProvider.swift -// FlowCrypt -// -// Created by Evgenii Kyivskyi on 10/20/21 -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import Foundation - -protocol DraftsListProvider { - func fetchDrafts(using context: FetchMessageContext) async throws -> MessageContext -} diff --git a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift index 48f8b3cc2..1c149bb17 100644 --- a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift @@ -63,5 +63,5 @@ extension String { static let bcc = "bcc" static let replyTo = "reply-to" static let inReplyTo = "in-reply-to" - static let identifier = "Message-ID" + static let identifier = "message-id" } diff --git a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift index 1e2f90167..d90245786 100644 --- a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift @@ -89,12 +89,6 @@ final class MailProvider { } } - var draftsProvider: DraftsListProvider? { - get throws { - resolveOptionalService(of: DraftsListProvider.self) - } - } - var messagesThreadProvider: MessagesThreadProvider { get throws { try resolveService(of: MessagesThreadProvider.self) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 2e87ccd8d..0578c13fe 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -9,6 +9,27 @@ import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: DraftGateway { + func fetchDraftId(messageId: String) async throws -> String? { + let query = GTLRGmailQuery_UsersDraftsList.query(withUserId: .me) + query.q = "rfc822msgid:\(messageId)" + query.maxResults = 1 + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + gmailService.executeQuery(query) { _, data, error in + if let error = error { + return continuation.resume(throwing: GmailServiceError.providerError(error)) + } + + guard let list = data as? GTLRGmail_ListDraftsResponse else { + return continuation.resume(throwing: AppErr.cast("GTLRGmail_ListDraftsResponse")) + } + + let draftId = list.drafts?.first?.identifier + return continuation.resume(returning: draftId) + } + } + } + func saveDraft(input: MessageGatewayInput, draftId: String?) async throws -> GTLRGmail_Draft { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in guard let raw = GTLREncodeBase64(input.mime) else { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift index 22fc389bd..c873105d9 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift @@ -19,6 +19,7 @@ protocol MessageGateway { } protocol DraftGateway { + func fetchDraftId(messageId: String) async throws -> String? func saveDraft(input: MessageGatewayInput, draftId: String?) async throws -> GTLRGmail_Draft func deleteDraft(with identifier: String) async throws } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift index 7a3b9dcc0..7bfbe32c7 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift @@ -9,7 +9,7 @@ import GoogleAPIClientForREST_Gmail extension Message { - init(gmailMessage: GTLRGmail_Message, draftIdentifier: String? = nil) throws { + init(gmailMessage: GTLRGmail_Message) throws { guard let payload = gmailMessage.payload else { throw GmailServiceError.missingMessagePayload } @@ -38,6 +38,7 @@ extension Message { var bcc: String? var replyTo: String? var inReplyTo: String? + var rfc822MsgId: String? for messageHeader in messageHeaders.compactMap({ $0 }) { guard let name = messageHeader.name?.lowercased(), @@ -52,6 +53,7 @@ extension Message { case .bcc: bcc = value case .replyTo: replyTo = value case .inReplyTo: inReplyTo = value + case .identifier: rfc822MsgId = value default: break } } @@ -68,7 +70,7 @@ extension Message { body: body, attachments: attachments, threadId: gmailMessage.threadId, - draftIdentifier: draftIdentifier, + rfc822MsgId: rfc822MsgId, raw: gmailMessage.raw, to: to, cc: cc, diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift index 1b14a6253..37eb9587a 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift @@ -39,62 +39,6 @@ extension GmailService: MessagesListProvider { } } -extension GmailService: DraftsListProvider { - func fetchDrafts(using context: FetchMessageContext) async throws -> MessageContext { - return try await withThrowingTaskGroup(of: Message.self) { taskGroup in - let list = try await fetchDraftsList(using: context) - - for draft in list.drafts ?? [] { - taskGroup.addTask { - try await self.fetchFullMessage( - with: draft.message?.identifier ?? "", - draftIdentifier: draft.identifier - ) - } - } - - var messages: [Message] = [] - for try await result in taskGroup { - messages.append(result) - } - messages.sort(by: { $0.date > $1.date }) - - return MessageContext( - messages: messages, - pagination: .byNextPage(token: list.nextPageToken) - ) - } - } - - private func fetchDraftsList(using context: FetchMessageContext) async throws -> GTLRGmail_ListDraftsResponse { - let query = GTLRGmailQuery_UsersDraftsList.query(withUserId: .me) - - if let pagination = context.pagination { - guard case let .byNextPage(token) = pagination else { - throw GmailServiceError.paginationError(pagination) - } - query.pageToken = token - } - - if let count = context.count { - query.maxResults = UInt(count) - } - - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - gmailService.executeQuery(query) { _, data, error in - if let error = error { - return continuation.resume(throwing: GmailServiceError.providerError(error)) - } - - guard let messageList = data as? GTLRGmail_ListDraftsResponse else { - return continuation.resume(throwing: AppErr.cast("GTLRGmail_ListDraftsResponse")) - } - return continuation.resume(returning: messageList) - } - } - } -} - extension GmailService { func fetchMessagesList(using context: FetchMessageContext) async throws -> GTLRGmail_ListMessagesResponse { let query = GTLRGmailQuery_UsersMessagesList.query(withUserId: .me) @@ -131,7 +75,7 @@ extension GmailService { } } - private func fetchFullMessage(with identifier: String, draftIdentifier: String? = nil) async throws -> Message { + private func fetchFullMessage(with identifier: String) async throws -> Message { let query = GTLRGmailQuery_UsersMessagesGet.query(withUserId: .me, identifier: identifier) query.format = kGTLRGmailFormatFull return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in @@ -145,10 +89,8 @@ extension GmailService { } do { - return continuation.resume(returning: try Message( - gmailMessage: gmailMessage, - draftIdentifier: draftIdentifier) - ) + let message = try Message(gmailMessage: gmailMessage) + return continuation.resume(returning: message) } catch { return continuation.resume(throwing: error) } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index 03468c42e..d2c1e4a8e 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -23,7 +23,7 @@ struct Message: Hashable { let attachmentIds: [String] var attachments: [MessageAttachment] let threadId: String? - let draftIdentifier: String? + let rfc822MsgId: String? var raw: String? let body: MessageBody let inReplyTo: String? @@ -62,7 +62,7 @@ struct Message: Hashable { body: MessageBody, attachments: [MessageAttachment] = [], threadId: String? = nil, - draftIdentifier: String? = nil, + rfc822MsgId: String? = nil, raw: String? = nil, to: String? = nil, cc: String? = nil, @@ -80,7 +80,7 @@ struct Message: Hashable { self.attachments = attachments self.body = body self.threadId = threadId - self.draftIdentifier = draftIdentifier + self.rfc822MsgId = rfc822MsgId self.raw = raw self.to = Self.parseRecipients(to) self.cc = Self.parseRecipients(cc) diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift index 2dc14f789..62d4f1914 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift @@ -16,19 +16,12 @@ protocol MessagesThreadProvider { extension GmailService: MessagesThreadProvider { func fetchThreads(using context: FetchMessageContext) async throws -> MessageThreadContext { let threadsList = try await getThreadsList(using: context) - let requests = threadsList.threads? - .compactMap { thread -> (String, String?)? in - guard let id = thread.identifier else { - return nil - } - return (id, thread.snippet) - } - ?? [] + let identifiers = threadsList.threads?.compactMap(\.identifier) ?? [] return try await withThrowingTaskGroup(of: MessageThread.self) { taskGroup in var messageThreadsById: [String: MessageThread] = [:] - for request in requests { + for identifier in identifiers { taskGroup.addTask { - try await self.getThread(with: request.0, snippet: request.1, path: context.folderPath ?? "") + try await self.getThread(identifier: identifier, path: context.folderPath ?? "") } } for try await result in taskGroup { @@ -36,7 +29,7 @@ extension GmailService: MessagesThreadProvider { messageThreadsById[id] = result } } - let messageThreads = requests.compactMap { messageThreadsById[$0.0] } + let messageThreads = identifiers.compactMap { messageThreadsById[$0] } return MessageThreadContext( threads: messageThreads, pagination: .byNextPage(token: threadsList.nextPageToken) @@ -63,7 +56,7 @@ extension GmailService: MessagesThreadProvider { }.value } - private func getThread(with identifier: String, snippet: String?, path: String) async throws -> MessageThread { + private func getThread(identifier: String, path: String) async throws -> MessageThread { return try await Task.retrying { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in self.gmailService.executeQuery( @@ -81,7 +74,7 @@ extension GmailService: MessagesThreadProvider { let result = MessageThread( identifier: thread.identifier, - snippet: snippet, + snippet: thread.snippet, path: path, messages: messages ) diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index fb11dcfe5..e39ef0b7b 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -222,7 +222,12 @@ final class ComposeMessageService { // MARK: - Drafts private var draftId: String? - func encryptAndSaveDraft(message: SendableMsg, threadId: String?, draftId: String?) async throws { + + func fetchDraftId(messageId: String) async throws { + self.draftId = try await draftGateway?.fetchDraftId(messageId: messageId) + } + + func encryptAndSaveDraft(message: SendableMsg, threadId: String?) async throws { do { let mime = try await core.composeEmail( msg: message, @@ -234,7 +239,7 @@ final class ComposeMessageService { mime: mime, threadId: threadId ), - draftId: draftId + draftId: self.draftId ).identifier } catch { throw ComposeMessageError.gatewayError(error) From 8667f4184d76520d57c5a80c23caa757488fc59d Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 16 Sep 2022 15:45:41 +0300 Subject: [PATCH 13/56] update drafts in thread view --- .../Compose/ComposeViewDecorator.swift | 4 +- .../ComposeViewController+Setup.swift | 3 -- .../Threads/ThreadDetailsViewController.swift | 42 ++++++++++++------- .../MessagesList Provider/Model/Message.swift | 11 ----- .../Resources/en.lproj/Localizable.strings | 2 +- .../Mocks/DraftGatewayMock.swift | 4 ++ .../Extensions/StringExtensions.swift | 11 ++++- FlowCryptUI/Cell Nodes/LabelCellNode.swift | 1 + 8 files changed, 45 insertions(+), 33 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index ff44c1e9f..7cd78d3e1 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -107,7 +107,7 @@ struct ComposeViewDecorator { guard let info = input.type.info else { return NSAttributedString(string: "") } let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .short + dateFormatter.dateStyle = .medium dateFormatter.timeStyle = .none let date = dateFormatter.string(from: info.sentDate) @@ -116,7 +116,7 @@ struct ComposeViewDecorator { dateFormatter.timeStyle = .short let time = dateFormatter.string(from: info.sentDate) - let from = info.sender?.email ?? "unknown sender" + let from = info.sender?.formatted ?? "unknown sender" let text: String = "\n\n" + "compose_quote_from".localizeWithArguments(date, time, from) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index bea5a38bc..7b660087b 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -84,8 +84,6 @@ extension ComposeViewController { } if input.isPgp { - // showSpinner("processing_title".localized) - let message = Message( identifier: .random, date: info.sentDate, @@ -118,7 +116,6 @@ extension ComposeViewController { } } } else { - contextToSend.message = info.text reload(sections: Section.recipientsSections) didFinishSetup = true } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 625cc6f17..12ee6d1ee 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -126,7 +126,7 @@ extension ThreadDetailsViewController { guard let self = self else { return } if let processedMessage = self.input[indexPath.section - 1].processedMessage { - self.handleReceived(message: processedMessage, at: indexPath) + self.handle(processedMessage: processedMessage, at: indexPath) } else { self.fetchDecryptAndRenderMsg(at: indexPath) } @@ -374,14 +374,14 @@ extension ThreadDetailsViewController { indexPath: indexPath ) } - handleReceived(message: processedMessage, at: indexPath) + handle(processedMessage: processedMessage, at: indexPath) } catch { - handleError(error, at: indexPath) + handle(error: error, at: indexPath) } } } - private func handleReceived(message processedMessage: ProcessedMessage, at indexPath: IndexPath) { + private func handle(processedMessage: ProcessedMessage, at indexPath: IndexPath) { hideSpinner() let messageIndex = indexPath.section - 1 @@ -405,7 +405,7 @@ extension ThreadDetailsViewController { } } - private func handleError(_ error: Error, at indexPath: IndexPath) { + private func handle(error: Error, at indexPath: IndexPath) { logger.logInfo("Error \(error)") hideSpinner() @@ -475,20 +475,23 @@ extension ThreadDetailsViewController { passPhrase, userEmail: appContext.user.email ) - let message = input[indexPath.section - 1].rawMessage + if matched { + let message = input[indexPath.section - 1].rawMessage + let processedMessage = try await messageService.decryptAndProcess( message: message, onlyLocalKeys: false, userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) - handleReceived(message: processedMessage, at: indexPath) + + handle(processedMessage: processedMessage, at: indexPath) } else { handleWrongPassPhrase(passPhrase, indexPath: indexPath) } } catch { - handleError(error, at: indexPath) + handle(error: error, at: indexPath) } } } @@ -507,7 +510,7 @@ extension ThreadDetailsViewController { userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) - handleReceived(message: processedMessage, at: indexPath) + handle(processedMessage: processedMessage, at: indexPath) } catch { let message = "message_signature_fail_reason".localizeWithArguments(error.errorMessage) input[indexPath.section - 1].processedMessage?.signature = .error(message) @@ -633,7 +636,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } if message.rawMessage.isDraft { - return self.draftNode(messageIndex: messageIndex) + return self.draftNode(messageIndex: messageIndex, isExpanded: message.isExpanded) } guard let processedMessage = message.processedMessage else { @@ -678,16 +681,25 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { section > 0 && section < input.count ? 1 / UIScreen.main.nativeScale : 0 } - private func draftNode(messageIndex: Int) -> ASCellNode { - let message = input[messageIndex] - let messageData = message.processedMessage?.message ?? message.rawMessage + private func draftNode(messageIndex: Int, isExpanded: Bool) -> ASCellNode { + let data = input[messageIndex] + + let body: String + if let processedMessage = data.processedMessage { + body = processedMessage.text + } else if data.rawMessage.isPgp { + body = "Waiting for pass phrase to open draft..." + } else { + body = data.rawMessage.body.text + } + return LabelCellNode( input: .init( title: "compose_draft".localized.attributed(color: .red), - text: messageData.body.textWithoutThreadQuote.attributed(color: .secondaryLabel), + text: body.removingMailThreadQuote().attributed(color: .secondaryLabel), actionButtonImageName: "trash", action: { [weak self] in - self?.deleteDraft(id: messageData.identifier, at: messageIndex) + self?.deleteDraft(id: data.rawMessage.identifier, at: messageIndex) } ) ) diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index d2c1e4a8e..e1be40f44 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -147,14 +147,3 @@ struct MessageBody: Hashable { let text: String let html: String? } - -extension MessageBody { - var textWithoutThreadQuote: String { - guard let range = text.range( - of: "On [a-zA-Z]*, [a-zA-Z0-9 ]* at [0-9:]*, .*", - options: [.regularExpression] - ) else { return text } - - return text[text.startIndex.. String? { + return nil + } + func saveDraft(input: MessageGatewayInput, draftId: String?) async throws -> GTLRGmail_Draft { return GTLRGmail_Draft() } diff --git a/FlowCryptCommon/Extensions/StringExtensions.swift b/FlowCryptCommon/Extensions/StringExtensions.swift index 7ad1df893..fc5bab734 100644 --- a/FlowCryptCommon/Extensions/StringExtensions.swift +++ b/FlowCryptCommon/Extensions/StringExtensions.swift @@ -84,12 +84,21 @@ public extension String { } func removingHtmlTags() -> String? { - return try? NSAttributedString( + try? NSAttributedString( data: self.data(using: .utf8)!, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil ).string } + + func removingMailThreadQuote() -> String { + guard let range = range( + of: "On [a-zA-Z0-9, ]*, at [a-zA-Z0-9: ]*, .* wrote:", + options: [.regularExpression] + ) else { return self } + + return self[startIndex.. Date: Mon, 19 Sep 2022 16:50:57 +0300 Subject: [PATCH 14/56] reload threads view after message sending --- FlowCrypt/App/AppContext.swift | 2 +- .../Compose/ComposeViewController.swift | 18 ++- .../Compose/ComposeViewControllerInput.swift | 2 +- .../ComposeViewController+MessageSend.swift | 9 +- .../ComposeViewController+Setup.swift | 31 ++--- .../ComposeViewController+TapActions.swift | 2 +- .../Inbox/InboxViewController.swift | 20 ++- .../Threads/ThreadDetailsViewController.swift | 116 ++++++++++++++---- .../Gmail/GmailServiceError.swift | 4 +- .../Mail Provider/MailProvider.swift | 2 +- .../Message Gateway/GmailService+send.swift | 15 ++- .../Message Gateway/Imap+send.swift | 6 +- .../Message Gateway/MessageGateway.swift | 2 +- .../Message Provider/MessageService.swift | 4 +- .../Gmail+MessagesList.swift | 25 +--- .../MessagesList Provider/Model/Message.swift | 5 +- .../Backup Services/BackupService.swift | 8 +- .../ComposeMessageService.swift | 11 +- .../Mocks/MessageGatewayMock.swift | 5 +- Gemfile.lock | 18 +-- 20 files changed, 199 insertions(+), 106 deletions(-) diff --git a/FlowCrypt/App/AppContext.swift b/FlowCrypt/App/AppContext.swift index 572167e6d..ad9d3aca7 100644 --- a/FlowCrypt/App/AppContext.swift +++ b/FlowCrypt/App/AppContext.swift @@ -108,7 +108,7 @@ class AppContext { let mailProvider = try getRequiredMailProvider() return BackupService( backupProvider: try mailProvider.backupProvider, - messageSender: try mailProvider.messageSender + messageGateway: try mailProvider.messageGateway ) } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 595908d6e..676ef1f14 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -97,7 +97,11 @@ final class ComposeViewController: TableNodeViewController { var composeSubjectNode: ASCellNode? var sendAsList: [SendAsModel] = [] - let onDelete: ((Identifier) -> Void)? + let handleAction: ((ComposeMessageAction) -> Void)? + + enum ComposeMessageAction { + case create(Message), update(Message), delete(Identifier), sent(Identifier) + } init( appContext: AppContextWithUser, @@ -108,7 +112,7 @@ final class ComposeViewController: TableNodeViewController { filesManager: FilesManagerType = FilesManager(), photosManager: PhotosManagerType = PhotosManager(), keyMethods: KeyMethodsType = KeyMethods(), - onDelete: ((Identifier) -> Void)? = nil + handleAction: ((ComposeMessageAction) -> Void)? = nil ) async throws { self.appContext = appContext self.input = input @@ -162,7 +166,7 @@ final class ComposeViewController: TableNodeViewController { subject: input.subject, attachments: input.attachments ) - self.onDelete = onDelete + self.handleAction = handleAction super.init(node: TableNode()) } @@ -259,3 +263,11 @@ final class ComposeViewController: TableNodeViewController { } extension ComposeViewController: FilesManagerPresenter {} + +/* +fixes + - save draft when tapping back + - add draft when going back from compose to thread view + - delete draft from list on send + - reload drafts list when going back from compose or thread screen +*/ diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index cf23df4b4..b340ec505 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -122,7 +122,7 @@ extension ComposeMessageInput.MessageQuoteInfo { self.text = processed?.text ?? message.body.text self.threadId = message.threadId self.rfc822MsgId = message.rfc822MsgId - self.replyToMsgId = nil // TODO: draft.rawMessage.replyToMsgId, + self.replyToMsgId = message.replyToMsgId self.inReplyTo = message.inReplyTo self.attachments = processed?.attachments ?? message.attachments } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index 1d5fe2d91..881e105f1 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -28,15 +28,18 @@ extension ComposeViewController { try await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg( - input: self.input, - contextToSend: self.contextToSend + input: input, + contextToSend: contextToSend ) UIApplication.shared.isIdleTimerDisabled = true - try await composeMessageService.encryptAndSend( + let identifier = try await composeMessageService.encryptAndSend( message: sendableMsg, threadId: input.threadId ) + handleSuccessfullySentMessage() + + handleAction?(.sent(identifier)) } private func handleSuccessfullySentMessage() { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 7b660087b..f28de3fb1 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -116,6 +116,9 @@ extension ComposeViewController { } } } else { + if case .draft = input.type { + contextToSend.message = input.text + } reload(sections: Section.recipientsSections) didFinishSetup = true } @@ -149,19 +152,19 @@ extension ComposeViewController { // MARK: - NavigationChildController extension ComposeViewController: NavigationChildController { func handleBackButtonTap() { - // TODO: - navigationController?.popViewController(animated: true) -// if let keyPair = signingKeyWithMissingPassphrase { -// requestMissingPassPhraseWithModal(for: keyPair, isDraft: true, withDiscard: true) -// } else { -// saveDraftIfNeeded(withAlert: true) { [weak self] error in -// guard let self = self else { return } -// if case .missingPassphrase(let keyPair) = error as? ComposeMessageError { -// self.requestMissingPassPhraseWithModal(for: keyPair) -// } else { -// self.navigationController?.popViewController(animated: true) -// } -// } -// } + if let keyPair = signingKeyWithMissingPassphrase { + requestMissingPassPhraseWithModal(for: keyPair, isDraft: true, withDiscard: true) + } else { + saveDraftIfNeeded(withAlert: true) { [weak self] error in + guard let self = self else { return } + if case .missingPassPhrase(let keyPair) = error as? ComposeMessageError { + self.requestMissingPassPhraseWithModal(for: keyPair) + } else if let error = error { + self.handle(error: error) + } else { + self.navigationController?.popViewController(animated: true) + } + } + } } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index 6b795cde7..64fa661fd 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -44,7 +44,7 @@ extension ComposeViewController { try await self.composeMessageService.deleteDraft(messageId: messageId) if let messageId = messageId { - self.onDelete?(Identifier(stringId: messageId)) + self.handleAction?(.delete(Identifier(stringId: messageId))) } self.navigationController?.popViewController(animated: true) diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 82cfad6cf..308c03c10 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -636,11 +636,21 @@ extension InboxViewController { let controller = try await ComposeViewController( appContext: appContext, input: .init(type: .draft(draftInfo)), - onDelete: { [weak self] identifier in - guard let self = self, - let index = self.inboxInput.firstIndex(where: { $0.wrappedMessage?.identifier == identifier }) else { return } - self.inboxInput.remove(at: index) - self.tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + handleAction: { [weak self] action in + guard let self = self else { return } + + switch action { + case .create, .update: + // todo + break + case .sent(let message): + break + case .delete(let identifier): + guard let index = self.inboxInput.firstIndex(where: { $0.wrappedMessage?.identifier == identifier }) + else { return } + self.inboxInput.remove(at: index) + self.tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + } } ) navigationController?.pushViewController(controller, animated: true) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 12ee6d1ee..db4c2022d 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -38,7 +38,7 @@ final class ThreadDetailsViewController: TableNodeViewController { private let messageService: MessageService private let messageOperationsProvider: MessageOperationsProvider private let threadOperationsProvider: MessagesThreadOperationsProvider - private let thread: MessageThread + private var thread: MessageThread private var input: [ThreadDetailsViewController.Input] let trashFolderProvider: TrashFolderProviderType @@ -97,13 +97,19 @@ final class ThreadDetailsViewController: TableNodeViewController { setupNavigationBar(thread: thread) expandThreadMessageAndMarkAsRead() + + Task { + try await decryptDrafts() + } } private func expandThreadMessageAndMarkAsRead() { Task { try await threadOperationsProvider.mark(thread: thread, asRead: true, in: currentFolderPath) } - let indexOfSectionToExpand = input.firstIndex(where: { !$0.rawMessage.isRead }) ?? input.count - 1 + let indexOfSectionToExpand = input.firstIndex(where: { !$0.rawMessage.isRead }) + ?? input.firstIndex(where: { !$0.rawMessage.isRead && !$0.rawMessage.isDraft }) + ?? input.count - 1 let indexPath = IndexPath(row: 0, section: indexOfSectionToExpand + 1) handleExpandTap(at: indexPath) } @@ -164,18 +170,37 @@ extension ThreadDetailsViewController { let controller = try await ComposeViewController( appContext: appContext, input: .init(type: .draft(draftInfo)), - onDelete: { [weak self] identifier in - guard let self = self, - let index = self.input.firstIndex(where: { $0.rawMessage.identifier == identifier }) - else { return } - - self.input.remove(at: index) - self.node.deleteSections([index + 1], with: .automatic) + handleAction: { [weak self] action in + guard let self = self else { return } + + switch action { + case .create, .update: + // todo + break + case .sent(let identifier): + Task { + let processedMessage = try await self.messageService.getAndProcess( + identifier: identifier, + folder: self.thread.path, + onlyLocalKeys: false, + userEmail: self.appContext.user.email, + isUsingKeyManager: self.appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + let indexPath = IndexPath(row: 0, section: self.input.count) + self.handle(processedMessage: processedMessage, at: indexPath) + } + case .delete(let identifier): + guard let index = self.input.firstIndex(where: { $0.rawMessage.identifier == identifier }) + else { return } + + self.input.remove(at: index) + self.node.deleteSections([index + 1], with: .automatic) + } } ) navigationController?.pushViewController(controller, animated: true) } catch { - showAlert(message: error.localizedDescription) + showAlert(message: error.errorMessage) } } } @@ -339,11 +364,38 @@ extension ThreadDetailsViewController { do { let composeVC = try await ComposeViewController( appContext: appContext, - input: ComposeMessageInput(type: composeType) + input: ComposeMessageInput(type: composeType), + handleAction: { [weak self] action in + guard let self = self else { return } + + switch action { + case .create, .update: + // todo + break + case .sent(let identifier): + Task { + let processedMessage = try await self.messageService.getAndProcess( + identifier: identifier, + folder: self.thread.path, + onlyLocalKeys: false, + userEmail: self.appContext.user.email, + isUsingKeyManager: self.appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + let indexPath = IndexPath(row: 0, section: self.input.count) + self.handle(processedMessage: processedMessage, at: indexPath) + } + case .delete(let identifier): + guard let index = self.input.firstIndex(where: { $0.rawMessage.identifier == identifier }) + else { return } + + self.input.remove(at: index) + self.node.deleteSections([index + 1], with: .automatic) + } + } ) navigationController?.pushViewController(composeVC, animated: true) } catch { - showAlert(message: error.localizedDescription) + showAlert(message: error.errorMessage) } } } @@ -359,7 +411,7 @@ extension ThreadDetailsViewController { Task { do { var processedMessage = try await messageService.getAndProcess( - message: message, + identifier: message.identifier, folder: thread.path, onlyLocalKeys: true, userEmail: appContext.user.email, @@ -479,14 +531,18 @@ extension ThreadDetailsViewController { if matched { let message = input[indexPath.section - 1].rawMessage - let processedMessage = try await messageService.decryptAndProcess( - message: message, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager - ) + if !message.isDraft { + let processedMessage = try await messageService.decryptAndProcess( + message: message, + onlyLocalKeys: false, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + + handle(processedMessage: processedMessage, at: indexPath) + } - handle(processedMessage: processedMessage, at: indexPath) + try await decryptDrafts() } else { handleWrongPassPhrase(passPhrase, indexPath: indexPath) } @@ -496,6 +552,24 @@ extension ThreadDetailsViewController { } } + private func decryptDrafts() async throws { + for (index, data) in input.enumerated() { + guard data.rawMessage.isDraft && data.rawMessage.isPgp else { continue } + let indexPath = IndexPath(row: 0, section: index + 1) + do { + let processedMessage = try await messageService.decryptAndProcess( + message: data.rawMessage, + onlyLocalKeys: false, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + handle(processedMessage: processedMessage, at: indexPath) + } catch { + handle(error: error, at: indexPath) + } + } + } + private func retryVerifyingSignatureWithRemotelyFetchedKeys( message: Message, folder: String, @@ -504,7 +578,7 @@ extension ThreadDetailsViewController { Task { do { let processedMessage = try await messageService.getAndProcess( - message: message, + identifier: message.identifier, folder: thread.path, onlyLocalKeys: false, userEmail: appContext.user.email, diff --git a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailServiceError.swift b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailServiceError.swift index d803847e5..fe2fc8512 100644 --- a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailServiceError.swift +++ b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailServiceError.swift @@ -54,9 +54,9 @@ extension GmailServiceError { static func convert(from error: NSError) -> GmailServiceError { switch error.code { case -10: // invalid_grant error code - return GmailServiceError.invalidGrant(error) + return .invalidGrant(error) default: - return GmailServiceError.providerError(error) + return .providerError(error) } } } diff --git a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift index d90245786..18b7a7276 100644 --- a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift @@ -29,7 +29,7 @@ final class MailProvider { } private let services: [MailServiceProvider] - var messageSender: MessageGateway { + var messageGateway: MessageGateway { get throws { try resolveService(of: MessageGateway.self) } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift index 91c09c59d..b83dde968 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift @@ -10,8 +10,8 @@ import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: MessageGateway { - func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws -> Identifier { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in guard let raw = GTLREncodeBase64(input.mime) else { return continuation.resume(throwing: GmailServiceError.messageEncode) } @@ -28,12 +28,19 @@ extension GmailService: MessageGateway { uploadParameters: nil ) - gmailService.executeQuery(querySend) { [weak self] _, _, error in + gmailService.executeQuery(querySend) { [weak self] _, data, error in self?.progressHandler = nil + if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - return continuation.resume() + + guard let gmailMessage = data as? GTLRGmail_Message else { + return continuation.resume(throwing: AppErr.cast("GTLRGmail_Message")) + } + + let identifier = Identifier(stringId: gmailMessage.identifier) + return continuation.resume(returning: identifier) } } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift index 52f281a0f..b36ffb534 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift @@ -5,8 +5,8 @@ import Foundation extension Imap: MessageGateway { - func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws { - try await withCheckedThrowingContinuation { [weak self] (continuation: CheckedContinuation) in + func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws -> Identifier { + try await withCheckedThrowingContinuation { [weak self] (continuation: CheckedContinuation) in do { let session = try self?.smtpSess session?.sendOperation(with: input.mime) @@ -14,7 +14,7 @@ extension Imap: MessageGateway { if let error = error { return continuation.resume(throwing: error) } - return continuation.resume() + return continuation.resume(throwing: AppErr.unexpected("Not implemented")) } } catch { return continuation.resume(throwing: ImapError.noSession) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift index c873105d9..17b2eb697 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift @@ -15,7 +15,7 @@ struct MessageGatewayInput { } protocol MessageGateway { - func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws + func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws -> Identifier } protocol DraftGateway { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 953b311a7..ff448040f 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -88,14 +88,14 @@ final class MessageService { // MARK: - Message processing func getAndProcess( - message: Message, + identifier: Identifier, folder: String, onlyLocalKeys: Bool, userEmail: String, isUsingKeyManager: Bool ) async throws -> ProcessedMessage { let message = try await messageProvider.fetchMsg( - id: message.identifier, + id: identifier, folder: folder ) if message.isPgp { diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift index 37eb9587a..7d5a6ba14 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift @@ -22,7 +22,7 @@ extension GmailService: MessagesListProvider { if let self = self { for identifier in messageIdentifiers { taskGroup.addTask { - try await self.fetchFullMessage(with: identifier) + try await self.fetchMsg(id: Identifier(stringId: identifier), folder: "") } } @@ -74,27 +74,4 @@ extension GmailService { } } } - - private func fetchFullMessage(with identifier: String) async throws -> Message { - let query = GTLRGmailQuery_UsersMessagesGet.query(withUserId: .me, identifier: identifier) - query.format = kGTLRGmailFormatFull - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - gmailService.executeQuery(query) { _, data, error in - if let error = error { - return continuation.resume(throwing: GmailServiceError.providerError(error)) - } - - guard let gmailMessage = data as? GTLRGmail_Message else { - return continuation.resume(throwing: AppErr.cast("GTLRGmail_Message")) - } - - do { - let message = try Message(gmailMessage: gmailMessage) - return continuation.resume(returning: message) - } catch { - return continuation.resume(throwing: error) - } - } - } - } } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index e1be40f44..2cca1520e 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -27,6 +27,7 @@ struct Message: Hashable { var raw: String? let body: MessageBody let inReplyTo: String? + let replyToMsgId: String? private(set) var labels: [MessageLabel] var isRead: Bool { @@ -68,7 +69,8 @@ struct Message: Hashable { cc: String? = nil, bcc: String? = nil, replyTo: String? = nil, - inReplyTo: String? = nil + inReplyTo: String? = nil, + replyToMsgId: String? = nil ) { self.identifier = identifier self.date = date @@ -87,6 +89,7 @@ struct Message: Hashable { self.bcc = Self.parseRecipients(bcc) self.replyTo = Self.parseRecipients(replyTo) self.inReplyTo = inReplyTo + self.replyToMsgId = replyToMsgId } } diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift index 280649cc9..89f9a7c5b 100644 --- a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift @@ -11,16 +11,16 @@ import UIKit final class BackupService { let backupProvider: BackupProvider let core: Core - let messageSender: MessageGateway + let messageGateway: MessageGateway init( backupProvider: BackupProvider, core: Core = .shared, - messageSender: MessageGateway + messageGateway: MessageGateway ) { self.backupProvider = backupProvider self.core = core - self.messageSender = messageSender + self.messageGateway = messageGateway } } @@ -70,7 +70,7 @@ extension BackupService: BackupServiceType { ) let t = try await core.composeEmail(msg: message, fmt: .plain) - try await messageSender.sendMail( + try await messageGateway.sendMail( input: MessageGatewayInput(mime: t.mimeEncoded, threadId: nil), progressHandler: nil ) diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index e39ef0b7b..8d2ddc96c 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -234,13 +234,14 @@ final class ComposeMessageService { fmt: .encryptInline ).mimeEncoded - self.draftId = try await draftGateway?.saveDraft( + let draft = try await draftGateway?.saveDraft( input: MessageGatewayInput( mime: mime, threadId: threadId ), draftId: self.draftId - ).identifier + ) + self.draftId = draft?.identifier } catch { throw ComposeMessageError.gatewayError(error) } @@ -256,7 +257,7 @@ final class ComposeMessageService { } // MARK: - Encrypt and Send - func encryptAndSend(message: SendableMsg, threadId: String?) async throws { + func encryptAndSend(message: SendableMsg, threadId: String?) async throws -> Identifier { do { onStateChanged?(.startComposing) @@ -277,7 +278,7 @@ final class ComposeMessageService { threadId: threadId ) - try await appContext.getRequiredMailProvider().messageSender.sendMail( + let identifier = try await appContext.getRequiredMailProvider().messageGateway.sendMail( input: input, progressHandler: { [weak self] progress in let progressToShow = hasPassword ? 0.5 + progress / 2 : progress @@ -291,6 +292,8 @@ final class ComposeMessageService { } onStateChanged?(.messageSent) + + return identifier } catch { throw ComposeMessageError.gatewayError(error) } diff --git a/FlowCryptAppTests/Mocks/MessageGatewayMock.swift b/FlowCryptAppTests/Mocks/MessageGatewayMock.swift index 68d258941..32d73538f 100644 --- a/FlowCryptAppTests/Mocks/MessageGatewayMock.swift +++ b/FlowCryptAppTests/Mocks/MessageGatewayMock.swift @@ -10,10 +10,11 @@ import Foundation class MessageGatewayMock: MessageGateway { - var sendMailResult: ((Data) -> (Result))! - func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws { + var sendMailResult: ((Data) -> (Result))! + func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws -> Identifier { if case .failure(let error) = sendMailResult(input.mime) { throw error } + return .random } } diff --git a/Gemfile.lock b/Gemfile.lock index 7ea8df387..4f2bfec97 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,8 +17,8 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.628.0) - aws-sdk-core (3.145.0) + aws-partitions (1.631.0) + aws-sdk-core (3.149.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) @@ -116,7 +116,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.209.1) + fastlane (2.210.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -160,9 +160,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.26.0) - google-apis-core (>= 0.7, < 2.a) - google-apis-core (0.7.0) + google-apis-androidpublisher_v3 (0.27.0) + google-apis-core (>= 0.7.2, < 2.a) + google-apis-core (0.8.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -171,8 +171,8 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.13.0) - google-apis-core (>= 0.7, < 2.a) + google-apis-iamcredentials_v1 (0.14.0) + google-apis-core (>= 0.7.2, < 2.a) google-apis-playcustomapp_v1 (0.10.0) google-apis-core (>= 0.7, < 2.a) google-apis-storage_v1 (0.17.0) @@ -183,7 +183,7 @@ GEM google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.3.0) - google-cloud-storage (1.39.0) + google-cloud-storage (1.41.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) From 3e829dde7491c0648fc0262ae42e29a89b1edc02 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 20 Sep 2022 09:32:59 +0300 Subject: [PATCH 15/56] code cleanup --- FlowCrypt.xcodeproj/project.pbxproj | 4 + FlowCrypt/App/AppContext.swift | 1 - .../View Controllers/BlurViewController.swift | 2 +- .../Compose/ComposeMessageAction.swift | 13 ++ .../ComposeRecipientPopupViewController.swift | 1 - .../Compose/ComposeViewController.swift | 17 ++- .../ComposeViewController+Setup.swift | 2 +- .../Inbox/InboxViewController+Factory.swift | 1 - .../Inbox/InboxViewController.swift | 1 - .../BackupSelectKeyViewController.swift | 1 - .../ContactDetailDecorator.swift | 1 - .../ContactKeyDetailDecorator.swift | 1 - .../Key List/KeySettingsViewDecorator.swift | 1 - .../SetupImap/SetupImapViewController.swift | 9 +- .../Threads/ThreadDetailsViewController.swift | 136 ++++++++---------- FlowCrypt/Core/CoreHost.swift | 1 - FlowCrypt/Core/CoreTypes.swift | 1 - FlowCrypt/Core/Models/KeyDetails.swift | 1 - FlowCrypt/Functionality/Api/ApiCall.swift | 1 - .../EmailKeyManagerApi.swift | 1 - .../Api/Remote Pub Key Apis/AttesterApi.swift | 1 - .../WkdUrlConstructor.swift | 1 - .../Encrypted Storage/KeyChainService.swift | 1 - .../DataManager/SessionService.swift | 1 - .../Mail Provider/Gmail/GmailService.swift | 1 - .../Mail Provider/Imap/Imap+Other.swift | 1 - .../Mail Provider/Imap/Imap+messages.swift | 1 - .../Mail Provider/Imap/Imap+msg.swift | 1 - .../Mail Provider/Imap/Imap+retry.swift | 1 - .../Mail Provider/Imap/Imap+session.swift | 1 - .../Mail Provider/Imap/ImapHelper.swift | 1 - .../Imap/MessageKindProviderType.swift | 1 - .../ConnectionType.swift | 1 - .../IMAPConnectionParameters.swift | 1 - .../MailSettingsCredentials.swift | 1 - .../Mail Sessions Providers/SMTPSession.swift | 1 - .../SessionCredentialsProvider.swift | 1 - .../Mail Provider/MailProvider.swift | 1 - .../MailServiceProviderType.swift | 1 - .../Message Gateway/GmailService+draft.swift | 2 +- .../Message Gateway/GmailService+send.swift | 1 - .../Message Gateway/MessageGateway.swift | 1 - .../Message Provider/MessageService.swift | 1 - .../Gmail+MessageOperations.swift | 1 - .../Imap+MessageOperations.swift | 1 - .../MessageOperationsProvider.swift | 1 - .../Imap+MessagesList.swift | 1 - .../MessagesList Provider/Model/Message.swift | 1 - .../Model/MessageLabel.swift | 1 - .../SearchMessage Provider/Imap+Search.swift | 1 - .../MessagesThreadOperationsProvider.swift | 1 - FlowCrypt/Functionality/Pgp/KeyMethods.swift | 1 - .../PhotosManager/PhotosManager.swift | 2 - .../ClientConfiguration.swift | 1 - .../ClientConfigurationService.swift | 1 - .../LocalClientConfiguration.swift | 1 - .../ComposeMessageService.swift | 2 - .../Functionality/Services/EKMVcHelper.swift | 1 - .../Folders Services/FoldersService.swift | 1 - .../LocalFoldersProvider.swift | 1 - .../GmailService+folders.swift | 1 - .../Imap+folders.swift | 1 - .../GoogleUserService.swift | 1 - .../LocalContactsProvider.swift | 1 - .../KeyAndPassPhraseStorage.swift | 1 - .../SendAs Services/LocalSendAsProvider.swift | 1 - .../GmailService+SendAs.swift | 1 - .../SendAs Services/SendAsService.swift | 1 - FlowCrypt/Models/Common/Recipient.swift | 1 - .../ClientConfigurationRealmObject.swift | 1 - .../Realm Models/FolderRealmObject.swift | 1 - .../Realm Models/KeypairRealmObject.swift | 1 - .../Realm Models/PubKeyRealmObject.swift | 1 - .../Realm Models/RecipientRealmObject.swift | 1 - .../Realm Models/SendAsRealmObject.swift | 1 - .../Models/Realm Models/UserRealmObject.swift | 1 - .../ClientConfigurationTests.swift | 1 - .../ClientConfigurationProviderMock.swift | 1 - .../Mocks/EnterpriseServerApiMock.swift | 1 - .../OrganisationalRulesServiceMock.swift | 1 - .../PassPhraseStorageMock.swift | 1 - .../Mocks/CoreComposeMessageMock.swift | 1 - .../Mocks/LocalContactsProviderMock.swift | 1 - .../Mocks/MessageGatewayMock.swift | 1 - FlowCryptAppTests/TestData.swift | 1 - FlowCryptCommon/Extensions/Then.swift | 1 - .../Cell Nodes/ContactKeyCellNode.swift | 1 - 87 files changed, 93 insertions(+), 173 deletions(-) create mode 100644 FlowCrypt/Controllers/Compose/ComposeMessageAction.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 4fc9fa948..5561ae2a4 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -117,6 +117,7 @@ 51B9EE6F27567B520080B2D5 /* MessageRecipientsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */; }; 51C0C1EF271982A1000C9738 /* MailCore in Frameworks */ = {isa = PBXBuildFile; productRef = 51C0C1EE271982A1000C9738 /* MailCore */; }; 51C0C63828D1E42A003C540E /* ComposeViewController+ErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0C63728D1E42A003C540E /* ComposeViewController+ErrorHandling.swift */; }; + 51CE196728D8AC5300A5B200 /* ComposeMessageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE196628D8AC5300A5B200 /* ComposeMessageAction.swift */; }; 51DA5BD62721AB07001C4359 /* PubKeyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DA5BD52721AB07001C4359 /* PubKeyState.swift */; }; 51DA5BDA2722C82E001C4359 /* RecipientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DA5BD92722C82E001C4359 /* RecipientTests.swift */; }; 51DAD9BD273E7DD20076CBA7 /* BadgeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */; }; @@ -584,6 +585,7 @@ 51B7421A27F318D300E702C8 /* XCTestCaseExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtension.swift; sourceTree = ""; }; 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRecipientsNode.swift; sourceTree = ""; }; 51C0C63728D1E42A003C540E /* ComposeViewController+ErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewController+ErrorHandling.swift"; sourceTree = ""; }; + 51CE196628D8AC5300A5B200 /* ComposeMessageAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageAction.swift; sourceTree = ""; }; 51DA5BD52721AB07001C4359 /* PubKeyState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKeyState.swift; sourceTree = ""; }; 51DA5BD92722C82E001C4359 /* RecipientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientTests.swift; sourceTree = ""; }; 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeNode.swift; sourceTree = ""; }; @@ -959,6 +961,7 @@ 04B4728F1ECE29F600B8266F /* Compose */ = { isa = PBXGroup; children = ( + 51CE196628D8AC5300A5B200 /* ComposeMessageAction.swift */, 32DCAAE9F459F48178CAF8F5 /* ComposeViewController.swift */, D269E02624103A20000495C3 /* ComposeViewControllerInput.swift */, 9F23EA4F237217140017DFED /* ComposeViewDecorator.swift */, @@ -2753,6 +2756,7 @@ 2CC50FB12744167A0051629A /* Folder.swift in Sources */, 514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */, D2F41373243CC7990066AFB5 /* UserRealmObject.swift in Sources */, + 51CE196728D8AC5300A5B200 /* ComposeMessageAction.swift in Sources */, 5A39F42D239EC321001F4607 /* SettingsViewController.swift in Sources */, 5ADEDCBC23A4329000EC495E /* PublicKeyDetailViewController.swift in Sources */, 21489B80267CC39E00BDE4AC /* ClientConfigurationService.swift in Sources */, diff --git a/FlowCrypt/App/AppContext.swift b/FlowCrypt/App/AppContext.swift index ad9d3aca7..6745f1215 100644 --- a/FlowCrypt/App/AppContext.swift +++ b/FlowCrypt/App/AppContext.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a.s. All rights reserved. // -import Foundation import UIKit class AppContext { diff --git a/FlowCrypt/Common UI/View Controllers/BlurViewController.swift b/FlowCrypt/Common UI/View Controllers/BlurViewController.swift index f63a57c1b..82292a496 100644 --- a/FlowCrypt/Common UI/View Controllers/BlurViewController.swift +++ b/FlowCrypt/Common UI/View Controllers/BlurViewController.swift @@ -5,7 +5,7 @@ // Created by Parag Dulam on 24/10/21 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation + import UIKit final class BlurViewController: UIViewController { diff --git a/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift b/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift new file mode 100644 index 000000000..32c929cb0 --- /dev/null +++ b/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift @@ -0,0 +1,13 @@ +// +// ComposeMessageAction.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 19/09/22 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +enum ComposeMessageAction { + case create(Message), update(Message), delete(Identifier), sent(Identifier) +} diff --git a/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift b/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift index 82589fcc0..b8be5b130 100644 --- a/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeRecipientPopupViewController.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import AsyncDisplayKit import FlowCryptUI diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 676ef1f14..394f65864 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -6,7 +6,6 @@ import AsyncDisplayKit import Combine import FlowCryptCommon import FlowCryptUI -import Foundation /** * View controller to compose the message and send it @@ -99,10 +98,6 @@ final class ComposeViewController: TableNodeViewController { let handleAction: ((ComposeMessageAction) -> Void)? - enum ComposeMessageAction { - case create(Message), update(Message), delete(Identifier), sent(Identifier) - } - init( appContext: AppContextWithUser, decorator: ComposeViewDecorator = ComposeViewDecorator(), @@ -271,3 +266,15 @@ fixes - delete draft from list on send - reload drafts list when going back from compose or thread screen */ + +/* ui test +- open compose +- create draft + - go back + - go to drafts folder + - check if folder there + - go to inbox + - open existing thread + - create reply + - check draft + */ diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index f28de3fb1..398387d18 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -158,7 +158,7 @@ extension ComposeViewController: NavigationChildController { saveDraftIfNeeded(withAlert: true) { [weak self] error in guard let self = self else { return } if case .missingPassPhrase(let keyPair) = error as? ComposeMessageError { - self.requestMissingPassPhraseWithModal(for: keyPair) + self.requestMissingPassPhraseWithModal(for: keyPair, isDraft: true, withDiscard: true) } else if let error = error { self.handle(error: error) } else { diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift b/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift index e28c09cd4..0826769d4 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import UIKit class InboxViewControllerFactory { diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 308c03c10..9781d5651 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -5,7 +5,6 @@ import AsyncDisplayKit import FlowCryptCommon import FlowCryptUI -import Foundation @MainActor class InboxViewController: ViewController { diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift index 37c61c12f..47bed1ae7 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift @@ -8,7 +8,6 @@ import AsyncDisplayKit import FlowCryptUI -import Foundation final class BackupSelectKeyViewController: TableNodeViewController { diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift index e495c0d66..9ea0b8f37 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift @@ -7,7 +7,6 @@ // import FlowCryptUI -import Foundation struct ContactDetailDecorator { let title = "contact_detail_screen_title".localized diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift index c90039738..62ea9037a 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift @@ -7,7 +7,6 @@ // import FlowCryptUI -import Foundation struct ContactKeyDetailDecorator { let title = "contact_key_detail_screen_title".localized diff --git a/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewDecorator.swift b/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewDecorator.swift index 33b807346..852c176a9 100644 --- a/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewDecorator.swift +++ b/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewDecorator.swift @@ -9,7 +9,6 @@ import FlowCryptCommon import FlowCryptUI import UIKit -import Foundation extension DateFormatter { static let keySettingsFormatter: DateFormatter = { diff --git a/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift b/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift index 93379a29c..5bc598099 100644 --- a/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift +++ b/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift @@ -185,10 +185,7 @@ extension SetupImapViewController { private func update(for newState: State) { state = newState - node.reloadSections( - IndexSet(integer: 3), - with: .fade - ) + node.reloadSections([3], with: .fade) node.scrollToRow( at: IndexPath(row: 2, section: 3), @@ -292,14 +289,14 @@ extension SetupImapViewController { private func reloadImapSection() { node.reloadSections( - IndexSet(integer: Section.imap(.port).section), + [Section.imap(.port).section], with: .none ) } private func reloadSmtpSection() { node.reloadSections( - IndexSet(integer: Section.smtp(.port).section), + [Section.smtp(.port).section], with: .none ) } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index db4c2022d..ae6842f3d 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -9,7 +9,6 @@ import AsyncDisplayKit import FlowCryptUI import FlowCryptCommon -import Foundation import UIKit final class ThreadDetailsViewController: TableNodeViewController { @@ -97,10 +96,6 @@ final class ThreadDetailsViewController: TableNodeViewController { setupNavigationBar(thread: thread) expandThreadMessageAndMarkAsRead() - - Task { - try await decryptDrafts() - } } private func expandThreadMessageAndMarkAsRead() { @@ -122,7 +117,7 @@ extension ThreadDetailsViewController { if input[indexPath.section - 1].isExpanded { UIView.animate( - withDuration: 0.3, + withDuration: 0.2, animations: { if let threadNode = self.node.nodeForRow(at: indexPath) as? ThreadMessageInfoCellNode { threadNode.expandNode.view.alpha = 0 @@ -140,7 +135,7 @@ extension ThreadDetailsViewController { ) } else { UIView.animate(withDuration: 0.3) { - self.node.reloadSections(IndexSet(integer: indexPath.section), with: .automatic) + self.node.reloadSections([indexPath.section], with: .automatic) } } } @@ -149,7 +144,7 @@ extension ThreadDetailsViewController { input[indexPath.section - 1].shouldShowRecipientsList.toggle() UIView.animate(withDuration: 0.3) { - self.node.reloadSections(IndexSet(integer: indexPath.section), with: .automatic) + self.node.reloadSections([indexPath.section], with: .automatic) } } @@ -171,31 +166,7 @@ extension ThreadDetailsViewController { appContext: appContext, input: .init(type: .draft(draftInfo)), handleAction: { [weak self] action in - guard let self = self else { return } - - switch action { - case .create, .update: - // todo - break - case .sent(let identifier): - Task { - let processedMessage = try await self.messageService.getAndProcess( - identifier: identifier, - folder: self.thread.path, - onlyLocalKeys: false, - userEmail: self.appContext.user.email, - isUsingKeyManager: self.appContext.clientConfigurationService.configuration.isUsingKeyManager - ) - let indexPath = IndexPath(row: 0, section: self.input.count) - self.handle(processedMessage: processedMessage, at: indexPath) - } - case .delete(let identifier): - guard let index = self.input.firstIndex(where: { $0.rawMessage.identifier == identifier }) - else { return } - - self.input.remove(at: index) - self.node.deleteSections([index + 1], with: .automatic) - } + self?.handleComposeMessageAction(action) } ) navigationController?.pushViewController(controller, animated: true) @@ -228,6 +199,31 @@ extension ThreadDetailsViewController { present(alert, animated: true, completion: nil) } + private func handleComposeMessageAction(_ action: ComposeMessageAction) { + switch action { + case .create, .update: + break + case .sent(let identifier): + Task { + let processedMessage = try await self.messageService.getAndProcess( + identifier: identifier, + folder: self.thread.path, + onlyLocalKeys: false, + userEmail: self.appContext.user.email, + isUsingKeyManager: self.appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + let indexPath = IndexPath(row: 0, section: self.input.count) + self.handle(processedMessage: processedMessage, at: indexPath) + } + case .delete(let identifier): + guard let index = self.input.firstIndex(where: { $0.rawMessage.identifier == identifier }) + else { return } + + self.input.remove(at: index) + self.node.deleteSections([index + 1], with: .automatic) + } + } + private func createComposeNewMessageAlertAction(at indexPath: IndexPath, type: MessageQuoteType) -> UIAlertAction { let action = UIAlertAction( title: type.actionLabel, @@ -366,31 +362,7 @@ extension ThreadDetailsViewController { appContext: appContext, input: ComposeMessageInput(type: composeType), handleAction: { [weak self] action in - guard let self = self else { return } - - switch action { - case .create, .update: - // todo - break - case .sent(let identifier): - Task { - let processedMessage = try await self.messageService.getAndProcess( - identifier: identifier, - folder: self.thread.path, - onlyLocalKeys: false, - userEmail: self.appContext.user.email, - isUsingKeyManager: self.appContext.clientConfigurationService.configuration.isUsingKeyManager - ) - let indexPath = IndexPath(row: 0, section: self.input.count) - self.handle(processedMessage: processedMessage, at: indexPath) - } - case .delete(let identifier): - guard let index = self.input.firstIndex(where: { $0.rawMessage.identifier == identifier }) - else { return } - - self.input.remove(at: index) - self.node.deleteSections([index + 1], with: .automatic) - } + self?.handleComposeMessageAction(action) } ) navigationController?.pushViewController(composeVC, animated: true) @@ -446,14 +418,19 @@ extension ThreadDetailsViewController { UIView.animate( withDuration: 0.2, animations: { - self.node.reloadSections(IndexSet(integer: indexPath.section), with: .automatic) + if indexPath.section < self.node.numberOfSections { + self.node.reloadSections([indexPath.section], with: .automatic) + } else { + self.node.insertSections([indexPath.section], with: .automatic) + } }, completion: { [weak self] _ in self?.node.scrollToRow(at: indexPath, at: .middle, animated: true) + self?.decryptDrafts() }) } else { input[messageIndex].processedMessage?.signature = processedMessage.signature - node.reloadSections(IndexSet(integer: indexPath.section), with: .automatic) + node.reloadSections([indexPath.section], with: .automatic) } } @@ -541,8 +518,6 @@ extension ThreadDetailsViewController { handle(processedMessage: processedMessage, at: indexPath) } - - try await decryptDrafts() } else { handleWrongPassPhrase(passPhrase, indexPath: indexPath) } @@ -552,20 +527,22 @@ extension ThreadDetailsViewController { } } - private func decryptDrafts() async throws { - for (index, data) in input.enumerated() { - guard data.rawMessage.isDraft && data.rawMessage.isPgp else { continue } - let indexPath = IndexPath(row: 0, section: index + 1) - do { - let processedMessage = try await messageService.decryptAndProcess( - message: data.rawMessage, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager - ) - handle(processedMessage: processedMessage, at: indexPath) - } catch { - handle(error: error, at: indexPath) + private func decryptDrafts() { + Task { + for (index, data) in input.enumerated() { + guard data.rawMessage.isDraft && data.rawMessage.isPgp && data.processedMessage == nil else { continue } + let indexPath = IndexPath(row: 0, section: index + 1) + do { + let processedMessage = try await messageService.decryptAndProcess( + message: data.rawMessage, + onlyLocalKeys: false, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + handle(processedMessage: processedMessage, at: indexPath) + } catch { + handle(error: error, at: indexPath) + } } } } @@ -773,13 +750,13 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { text: body.removingMailThreadQuote().attributed(color: .secondaryLabel), actionButtonImageName: "trash", action: { [weak self] in - self?.deleteDraft(id: data.rawMessage.identifier, at: messageIndex) + self?.deleteDraft(id: data.rawMessage.identifier) } ) ) } - private func deleteDraft(id: Identifier, at index: Int) { + private func deleteDraft(id: Identifier) { showAlertWithAction( title: "draft_delete_confirmation".localized, message: nil, @@ -791,6 +768,9 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { id: id, from: nil ) + + guard let index = self?.input.firstIndex(where: { $0.rawMessage.identifier == id }) else { return } + self?.input.remove(at: index) self?.node.deleteSections([index + 1], with: .automatic) } diff --git a/FlowCrypt/Core/CoreHost.swift b/FlowCrypt/Core/CoreHost.swift index 30d0d2e96..c39ae1450 100644 --- a/FlowCrypt/Core/CoreHost.swift +++ b/FlowCrypt/Core/CoreHost.swift @@ -4,7 +4,6 @@ import CommonCrypto // for hashing import FlowCryptCommon -import Foundation import IDZSwiftCommonCrypto // for aes import JavaScriptCore // for export to js import Security // for rng diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index 0627c4bf4..466df14c6 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -2,7 +2,6 @@ // © 2017-2019 FlowCrypt Limited. All rights reserved. // -import Foundation import RealmSwift struct CoreRes { diff --git a/FlowCrypt/Core/Models/KeyDetails.swift b/FlowCrypt/Core/Models/KeyDetails.swift index 11da3a464..acd96f5ce 100644 --- a/FlowCrypt/Core/Models/KeyDetails.swift +++ b/FlowCrypt/Core/Models/KeyDetails.swift @@ -8,7 +8,6 @@ import FlowCryptCommon import MailCore -import Foundation protocol ArmoredPrvWithIdentity { var primaryFingerprint: String { get throws } diff --git a/FlowCrypt/Functionality/Api/ApiCall.swift b/FlowCrypt/Functionality/Api/ApiCall.swift index df746d6e6..43ce705fd 100644 --- a/FlowCrypt/Functionality/Api/ApiCall.swift +++ b/FlowCrypt/Functionality/Api/ApiCall.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import FlowCryptCommon enum ApiCall {} diff --git a/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift b/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift index 7c56c6bb8..e426c2642 100644 --- a/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift +++ b/FlowCrypt/Functionality/Api/Remote Private Key Apis/EmailKeyManagerApi.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import FlowCryptCommon protocol EmailKeyManagerApiType { diff --git a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/AttesterApi.swift b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/AttesterApi.swift index ab074e19d..53f986c3c 100644 --- a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/AttesterApi.swift +++ b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/AttesterApi.swift @@ -2,7 +2,6 @@ // © 2017-2019 FlowCrypt Limited. All rights reserved. // -import Foundation import FlowCryptCommon protocol AttesterApiType { diff --git a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdUrlConstructor.swift b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdUrlConstructor.swift index 83196f6e1..e9d5667b7 100644 --- a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdUrlConstructor.swift +++ b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdUrlConstructor.swift @@ -7,7 +7,6 @@ // import CryptoKit -import Foundation /// WKD - Web Key Directory, follow this link for more information https://wiki.gnupg.org/WKD diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift index 1cc4e9ba2..c52054d35 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation import Security import UIKit diff --git a/FlowCrypt/Functionality/DataManager/SessionService.swift b/FlowCrypt/Functionality/DataManager/SessionService.swift index 7b6ef92c5..bbd3585ec 100644 --- a/FlowCrypt/Functionality/DataManager/SessionService.swift +++ b/FlowCrypt/Functionality/DataManager/SessionService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation enum SessionType: CustomStringConvertible { case google(_ email: String, name: String, token: String) diff --git a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift index 1c149bb17..9462bf895 100644 --- a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation import GoogleAPIClientForREST_Gmail class GmailService: MailServiceProvider { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+Other.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+Other.swift index 3fb85a03e..a81560ad5 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+Other.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+Other.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore extension Imap { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+messages.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+messages.swift index e9881ed66..d48ee9a8a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+messages.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+messages.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore extension Imap { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift index debbac664..e431f7322 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift @@ -2,7 +2,6 @@ // © 2017-2019 FlowCrypt Limited. All rights reserved. // -import Foundation import MailCore extension Imap { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift index 145ff9959..9fdb742fe 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore extension Imap { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift index ab1f007dd..77b286491 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation import MailCore extension Imap { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/ImapHelper.swift b/FlowCrypt/Functionality/Mail Provider/Imap/ImapHelper.swift index 13f86f2a4..2ea4ea4a6 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/ImapHelper.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/ImapHelper.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore protocol ImapHelperType { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/MessageKindProviderType.swift b/FlowCrypt/Functionality/Mail Provider/Imap/MessageKindProviderType.swift index 21ef99932..bdeed4465 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/MessageKindProviderType.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/MessageKindProviderType.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore protocol MessageKindProviderType { diff --git a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/ConnectionType.swift b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/ConnectionType.swift index 42d494a79..3c0839ba6 100644 --- a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/ConnectionType.swift +++ b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/ConnectionType.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore enum AuthType { diff --git a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/IMAPConnectionParameters.swift b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/IMAPConnectionParameters.swift index ff94ea365..f0179af42 100644 --- a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/IMAPConnectionParameters.swift +++ b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/IMAPConnectionParameters.swift @@ -6,7 +6,6 @@ // Copyright © 2020 FlowCrypt Limited. All rights reserved. // -import Foundation import MailCore struct IMAPSession { diff --git a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/MailSettingsCredentials.swift b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/MailSettingsCredentials.swift index 532597cd7..d9fbd8510 100644 --- a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/MailSettingsCredentials.swift +++ b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/MailSettingsCredentials.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore struct MailSettingsCredentials { diff --git a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SMTPSession.swift b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SMTPSession.swift index 74fa7b624..99ff35acd 100644 --- a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SMTPSession.swift +++ b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SMTPSession.swift @@ -6,7 +6,6 @@ // Copyright © 2020 FlowCrypt Limited. All rights reserved. // -import Foundation import MailCore struct SMTPSession { diff --git a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SessionCredentialsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SessionCredentialsProvider.swift index 4da87a38d..afe07d4b1 100644 --- a/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SessionCredentialsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Mail Sessions Providers/SessionCredentialsProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore protocol SessionCredentialsProvider { diff --git a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift index 18b7a7276..ba6f63abd 100644 --- a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail import UIKit diff --git a/FlowCrypt/Functionality/Mail Provider/MailServiceProviderType.swift b/FlowCrypt/Functionality/Mail Provider/MailServiceProviderType.swift index e0015a6bc..a7a9d906f 100644 --- a/FlowCrypt/Functionality/Mail Provider/MailServiceProviderType.swift +++ b/FlowCrypt/Functionality/Mail Provider/MailServiceProviderType.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail enum MailServiceProviderType { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 0578c13fe..a3ef362ec 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -5,7 +5,7 @@ // Created by Evgenii Kyivskyi on 10/22/21 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation + import GoogleAPIClientForREST_Gmail extension GmailService: DraftGateway { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift index b83dde968..f6216a431 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: MessageGateway { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift index 17b2eb697..ecf8d5487 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail struct MessageGatewayInput { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index ff448040f..b787de1d2 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import FlowCryptCommon import UIKit diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift index cd9bf6f17..f8282a878 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: MessageOperationsProvider { diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift index 828730344..0c0f7d3fe 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Imap+MessageOperations.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore extension Imap: MessageOperationsProvider { diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift index f327a2407..2d2017274 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/MessageOperationsProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import FlowCryptCommon protocol MessageOperationsProvider { diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Imap+MessagesList.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Imap+MessagesList.swift index 2f6feaa3a..132043997 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Imap+MessagesList.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Imap+MessagesList.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore extension Imap: MessagesListProvider { diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index 2cca1520e..a416a505f 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation import GoogleAPIClientForREST_Gmail struct Message: Hashable { diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageLabel.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageLabel.swift index 4afa725d0..b8d286db3 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageLabel.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageLabel.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore enum MessageLabel: Equatable, Hashable { diff --git a/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/Imap+Search.swift b/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/Imap+Search.swift index 9ea46087c..696e7ba71 100644 --- a/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/Imap+Search.swift +++ b/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/Imap+Search.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore // MARK: - MessageSearchProvider diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift index 5ffa3a38e..0d3e8c556 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail protocol MessagesThreadOperationsProvider { diff --git a/FlowCrypt/Functionality/Pgp/KeyMethods.swift b/FlowCrypt/Functionality/Pgp/KeyMethods.swift index cd3ff8518..af99f43c0 100644 --- a/FlowCrypt/Functionality/Pgp/KeyMethods.swift +++ b/FlowCrypt/Functionality/Pgp/KeyMethods.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation protocol KeyMethodsType { func filterByPassPhraseMatch(keys: [T], passPhrase: String) async throws -> [T] diff --git a/FlowCrypt/Functionality/PhotosManager/PhotosManager.swift b/FlowCrypt/Functionality/PhotosManager/PhotosManager.swift index d11f3e259..a0c6175d3 100644 --- a/FlowCrypt/Functionality/PhotosManager/PhotosManager.swift +++ b/FlowCrypt/Functionality/PhotosManager/PhotosManager.swift @@ -6,8 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation -import UIKit import Photos import PhotosUI diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift index f136f774d..72ccab275 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation /// Organisational rules, set domain-wide, and delivered from FlowCrypt Backend /// These either enforce, alter or forbid various behavior to fit customer needs diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift index 73c06e8ec..4b7297583 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation protocol ClientConfigurationServiceType { var configuration: ClientConfiguration { get async throws } diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift index f325518a4..38c2f22d9 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift protocol LocalClientConfigurationType { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 8d2ddc96c..eb639044b 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -7,9 +7,7 @@ // import FlowCryptUI -import Foundation import FlowCryptCommon -import UIKit typealias RecipientState = RecipientEmailsCellNode.Input.State diff --git a/FlowCrypt/Functionality/Services/EKMVcHelper.swift b/FlowCrypt/Functionality/Services/EKMVcHelper.swift index a229e10cb..18eb4ce48 100644 --- a/FlowCrypt/Functionality/Services/EKMVcHelper.swift +++ b/FlowCrypt/Functionality/Services/EKMVcHelper.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import UIKit protocol EKMVcHelperType { diff --git a/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift b/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift index b4aa2c57c..2723e9550 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation protocol TrashFolderProviderType { var trashFolderPath: String? { get async throws } diff --git a/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift b/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift index 7caf2aa76..eb2cdeb70 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift protocol LocalFoldersProviderType { diff --git a/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/GmailService+folders.swift b/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/GmailService+folders.swift index 3ebea2ab1..c8fdf441e 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/GmailService+folders.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/GmailService+folders.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: RemoteFoldersProviderType { diff --git a/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift b/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift index 0ee03d089..63d2e595d 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/RemoteFoldersProviderType/Imap+folders.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation import MailCore // MARK: - RemoteFoldersProviderType diff --git a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift index 0797f7070..45a6c5d8d 100644 --- a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift @@ -9,7 +9,6 @@ import AppAuth import Combine import FlowCryptCommon -import Foundation import GTMAppAuth import RealmSwift import GoogleAPIClientForREST_Oauth2 diff --git a/FlowCrypt/Functionality/Services/Local Contacts Service/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Contacts Service/LocalContactsProvider.swift index f24d400f7..e79dd935a 100644 --- a/FlowCrypt/Functionality/Services/Local Contacts Service/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Contacts Service/LocalContactsProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift import FlowCryptCommon diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift index 224ca83e4..d8c697bcc 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyAndPassPhraseStorage.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import FlowCryptCommon protocol KeyAndPassPhraseStorageType { diff --git a/FlowCrypt/Functionality/Services/SendAs Services/LocalSendAsProvider.swift b/FlowCrypt/Functionality/Services/SendAs Services/LocalSendAsProvider.swift index db343237b..8aa04e125 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/LocalSendAsProvider.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/LocalSendAsProvider.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift protocol LocalSendAsProviderType { diff --git a/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/GmailService+SendAs.swift b/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/GmailService+SendAs.swift index a29fe33ef..c502aae8e 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/GmailService+SendAs.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/RemoteSendAsProviderType/GmailService+SendAs.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import GoogleAPIClientForREST_Gmail extension GmailService: RemoteSendAsProviderType { diff --git a/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift b/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift index e375cfdb4..a442c554f 100644 --- a/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift +++ b/FlowCrypt/Functionality/Services/SendAs Services/SendAsService.swift @@ -7,7 +7,6 @@ // import FlowCryptCommon -import Foundation protocol SendAsServiceType { func fetchList(isForceReload: Bool, for user: User) async throws -> [SendAsModel] diff --git a/FlowCrypt/Models/Common/Recipient.swift b/FlowCrypt/Models/Common/Recipient.swift index b90ac418e..919f56201 100644 --- a/FlowCrypt/Models/Common/Recipient.swift +++ b/FlowCrypt/Models/Common/Recipient.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import MailCore import GoogleAPIClientForREST_PeopleService diff --git a/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift b/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift index 7e464c63f..67452c736 100644 --- a/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift final class ClientConfigurationRealmObject: Object { diff --git a/FlowCrypt/Models/Realm Models/FolderRealmObject.swift b/FlowCrypt/Models/Realm Models/FolderRealmObject.swift index 3ef12be67..e8218108b 100644 --- a/FlowCrypt/Models/Realm Models/FolderRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/FolderRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift final class FolderRealmObject: Object { diff --git a/FlowCrypt/Models/Realm Models/KeypairRealmObject.swift b/FlowCrypt/Models/Realm Models/KeypairRealmObject.swift index 7b7b79af6..d7210e9c8 100644 --- a/FlowCrypt/Models/Realm Models/KeypairRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/KeypairRealmObject.swift @@ -2,7 +2,6 @@ // © 2017-2019 FlowCrypt Limited. All rights reserved. // -import Foundation import RealmSwift import CryptoKit diff --git a/FlowCrypt/Models/Realm Models/PubKeyRealmObject.swift b/FlowCrypt/Models/Realm Models/PubKeyRealmObject.swift index 54e99a9ec..95e1a3542 100644 --- a/FlowCrypt/Models/Realm Models/PubKeyRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/PubKeyRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift enum PubKeyObjectError: Error { diff --git a/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift b/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift index 0d03e500d..71c7b0d9c 100644 --- a/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift final class RecipientRealmObject: Object { diff --git a/FlowCrypt/Models/Realm Models/SendAsRealmObject.swift b/FlowCrypt/Models/Realm Models/SendAsRealmObject.swift index 5b2e5fb9a..282c68219 100644 --- a/FlowCrypt/Models/Realm Models/SendAsRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/SendAsRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift final class SendAsRealmObject: Object { diff --git a/FlowCrypt/Models/Realm Models/UserRealmObject.swift b/FlowCrypt/Models/Realm Models/UserRealmObject.swift index 100d897b8..61f9690a7 100644 --- a/FlowCrypt/Models/Realm Models/UserRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/UserRealmObject.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import RealmSwift final class UserRealmObject: Object { diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationTests.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationTests.swift index 626881204..59c7a8a7a 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationTests.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationTests.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation import XCTest class ClientConfigurationTests: XCTestCase { diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift index 03f67769e..88eb8fbe2 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation class LocalClientConfigurationMock: LocalClientConfigurationType { var raw: RawClientConfiguration? diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift index 557d7bcd1..7deb33312 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation final class EnterpriseServerApiMock: EnterpriseServerApiType { var email = "example@flowcrypt.test" diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift index 8de13c104..fc3987fed 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation final class OrganisationalRulesServiceMock: ClientConfigurationServiceType { var fetchOrganisationalRulesForCurrentUserResult: Result = .failure(MockError()) diff --git a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift index 034746d0c..ec8da204f 100644 --- a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift +++ b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation class PassPhraseStorageMock: PassPhraseStorageType { diff --git a/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift b/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift index 21919444c..46589da0b 100644 --- a/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift +++ b/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation class CoreComposeMessageMock: CoreComposeMessageType, KeyParser { var composeEmailResult: ((SendableMsg, MsgFmt) -> (CoreRes.ComposeEmail))! diff --git a/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift b/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift index d33722b38..f085e91b1 100644 --- a/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift +++ b/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation final class LocalContactsProviderMock: LocalContactsProviderType { diff --git a/FlowCryptAppTests/Mocks/MessageGatewayMock.swift b/FlowCryptAppTests/Mocks/MessageGatewayMock.swift index 32d73538f..c328a0986 100644 --- a/FlowCryptAppTests/Mocks/MessageGatewayMock.swift +++ b/FlowCryptAppTests/Mocks/MessageGatewayMock.swift @@ -7,7 +7,6 @@ // @testable import FlowCrypt -import Foundation class MessageGatewayMock: MessageGateway { var sendMailResult: ((Data) -> (Result))! diff --git a/FlowCryptAppTests/TestData.swift b/FlowCryptAppTests/TestData.swift index c58e4c5f8..dc3ec7be1 100644 --- a/FlowCryptAppTests/TestData.swift +++ b/FlowCryptAppTests/TestData.swift @@ -2,7 +2,6 @@ // © 2017-2019 FlowCrypt Limited. All rights reserved. // -import Foundation @testable import FlowCrypt struct TestData { diff --git a/FlowCryptCommon/Extensions/Then.swift b/FlowCryptCommon/Extensions/Then.swift index afb5e129c..17ae6fbf3 100644 --- a/FlowCryptCommon/Extensions/Then.swift +++ b/FlowCryptCommon/Extensions/Then.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import Foundation import UIKit public protocol Then {} diff --git a/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift b/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift index 13de93abe..1a03e91ae 100644 --- a/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift @@ -7,7 +7,6 @@ // import AsyncDisplayKit -import Foundation public final class ContactKeyCellNode: CellNode { public struct Input { From fa1a07a3184b78c622d6eb048343db5a46372871 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 20 Sep 2022 14:55:22 +0300 Subject: [PATCH 16/56] update thread view on draft update --- FlowCrypt.xcodeproj/project.pbxproj | 4 + .../Compose/ComposeMessageAction.swift | 5 +- .../ComposeViewController+MessageSend.swift | 4 +- .../ComposeViewController+Setup.swift | 5 +- .../Threads/ThreadDetailsViewController.swift | 98 ++++++++++++------- .../Message Gateway/GmailService+draft.swift | 55 +++++++++-- .../Message Gateway/MessageGateway.swift | 9 +- .../Model/MessageDraft.swift | 23 +++++ .../ComposeMessageService.swift | 22 +++-- 9 files changed, 159 insertions(+), 66 deletions(-) create mode 100644 FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageDraft.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 5561ae2a4..4fd9e9ffa 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -99,6 +99,7 @@ 514C34DD276CE1C000FCAB79 /* ComposeMessageRecipient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */; }; 514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */; }; 5152F196277E5AED00BE8A5B /* MessageUploadDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */; }; + 51592FB528D9B03600FE9ACC /* MessageDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51592FB428D9B03600FE9ACC /* MessageDraft.swift */; }; 515B7C2528119CED0066AB4F /* GMP in Frameworks */ = {isa = PBXBuildFile; productRef = 515B7C2428119CED0066AB4F /* GMP */; }; 5165ABCC27B526D100CCC379 /* RecipientEmailTextFieldNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165ABCB27B526D100CCC379 /* RecipientEmailTextFieldNode.swift */; }; 51689F3F2795C1D90050A9B8 /* ProcessedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */; }; @@ -568,6 +569,7 @@ 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageRecipient.swift; sourceTree = ""; }; 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeMessageService+State.swift"; sourceTree = ""; }; 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageUploadDetails.swift; sourceTree = ""; }; + 51592FB428D9B03600FE9ACC /* MessageDraft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDraft.swift; sourceTree = ""; }; 5165ABCB27B526D100CCC379 /* RecipientEmailTextFieldNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientEmailTextFieldNode.swift; sourceTree = ""; }; 51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedMessage.swift; sourceTree = ""; }; 5168FB0A274F94D300131072 /* MessageAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageAttachment.swift; sourceTree = ""; }; @@ -1771,6 +1773,7 @@ 9FE1B39F2565B0CD00D6D086 /* Message.swift */, 9F5C2A76257D705100DE9B4B /* MessageLabel.swift */, 51938DC0274CC291007AD57B /* MessageQuoteType.swift */, + 51592FB428D9B03600FE9ACC /* MessageDraft.swift */, ); path = Model; sourceTree = ""; @@ -2840,6 +2843,7 @@ D2891AC224C59EFA008918E3 /* KeyAndPassPhraseStorage.swift in Sources */, D269E02724103A20000495C3 /* ComposeViewControllerInput.swift in Sources */, 510BB63527BE92CC00B1011F /* RecipientBase.swift in Sources */, + 51592FB528D9B03600FE9ACC /* MessageDraft.swift in Sources */, 9FAFD75D2714A06400321FA4 /* InboxProviders.swift in Sources */, 2C141B2C274572D50038A3F8 /* Recipient.swift in Sources */, 9F0C3C142316E69300299985 /* User.swift in Sources */, diff --git a/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift b/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift index 32c929cb0..bfee0862f 100644 --- a/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift +++ b/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift @@ -9,5 +9,8 @@ import Foundation enum ComposeMessageAction { - case create(Message), update(Message), delete(Identifier), sent(Identifier) + case create(Identifier), + update(Identifier), + delete(Identifier), + sent(Identifier?, Identifier) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index 881e105f1..da1c33e57 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -37,9 +37,9 @@ extension ComposeViewController { threadId: input.threadId ) - handleSuccessfullySentMessage() + handleAction?(.sent(composeMessageService.draft?.id, identifier)) - handleAction?(.sent(identifier)) + handleSuccessfullySentMessage() } private func handleSuccessfullySentMessage() { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 398387d18..e8223600d 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -61,7 +61,7 @@ extension ComposeViewController { if case .draft = input.type, let messageId = input.type.info?.rfc822MsgId { Task { - try await composeMessageService.fetchDraftId(messageId: messageId) + try await composeMessageService.fetchDraft(for: messageId) } } @@ -162,6 +162,9 @@ extension ComposeViewController: NavigationChildController { } else if let error = error { self.handle(error: error) } else { + if let messageId = self.composeMessageService.draft?.messageId { + self.handleAction?(.create(messageId)) + } self.navigationController?.popViewController(animated: true) } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index ae6842f3d..2dded9c1f 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -21,10 +21,11 @@ final class ThreadDetailsViewController: TableNodeViewController { var shouldShowRecipientsList: Bool var processedMessage: ProcessedMessage? - init(message: Message, isExpanded: Bool = false, shouldShowRecipientsList: Bool = false) { + init(message: Message, isExpanded: Bool = false, shouldShowRecipientsList: Bool = false, processedMessage: ProcessedMessage? = nil) { self.rawMessage = message self.isExpanded = isExpanded self.shouldShowRecipientsList = shouldShowRecipientsList + self.processedMessage = processedMessage } } @@ -201,26 +202,46 @@ extension ThreadDetailsViewController { private func handleComposeMessageAction(_ action: ComposeMessageAction) { switch action { - case .create, .update: + case .create(let messageId): + Task { + let processedMessage = try await messageService.getAndProcess( + identifier: messageId, + folder: thread.path, + onlyLocalKeys: false, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + + let indexPath = IndexPath(row: 0, section: self.input.count + 1) + self.handle(processedMessage: processedMessage, at: indexPath) + } + case .update: break - case .sent(let identifier): + case let .sent(draftId, identifier): Task { - let processedMessage = try await self.messageService.getAndProcess( + if let draftId = draftId { + guard let index = input.firstIndex(where: { $0.rawMessage.identifier == draftId }) else { return } + + input.remove(at: index) + node.deleteSections([index + 1], with: .automatic) + } + + let processedMessage = try await messageService.getAndProcess( identifier: identifier, - folder: self.thread.path, + folder: thread.path, onlyLocalKeys: false, - userEmail: self.appContext.user.email, - isUsingKeyManager: self.appContext.clientConfigurationService.configuration.isUsingKeyManager + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) let indexPath = IndexPath(row: 0, section: self.input.count) self.handle(processedMessage: processedMessage, at: indexPath) } case .delete(let identifier): - guard let index = self.input.firstIndex(where: { $0.rawMessage.identifier == identifier }) + guard let index = input.firstIndex(where: { $0.rawMessage.identifier == identifier }) else { return } - self.input.remove(at: index) - self.node.deleteSections([index + 1], with: .automatic) + input.remove(at: index) + node.deleteSections([index + 1], with: .automatic) } } @@ -392,7 +413,7 @@ extension ThreadDetailsViewController { if case .missingPubkey = processedMessage.signature { processedMessage.signature = .pending - retryVerifyingSignatureWithRemotelyFetchedKeys( + await retryVerifyingSignatureWithRemotelyFetchedKeys( message: message, folder: thread.path, indexPath: indexPath @@ -409,11 +430,16 @@ extension ThreadDetailsViewController { hideSpinner() let messageIndex = indexPath.section - 1 - let isAlreadyProcessed = input[messageIndex].processedMessage != nil + let isAlreadyProcessed = messageIndex < input.count && input[messageIndex].processedMessage != nil if !isAlreadyProcessed { - input[messageIndex].processedMessage = processedMessage - input[messageIndex].isExpanded = true + if messageIndex < input.count { + input[messageIndex].rawMessage = processedMessage.message + input[messageIndex].processedMessage = processedMessage + input[messageIndex].isExpanded = true + } else { + input.append(Input(message: processedMessage.message, isExpanded: true, processedMessage: processedMessage)) + } UIView.animate( withDuration: 0.2, @@ -508,16 +534,14 @@ extension ThreadDetailsViewController { if matched { let message = input[indexPath.section - 1].rawMessage - if !message.isDraft { - let processedMessage = try await messageService.decryptAndProcess( - message: message, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager - ) + let processedMessage = try await messageService.decryptAndProcess( + message: message, + onlyLocalKeys: false, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) - handle(processedMessage: processedMessage, at: indexPath) - } + handle(processedMessage: processedMessage, at: indexPath) } else { handleWrongPassPhrase(passPhrase, indexPath: indexPath) } @@ -551,21 +575,19 @@ extension ThreadDetailsViewController { message: Message, folder: String, indexPath: IndexPath - ) { - Task { - do { - let processedMessage = try await messageService.getAndProcess( - identifier: message.identifier, - folder: thread.path, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager - ) - handle(processedMessage: processedMessage, at: indexPath) - } catch { - let message = "message_signature_fail_reason".localizeWithArguments(error.errorMessage) - input[indexPath.section - 1].processedMessage?.signature = .error(message) - } + ) async { + do { + let processedMessage = try await messageService.getAndProcess( + identifier: message.identifier, + folder: thread.path, + onlyLocalKeys: false, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + handle(processedMessage: processedMessage, at: indexPath) + } catch { + let message = "message_signature_fail_reason".localizeWithArguments(error.errorMessage) + input[indexPath.section - 1].processedMessage?.signature = .error(message) } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index a3ef362ec..00c7402b1 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -9,12 +9,42 @@ import GoogleAPIClientForREST_Gmail extension GmailService: DraftGateway { - func fetchDraftId(messageId: String) async throws -> String? { + func fetchMessage(draftIdentifier: Identifier) async throws -> Message? { + guard let id = draftIdentifier.stringId else { return nil } + + let query = GTLRGmailQuery_UsersDraftsGet.query(withUserId: .me, identifier: id) + query.format = kGTLRGmailFormatFull + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + gmailService.executeQuery(query) { _, data, error in + if let error = error { + return continuation.resume(throwing: GmailServiceError.providerError(error)) + } + + guard let draft = data as? GTLRGmail_Draft else { + return continuation.resume(throwing: AppErr.cast("GTLRGmail_Draft")) + } + + guard let gmailMessage = draft.message else { + return continuation.resume(throwing: AppErr.cast("GTLRGmail_Draft")) + } + + do { + let message = try Message(gmailMessage: gmailMessage) + return continuation.resume(returning: message) + } catch { + return continuation.resume(throwing: error) + } + } + } + } + + func fetchDraft(for messageId: Identifier) async throws -> MessageDraft? { let query = GTLRGmailQuery_UsersDraftsList.query(withUserId: .me) query.q = "rfc822msgid:\(messageId)" query.maxResults = 1 - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in gmailService.executeQuery(query) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) @@ -24,14 +54,17 @@ extension GmailService: DraftGateway { return continuation.resume(throwing: AppErr.cast("GTLRGmail_ListDraftsResponse")) } - let draftId = list.drafts?.first?.identifier - return continuation.resume(returning: draftId) + guard let gmailDraft = list.drafts?.first else { + return continuation.resume(returning: nil) + } + let draft = MessageDraft(gmailDraft: gmailDraft) + return continuation.resume(returning: draft) } } } - func saveDraft(input: MessageGatewayInput, draftId: String?) async throws -> GTLRGmail_Draft { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageDraft { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in guard let raw = GTLREncodeBase64(input.mime) else { return continuation.resume(throwing: GmailServiceError.messageEncode) } @@ -39,13 +72,14 @@ extension GmailService: DraftGateway { let draftQuery = createQueryForDraftAction( raw: raw, threadId: input.threadId, - draftId: draftId + draftId: draftId?.stringId ) gmailService.executeQuery(draftQuery) { _, object, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) - } else if let draft = object as? GTLRGmail_Draft { + } else if let gmailDraft = object as? GTLRGmail_Draft { + let draft = MessageDraft(gmailDraft: gmailDraft) return continuation.resume(returning: draft) } else { return continuation.resume(throwing: GmailServiceError.failedToParseData(nil)) @@ -54,9 +88,10 @@ extension GmailService: DraftGateway { } } - func deleteDraft(with identifier: String) async throws { + func deleteDraft(with identifier: Identifier) async throws { + guard let id = identifier.stringId else { return } try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let query = GTLRGmailQuery_UsersDraftsDelete.query(withUserId: .me, identifier: identifier) + let query = GTLRGmailQuery_UsersDraftsDelete.query(withUserId: .me, identifier: id) gmailService.executeQuery(query) { _, _, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift index ecf8d5487..6add0593a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift @@ -6,7 +6,7 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import GoogleAPIClientForREST_Gmail +import Foundation struct MessageGatewayInput { let mime: Data @@ -18,7 +18,8 @@ protocol MessageGateway { } protocol DraftGateway { - func fetchDraftId(messageId: String) async throws -> String? - func saveDraft(input: MessageGatewayInput, draftId: String?) async throws -> GTLRGmail_Draft - func deleteDraft(with identifier: String) async throws + func fetchMessage(draftIdentifier: Identifier) async throws -> Message? + func fetchDraft(for messageId: Identifier) async throws -> MessageDraft? + func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageDraft + func deleteDraft(with identifier: Identifier) async throws } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageDraft.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageDraft.swift new file mode 100644 index 000000000..0c517ff9c --- /dev/null +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageDraft.swift @@ -0,0 +1,23 @@ +// +// MessageDraft.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 20/09/22 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import GoogleAPIClientForREST_Gmail + +struct MessageDraft { + let id: Identifier + let threadId: String? + let messageId: Identifier? +} + +extension MessageDraft { + init(gmailDraft: GTLRGmail_Draft) { + self.id = Identifier(stringId: gmailDraft.identifier) + self.threadId = gmailDraft.message?.threadId + self.messageId = Identifier(stringId: gmailDraft.message?.identifier) + } +} diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index eb639044b..81c4b4e75 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -219,10 +219,13 @@ final class ComposeMessageService { } // MARK: - Drafts - private var draftId: String? + private(set) var draft: MessageDraft? - func fetchDraftId(messageId: String) async throws { - self.draftId = try await draftGateway?.fetchDraftId(messageId: messageId) + func fetchDraft(for messageId: String) async throws { + guard draft == nil else { return } + + let identifier = Identifier(stringId: messageId) + self.draft = try await draftGateway?.fetchDraft(for: identifier) } func encryptAndSaveDraft(message: SendableMsg, threadId: String?) async throws { @@ -232,22 +235,21 @@ final class ComposeMessageService { fmt: .encryptInline ).mimeEncoded - let draft = try await draftGateway?.saveDraft( + self.draft = try await draftGateway?.saveDraft( input: MessageGatewayInput( mime: mime, threadId: threadId ), - draftId: self.draftId + draftId: self.draft?.id ) - self.draftId = draft?.identifier } catch { throw ComposeMessageError.gatewayError(error) } } func deleteDraft(messageId: String?) async throws { - if let draftId = draftId { - try await draftGateway?.deleteDraft(with: draftId) + if let draft = draft { + try await draftGateway?.deleteDraft(with: draft.id) } else if let messageId = messageId { let id = Identifier(stringId: messageId) try await messageOperationsProvider.deleteMessage(id: id, from: nil) @@ -285,8 +287,8 @@ final class ComposeMessageService { ) // cleaning any draft saved/created/fetched during editing - if let draftId = draftId { - try await draftGateway?.deleteDraft(with: draftId) + if let draft = draft { + try await draftGateway?.deleteDraft(with: draft.id) } onStateChanged?(.messageSent) From 46c3a37783dc5d220dcb26b617c46a3b215e0e03 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 21 Sep 2022 12:33:40 +0300 Subject: [PATCH 17/56] fixes for draft updates --- .../xcshareddata/swiftpm/Package.resolved | 8 ++--- .../Compose/ComposeMessageAction.swift | 3 +- .../Compose/ComposeViewController.swift | 4 --- .../Compose/ComposeViewControllerInput.swift | 4 +-- .../ComposeViewController+MessageSend.swift | 2 +- .../ComposeViewController+Setup.swift | 5 ++-- .../ComposeViewController+TapActions.swift | 2 +- .../Inbox/InboxViewController.swift | 4 +-- .../Threads/ThreadDetailsViewController.swift | 29 ++++++++++++------- .../Message Gateway/GmailService+draft.swift | 4 ++- .../Threads/MessagesThreadProvider.swift | 12 ++++---- .../ComposeMessageService.swift | 5 ++-- 12 files changed, 44 insertions(+), 38 deletions(-) diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index f0536fe66..5d34ef24d 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-cocoa", "state" : { - "revision" : "28c488974f544c9affc4eacdd5b9dfc6785ebbc0", - "version" : "10.29.0" + "revision" : "437fd367c4fcdaf7c532f1f40cb4ed5fab804e7c", + "version" : "10.30.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-core", "state" : { - "revision" : "5da7744b4056ad185c025bccf0924f17f73f7a91", - "version" : "12.6.0" + "revision" : "18abbb4e9dc268620fa499923a92921bf26db8c6", + "version" : "12.7.0" } }, { diff --git a/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift b/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift index bfee0862f..1ec65e0d3 100644 --- a/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift +++ b/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift @@ -9,8 +9,7 @@ import Foundation enum ComposeMessageAction { - case create(Identifier), - update(Identifier), + case update(MessageDraft, Identifier?), delete(Identifier), sent(Identifier?, Identifier) } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 394f65864..dfdabf6a9 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -260,10 +260,6 @@ final class ComposeViewController: TableNodeViewController { extension ComposeViewController: FilesManagerPresenter {} /* -fixes - - save draft when tapping back - - add draft when going back from compose to thread view - - delete draft from list on send - reload drafts list when going back from compose or thread screen */ diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index b340ec505..29bcf8f39 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -12,7 +12,7 @@ struct ComposeMessageInput: Equatable { static let empty = ComposeMessageInput(type: .idle) struct MessageQuoteInfo: Equatable { - let id: String? + let id: Identifier? let recipients: [Recipient] let ccRecipients: [Recipient] let bccRecipients: [Recipient] @@ -112,7 +112,7 @@ extension ComposeMessageInput.InputType { extension ComposeMessageInput.MessageQuoteInfo { init(message: Message, processed: ProcessedMessage? = nil) { - self.id = message.identifier.stringId + self.id = message.identifier self.recipients = message.to self.ccRecipients = message.cc self.bccRecipients = message.bcc diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index da1c33e57..41365fbda 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -37,7 +37,7 @@ extension ComposeViewController { threadId: input.threadId ) - handleAction?(.sent(composeMessageService.draft?.id, identifier)) + handleAction?(.sent(input.type.info?.id, identifier)) handleSuccessfullySentMessage() } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index e8223600d..0c16df6c5 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -162,8 +162,9 @@ extension ComposeViewController: NavigationChildController { } else if let error = error { self.handle(error: error) } else { - if let messageId = self.composeMessageService.draft?.messageId { - self.handleAction?(.create(messageId)) + if let draft = self.composeMessageService.draft { + let initialMessageId = self.input.type.info?.id + self.handleAction?(.update(draft, initialMessageId)) } self.navigationController?.popViewController(animated: true) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index 64fa661fd..724125890 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -44,7 +44,7 @@ extension ComposeViewController { try await self.composeMessageService.deleteDraft(messageId: messageId) if let messageId = messageId { - self.handleAction?(.delete(Identifier(stringId: messageId))) + self.handleAction?(.delete(messageId)) } self.navigationController?.popViewController(animated: true) diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 9781d5651..d51d3557c 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -639,10 +639,10 @@ extension InboxViewController { guard let self = self else { return } switch action { - case .create, .update: + case .update: // todo break - case .sent(let message): + case .sent(let draftId, let messageId): break case .delete(let identifier): guard let index = self.inboxInput.firstIndex(where: { $0.wrappedMessage?.identifier == identifier }) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 2dded9c1f..72743ba52 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -21,7 +21,12 @@ final class ThreadDetailsViewController: TableNodeViewController { var shouldShowRecipientsList: Bool var processedMessage: ProcessedMessage? - init(message: Message, isExpanded: Bool = false, shouldShowRecipientsList: Bool = false, processedMessage: ProcessedMessage? = nil) { + init( + message: Message, + isExpanded: Bool = false, + shouldShowRecipientsList: Bool = false, + processedMessage: ProcessedMessage? = nil + ) { self.rawMessage = message self.isExpanded = isExpanded self.shouldShowRecipientsList = shouldShowRecipientsList @@ -202,8 +207,9 @@ extension ThreadDetailsViewController { private func handleComposeMessageAction(_ action: ComposeMessageAction) { switch action { - case .create(let messageId): + case let .update(draft, previousMessageId): Task { + guard let messageId = draft.messageId else { return } let processedMessage = try await messageService.getAndProcess( identifier: messageId, folder: thread.path, @@ -212,16 +218,18 @@ extension ThreadDetailsViewController { isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) - let indexPath = IndexPath(row: 0, section: self.input.count + 1) + let indexPath: IndexPath + if let index = input.firstIndex(where: { $0.rawMessage.identifier == previousMessageId }) { + indexPath = IndexPath(row: 0, section: index + 1) + } else { + indexPath = IndexPath(row: 0, section: input.count + 1) + } + self.handle(processedMessage: processedMessage, at: indexPath) } - case .update: - break case let .sent(draftId, identifier): Task { - if let draftId = draftId { - guard let index = input.firstIndex(where: { $0.rawMessage.identifier == draftId }) else { return } - + if let draftId = draftId, let index = input.firstIndex(where: { $0.rawMessage.identifier == draftId }) { input.remove(at: index) node.deleteSections([index + 1], with: .automatic) } @@ -233,7 +241,7 @@ extension ThreadDetailsViewController { userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) - let indexPath = IndexPath(row: 0, section: self.input.count) + let indexPath = IndexPath(row: 0, section: input.count + 1) self.handle(processedMessage: processedMessage, at: indexPath) } case .delete(let identifier): @@ -455,7 +463,8 @@ extension ThreadDetailsViewController { self?.decryptDrafts() }) } else { - input[messageIndex].processedMessage?.signature = processedMessage.signature + input[messageIndex].rawMessage = processedMessage.message + input[messageIndex].processedMessage = processedMessage node.reloadSections([indexPath.section], with: .automatic) } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 00c7402b1..39b009262 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -40,8 +40,10 @@ extension GmailService: DraftGateway { } func fetchDraft(for messageId: Identifier) async throws -> MessageDraft? { + guard let id = messageId.stringId else { return nil } + let query = GTLRGmailQuery_UsersDraftsList.query(withUserId: .me) - query.q = "rfc822msgid:\(messageId)" + query.q = "rfc822msgid:\(id)" query.maxResults = 1 return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift index 62d4f1914..54b491fcb 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift @@ -66,19 +66,19 @@ extension GmailService: MessagesThreadProvider { return continuation.resume(throwing: GmailServiceError.providerError(error)) } - guard let thread = data as? GTLRGmail_Thread else { + guard let gmailThread = data as? GTLRGmail_Thread else { return continuation.resume(throwing: AppErr.cast("GTLRGmail_Thread")) } - let messages = thread.messages?.compactMap { try? Message(gmailMessage: $0) } ?? [] + let messages = gmailThread.messages?.compactMap { try? Message(gmailMessage: $0) } ?? [] - let result = MessageThread( - identifier: thread.identifier, - snippet: thread.snippet, + let thread = MessageThread( + identifier: gmailThread.identifier, + snippet: gmailThread.snippet, path: path, messages: messages ) - return continuation.resume(returning: result) + return continuation.resume(returning: thread) } } }.value diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 81c4b4e75..3a0105419 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -247,12 +247,11 @@ final class ComposeMessageService { } } - func deleteDraft(messageId: String?) async throws { + func deleteDraft(messageId: Identifier?) async throws { if let draft = draft { try await draftGateway?.deleteDraft(with: draft.id) } else if let messageId = messageId { - let id = Identifier(stringId: messageId) - try await messageOperationsProvider.deleteMessage(id: id, from: nil) + try await messageOperationsProvider.deleteMessage(id: messageId, from: nil) } } From f59bc516f54b92a023d9706797afd95447108a26 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 21 Sep 2022 16:19:13 +0300 Subject: [PATCH 18/56] fix draft ids inconsistency --- FlowCrypt.xcodeproj/project.pbxproj | 8 ++-- .../Compose/ComposeMessageAction.swift | 6 +-- .../Compose/ComposeViewController.swift | 1 + .../ComposeViewController+MessageSend.swift | 8 +++- .../ComposeViewController+Setup.swift | 8 ++-- .../ComposeViewController+TapActions.swift | 3 +- .../Inbox/InboxViewController.swift | 5 ++- .../Threads/ThreadDetailsViewController.swift | 18 ++++---- .../Message Gateway/GmailService+draft.swift | 42 +++---------------- .../Message Gateway/MessageGateway.swift | 5 +-- ...ageDraft.swift => MessageIdentifier.swift} | 13 +++--- .../ComposeMessageService.swift | 20 ++++----- .../Mocks/DraftGatewayMock.swift | 8 ++-- Gemfile | 6 +-- Gemfile.lock | 18 +------- 15 files changed, 66 insertions(+), 103 deletions(-) rename FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/{MessageDraft.swift => MessageIdentifier.swift} (60%) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 4fd9e9ffa..30ac851d1 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -99,7 +99,7 @@ 514C34DD276CE1C000FCAB79 /* ComposeMessageRecipient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */; }; 514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */; }; 5152F196277E5AED00BE8A5B /* MessageUploadDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */; }; - 51592FB528D9B03600FE9ACC /* MessageDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51592FB428D9B03600FE9ACC /* MessageDraft.swift */; }; + 51592FB528D9B03600FE9ACC /* MessageIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51592FB428D9B03600FE9ACC /* MessageIdentifier.swift */; }; 515B7C2528119CED0066AB4F /* GMP in Frameworks */ = {isa = PBXBuildFile; productRef = 515B7C2428119CED0066AB4F /* GMP */; }; 5165ABCC27B526D100CCC379 /* RecipientEmailTextFieldNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165ABCB27B526D100CCC379 /* RecipientEmailTextFieldNode.swift */; }; 51689F3F2795C1D90050A9B8 /* ProcessedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */; }; @@ -569,7 +569,7 @@ 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageRecipient.swift; sourceTree = ""; }; 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeMessageService+State.swift"; sourceTree = ""; }; 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageUploadDetails.swift; sourceTree = ""; }; - 51592FB428D9B03600FE9ACC /* MessageDraft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDraft.swift; sourceTree = ""; }; + 51592FB428D9B03600FE9ACC /* MessageIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIdentifier.swift; sourceTree = ""; }; 5165ABCB27B526D100CCC379 /* RecipientEmailTextFieldNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientEmailTextFieldNode.swift; sourceTree = ""; }; 51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedMessage.swift; sourceTree = ""; }; 5168FB0A274F94D300131072 /* MessageAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageAttachment.swift; sourceTree = ""; }; @@ -1773,7 +1773,7 @@ 9FE1B39F2565B0CD00D6D086 /* Message.swift */, 9F5C2A76257D705100DE9B4B /* MessageLabel.swift */, 51938DC0274CC291007AD57B /* MessageQuoteType.swift */, - 51592FB428D9B03600FE9ACC /* MessageDraft.swift */, + 51592FB428D9B03600FE9ACC /* MessageIdentifier.swift */, ); path = Model; sourceTree = ""; @@ -2843,7 +2843,7 @@ D2891AC224C59EFA008918E3 /* KeyAndPassPhraseStorage.swift in Sources */, D269E02724103A20000495C3 /* ComposeViewControllerInput.swift in Sources */, 510BB63527BE92CC00B1011F /* RecipientBase.swift in Sources */, - 51592FB528D9B03600FE9ACC /* MessageDraft.swift in Sources */, + 51592FB528D9B03600FE9ACC /* MessageIdentifier.swift in Sources */, 9FAFD75D2714A06400321FA4 /* InboxProviders.swift in Sources */, 2C141B2C274572D50038A3F8 /* Recipient.swift in Sources */, 9F0C3C142316E69300299985 /* User.swift in Sources */, diff --git a/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift b/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift index 1ec65e0d3..9d5a38c10 100644 --- a/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift +++ b/FlowCrypt/Controllers/Compose/ComposeMessageAction.swift @@ -9,7 +9,7 @@ import Foundation enum ComposeMessageAction { - case update(MessageDraft, Identifier?), - delete(Identifier), - sent(Identifier?, Identifier) + case update(MessageIdentifier), + delete(MessageIdentifier), + sent(MessageIdentifier) } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index dfdabf6a9..a2612a6f5 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -261,6 +261,7 @@ extension ComposeViewController: FilesManagerPresenter {} /* - reload drafts list when going back from compose or thread screen + - check drafts for forward and reply all */ /* ui test diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index 41365fbda..6fea68351 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -13,6 +13,7 @@ import FlowCryptUI extension ComposeViewController { func sendMessage() async throws { view.endEditing(true) + stopDraftTimer() navigationItem.rightBarButtonItem?.isEnabled = false let spinnerTitle = contextToSend.attachments.isEmpty ? "sending_title" : "encrypting_title" @@ -37,7 +38,12 @@ extension ComposeViewController { threadId: input.threadId ) - handleAction?(.sent(input.type.info?.id, identifier)) + let messageIdentifier = MessageIdentifier( + draftId: input.type.info?.id, + threadId: input.threadId, + messageId: identifier + ) + handleAction?(.sent(messageIdentifier)) handleSuccessfullySentMessage() } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 0c16df6c5..ba408acce 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -61,7 +61,7 @@ extension ComposeViewController { if case .draft = input.type, let messageId = input.type.info?.rfc822MsgId { Task { - try await composeMessageService.fetchDraft(for: messageId) + try await composeMessageService.fetchDraftIdentifier(for: messageId) } } @@ -162,9 +162,9 @@ extension ComposeViewController: NavigationChildController { } else if let error = error { self.handle(error: error) } else { - if let draft = self.composeMessageService.draft { - let initialMessageId = self.input.type.info?.id - self.handleAction?(.update(draft, initialMessageId)) + if var messageIdentifier = self.composeMessageService.messageIdentifier { + messageIdentifier.draftMessageId = self.input.type.info?.id + self.handleAction?(.update(messageIdentifier)) } self.navigationController?.popViewController(animated: true) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index 724125890..bcafd78cd 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -44,7 +44,8 @@ extension ComposeViewController { try await self.composeMessageService.deleteDraft(messageId: messageId) if let messageId = messageId { - self.handleAction?(.delete(messageId)) + let identifier = MessageIdentifier(messageId: messageId) + self.handleAction?(.delete(identifier)) } self.navigationController?.popViewController(animated: true) diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index d51d3557c..aa2d589ec 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -642,10 +642,11 @@ extension InboxViewController { case .update: // todo break - case .sent(let draftId, let messageId): + case .sent(let messageIdentifier): + // todo break case .delete(let identifier): - guard let index = self.inboxInput.firstIndex(where: { $0.wrappedMessage?.identifier == identifier }) + guard let index = self.inboxInput.firstIndex(where: { $0.wrappedMessage?.identifier == identifier.messageId }) else { return } self.inboxInput.remove(at: index) self.tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 72743ba52..744296520 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -207,9 +207,10 @@ extension ThreadDetailsViewController { private func handleComposeMessageAction(_ action: ComposeMessageAction) { switch action { - case let .update(draft, previousMessageId): + case .update(let messageIdentifier): Task { - guard let messageId = draft.messageId else { return } + guard let messageId = messageIdentifier.messageId else { return } + let processedMessage = try await messageService.getAndProcess( identifier: messageId, folder: thread.path, @@ -219,7 +220,7 @@ extension ThreadDetailsViewController { ) let indexPath: IndexPath - if let index = input.firstIndex(where: { $0.rawMessage.identifier == previousMessageId }) { + if let index = input.firstIndex(where: { $0.rawMessage.identifier == messageIdentifier.draftMessageId }) { indexPath = IndexPath(row: 0, section: index + 1) } else { indexPath = IndexPath(row: 0, section: input.count + 1) @@ -227,13 +228,14 @@ extension ThreadDetailsViewController { self.handle(processedMessage: processedMessage, at: indexPath) } - case let .sent(draftId, identifier): + case .sent(let messageIdentifier): Task { - if let draftId = draftId, let index = input.firstIndex(where: { $0.rawMessage.identifier == draftId }) { + if let draftId = messageIdentifier.draftId, let index = input.firstIndex(where: { $0.rawMessage.identifier == draftId }) { input.remove(at: index) node.deleteSections([index + 1], with: .automatic) } + guard let identifier = messageIdentifier.messageId else { return } // todo - throw let processedMessage = try await messageService.getAndProcess( identifier: identifier, folder: thread.path, @@ -241,11 +243,11 @@ extension ThreadDetailsViewController { userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) - let indexPath = IndexPath(row: 0, section: input.count + 1) + let indexPath = IndexPath(row: 0, section: self.input.count + 1) self.handle(processedMessage: processedMessage, at: indexPath) } - case .delete(let identifier): - guard let index = input.firstIndex(where: { $0.rawMessage.identifier == identifier }) + case .delete(let messageIdentifier): + guard let index = input.firstIndex(where: { $0.rawMessage.identifier == messageIdentifier.messageId }) else { return } input.remove(at: index) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 39b009262..a2060feb9 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -9,44 +9,14 @@ import GoogleAPIClientForREST_Gmail extension GmailService: DraftGateway { - func fetchMessage(draftIdentifier: Identifier) async throws -> Message? { - guard let id = draftIdentifier.stringId else { return nil } - - let query = GTLRGmailQuery_UsersDraftsGet.query(withUserId: .me, identifier: id) - query.format = kGTLRGmailFormatFull - - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - gmailService.executeQuery(query) { _, data, error in - if let error = error { - return continuation.resume(throwing: GmailServiceError.providerError(error)) - } - - guard let draft = data as? GTLRGmail_Draft else { - return continuation.resume(throwing: AppErr.cast("GTLRGmail_Draft")) - } - - guard let gmailMessage = draft.message else { - return continuation.resume(throwing: AppErr.cast("GTLRGmail_Draft")) - } - - do { - let message = try Message(gmailMessage: gmailMessage) - return continuation.resume(returning: message) - } catch { - return continuation.resume(throwing: error) - } - } - } - } - - func fetchDraft(for messageId: Identifier) async throws -> MessageDraft? { + func fetchDraftIdentifier(for messageId: Identifier) async throws -> MessageIdentifier? { guard let id = messageId.stringId else { return nil } let query = GTLRGmailQuery_UsersDraftsList.query(withUserId: .me) query.q = "rfc822msgid:\(id)" query.maxResults = 1 - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in gmailService.executeQuery(query) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) @@ -59,14 +29,14 @@ extension GmailService: DraftGateway { guard let gmailDraft = list.drafts?.first else { return continuation.resume(returning: nil) } - let draft = MessageDraft(gmailDraft: gmailDraft) + let draft = MessageIdentifier(gmailDraft: gmailDraft) return continuation.resume(returning: draft) } } } - func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageDraft { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageIdentifier { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in guard let raw = GTLREncodeBase64(input.mime) else { return continuation.resume(throwing: GmailServiceError.messageEncode) } @@ -81,7 +51,7 @@ extension GmailService: DraftGateway { if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) } else if let gmailDraft = object as? GTLRGmail_Draft { - let draft = MessageDraft(gmailDraft: gmailDraft) + let draft = MessageIdentifier(gmailDraft: gmailDraft) return continuation.resume(returning: draft) } else { return continuation.resume(throwing: GmailServiceError.failedToParseData(nil)) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift index 6add0593a..243275d5b 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift @@ -18,8 +18,7 @@ protocol MessageGateway { } protocol DraftGateway { - func fetchMessage(draftIdentifier: Identifier) async throws -> Message? - func fetchDraft(for messageId: Identifier) async throws -> MessageDraft? - func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageDraft + func fetchDraftIdentifier(for messageId: Identifier) async throws -> MessageIdentifier? + func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageIdentifier func deleteDraft(with identifier: Identifier) async throws } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageDraft.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageIdentifier.swift similarity index 60% rename from FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageDraft.swift rename to FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageIdentifier.swift index 0c517ff9c..f46f7dee5 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageDraft.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageIdentifier.swift @@ -8,15 +8,16 @@ import GoogleAPIClientForREST_Gmail -struct MessageDraft { - let id: Identifier - let threadId: String? - let messageId: Identifier? +struct MessageIdentifier { + var draftId: Identifier? + var threadId: String? + var messageId: Identifier? + var draftMessageId: Identifier? } -extension MessageDraft { +extension MessageIdentifier { init(gmailDraft: GTLRGmail_Draft) { - self.id = Identifier(stringId: gmailDraft.identifier) + self.draftId = Identifier(stringId: gmailDraft.identifier) self.threadId = gmailDraft.message?.threadId self.messageId = Identifier(stringId: gmailDraft.message?.identifier) } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 3a0105419..14d8f4b3e 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -219,13 +219,11 @@ final class ComposeMessageService { } // MARK: - Drafts - private(set) var draft: MessageDraft? - - func fetchDraft(for messageId: String) async throws { - guard draft == nil else { return } + private(set) var messageIdentifier: MessageIdentifier? + func fetchDraftIdentifier(for messageId: String) async throws { let identifier = Identifier(stringId: messageId) - self.draft = try await draftGateway?.fetchDraft(for: identifier) + self.messageIdentifier = try await draftGateway?.fetchDraftIdentifier(for: identifier) } func encryptAndSaveDraft(message: SendableMsg, threadId: String?) async throws { @@ -235,12 +233,12 @@ final class ComposeMessageService { fmt: .encryptInline ).mimeEncoded - self.draft = try await draftGateway?.saveDraft( + self.messageIdentifier = try await draftGateway?.saveDraft( input: MessageGatewayInput( mime: mime, threadId: threadId ), - draftId: self.draft?.id + draftId: self.messageIdentifier?.draftId ) } catch { throw ComposeMessageError.gatewayError(error) @@ -248,8 +246,8 @@ final class ComposeMessageService { } func deleteDraft(messageId: Identifier?) async throws { - if let draft = draft { - try await draftGateway?.deleteDraft(with: draft.id) + if let draftId = messageIdentifier?.draftId { + try await draftGateway?.deleteDraft(with: draftId) } else if let messageId = messageId { try await messageOperationsProvider.deleteMessage(id: messageId, from: nil) } @@ -286,8 +284,8 @@ final class ComposeMessageService { ) // cleaning any draft saved/created/fetched during editing - if let draft = draft { - try await draftGateway?.deleteDraft(with: draft.id) + if let draftId = messageIdentifier?.draftId { + try await draftGateway?.deleteDraft(with: draftId) } onStateChanged?(.messageSent) diff --git a/FlowCryptAppTests/Mocks/DraftGatewayMock.swift b/FlowCryptAppTests/Mocks/DraftGatewayMock.swift index c77f383d1..e8ccc271d 100644 --- a/FlowCryptAppTests/Mocks/DraftGatewayMock.swift +++ b/FlowCryptAppTests/Mocks/DraftGatewayMock.swift @@ -10,13 +10,13 @@ import GoogleAPIClientForREST_Gmail class DraftGatewayMock: DraftGateway { - func fetchDraftId(messageId: String) async throws -> String? { + func fetchDraftIdentifier(for messageId: Identifier) async throws -> MessageIdentifier? { return nil } - func saveDraft(input: MessageGatewayInput, draftId: String?) async throws -> GTLRGmail_Draft { - return GTLRGmail_Draft() + func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageIdentifier { + return MessageIdentifier(draftId: draftId ?? .random, threadId: nil, messageId: nil) } - func deleteDraft(with identifier: String) async {} + func deleteDraft(with identifier: Identifier) async {} } diff --git a/Gemfile b/Gemfile index ab5236013..1e9a2fd1c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,7 @@ source 'https://rubygems.org' -gem "fastlane", ">= 2.134.0" -gem "xcode-install", ">= 2.6.1" -gem "cocoapods", ">= 1.10.2" -gem "rest-client" +gem "fastlane", ">= 2.210.0" +gem "cocoapods", ">= 1.11.3" # Fastlane plugins from fastlane/Pluginfile gem 'fastlane-plugin-semaphore' diff --git a/Gemfile.lock b/Gemfile.lock index b27127832..4980b857e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -199,7 +199,6 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-accept (1.7.0) http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) @@ -209,9 +208,6 @@ GEM json (2.6.2) jwt (2.5.0) memoist (0.16.2) - mime-types (3.4.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) mini_magick (4.11.0) mini_mime (1.1.2) minitest (5.16.3) @@ -231,11 +227,6 @@ GEM declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - rest-client (2.1.0) - http-accept (>= 1.7.0, < 2.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 4.0) - netrc (~> 0.8) retriable (3.1.2) rexml (3.2.5) rouge (2.0.7) @@ -270,9 +261,6 @@ GEM unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) - xcode-install (2.8.1) - claide (>= 0.9.1) - fastlane (>= 2.1.0, < 3.0.0) xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) @@ -292,11 +280,9 @@ PLATFORMS x86_64-darwin-20 DEPENDENCIES - cocoapods (>= 1.10.2) - fastlane (>= 2.134.0) + cocoapods (>= 1.11.3) + fastlane (>= 2.210.0) fastlane-plugin-semaphore - rest-client - xcode-install (>= 2.6.1) BUNDLED WITH 2.3.6 From be8b4e0c37a86132f18e336af6a736d450b15982 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 21 Sep 2022 22:36:56 +0300 Subject: [PATCH 19/56] fix threads ordering in different folders --- .../Controllers/Inbox/InboxProviders.swift | 16 ++++++++++------ .../Controllers/Inbox/InboxRenderable.swift | 19 ++++++++----------- .../Inbox/InboxViewController.swift | 13 ++++++++++--- .../Controllers/Threads/MessageThread.swift | 11 +++++++++++ .../Threads/MessagesThreadProvider.swift | 1 + 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/FlowCrypt/Controllers/Inbox/InboxProviders.swift b/FlowCrypt/Controllers/Inbox/InboxProviders.swift index 6a3f0aa1e..696a0582d 100644 --- a/FlowCrypt/Controllers/Inbox/InboxProviders.swift +++ b/FlowCrypt/Controllers/Inbox/InboxProviders.swift @@ -28,12 +28,16 @@ class InboxMessageThreadsProvider: InboxDataProvider { func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { let result = try await provider.fetchThreads(using: context) - let inboxData = result.threads.map { - InboxRenderable( - thread: $0, - folderPath: context.folderPath - ) - } + let inboxData = result.threads + .sorted(by: { + $0.latestMessageDate(with: context.folderPath) > $1.latestMessageDate(with: context.folderPath) + }) + .map { + InboxRenderable( + thread: $0, + folderPath: context.folderPath + ) + } let inboxContext = InboxContext( data: inboxData, diff --git a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift index 3fa2889d7..690ee7c3a 100644 --- a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift +++ b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift @@ -69,13 +69,8 @@ extension InboxRenderable { self.messageCount = thread.messages.count self.subtitle = thread.subject ?? "message_missing_subject".localized self.isRead = thread.isRead - let date = thread.messages.last?.date - if let date = date { - self.dateString = DateFormatter().formatDate(date) - } else { - self.dateString = "" - } - self.date = date ?? Date() + self.date = thread.latestMessageDate(with: folderPath) + self.dateString = DateFormatter().formatDate(date) self.wrappedType = .thread(thread) self.folderPath = folderPath @@ -143,12 +138,14 @@ extension InboxRenderable { } mutating func updateBadge() { - // show 'inbox' badge in 'All Mail' folder + // show 'inbox' badge in 'All Mail' and 'Drafts' folders switch wrappedType { case .thread(let thread): - self.badge = folderPath.isEmptyOrNil && thread.isInbox - ? "folder_all_inbox".localized.lowercased() - : nil + guard thread.isInbox, + folderPath.isEmptyOrNil || folderPath == MessageLabel.draft.value + else { self.badge = nil; return } + + self.badge = "folder_all_inbox".localized.lowercased() case .message: self.badge = nil } diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index aa2d589ec..ae82b85d4 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -642,12 +642,19 @@ extension InboxViewController { case .update: // todo break - case .sent(let messageIdentifier): + case .sent(let identifier): // todo break case .delete(let identifier): - guard let index = self.inboxInput.firstIndex(where: { $0.wrappedMessage?.identifier == identifier.messageId }) - else { return } + guard let index = self.inboxInput.firstIndex(where: { + if let threadId = $0.wrappedThread?.identifier { + return threadId == identifier.threadId + } else if let messageId = $0.wrappedMessage?.identifier { + return messageId == identifier.messageId + } + return false + }) else { return } + self.inboxInput.remove(at: index) self.tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) } diff --git a/FlowCrypt/Controllers/Threads/MessageThread.swift b/FlowCrypt/Controllers/Threads/MessageThread.swift index 60427cbf4..ee1698e2c 100644 --- a/FlowCrypt/Controllers/Threads/MessageThread.swift +++ b/FlowCrypt/Controllers/Threads/MessageThread.swift @@ -45,6 +45,17 @@ struct MessageThread: Equatable { var isRead: Bool { !messages.contains(where: { !$0.isRead }) } + + private func messages(with label: String?) -> [Message] { + guard let label = label else { return messages } + + let messageLabel = MessageLabel(gmailLabel: label) + return messages.filter { $0.labels.contains(messageLabel) } + } + + func latestMessageDate(with label: String?) -> Date { + messages(with: label).map(\.date).max() ?? .distantPast + } } extension MessageThread { diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift index 54b491fcb..2748dde2e 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift @@ -17,6 +17,7 @@ extension GmailService: MessagesThreadProvider { func fetchThreads(using context: FetchMessageContext) async throws -> MessageThreadContext { let threadsList = try await getThreadsList(using: context) let identifiers = threadsList.threads?.compactMap(\.identifier) ?? [] + return try await withThrowingTaskGroup(of: MessageThread.self) { taskGroup in var messageThreadsById: [String: MessageThread] = [:] for identifier in identifiers { From a2bb142babf81e7afa7635d382b3638bc02c298a Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 22 Sep 2022 15:42:22 +0300 Subject: [PATCH 20/56] refresh inbox on thread update --- .../ComposeViewController+TapActions.swift | 5 +- .../Controllers/Inbox/InboxProviders.swift | 18 +++++-- .../Inbox/InboxViewController.swift | 48 +++++++++++++------ .../Threads/ThreadDetailsViewController.swift | 35 ++++++++------ .../Message Provider/Gmail+Message.swift | 4 +- .../Message Provider/Imap+Message.swift | 4 +- .../Message Provider/MessageProvider.swift | 4 +- .../Message Provider/MessageService.swift | 4 +- .../Gmail+MessagesList.swift | 2 +- .../MessagesListProvider.swift | 1 + .../Threads/MessagesThreadProvider.swift | 5 +- 11 files changed, 87 insertions(+), 43 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index bcafd78cd..8ab8c43f0 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -44,7 +44,10 @@ extension ComposeViewController { try await self.composeMessageService.deleteDraft(messageId: messageId) if let messageId = messageId { - let identifier = MessageIdentifier(messageId: messageId) + let identifier = MessageIdentifier( + threadId: self.input.type.info?.threadId, + messageId: messageId + ) self.handleAction?(.delete(identifier)) } diff --git a/FlowCrypt/Controllers/Inbox/InboxProviders.swift b/FlowCrypt/Controllers/Inbox/InboxProviders.swift index 696a0582d..84882aa9d 100644 --- a/FlowCrypt/Controllers/Inbox/InboxProviders.swift +++ b/FlowCrypt/Controllers/Inbox/InboxProviders.swift @@ -14,7 +14,8 @@ struct InboxContext { } protocol InboxDataProvider { - func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext + func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxRenderable? + func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext } // used when displaying conversations (threads) in inbox (Gmail API default) @@ -25,7 +26,13 @@ class InboxMessageThreadsProvider: InboxDataProvider { self.provider = provider } - func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { + func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxRenderable? { + guard let id = identifier.stringId else { return nil } + let thread = try await provider.fetchThread(identifier: id, path: path) + return InboxRenderable(thread: thread, folderPath: path) + } + + func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { let result = try await provider.fetchThreads(using: context) let inboxData = result.threads @@ -56,7 +63,12 @@ class InboxMessageListProvider: InboxDataProvider { self.provider = provider } - func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { + func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxRenderable? { + let message = try await provider.fetchMessage(id: identifier, folder: path) + return InboxRenderable(message: message) + } + + func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { let result = try await provider.fetchMessages(using: context) let inboxData = result.messages.map(InboxRenderable.init) diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index ae82b85d4..628fc7b07 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -213,8 +213,7 @@ extension InboxViewController { count: numberOfInboxItemsToLoad, searchQuery: getSearchQuery(), pagination: currentMessagesListPagination() - ), - userEmail: appContext.user.email + ) ) state = .refresh handleEndFetching(with: context, context: batchContext) @@ -240,8 +239,7 @@ extension InboxViewController { folderPath: viewModel.path, count: messagesToLoad(), pagination: pagination - ), - userEmail: appContext.user.email + ) ) state = .fetched(context.pagination) handleEndFetching(with: context, context: batchContext) @@ -431,13 +429,12 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { } func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { - var rowNumber = indexPath.row - if shouldShowEmptyView { - rowNumber -= 1 - } + let rowNumber = shouldShowEmptyView ? indexPath.row - 1 : indexPath.row + guard let message = inboxInput[safe: rowNumber] else { return } + tableNode.deselectRow(at: indexPath, animated: true) open(message: message, path: viewModel.path) } @@ -639,7 +636,7 @@ extension InboxViewController { guard let self = self else { return } switch action { - case .update: + case .update(let identifier): // todo break case .sent(let identifier): @@ -682,13 +679,36 @@ extension InboxViewController { do { let viewController = try await ThreadDetailsViewController( appContext: appContext, - thread: thread - ) { [weak self] action, message in - self?.handleMessageOperation(message: message, action: action) - } + thread: thread, + onComposeMessageAction: { [weak self] action in + guard let self = self else { return } + + switch action { + case .update(let identifier), .sent(let identifier): + if let threadId = identifier.threadId { + if let index = self.inboxInput.firstIndex(where: { $0.wrappedThread?.identifier == threadId }) { + Task { + if let inboxItem = try await self.inboxDataProvider.fetchInboxItem( + identifier: Identifier(stringId: threadId), + path: self.path + ) { + self.inboxInput[index] = inboxItem + self.tableNode.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + } + } + } + } + case .delete(let identifier): + print(identifier) + } + }, + completion: { [weak self] action, message in + self?.handleMessageOperation(message: message, action: action) + } + ) navigationController?.pushViewController(viewController, animated: true) } catch { - showAlert(message: error.localizedDescription) + showAlert(message: error.errorMessage) } } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 744296520..9a2356830 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -50,12 +50,14 @@ final class ThreadDetailsViewController: TableNodeViewController { var currentFolderPath: String { thread.path } + private let onComposeMessageAction: ((ComposeMessageAction) -> Void)? private let onComplete: MessageActionCompletion init( appContext: AppContextWithUser, messageService: MessageService? = nil, thread: MessageThread, + onComposeMessageAction: ((ComposeMessageAction) -> Void)?, completion: @escaping MessageActionCompletion ) async throws { self.appContext = appContext @@ -82,6 +84,7 @@ final class ThreadDetailsViewController: TableNodeViewController { ) ) self.thread = thread + self.onComposeMessageAction = onComposeMessageAction self.onComplete = completion self.input = thread.messages .sorted(by: >) @@ -206,6 +209,8 @@ extension ThreadDetailsViewController { } private func handleComposeMessageAction(_ action: ComposeMessageAction) { + onComposeMessageAction?(action) + switch action { case .update(let messageIdentifier): Task { @@ -615,36 +620,38 @@ extension ThreadDetailsViewController { } extension ThreadDetailsViewController: MessageActionsHandler { - private func handleSuccessfulMessage(action: MessageAction) { + private func handle(action: MessageAction, error: Error? = nil) { hideSpinner() + + if let error = error { + logger.logError("\(action.error ?? "Error: ") \(error)") + return + } + onComplete( action, .init(thread: thread, folderPath: currentFolderPath) ) - navigationController?.popViewController(animated: true) - } - private func handleMessageAction(error: Error, action: MessageAction) { - logger.logError("\(action.error ?? "Error: ") \(error)") - hideSpinner() + navigationController?.popViewController(animated: true) } func permanentlyDelete() { logger.logInfo("permanently delete") - handle(action: .permanentlyDelete) + perform(action: .permanentlyDelete) } func moveToTrash(with trashPath: String) { logger.logInfo("move to trash \(trashPath)") - handle(action: .moveToTrash) + perform(action: .moveToTrash) } func handleArchiveTap() { - handle(action: .archive) + perform(action: .archive) } func handleMoveToInboxTap() { - handle(action: .moveToInbox) + perform(action: .moveToInbox) } func handleMarkUnreadTap() { @@ -652,10 +659,10 @@ extension ThreadDetailsViewController: MessageActionsHandler { guard messages.isNotEmpty else { return } - handle(action: .markAsRead(false)) + perform(action: .markAsRead(false)) } - func handle(action: MessageAction) { + func perform(action: MessageAction) { Task { do { showSpinner() @@ -676,9 +683,9 @@ extension ThreadDetailsViewController: MessageActionsHandler { try await threadOperationsProvider.delete(thread: thread) } - handleSuccessfulMessage(action: action) + handle(action: action) } catch { - handleMessageAction(error: error, action: action) + handle(action: action, error: error) } } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift index bb3b97ebd..5d3cadb6a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift @@ -11,7 +11,7 @@ import GTMSessionFetcherCore extension GmailService: MessageProvider { - func fetchMsg( + func fetchMessage( id: Identifier, folder: String ) async throws -> Message { @@ -40,7 +40,7 @@ extension GmailService: MessageProvider { } } - func fetchRawMsg(id: Identifier) async throws -> String { + func fetchRawMessage(id: Identifier) async throws -> String { guard let identifier = id.stringId else { throw GmailServiceError.missingMessageInfo("id") } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift index 9b87f0c94..e6f0958e8 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift @@ -9,7 +9,7 @@ import Foundation extension Imap: MessageProvider { - func fetchMsg( + func fetchMessage( id: Identifier, folder: String ) async throws -> Message { @@ -25,7 +25,7 @@ extension Imap: MessageProvider { // }) } - func fetchRawMsg(id: Identifier) async throws -> String { + func fetchRawMessage(id: Identifier) async throws -> String { throw AppErr.unexpected("Not implemented") } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageProvider.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageProvider.swift index d443588d0..3a42714f3 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageProvider.swift @@ -9,8 +9,8 @@ import Foundation protocol MessageProvider { - func fetchMsg(id: Identifier, folder: String) async throws -> Message - func fetchRawMsg(id: Identifier) async throws -> String + func fetchMessage(id: Identifier, folder: String) async throws -> Message + func fetchRawMessage(id: Identifier) async throws -> String func fetchAttachment( id: Identifier, messageId: Identifier, diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index b787de1d2..48cb71da7 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -93,7 +93,7 @@ final class MessageService { userEmail: String, isUsingKeyManager: Bool ) async throws -> ProcessedMessage { - let message = try await messageProvider.fetchMsg( + let message = try await messageProvider.fetchMessage( id: identifier, folder: folder ) @@ -130,7 +130,7 @@ final class MessageService { var message = message if message.hasSignatureAttachment { // raw data is needed for verification of detached signature - message.raw = try await messageProvider.fetchRawMsg(id: message.identifier) + message.raw = try await messageProvider.fetchRawMessage(id: message.identifier) } let encrypted = message.raw ?? message.body.text diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift index 7d5a6ba14..d0763950d 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift @@ -22,7 +22,7 @@ extension GmailService: MessagesListProvider { if let self = self { for identifier in messageIdentifiers { taskGroup.addTask { - try await self.fetchMsg(id: Identifier(stringId: identifier), folder: "") + try await self.fetchMessage(id: Identifier(stringId: identifier), folder: "") } } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/MessagesListProvider.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/MessagesListProvider.swift index 0754e97dd..9367db490 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/MessagesListProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/MessagesListProvider.swift @@ -33,5 +33,6 @@ enum MessagesListPagination { } protocol MessagesListProvider { + func fetchMessage(id: Identifier, folder: String) async throws -> Message func fetchMessages(using context: FetchMessageContext) async throws -> MessageContext } diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift index 2748dde2e..647b30a9d 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift @@ -10,6 +10,7 @@ import FlowCryptCommon import GoogleAPIClientForREST_Gmail protocol MessagesThreadProvider { + func fetchThread(identifier: String, path: String) async throws -> MessageThread func fetchThreads(using context: FetchMessageContext) async throws -> MessageThreadContext } @@ -22,7 +23,7 @@ extension GmailService: MessagesThreadProvider { var messageThreadsById: [String: MessageThread] = [:] for identifier in identifiers { taskGroup.addTask { - try await self.getThread(identifier: identifier, path: context.folderPath ?? "") + try await self.fetchThread(identifier: identifier, path: context.folderPath ?? "") } } for try await result in taskGroup { @@ -57,7 +58,7 @@ extension GmailService: MessagesThreadProvider { }.value } - private func getThread(identifier: String, path: String) async throws -> MessageThread { + func fetchThread(identifier: String, path: String) async throws -> MessageThread { return try await Task.retrying { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in self.gmailService.executeQuery( From 3a9f9381e8bd05efa27f94e330dcb8477e8e8c3a Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 22 Sep 2022 16:52:04 +0300 Subject: [PATCH 21/56] temporary disable mock draft endpoint --- .../api-mocks/apis/google/google-endpoints.ts | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/appium/api-mocks/apis/google/google-endpoints.ts b/appium/api-mocks/apis/google/google-endpoints.ts index 535c64b11..d9adb8e04 100644 --- a/appium/api-mocks/apis/google/google-endpoints.ts +++ b/appium/api-mocks/apis/google/google-endpoints.ts @@ -8,7 +8,7 @@ import { isDelete, isGet, isPost, isPut, parseResourceId } from '../../lib/mock- import { oauth } from '../../lib/oauth'; import { GoogleMockAccountEmail } from './google-messages'; -type DraftSaveModel = { message: { raw: string, threadId: string } }; +// type DraftSaveModel = { message: { raw: string, threadId: string } }; type LabelsModifyModel = { addLabelIds: string[], removeLabelIds: string[] } interface BatchModifyInterface { ids: string[]; @@ -170,28 +170,29 @@ export const getMockGoogleEndpoints = ( } return {} }, - '/gmail/v1/users/me/drafts': async (parsedReq, req) => { - if (isPost(req)) { - const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); - const body = parsedReq.body as DraftSaveModel; - if (body && body.message && body.message.raw && typeof body.message.raw === 'string') { - if (body.message.threadId && !(await GoogleData.withInitializedData(acct, googleConfig)).getThreads().find(t => t.id === body.message.threadId)) { - throw new HttpErr('The thread you are replying to not found', 404); - } - const decoded = await Parse.convertBase64ToMimeMsg(body.message.raw); - if (!decoded.text?.startsWith('[flowcrypt:') && !decoded.text?.startsWith('(saving of this draft was interrupted - to decrypt it, send it to yourself)')) { - throw new Error(`The "flowcrypt" draft prefix was not found in the draft. Instead starts with: ${decoded.text?.substring(0, 100)}`); - } - return { - id: 'mockfakedraftsave', message: { - id: 'mockfakedmessageraftsave', - labelIds: ['DRAFT'], - threadId: body.message.threadId - } - }; - } - } - throw new HttpErr(`Method not implemented for ${req.url}: ${req.method}`); + '/gmail/v1/users/me/drafts': async () => { + return {} + // if (isPost(req)) { + // const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); + // const body = parsedReq.body as DraftSaveModel; + // if (body && body.message && body.message.raw && typeof body.message.raw === 'string') { + // if (body.message.threadId && !(await GoogleData.withInitializedData(acct, googleConfig)).getThreads().find(t => t.id === body.message.threadId)) { + // throw new HttpErr('The thread you are replying to not found', 404); + // } + // const decoded = await Parse.convertBase64ToMimeMsg(body.message.raw); + // if (!decoded.text?.startsWith('[flowcrypt:') && !decoded.text?.startsWith('(saving of this draft was interrupted - to decrypt it, send it to yourself)')) { + // throw new Error(`The "flowcrypt" draft prefix was not found in the draft. Instead starts with: ${decoded.text?.substring(0, 100)}`); + // } + // return { + // id: 'mockfakedraftsave', message: { + // id: 'mockfakedmessageraftsave', + // labelIds: ['DRAFT'], + // threadId: body.message.threadId + // } + // }; + // } + // } + // throw new HttpErr(`Method not implemented for ${req.url}: ${req.method}`); }, '/gmail/v1/users/me/drafts/?': async (parsedReq, req) => { const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); From 1a6a3b9a25cc515f583c1cbc1faf074c008dce36 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 23 Sep 2022 13:22:34 +0300 Subject: [PATCH 22/56] fix ui tests --- .../Threads/ThreadDetailsViewController.swift | 30 +++--- .../message-export-180b3b5efbdc46da.json | 76 +++++++++++++++ .../message-export-180b3b8f20d6cbd5.json | 6 +- .../message-export-180b3b8f20d6cbd6.json | 94 ------------------- ...ckReplyAndForwardForEncryptedEmail.spec.ts | 2 +- 5 files changed, 96 insertions(+), 112 deletions(-) create mode 100644 appium/api-mocks/apis/google/exported-messages/message-export-180b3b5efbdc46da.json delete mode 100644 appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd6.json diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 9a2356830..482841ef8 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -428,7 +428,7 @@ extension ThreadDetailsViewController { if case .missingPubkey = processedMessage.signature { processedMessage.signature = .pending - await retryVerifyingSignatureWithRemotelyFetchedKeys( + retryVerifyingSignatureWithRemotelyFetchedKeys( message: message, folder: thread.path, indexPath: indexPath @@ -591,19 +591,21 @@ extension ThreadDetailsViewController { message: Message, folder: String, indexPath: IndexPath - ) async { - do { - let processedMessage = try await messageService.getAndProcess( - identifier: message.identifier, - folder: thread.path, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager - ) - handle(processedMessage: processedMessage, at: indexPath) - } catch { - let message = "message_signature_fail_reason".localizeWithArguments(error.errorMessage) - input[indexPath.section - 1].processedMessage?.signature = .error(message) + ) { + Task { + do { + let processedMessage = try await messageService.getAndProcess( + identifier: message.identifier, + folder: thread.path, + onlyLocalKeys: false, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + handle(processedMessage: processedMessage, at: indexPath) + } catch { + let message = "message_signature_fail_reason".localizeWithArguments(error.errorMessage) + input[indexPath.section - 1].processedMessage?.signature = .error(message) + } } } diff --git a/appium/api-mocks/apis/google/exported-messages/message-export-180b3b5efbdc46da.json b/appium/api-mocks/apis/google/exported-messages/message-export-180b3b5efbdc46da.json new file mode 100644 index 000000000..2e683cc75 --- /dev/null +++ b/appium/api-mocks/apis/google/exported-messages/message-export-180b3b5efbdc46da.json @@ -0,0 +1,76 @@ +{ + "acctEmail": "e2e.enterprise.test@flowcrypt.com", + "full": { + "id": "180b3b5efbdc46da", + "threadId": "180b3b5efbdc46da", + "labelIds": [ + "CATEGORY_PERSONAL", + "INBOX" + ], + "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt Email Encryption 8.2.7 Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAz8it/geQ2cIvZHYP5qii9N7LZUUTGAn/vYfPhDC4", + "payload": { + "partId": "", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { + "name": "X-Gm-Message-State", + "value": "AOAM531gNh/ZbIBtNaSbpmsTIrCxGynC5ug0qmpykuhO2lGmSZekWkmB Oiylv/cDcGIk6xRrvb74+Azez4oOVVSJvsIVl9mnfLMJ9hf1iQ==" + }, + { + "name": "Openpgp", + "value": "id=B98A4D267F824CCC" + }, + { + "name": "From", + "value": "Dmitry at FlowCrypt " + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Date", + "value": "Wed, 11 May 2022 08:21:25 -0700" + }, + { + "name": "Subject", + "value": "new message for reply" + }, + { + "name": "To", + "value": "e2e enterprise test at FlowCrypt , demo@flowcrypt.com" + }, + { + "name": "Cc", + "value": "robot@flowcrypt.com" + }, + { + "name": "Content-Type", + "value": "text/plain; charset=\"UTF-8\"" + } + ], + "body": { + "size": 1750, + "data": "LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgRW1haWwgRW5jcnlwdGlvbiA4LjIuNw0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp3VjREVDJabFNtaFoxR29TQVFkQXo4aXQvZ2VRMmNJdlpIWVA1cWlpOU43TFpVVVRHQW4vdllmUGhEQzQNCm1VZ3docXZXcUZHbEwzZWluVmdscWdhVjFBL3RDWFFvbFh3UEgrT0JpYjVEdmdURXJkanorK2N6QWZoTg0KaW4wenVOWFh3VjREWmo0K3BvRzdLWjRTQVFkQVFsWVNVSnQwbTFUcjd3WWVJU0RBWTlneThCQndONTR5DQorTm53TzJRcHpIVXdBNXg2Z05vZmMrNUZKS3NUZ1hGS2FnZjdCSXVobnJKdEJOcnloM1NDYmdQa0oxRVENCkRtSWxCRXRNUHAzMG1PQ0h3Y0ZNQTNpRU8vdWJteHRaQVEvL1lvV0NLSmdEZzU4REc0ZUh2Y3NrVkJ3Lw0KQVVGd29ybUMxUHhhWHZqUDZCYzlVN3R4TU5pNWpTZ3dtOEpZQ3MzL0JFQ3FDRkFEditOb2ZsTGJoMEZODQo2eXhTTUxzampvc1cwcEN3N2xuYlFvb0U5dHB5NnRSanFDa25EZDJ5dmRwTUhnUE5TM2pacGZ6dTROd2UNCnZtUWdKbWVKcCtUWmlOZmM3d25aLzR3c0t3UmE5b3d3QmI2Zm12SUR3YmpVNUZrT29sNHU5d2NxUndHNg0KdngvSVZSZ0x5ZHVkNkFtM05adlBiSy8zUFg4REx5TkQyTnFkQjl6K2luM0NrQ2NUUFlNdWxUMThtRlBDDQpPWkNZWjdNZExDd3V3U0pBdUlhZC9YTkdvc09mWGtEc1dCOVhyNjFVdGhQNmpnbzFORzlaTHgydXVzQzYNClluMFpHajhXeFZVbkgzWnJBaVhVTlJqemxFUTAzbFlWcklWUmVQbG9XSm9mY1ErL3llNjhqN0srOEZRcA0KYWdoTEFtdWt5d0o5bVRMeU1PVjMrU3pjVDdoazd3Ym1KTXprVzZtUDV5ekRLbE5XbUVwMEJCWjBodzREDQptTXRhKzlzbEFTbjY0WTY3ejNaRFFwUktDOWh5dnQ0Znh4VFh0V0JXWXBiRFBJTlVrdHV4ZndqSG0vZzINCjA4RFpCMEdEeDgwUk1nenhUQlhuOWlTSndEa29WWGxoQWtCbC9aZFg2dytaOGlSYW56SktoN2s1aUF2Rw0KRzk1UXBDWkFnYXlOcVBHWjlFbDVtU2pqUnpSZGlxenhZbStkNjI3QmI4RVdFZVN6TE82VXBqSEwvMzhJDQpXbnl4aGIwb1ZSUVh2bFZEcWZOWDlyRlF5UGNHWVdkenVINDlFSVBTOXdMNFE0RklaWHZBYkhTckJ3VEINClhnTlZEcnpCUXVKckR4SUJCMEQrOU9PTjRQdXdDWWY5YkNtZ29xeEVHTXpFQzM2L1k0V1gvNTFlamNkcg0KUURCTGtkQVlpajdGZGtTNjVwYzVxQUcrK3FyaHNSRnJxTWxWclRnS1RVVEd2YTQzZjMwRlNXaGVZQVhEDQo1NDRpU0VuQlhnUDMzWXd2dnFtTmRSSUJCMEFtL3AzM0YxeTI1MEZ0S0NSR2VWd0xFQVRwdWdnSEFQZWwNCmFZWEk2VEJvQlRDOWNWY2dYZXFuU25KL3pJVnhKQTNwZmJQME9qeVZGL2FRNUhGN3RGeFBacGY3c0hpLw0KMUNCaFNVNDVYUnJqSzVMU3dDd0JvUkwvLzQrcEpYZzJpS24rUTkwN2RwMjNJU3lDUkpiVzVYZ3VQZFlJDQprbFQ0ME5zK281SkkrU1I5Y3FPa3hmNlFMK3pBTWVOSW5IWXNzdHlYOEdVb21EaVdTYUlYamVSWGF1OUwNCnpNdGxncnpOcFhyUWJsRWcvVW9TbkUxY3hBNTZuNGRLMmM2bGVjVmhDVFFtS0RVcDBMODZzMW1UblRIMQ0KYmV4N1dsYmRkNEwvZm5xREsweGZqOWw5U05MSFppUGplK1piUktlMEN4VXBnM0trTGxET1U1bXFTbFliDQpQRS9kdXZhMUF3SU9QUFBzZzBLdjFRalR4SFFlSDhVUG9Ec2MybVNzdVZkYUFObFN2NUxvR1IvSWZOZnENCnBvU2NuTjdxMkVSOXhwZ0lpUWdBQS9RckNOSVNYcWZyMFE9PQ0KPUE1RXoNCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0NCg==" + } + }, + "sizeEstimate": 6475, + "historyId": "190359", + "internalDate": "1652282485000" + }, + "attachments": {}, + "raw": { + "id": "180b3b5efbdc46da", + "threadId": "180b3b5efbdc46da", + "labelIds": [ + "CATEGORY_PERSONAL", + "INBOX" + ], + "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt Email Encryption 8.2.7 Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAz8it/geQ2cIvZHYP5qii9N7LZUUTGAn/vYfPhDC4", + "sizeEstimate": 6475, + "raw": "RGVsaXZlcmVkLVRvOiBlMmUuZW50ZXJwcmlzZS50ZXN0QGZsb3djcnlwdC5jb20NClJlY2VpdmVkOiBieSAyMDAyOmEwNTo2NjM4OmFjNjowOjA6MDowIHdpdGggU01UUCBpZCBtNmNzcDcwODg1NDdqYWI7DQogICAgICAgIFdlZCwgMTEgTWF5IDIwMjIgMDg6MjE6MjYgLTA3MDAgKFBEVCkNClgtUmVjZWl2ZWQ6IGJ5IDIwMDI6YTA1OjYwMDA6MWE4ZTpiMDoyMGM6YmU4YzoxMGE0IHdpdGggU01UUCBpZCBmMTQtMjAwMjBhMDU2MDAwMWE4ZTAwYjAwMjBjYmU4YzEwYTRtcjE2MTMxNzQwd3J5LjQzNy4xNjUyMjgyNDg2Njc1Ow0KICAgICAgICBXZWQsIDExIE1heSAyMDIyIDA4OjIxOjI2IC0wNzAwIChQRFQpDQpBUkMtU2VhbDogaT0xOyBhPXJzYS1zaGEyNTY7IHQ9MTY1MjI4MjQ4NjsgY3Y9bm9uZTsNCiAgICAgICAgZD1nb29nbGUuY29tOyBzPWFyYy0yMDE2MDgxNjsNCiAgICAgICAgYj1vMTljMjZsMjd6UWZ6cE1UMjR6RG50WXRtTll4cTRXcTdFN2pSZUJMWEJWMDBkRDBiVDhnUml2MFJ5MWZBclhuOWgNCiAgICAgICAgIDN6bXBFeHdLbXZhRnk5OWRsZDVZRHJMcCtSM2F5dzNKREdIc2dMRDRsK1c1bm5OcUhxTXNoeVpYeHJwaTBneEppZ0lSDQogICAgICAgICBTUzZ4eHNTYVdHckIvU3JSTHlXTlQ5ME9YenV5bkN3T2h2d05sQUpQeVV0SUY0dTdoMXRudnJYMldPdVFqRy80c3VLRg0KICAgICAgICAgdDR5QWFZWHVIYkNPTzU5cWdVRzV0VDFtdTBydGQ3OGw2aGxlWGJtaEsvbXFNVW9ETlJYQmZJRTJKb1F5QTY0YkF4djQNCiAgICAgICAgIDE0NFR1cjNVTFEwRlFYNHNUemNDOGpJQ1RpbG9FUHM5MGJSd3kyNU5UMXhqZVhiczhWMmZ2Wkxuazd1eVo0QWhxUjRuDQogICAgICAgICBQQjNRPT0NCkFSQy1NZXNzYWdlLVNpZ25hdHVyZTogaT0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOyBkPWdvb2dsZS5jb207IHM9YXJjLTIwMTYwODE2Ow0KICAgICAgICBoPWNjOnRvOnN1YmplY3Q6bWVzc2FnZS1pZDpkYXRlOm1pbWUtdmVyc2lvbjpmcm9tOm9wZW5wZ3ANCiAgICAgICAgIDpka2ltLXNpZ25hdHVyZTsNCiAgICAgICAgYmg9ZjMxVzd3SS8xdWk2M2c4Tkgxb25HSU9rQUZ2Mm11ek5HbllVenA5VVh2Yz07DQogICAgICAgIGI9VEdVTEhDdjJMMkFyS0FJLzI4Y1JmYVg1TTllSE5GUGFXRDNwMytjVjNpdXhQenhhZzIzR2FtVTkvRFY3bHdGOEdQDQogICAgICAgICB0cGoyUlZLdld5d240MjZOV0FhR2gxWHdHeGhXWlBVTHZhaWNJWlp5cGdTbjllTjdxODdXNk9VM0tqa1V6cTVYQXFwTQ0KICAgICAgICAgcUlkS3J0ZFMrWXk1VUxrL0FJa1kxc0wyNHhiZ0dHb2hFbzljellPOGo0TGdaNDNBSm05WWRjd1YrdGh1WW94SGV5QmsNCiAgICAgICAgIFdZNHMvajVyY3Nid2pKZ0tBYnBFZlJtZ211NGorNzg0bmZlckZNcHB1d1BBSU9FdythRlp4a2FlUHRUQVBnbEVyZ1VxDQogICAgICAgICBsR0p4OUttVHA0L29LV2dlZjJJdTRudGMrTzRIVlZXSzFoRzArUzQwaGg0VUhDa2xobUNvbHNKZUNNZTBLS0hQejhEcg0KICAgICAgICAgQ3Urdz09DQpBUkMtQXV0aGVudGljYXRpb24tUmVzdWx0czogaT0xOyBteC5nb29nbGUuY29tOw0KICAgICAgIGRraW09cGFzcyBoZWFkZXIuaT1AZmxvd2NyeXB0LmNvbSBoZWFkZXIucz1nb29nbGUgaGVhZGVyLmI9TVk3WnJrdHo7DQogICAgICAgc3BmPXBhc3MgKGdvb2dsZS5jb206IGRvbWFpbiBvZiBkbWl0cnlAZmxvd2NyeXB0LmNvbSBkZXNpZ25hdGVzIDIwOS44NS4yMjAuNDEgYXMgcGVybWl0dGVkIHNlbmRlcikgc210cC5tYWlsZnJvbT1kbWl0cnlAZmxvd2NyeXB0LmNvbTsNCiAgICAgICBkbWFyYz1wYXNzIChwPVJFSkVDVCBzcD1SRUpFQ1QgZGlzPU5PTkUpIGhlYWRlci5mcm9tPWZsb3djcnlwdC5jb20NClJldHVybi1QYXRoOiA8ZG1pdHJ5QGZsb3djcnlwdC5jb20-DQpSZWNlaXZlZDogZnJvbSBtYWlsLXNvci1mNDEuZ29vZ2xlLmNvbSAobWFpbC1zb3ItZjQxLmdvb2dsZS5jb20uIFsyMDkuODUuMjIwLjQxXSkNCiAgICAgICAgYnkgbXguZ29vZ2xlLmNvbSB3aXRoIFNNVFBTIGlkIGMxMS0yMDAyMGFkZmMwNGIwMDAwMDBiMDAyMGM3ZGZiNjY5MXNvcjEzMTM5MTl3cmYuOS4yMDIyLjA1LjExLjA4LjIxLjI2DQogICAgICAgIGZvciA8ZTJlLmVudGVycHJpc2UudGVzdEBmbG93Y3J5cHQuY29tPg0KICAgICAgICAoR29vZ2xlIFRyYW5zcG9ydCBTZWN1cml0eSk7DQogICAgICAgIFdlZCwgMTEgTWF5IDIwMjIgMDg6MjE6MjYgLTA3MDAgKFBEVCkNClJlY2VpdmVkLVNQRjogcGFzcyAoZ29vZ2xlLmNvbTogZG9tYWluIG9mIGRtaXRyeUBmbG93Y3J5cHQuY29tIGRlc2lnbmF0ZXMgMjA5Ljg1LjIyMC40MSBhcyBwZXJtaXR0ZWQgc2VuZGVyKSBjbGllbnQtaXA9MjA5Ljg1LjIyMC40MTsNCkF1dGhlbnRpY2F0aW9uLVJlc3VsdHM6IG14Lmdvb2dsZS5jb207DQogICAgICAgZGtpbT1wYXNzIGhlYWRlci5pPUBmbG93Y3J5cHQuY29tIGhlYWRlci5zPWdvb2dsZSBoZWFkZXIuYj1NWTdacmt0ejsNCiAgICAgICBzcGY9cGFzcyAoZ29vZ2xlLmNvbTogZG9tYWluIG9mIGRtaXRyeUBmbG93Y3J5cHQuY29tIGRlc2lnbmF0ZXMgMjA5Ljg1LjIyMC40MSBhcyBwZXJtaXR0ZWQgc2VuZGVyKSBzbXRwLm1haWxmcm9tPWRtaXRyeUBmbG93Y3J5cHQuY29tOw0KICAgICAgIGRtYXJjPXBhc3MgKHA9UkVKRUNUIHNwPVJFSkVDVCBkaXM9Tk9ORSkgaGVhZGVyLmZyb209Zmxvd2NyeXB0LmNvbQ0KREtJTS1TaWduYXR1cmU6IHY9MTsgYT1yc2Etc2hhMjU2OyBjPXJlbGF4ZWQvcmVsYXhlZDsNCiAgICAgICAgZD1mbG93Y3J5cHQuY29tOyBzPWdvb2dsZTsNCiAgICAgICAgaD1vcGVucGdwOmZyb206bWltZS12ZXJzaW9uOmRhdGU6bWVzc2FnZS1pZDpzdWJqZWN0OnRvOmNjOw0KICAgICAgICBiaD1mMzFXN3dJLzF1aTYzZzhOSDFvbkdJT2tBRnYybXV6TkduWVV6cDlVWHZjPTsNCiAgICAgICAgYj1NWTdacmt0enZaeEdxb2JwNlhIRVBWUjA5VE9mamFod2NNS2lZcWxjRXRYVWZVMDZNODd2ZnpyZmpndDdEWkppWlQNCiAgICAgICAgIDZ3bytyQndvSWxuK3ZENWJKRnA3K2JqYVBZdkJ6U25PZTFzRnExRmpkWUxqQXBzUlpTTkw1RkFlOHRFNTRZWDBJNmZsDQogICAgICAgICB3TFlzRUFOWmxpQmE0VlBNMzV3VTF4V1BtSnpXSTJ2cHFWN1JnPQ0KWC1Hb29nbGUtREtJTS1TaWduYXR1cmU6IHY9MTsgYT1yc2Etc2hhMjU2OyBjPXJlbGF4ZWQvcmVsYXhlZDsNCiAgICAgICAgZD0xZTEwMC5uZXQ7IHM9MjAyMTAxMTI7DQogICAgICAgIGg9eC1nbS1tZXNzYWdlLXN0YXRlOm9wZW5wZ3A6ZnJvbTptaW1lLXZlcnNpb246ZGF0ZTptZXNzYWdlLWlkDQogICAgICAgICA6c3ViamVjdDp0bzpjYzsNCiAgICAgICAgYmg9ZjMxVzd3SS8xdWk2M2c4Tkgxb25HSU9rQUZ2Mm11ek5HbllVenA5VVh2Yz07DQogICAgICAgIGI9TkNyWEFBMUU0d2pWd1RVMzFScUNIZm84dUFWbHNlTFcxcy92bUhDcTBqSE9aRzhnN0RJOG5YRS8zK2MwZVV3UW5QDQogICAgICAgICA5QjNIT3ZCQS9IN3NwVzlyZi9VOUZxUndwcHd5QXE5bXQ1NWI1blpzdk5pNVRTR3hiZ0dEbUczbUxvcUNZR3lRZmZ2UA0KICAgICAgICAgNFQ3TnlSS2htbXNUYXVKZmdKRmVIUGg2VXBMeW9icWFpcnFUZWlKdDZJVUh0dXpzMFVwTzQvLy9LdXJQMUpoZUtQc0MNCiAgICAgICAgIE0yVWVrSlZXRmUrS25UaTNLckt1TnQzYURnN3BDYkZnUjV6bzZjcWU2SnlpM3RPZEdpbUNHbGtwanFheHl6Zm1OZC9RDQogICAgICAgICBhQ2dIQ0NLUmZhWkdTRkNOU2pNd0Z2TEQ0TCtFeWw1L1hEd3lyTGtidXpWQ0lzM1kwVlhBMzhwaEVTK29UREhBUEFEMQ0KICAgICAgICAgSmpQQT09DQpYLUdtLU1lc3NhZ2UtU3RhdGU6IEFPQU01MzFnTmgvWmJJQnROYVNicG1zVElyQ3hHeW5DNXVnMHFtcHlrdWhPMmxHbVNaZWtXa21CDQoJT2l5bHYvY0RjR0lrNnhScnZiNzQrQXplejRvT1ZWU0p2c0lWbDltbmZMTUo5aGYxaVE9PQ0KWC1Hb29nbGUtU210cC1Tb3VyY2U6IEFCZGhQSnlMcSsyRkNmRUtJM3U4N0Z3OG5vbTV3SjdMbWhGQUNBdlFRQ3VrVFBFSmJaWFF1MmZCTkI3Zks3SkFUME9RNDJIYllNN3p0engrN3FXZkV0aHQydzg9DQpYLVJlY2VpdmVkOiBieSAyMDAyOmE1ZDo1MjI4OjA6YjA6MjBhOmQ3ZTk6N2VkOCB3aXRoIFNNVFAgaWQNCiBpOC0yMDAyMGE1ZDUyMjgwMDAwMDBiMDAyMGFkN2U5N2VkOG1yMjI4MTY0Nzl3cmEuNjg3LjE2NTIyODI0ODYxMTY7IFdlZCwgMTENCiBNYXkgMjAyMiAwODoyMToyNiAtMDcwMCAoUERUKQ0KUmVjZWl2ZWQ6IGZyb20gNzE3Mjg0NzMwMjQ0IG5hbWVkIHVua25vd24gYnkgZ21haWxhcGkuZ29vZ2xlLmNvbSB3aXRoDQogSFRUUFJFU1Q7IFdlZCwgMTEgTWF5IDIwMjIgMDg6MjE6MjUgLTA3MDANCk9wZW5wZ3A6IGlkPUI5OEE0RDI2N0Y4MjRDQ0MNCkZyb206IERtaXRyeSBhdCBGbG93Q3J5cHQgPGRtaXRyeUBmbG93Y3J5cHQuY29tPg0KTUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IFdlZCwgMTEgTWF5IDIwMjIgMDg6MjE6MjUgLTA3MDANCk1lc3NhZ2UtSUQ6IDxDQUdBeXh2M0ZXMzZ0SGpPdHFPZzdIdjhoMGZXZ2Fodlgwb2tFRXJjQis5ZFV2ellzblFAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBuZXcgbWVzc2FnZSBmb3IgcmVwbHkNClRvOiBlMmUgZW50ZXJwcmlzZSB0ZXN0IGF0IEZsb3dDcnlwdCA8ZTJlLmVudGVycHJpc2UudGVzdEBmbG93Y3J5cHQuY29tPiwgZGVtb0BmbG93Y3J5cHQuY29tDQpDYzogcm9ib3RAZmxvd2NyeXB0LmNvbQ0KQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PSJVVEYtOCINCg0KLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgRW1haWwgRW5jcnlwdGlvbiA4LjIuNw0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp3VjREVDJabFNtaFoxR29TQVFkQXo4aXQvZ2VRMmNJdlpIWVA1cWlpOU43TFpVVVRHQW4vdllmUGhEQzQNCm1VZ3docXZXcUZHbEwzZWluVmdscWdhVjFBL3RDWFFvbFh3UEgrT0JpYjVEdmdURXJkanorK2N6QWZoTg0KaW4wenVOWFh3VjREWmo0K3BvRzdLWjRTQVFkQVFsWVNVSnQwbTFUcjd3WWVJU0RBWTlneThCQndONTR5DQorTm53TzJRcHpIVXdBNXg2Z05vZmMrNUZKS3NUZ1hGS2FnZjdCSXVobnJKdEJOcnloM1NDYmdQa0oxRVENCkRtSWxCRXRNUHAzMG1PQ0h3Y0ZNQTNpRU8vdWJteHRaQVEvL1lvV0NLSmdEZzU4REc0ZUh2Y3NrVkJ3Lw0KQVVGd29ybUMxUHhhWHZqUDZCYzlVN3R4TU5pNWpTZ3dtOEpZQ3MzL0JFQ3FDRkFEditOb2ZsTGJoMEZODQo2eXhTTUxzampvc1cwcEN3N2xuYlFvb0U5dHB5NnRSanFDa25EZDJ5dmRwTUhnUE5TM2pacGZ6dTROd2UNCnZtUWdKbWVKcCtUWmlOZmM3d25aLzR3c0t3UmE5b3d3QmI2Zm12SUR3YmpVNUZrT29sNHU5d2NxUndHNg0KdngvSVZSZ0x5ZHVkNkFtM05adlBiSy8zUFg4REx5TkQyTnFkQjl6K2luM0NrQ2NUUFlNdWxUMThtRlBDDQpPWkNZWjdNZExDd3V3U0pBdUlhZC9YTkdvc09mWGtEc1dCOVhyNjFVdGhQNmpnbzFORzlaTHgydXVzQzYNClluMFpHajhXeFZVbkgzWnJBaVhVTlJqemxFUTAzbFlWcklWUmVQbG9XSm9mY1ErL3llNjhqN0srOEZRcA0KYWdoTEFtdWt5d0o5bVRMeU1PVjMrU3pjVDdoazd3Ym1KTXprVzZtUDV5ekRLbE5XbUVwMEJCWjBodzREDQptTXRhKzlzbEFTbjY0WTY3ejNaRFFwUktDOWh5dnQ0Znh4VFh0V0JXWXBiRFBJTlVrdHV4ZndqSG0vZzINCjA4RFpCMEdEeDgwUk1nenhUQlhuOWlTSndEa29WWGxoQWtCbC9aZFg2dytaOGlSYW56SktoN2s1aUF2Rw0KRzk1UXBDWkFnYXlOcVBHWjlFbDVtU2pqUnpSZGlxenhZbStkNjI3QmI4RVdFZVN6TE82VXBqSEwvMzhJDQpXbnl4aGIwb1ZSUVh2bFZEcWZOWDlyRlF5UGNHWVdkenVINDlFSVBTOXdMNFE0RklaWHZBYkhTckJ3VEINClhnTlZEcnpCUXVKckR4SUJCMEQrOU9PTjRQdXdDWWY5YkNtZ29xeEVHTXpFQzM2L1k0V1gvNTFlamNkcg0KUURCTGtkQVlpajdGZGtTNjVwYzVxQUcrK3FyaHNSRnJxTWxWclRnS1RVVEd2YTQzZjMwRlNXaGVZQVhEDQo1NDRpU0VuQlhnUDMzWXd2dnFtTmRSSUJCMEFtL3AzM0YxeTI1MEZ0S0NSR2VWd0xFQVRwdWdnSEFQZWwNCmFZWEk2VEJvQlRDOWNWY2dYZXFuU25KL3pJVnhKQTNwZmJQME9qeVZGL2FRNUhGN3RGeFBacGY3c0hpLw0KMUNCaFNVNDVYUnJqSzVMU3dDd0JvUkwvLzQrcEpYZzJpS24rUTkwN2RwMjNJU3lDUkpiVzVYZ3VQZFlJDQprbFQ0ME5zK281SkkrU1I5Y3FPa3hmNlFMK3pBTWVOSW5IWXNzdHlYOEdVb21EaVdTYUlYamVSWGF1OUwNCnpNdGxncnpOcFhyUWJsRWcvVW9TbkUxY3hBNTZuNGRLMmM2bGVjVmhDVFFtS0RVcDBMODZzMW1UblRIMQ0KYmV4N1dsYmRkNEwvZm5xREsweGZqOWw5U05MSFppUGplK1piUktlMEN4VXBnM0trTGxET1U1bXFTbFliDQpQRS9kdXZhMUF3SU9QUFBzZzBLdjFRalR4SFFlSDhVUG9Ec2MybVNzdVZkYUFObFN2NUxvR1IvSWZOZnENCnBvU2NuTjdxMkVSOXhwZ0lpUWdBQS9RckNOSVNYcWZyMFE9PQ0KPUE1RXoNCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0NCg==", + "historyId": "190359", + "internalDate": "1652282485000" + } +} \ No newline at end of file diff --git a/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd5.json b/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd5.json index ab23db653..22bf3af27 100644 --- a/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd5.json +++ b/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd5.json @@ -4,7 +4,7 @@ "id": "180b3b8f20d6cbd5", "threadId": "180b3b5efbdc46da", "labelIds": [ - "INBOX" + "SENT" ], "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt iOS 0.2 Gmail Encryption Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAK+ZpNvOwRHDXhg/3BRx1bUjbq3HADZ+lzdlReVDB", "payload": { @@ -18,11 +18,11 @@ }, { "name": "To", - "value": "e2e.enterprise.test@flowcrypt.com" + "value": "Demo key 2 , Dmitry at FlowCrypt " }, { "name": "From", - "value": "dmitry@flowcrypt.com" + "value": "e2e.enterprise.test@flowcrypt.com" }, { "name": "Subject", diff --git a/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd6.json b/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd6.json deleted file mode 100644 index 259b0d994..000000000 --- a/appium/api-mocks/apis/google/exported-messages/message-export-180b3b8f20d6cbd6.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "acctEmail": "e2e.enterprise.test@flowcrypt.com", - "full": { - "id": "180b3b8f20d6cbd5", - "threadId": "180b3b5efbdc46da", - "labelIds": [ - "SENT" - ], - "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt iOS 0.2 Gmail Encryption Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAK+ZpNvOwRHDXhg/3BRx1bUjbq3HADZ+lzdlReVDB", - "payload": { - "partId": "", - "mimeType": "multipart/mixed", - "filename": "", - "headers": [ - { - "name": "Content-Type", - "value": "multipart/mixed; boundary=\"----sinikael-?=_1-16522826838370.6208943736908692\"" - }, - { - "name": "To", - "value": "Demo key 2 , Dmitry at FlowCrypt " - }, - { - "name": "From", - "value": "e2e.enterprise.test@flowcrypt.com" - }, - { - "name": "Subject", - "value": "new message for reply" - }, - { - "name": "Cc", - "value": "FlowCrypt Robot " - }, - { - "name": "In-Reply-To", - "value": "" - }, - { - "name": "References", - "value": "" - }, - { - "name": "Date", - "value": "Wed, 11 May 2022 08:24:44 -0700" - }, - { - "name": "MIME-Version", - "value": "1.0" - } - ], - "body": { - "size": 0 - }, - "parts": [ - { - "partId": "0", - "mimeType": "text/plain", - "filename": "", - "headers": [ - { - "name": "Content-Type", - "value": "text/plain" - }, - { - "name": "Content-Transfer-Encoding", - "value": "quoted-printable" - } - ], - "body": { - "size": 1698, - "data": "LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgaU9TIDAuMiBHbWFpbCBFbmNyeXB0aW9uDQpDb21tZW50OiBTZWFtbGVzc2x5IHNlbmQgYW5kIHJlY2VpdmUgZW5jcnlwdGVkIGVtYWlsDQoNCndWNERUMlpsU21oWjFHb1NBUWRBSytacE52T3dSSERYaGcvM0JSeDFiVWpicTNIQURaK2x6ZGxSZVZEQg0KSzE0d2FDUlNSZHkzS1ZYMW9CaEIvNHgrbEo4U1dOQzE1Vy9VTUFiUW5BcFNSQ1lGbWttamVXekNseG4yDQovK0x6dHhuYXdWNERaajQrcG9HN0taNFNBUWRBNTZjMEtmK2o3WGJXaXJaV1FzRUczaXlWMnU5djl4QUoNClhjRmROamZGSW1Nd2pNcWt0RkdNc3F3WmhKS0FueStIUitDTzF5SzJQcTJUZnBmWkJnOW9GQXdSLzd4eA0KdC93S280SzBCK3owZHdOdndjRk1BM2lFTy91Ym14dFpBUS8vWkZ4bTBPRG9qRTJrZG1JNjhTQVJydHAwDQpwblA0UUxKV2tOZDZQaDE5OEMwUEFESk9nb2NFQmVMWEdFeDJXUWlQMG5UcEZ0TnRaNDhzQ0NVNUw1T2QNCmhzSzBLdGxTTFBVRmdLSjBWbEl1K1FLOURxcU8yTU1lb2h2dGFxMGxkS2pETXh4bFVBS1krTnQxN2dwNw0KVmRvYmZ5UDRKZ2tEMnEzdGdpaGNLTDR6cWNMclQraExZay9lcEZlSWR3b2RDWUhtWDVhOVZHdDhobm1XDQpLa21PZ3Jra29ieEtWRVFPZVBEckF2am42RUN0eHZlK0lta2RkMEsvSXRnemQ5aG9XMkFhakl1MUVxdUMNCmdHblJHdGppbEFwNUZzcEpMbDRneHFvZHgrQ2lPeE1jdWhHS0EzZ3NlMFoxVjFYU2RDazFqUitkNUlhTA0KVmF1OHAvK2g2MGVkVmdtakRRYndEUEdBb1RoR0dWRmMvYlFiSVovMTNRZHF0aGFmN1RVeVJaWFUxUFZoDQpxVExxWDYxdXF6QktsZ3RFOXVaK1daWmdmSVdsRzdwanFQWjJ4c01sekVqTzRaT3hNNTZTYTlpWHA1UlINCkpHZ3l3Z002OU9HRkxPMzlMNlVRWXZzNjRmZnBlY1NnT0tmQlRRN3NUZ1pFWWErLzA2aGluWXBrT1RjeA0KTmpmZzVOcUttU1lwUzZ3YUhMVU14MmU3MDNxRHd6WDU4Qmh5SklyRi9rSVRwSzdjNVVPNXMrTk9ycjVYDQo4aHgzeTdIWDBrWkRvT1pwTFNjLzhHaTZLSCtDUFlkc2N4MFJZNEE5Y1g4MUNnd2lYQU90NFdrOFRXS00NCm4yZUpsdGxsNjZ5N0ZxZGhLZ0xDMVdqK21rWk0zVXY1eElDeW9FMmxRTVZNTk5zTmRiMHJjN1hOYmEvQg0KWGdQMzNZd3Z2cW1OZFJJQkIwRCtyRnZLTWc1ZEhiMW5IQWFjV0NhZ2VGekFERUM4a1NjVW5wa0hNQjVMDQpXREEwWlBEdzB0SFlDTk5JeWh0YWZrTFdjT2ZMNCtyNzhkRWhPemRXRm83eXJVQytUUmRxenNaNjkzeUsNCnpNMjhIc3JCWGdOVkRyekJRdUpyRHhJQkIwQU1zbkc2eERqYmIyNENwTzhuQ2tITGZXSTF0RUFGVTRydQ0KdWdkUWR3TWRDREJLUnJEalF3Y0twd3FIUVJac0ZMRzg5VGNWOVZvVEp2dUlscEFmQWxCQTRPMXI3cUxODQpZYXVrU28zU05hVmJDVUxTd0FVQlBuSkhEQlBHUSt6NHdsdTlLa1FzNjVtRmhMVjhxUGo2NnE2ViszOGcNCjFSMHNPeFVjRUd4QXBYanBQamRqSVhsWmd5RTlhSDYyaFk3SXpweXNMZUxLSHFlOFZNeVFUNEp1UElPNA0KNXBPdUJJV1I5K0hEbVVVbGJ5dHkvcUtrWUV3dVVuUXZUb1RaSUZLQXRheU51VEhxN1FlMXA3ZURhR2FvDQpLdFFHWUNSUkdyRlJZamsyb3BDMUJMWUVBS08yVFdKdUk4anFzdlhLVWdmRFNUdXZXdnlvS0RvbTQxTjQNCnYxZVFuN1JyejM1ejV5OG1yZ0MwMlhaclpSTHg2cWhtaFgyNGhPKzhUdz09DQo9UE8yTw0KLS0tLS1FTkQgUEdQIE1FU1NBR0UtLS0tLQ0K" - } - } - ] - }, - "sizeEstimate": 2613, - "historyId": "175344", - "internalDate": "1652282684000" - }, - "attachments": {}, - "raw": { - "id": "180b3b8f20d6cbd5", - "threadId": "180b3b5efbdc46da", - "labelIds": [ - "SENT" - ], - "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt iOS 0.2 Gmail Encryption Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAK+ZpNvOwRHDXhg/3BRx1bUjbq3HADZ+lzdlReVDB", - "sizeEstimate": 2613, - "raw": "UmVjZWl2ZWQ6IGZyb20gNjc5MzI2NzEzNDg3DQoJbmFtZWQgdW5rbm93bg0KCWJ5IGdtYWlsYXBpLmdvb2dsZS5jb20NCgl3aXRoIEhUVFBSRVNUOw0KCVdlZCwgMTEgTWF5IDIwMjIgMDg6MjQ6NDQgLTA3MDANCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KIGJvdW5kYXJ5PSItLS0tc2luaWthZWwtPz1fMS0xNjUyMjgyNjgzODM3MC42MjA4OTQzNzM2OTA4NjkyIg0KVG86IERlbW8ga2V5IDIgPGRlbW9AZmxvd2NyeXB0LmNvbT4sIERtaXRyeSBhdCBGbG93Q3J5cHQNCiA8ZG1pdHJ5QGZsb3djcnlwdC5jb20-DQpGcm9tOiBlMmUuZW50ZXJwcmlzZS50ZXN0QGZsb3djcnlwdC5jb20NClN1YmplY3Q6IG5ldyBtZXNzYWdlIGZvciByZXBseQ0KQ2M6IEZsb3dDcnlwdCBSb2JvdCA8cm9ib3RAZmxvd2NyeXB0LmNvbT4NCkluLVJlcGx5LVRvOg0KIDxDQUdBeXh2M0ZXMzZ0SGpPdHFPZzdIdjhoMGZXZ2Fodlgwb2tFRXJjQis5ZFV2ellzblFAbWFpbC5nbWFpbC5jb20-DQpSZWZlcmVuY2VzOg0KIDxDQUdBeXh2M0ZXMzZ0SGpPdHFPZzdIdjhoMGZXZ2Fodlgwb2tFRXJjQis5ZFV2ellzblFAbWFpbC5nbWFpbC5jb20-DQpEYXRlOiBXZWQsIDExIE1heSAyMDIyIDA4OjI0OjQ0IC0wNzAwDQpNZXNzYWdlLUlkOiA8Q0FHS2tKVVlYS3VvOGVxRXRwNFViQXZibkV3T3V0RTV1VStvc2phWktOdENDUVhLdEFRQG1haWwuZ21haWwuY29tPg0KTUlNRS1WZXJzaW9uOiAxLjANCg0KLS0tLS0tc2luaWthZWwtPz1fMS0xNjUyMjgyNjgzODM3MC42MjA4OTQzNzM2OTA4NjkyDQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUNCg0KLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgaU9TIDAuMiBHbWFpbCBFbmNyeXB0aW9uDQpDb21tZW50OiBTZWFtbGVzc2x5IHNlbmQgYW5kIHJlY2VpdmUgZW5jcnlwdGVkIGVtYWlsDQoNCndWNERUMlpsU21oWjFHb1NBUWRBSytacE52T3dSSERYaGcvM0JSeDFiVWpicTNIQURaK2x6ZGxSZVZEQg0KSzE0d2FDUlNSZHkzS1ZYMW9CaEIvNHgrbEo4U1dOQzE1Vy9VTUFiUW5BcFNSQ1lGbWttamVXekNseG4yDQovK0x6dHhuYXdWNERaajQrcG9HN0taNFNBUWRBNTZjMEtmK2o3WGJXaXJaV1FzRUczaXlWMnU5djl4QUoNClhjRmROamZGSW1Nd2pNcWt0RkdNc3F3WmhKS0FueStIUitDTzF5SzJQcTJUZnBmWkJnOW9GQXdSLzd4eA0KdC93S280SzBCK3owZHdOdndjRk1BM2lFTy91Ym14dFpBUS8vWkZ4bTBPRG9qRTJrZG1JNjhTQVJydHAwDQpwblA0UUxKV2tOZDZQaDE5OEMwUEFESk9nb2NFQmVMWEdFeDJXUWlQMG5UcEZ0TnRaNDhzQ0NVNUw1T2QNCmhzSzBLdGxTTFBVRmdLSjBWbEl1K1FLOURxcU8yTU1lb2h2dGFxMGxkS2pETXh4bFVBS1krTnQxN2dwNw0KVmRvYmZ5UDRKZ2tEMnEzdGdpaGNLTDR6cWNMclQraExZay9lcEZlSWR3b2RDWUhtWDVhOVZHdDhobm1XDQpLa21PZ3Jra29ieEtWRVFPZVBEckF2am42RUN0eHZlK0lta2RkMEsvSXRnemQ5aG9XMkFhakl1MUVxdUMNCmdHblJHdGppbEFwNUZzcEpMbDRneHFvZHgrQ2lPeE1jdWhHS0EzZ3NlMFoxVjFYU2RDazFqUitkNUlhTA0KVmF1OHAvK2g2MGVkVmdtakRRYndEUEdBb1RoR0dWRmMvYlFiSVovMTNRZHF0aGFmN1RVeVJaWFUxUFZoDQpxVExxWDYxdXF6QktsZ3RFOXVaK1daWmdmSVdsRzdwanFQWjJ4c01sekVqTzRaT3hNNTZTYTlpWHA1UlINCkpHZ3l3Z002OU9HRkxPMzlMNlVRWXZzNjRmZnBlY1NnT0tmQlRRN3NUZ1pFWWErLzA2aGluWXBrT1RjeA0KTmpmZzVOcUttU1lwUzZ3YUhMVU14MmU3MDNxRHd6WDU4Qmh5SklyRi9rSVRwSzdjNVVPNXMrTk9ycjVYDQo4aHgzeTdIWDBrWkRvT1pwTFNjLzhHaTZLSCtDUFlkc2N4MFJZNEE5Y1g4MUNnd2lYQU90NFdrOFRXS00NCm4yZUpsdGxsNjZ5N0ZxZGhLZ0xDMVdqK21rWk0zVXY1eElDeW9FMmxRTVZNTk5zTmRiMHJjN1hOYmEvQg0KWGdQMzNZd3Z2cW1OZFJJQkIwRCtyRnZLTWc1ZEhiMW5IQWFjV0NhZ2VGekFERUM4a1NjVW5wa0hNQjVMDQpXREEwWlBEdzB0SFlDTk5JeWh0YWZrTFdjT2ZMNCtyNzhkRWhPemRXRm83eXJVQytUUmRxenNaNjkzeUsNCnpNMjhIc3JCWGdOVkRyekJRdUpyRHhJQkIwQU1zbkc2eERqYmIyNENwTzhuQ2tITGZXSTF0RUFGVTRydQ0KdWdkUWR3TWRDREJLUnJEalF3Y0twd3FIUVJac0ZMRzg5VGNWOVZvVEp2dUlscEFmQWxCQTRPMXI3cUxODQpZYXVrU28zU05hVmJDVUxTd0FVQlBuSkhEQlBHUSt6NHdsdTlLa1FzNjVtRmhMVjhxUGo2NnE2ViszOGcNCjFSMHNPeFVjRUd4QXBYanBQamRqSVhsWmd5RTlhSDYyaFk3SXpweXNMZUxLSHFlOFZNeVFUNEp1UElPNA0KNXBPdUJJV1I5K0hEbVVVbGJ5dHkvcUtrWUV3dVVuUXZUb1RaSUZLQXRheU51VEhxN1FlMXA3ZURhR2FvDQpLdFFHWUNSUkdyRlJZamsyb3BDMUJMWUVBS08yVFdKdUk4anFzdlhLVWdmRFNUdXZXdnlvS0RvbTQxTjQNCnYxZVFuN1JyejM1ejV5OG1yZ0MwMlhaclpSTHg2cWhtaFgyNGhPKzhUdz0zRD0zRA0KPTNEUE8yTw0KLS0tLS1FTkQgUEdQIE1FU1NBR0UtLS0tLQ0KDQotLS0tLS1zaW5pa2FlbC0_PV8xLTE2NTIyODI2ODM4MzcwLjYyMDg5NDM3MzY5MDg2OTItLQ0K", - "historyId": "175344", - "internalDate": "1652282684000" - } -} \ No newline at end of file diff --git a/appium/tests/specs/mock/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts b/appium/tests/specs/mock/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts index 652f1234e..2f0f8c220 100644 --- a/appium/tests/specs/mock/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts +++ b/appium/tests/specs/mock/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts @@ -25,7 +25,7 @@ describe('INBOX: ', () => { const replySubject = `Re: ${emailSubject}`; const forwardSubject = `Fwd: ${emailSubject}`; - const quoteText = `${senderEmail} wrote:\n > ${emailText}`; + const quoteText = `${senderName} <${senderEmail}> wrote:\n > ${emailText}`; const mockApi = new MockApi(); From 235c24a34552302ad0a00c97a75e6e1d8c39143c Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 26 Sep 2022 15:26:21 +0300 Subject: [PATCH 23/56] replace InboxRenderable with InboxItem --- FlowCrypt.xcodeproj/project.pbxproj | 8 +- .../Compose/ComposeViewController.swift | 1 + .../ComposeViewController+MessageSend.swift | 2 +- .../ComposeViewController+TapActions.swift | 2 +- FlowCrypt/Controllers/Inbox/InboxItem.swift | 160 +++++++++++++++ .../Controllers/Inbox/InboxProviders.swift | 26 ++- .../Controllers/Inbox/InboxRenderable.swift | 153 -------------- .../Inbox/InboxViewController.swift | 193 +++++++++--------- .../Inbox/InboxViewDecorator.swift | 6 +- .../Controllers/Threads/MessageAction.swift | 2 +- .../Threads/MessageActionsHandler.swift | 10 +- .../Controllers/Threads/MessageThread.swift | 46 ----- .../Threads/ThreadDetailsViewController.swift | 58 +++--- .../Gmail+MessageOperations.swift | 11 +- .../Model/MessageIdentifier.swift | 4 +- .../Threads/Imap+ThreadOperations.swift | 18 +- .../MessagesThreadOperationsProvider.swift | 62 +++--- 17 files changed, 369 insertions(+), 393 deletions(-) create mode 100644 FlowCrypt/Controllers/Inbox/InboxItem.swift delete mode 100644 FlowCrypt/Controllers/Inbox/InboxRenderable.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 30ac851d1..02557681d 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ 9F7E8F19269C538E0021C07F /* NavigationChildController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7E8F18269C538E0021C07F /* NavigationChildController.swift */; }; 9F7E903926A1AD7A0021C07F /* KeyDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7E903826A1AD7A0021C07F /* KeyDetailsTests.swift */; }; 9F7ECCA7272C3FB4008A1770 /* TextImageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7ECCA6272C3FB4008A1770 /* TextImageNode.swift */; }; - 9F7ECCA9272C47DB008A1770 /* InboxRenderable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7ECCA8272C47DA008A1770 /* InboxRenderable.swift */; }; + 9F7ECCA9272C47DB008A1770 /* InboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7ECCA8272C47DA008A1770 /* InboxItem.swift */; }; 9F82D352256D74FA0069A702 /* InboxViewContainerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F82D351256D74FA0069A702 /* InboxViewContainerController.swift */; }; 9F883912271F242900669B56 /* ThreadDetailsDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F883911271F242900669B56 /* ThreadDetailsDecorator.swift */; }; 9F8839142721EB5000669B56 /* MessageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8839132721EB5000669B56 /* MessageAction.swift */; }; @@ -715,7 +715,7 @@ 9F7E8F18269C538E0021C07F /* NavigationChildController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationChildController.swift; sourceTree = ""; }; 9F7E903826A1AD7A0021C07F /* KeyDetailsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailsTests.swift; sourceTree = ""; }; 9F7ECCA6272C3FB4008A1770 /* TextImageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextImageNode.swift; sourceTree = ""; }; - 9F7ECCA8272C47DA008A1770 /* InboxRenderable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxRenderable.swift; sourceTree = ""; }; + 9F7ECCA8272C47DA008A1770 /* InboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxItem.swift; sourceTree = ""; }; 9F8220D426336626004B2009 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 9F8277952373732000E19C07 /* UIImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = ""; }; 9F82779D23737E3800E19C07 /* MessageTextSubjectNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTextSubjectNode.swift; sourceTree = ""; }; @@ -1944,7 +1944,7 @@ C132B9D71EC30E0B00763715 /* Inbox */ = { isa = PBXGroup; children = ( - 9F7ECCA8272C47DA008A1770 /* InboxRenderable.swift */, + 9F7ECCA8272C47DA008A1770 /* InboxItem.swift */, C132B9D81EC30E1D00763715 /* InboxViewController.swift */, 9FAFD75A2713880300321FA4 /* InboxViewController+Factory.swift */, 9FAFD7582713870800321FA4 /* InboxViewController+State.swift */, @@ -2831,7 +2831,7 @@ 215897E8267A553300423694 /* FilesManager.swift in Sources */, 9F5C2A77257D705100DE9B4B /* MessageLabel.swift in Sources */, 9F93623F2573D16F0009912F /* Gmail+Message.swift in Sources */, - 9F7ECCA9272C47DB008A1770 /* InboxRenderable.swift in Sources */, + 9F7ECCA9272C47DB008A1770 /* InboxItem.swift in Sources */, 042B140227F596C70018BDC4 /* ComposeRecipientPopupViewController.swift in Sources */, 9FC4112E2595EA8B001180A8 /* Gmail+Search.swift in Sources */, 049E606327FDB9C70089EE2A /* ComposeViewController+Keyboard.swift in Sources */, diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index a2612a6f5..760ff0f7e 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -260,6 +260,7 @@ final class ComposeViewController: TableNodeViewController { extension ComposeViewController: FilesManagerPresenter {} /* + - show empty view as inbox table header - reload drafts list when going back from compose or thread screen - check drafts for forward and reply all */ diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index 6fea68351..c00e300a3 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -40,7 +40,7 @@ extension ComposeViewController { let messageIdentifier = MessageIdentifier( draftId: input.type.info?.id, - threadId: input.threadId, + threadId: Identifier(stringId: input.threadId), messageId: identifier ) handleAction?(.sent(messageIdentifier)) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index 8ab8c43f0..bd320f265 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -45,7 +45,7 @@ extension ComposeViewController { if let messageId = messageId { let identifier = MessageIdentifier( - threadId: self.input.type.info?.threadId, + threadId: Identifier(stringId: self.input.type.info?.threadId), messageId: messageId ) self.handleAction?(.delete(identifier)) diff --git a/FlowCrypt/Controllers/Inbox/InboxItem.swift b/FlowCrypt/Controllers/Inbox/InboxItem.swift new file mode 100644 index 000000000..459c3312f --- /dev/null +++ b/FlowCrypt/Controllers/Inbox/InboxItem.swift @@ -0,0 +1,160 @@ +// +// InboxItem.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 11.10.2021 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import UIKit + +struct InboxItem: Equatable { + var messages: [Message] + let folderPath: String + let type: InboxItemType + + enum InboxItemType: Equatable { + case message(Identifier), thread(Identifier) + } +} + +extension InboxItem { + var threadId: String? { + switch type { + case .thread(let id): + return id.stringId + case .message: + return nil + } + } + var subject: String? { + messages + .compactMap(\.subject) + .first(where: { $0.isNotEmpty }) + } + + var labels: Set { + Set(messages.flatMap(\.labels)) + } + + var isInbox: Bool { + labels.contains(.inbox) + } + + var isDraft: Bool { + messages.count == 1 && labels.contains(.draft) + } + + var shouldShowMoveToInboxButton: Bool { + guard let firstMessageLabels = messages.first?.labels else { + return false + } + // Thread is treated as archived when labels don't contain `inbox` and first message label doesn't contain sent label + // https://github.com/FlowCrypt/flowcrypt-ios/pull/1769#discussion_r931874353 + return !isInbox && !firstMessageLabels.contains(.sent) + } + + var isRead: Bool { + !messages.contains(where: { !$0.isRead }) + } + + var subtitle: String { + if let subject = subject, subject.hasContent { + return subject + } else { + return "message_missing_subject".localized + } + } + + var date: Date { + latestMessageDate(with: folderPath) + } + + var dateString: String { + DateFormatter().formatDate(date) + } + + var title: NSAttributedString { + let style: NSAttributedString.Style = isRead + ? .regular(17) + : .bold(17) + + let textColor: UIColor = isRead + ? .lightGray + : .mainTextUnreadColor + + if folderPath == MessageLabel.sent.value || folderPath == MessageLabel.draft.value { + let recipients = messages + .flatMap(\.allRecipients) + .map(\.shortName) + .unique() + .joined(separator: ", ") + return "To: \(recipients)".attributed(style, color: textColor) + } else { + let hasDrafts = messages.contains(where: { $0.isDraft }) + let senderNames = messages + .compactMap(\.sender?.shortName) + .unique() + .joined(separator: ",") + .attributed(style, color: textColor) + + if hasDrafts { + let draftLabel = "compose_draft".localized.attributed(style, color: .red.withAlphaComponent(0.65)) + let title = senderNames.mutable() + title.append(",".attributed(style, color: textColor)) + title.append(draftLabel) + return title + } else { + return senderNames + } + } + } + + var badge: String? { + guard isInbox, folderPath.isEmpty || folderPath == MessageLabel.draft.value + else { + return nil + } + + return "folder_all_inbox".localized.lowercased() + } + + func messages(with label: String?) -> [Message] { + guard let label = label else { return messages } + + let messageLabel = MessageLabel(gmailLabel: label) + return messages.filter { $0.labels.contains(messageLabel) } + } + + func latestMessageDate(with label: String?) -> Date { + messages(with: label).map(\.date).max() ?? .distantPast + } +} + +extension InboxItem { + init(message: Message) { + self.messages = [message] + self.folderPath = "" + self.type = .message(message.identifier) + } + + init(thread: MessageThread, folderPath: String?) { + self.messages = thread.messages + self.folderPath = folderPath ?? "" + self.type = .thread(Identifier(stringId: thread.identifier)) + } + + mutating func update(labelsToAdd: [MessageLabel] = [], labelsToRemove: [MessageLabel] = []) { + for index in messages.indices { + messages[index].update(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) + } + } + + mutating func markAsRead(_ isRead: Bool) { + if isRead { + update(labelsToRemove: [.unread, .none]) + } else { + update(labelsToAdd: [.unread, .none]) + } + } +} diff --git a/FlowCrypt/Controllers/Inbox/InboxProviders.swift b/FlowCrypt/Controllers/Inbox/InboxProviders.swift index 84882aa9d..93aeb3ccc 100644 --- a/FlowCrypt/Controllers/Inbox/InboxProviders.swift +++ b/FlowCrypt/Controllers/Inbox/InboxProviders.swift @@ -9,12 +9,12 @@ import Foundation struct InboxContext { - let data: [InboxRenderable] + let data: [InboxItem] let pagination: MessagesListPagination } protocol InboxDataProvider { - func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxRenderable? + func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxItem? func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext } @@ -26,25 +26,29 @@ class InboxMessageThreadsProvider: InboxDataProvider { self.provider = provider } - func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxRenderable? { + func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxItem? { guard let id = identifier.stringId else { return nil } let thread = try await provider.fetchThread(identifier: id, path: path) - return InboxRenderable(thread: thread, folderPath: path) + return InboxItem( + messages: thread.messages, + folderPath: path, + type: .thread(identifier) + ) } func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { let result = try await provider.fetchThreads(using: context) let inboxData = result.threads - .sorted(by: { - $0.latestMessageDate(with: context.folderPath) > $1.latestMessageDate(with: context.folderPath) - }) .map { - InboxRenderable( + InboxItem( thread: $0, folderPath: context.folderPath ) } + .sorted(by: { + $0.latestMessageDate(with: context.folderPath) > $1.latestMessageDate(with: context.folderPath) + }) let inboxContext = InboxContext( data: inboxData, @@ -63,15 +67,15 @@ class InboxMessageListProvider: InboxDataProvider { self.provider = provider } - func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxRenderable? { + func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxItem? { let message = try await provider.fetchMessage(id: identifier, folder: path) - return InboxRenderable(message: message) + return InboxItem(message: message) } func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { let result = try await provider.fetchMessages(using: context) - let inboxData = result.messages.map(InboxRenderable.init) + let inboxData = result.messages.map(InboxItem.init) let inboxContext = InboxContext( data: inboxData, diff --git a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift deleted file mode 100644 index 690ee7c3a..000000000 --- a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// InboxRenderable.swift -// FlowCrypt -// -// Created by Anton Kharchevskyi on 11.10.2021 -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import UIKit - -struct InboxRenderable: Equatable { - enum WrappedType: Equatable { - case message(Message) - case thread(MessageThread) - } - - let title: NSAttributedString - let messageCount: Int - let subtitle: String - let dateString: String - var badge: String? - var isRead: Bool - - let date: Date - - var wrappedType: WrappedType - - private let folderPath: String? -} - -extension InboxRenderable { - var wrappedMessage: Message? { - guard case .message(let message) = wrappedType else { - return nil - } - return message - } - var wrappedThread: MessageThread? { - guard case .thread(let thread) = wrappedType else { - return nil - } - return thread - } -} - -extension InboxRenderable { - - init(message: Message) { - self.title = Self.messageTitle(for: message) - self.messageCount = 1 - if let subject = message.subject, subject.hasContent { - self.subtitle = subject - } else { - self.subtitle = "message_missing_subject".localized - } - - self.dateString = DateFormatter().formatDate(message.date) - self.isRead = message.isRead - self.date = message.date - self.wrappedType = .message(message) - self.badge = nil - self.folderPath = nil - } - - init(thread: MessageThread, folderPath: String?) { - - self.title = Self.messageTitle(for: thread, folderPath: folderPath, isRead: thread.isRead) - - self.messageCount = thread.messages.count - self.subtitle = thread.subject ?? "message_missing_subject".localized - self.isRead = thread.isRead - self.date = thread.latestMessageDate(with: folderPath) - self.dateString = DateFormatter().formatDate(date) - self.wrappedType = .thread(thread) - self.folderPath = folderPath - - self.updateBadge() - } - - private static func messageTitle(for thread: MessageThread, folderPath: String?, isRead: Bool) -> NSAttributedString { - let style: NSAttributedString.Style = isRead - ? .regular(17) - : .bold(17) - - let textColor: UIColor = isRead - ? .lightGray - : .mainTextUnreadColor - - if folderPath == MessageLabel.sent.value || folderPath == MessageLabel.draft.value { - let recipients = thread.messages - .flatMap(\.allRecipients) - .map(\.shortName) - .unique() - .joined(separator: ", ") - return "To: \(recipients)".attributed(style, color: textColor) - } else { - let hasDrafts = thread.messages.contains(where: { $0.isDraft }) - let senderNames = thread.messages - .compactMap(\.sender?.shortName) - .unique() - .joined(separator: ",") - .attributed(style, color: textColor) - - if hasDrafts { - let draftLabel = "compose_draft".localized.attributed(style, color: .red.withAlphaComponent(0.65)) - let title = senderNames.mutable() - title.append(",".attributed(style, color: textColor)) - title.append(draftLabel) - return title - } else { - return senderNames - } - } - } - - private static func messageTitle(for message: Message) -> NSAttributedString { - if message.labels.contains(.draft) { - let recipients = message.allRecipients.map(\.shortName).joined(separator: ", ") - let title = recipients.isEmpty ? "" : "To: \(recipients)" - return title.attributed(.regular(17), color: .lightGray) - } else { - let title = message.sender?.shortName ?? "message_unknown_sender".localized - return title.attributed() - } - } - - mutating func updateMessage(labelsToAdd: [MessageLabel], labelsToRemove: [MessageLabel]) { - switch wrappedType { - case .thread(var thread): - thread.update(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) - wrappedType = .thread(thread) - case .message(var message): - message.update(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) - wrappedType = .message(message) - } - - updateBadge() - } - - mutating func updateBadge() { - // show 'inbox' badge in 'All Mail' and 'Drafts' folders - switch wrappedType { - case .thread(let thread): - guard thread.isInbox, - folderPath.isEmptyOrNil || folderPath == MessageLabel.draft.value - else { self.badge = nil; return } - - self.badge = "folder_all_inbox".localized.lowercased() - case .message: - self.badge = nil - } - } -} diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 628fc7b07..32b202704 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -24,13 +24,13 @@ class InboxViewController: ViewController { private let inboxDataProvider: InboxDataProvider private let viewModel: InboxViewModel - private var inboxInput: [InboxRenderable] = [] + private var inboxInput: [InboxItem] = [] var state: InboxViewController.State = .idle private var inboxTitle: String { viewModel.folderName.isEmpty ? "Inbox" : viewModel.folderName } private var shouldShowEmptyView: Bool { - inboxInput.isNotEmpty && (viewModel.path == "SPAM" || viewModel.path == "TRASH") + inboxInput.isNotEmpty && (["SPAM", "TRASH"].contains(viewModel.path)) } var path: String { viewModel.path } @@ -431,12 +431,13 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { let rowNumber = shouldShowEmptyView ? indexPath.row - 1 : indexPath.row - guard let message = inboxInput[safe: rowNumber] else { + guard let inboxItem = inboxInput[safe: rowNumber] else { return } tableNode.deselectRow(at: indexPath, animated: true) - open(message: message, path: viewModel.path) + + open(inboxItem: inboxItem, path: viewModel.path) } private func cellNode(for indexPath: IndexPath, and size: CGSize) -> ASCellNodeBlock { @@ -522,13 +523,12 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { } } -// MARK: - MsgListViewController extension InboxViewController { - func getUpdatedIndex(for message: InboxRenderable) -> Int? { + func getUpdatedIndex(for inboxItem: InboxItem) -> Int? { let index = inboxInput.firstIndex(where: { - $0.title == message.title && $0.subtitle == message.subtitle && $0.wrappedType == message.wrappedType + $0.title == inboxItem.title && $0.subtitle == inboxItem.subtitle && $0.type == inboxItem.type }) - logger.logInfo("Try to update message at \(String(describing: index))") + logger.logInfo("Try to update inbox item at \(String(describing: index))") return index } @@ -536,18 +536,9 @@ extension InboxViewController { guard inboxInput.count > index else { return } logger.logInfo("Mark as read \(isRead) at \(index)") - inboxInput[index].isRead = isRead // Mark wrapped message/thread(all mails in thread) as read/unread - if var wrappedThread = inboxInput[index].wrappedThread { - for i in 0 ..< wrappedThread.messages.count { - wrappedThread.messages[i].markAsRead(isRead) - } - inboxInput[index].wrappedType = .thread(wrappedThread) - } else if var wrappedMessage = inboxInput[index].wrappedMessage { - wrappedMessage.markAsRead(isRead) - inboxInput[index].wrappedType = .message(wrappedMessage) - } + inboxInput[index].markAsRead(isRead) let animationDuration = 0.3 DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { [weak self] in @@ -558,7 +549,7 @@ extension InboxViewController { func updateMessage(labelsToAdd: [MessageLabel], labelsToRemove: [MessageLabel], at index: Int) { guard inboxInput.count > index else { return } - inboxInput[index].updateMessage(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) + inboxInput[index].update(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) let animationDuration = 0.3 DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { [weak self] in @@ -604,19 +595,31 @@ extension InboxViewController { } } - func open(message: InboxRenderable, path: String) { - switch message.wrappedType { - case .message(let message): - if message.isDraft { - open(draft: message, appContext: appContext) - } else { - open(message: message, path: path, appContext: appContext) - } - case .thread(let thread): - if let message = thread.messages.first, thread.messages.count == 1, message.isDraft { - open(draft: message, appContext: appContext) - } else { - open(thread: thread, appContext: appContext) + func open(inboxItem: InboxItem, path: String) { + if inboxItem.isDraft, let draft = inboxItem.messages.first { + open(draft: draft, appContext: appContext) + } else { + Task { + do { + let viewController = try await ThreadDetailsViewController( + appContext: appContext, + inboxItem: inboxItem, + onComposeMessageAction: { [weak self] action in + guard let self = self else { return } + + switch action { + case .update(let identifier), .sent(let identifier), .delete(let identifier): + self.fetchUpdatedInboxItem(identifier: identifier) + } + }, + completion: { [weak self] action, message in + self?.handleMessageOperation(message: message, action: action) + } + ) + navigationController?.pushViewController(viewController, animated: true) + } catch { + showAlert(message: error.errorMessage) + } } } } @@ -635,26 +638,33 @@ extension InboxViewController { handleAction: { [weak self] action in guard let self = self else { return } - switch action { - case .update(let identifier): - // todo - break - case .sent(let identifier): - // todo - break - case .delete(let identifier): - guard let index = self.inboxInput.firstIndex(where: { - if let threadId = $0.wrappedThread?.identifier { - return threadId == identifier.threadId - } else if let messageId = $0.wrappedMessage?.identifier { - return messageId == identifier.messageId - } - return false - }) else { return } - - self.inboxInput.remove(at: index) - self.tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) - } +// switch action { +// case .update(let identifier): +// // todo +// break +// case .sent(let identifier): +// guard let index = self.inboxInput.firstIndex(where: { +// if let threadId = $0.wrappedThread?.identifier { +// return threadId == identifier.threadId?.stringId +// } else if let messageId = $0.wrappedMessage?.identifier { +// return messageId == identifier.messageId +// } +// return false +// }) else { return } +// +// case .delete(let identifier): +// guard let index = self.inboxInput.firstIndex(where: { +// if let threadId = $0.wrappedThread?.identifier { +// return threadId == identifier.threadId?.stringId +// } else if let messageId = $0.wrappedMessage?.identifier { +// return messageId == identifier.messageId +// } +// return false +// }) else { return } +// +// self.inboxInput.remove(at: index) +// self.tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) +// } } ) navigationController?.pushViewController(controller, animated: true) @@ -664,57 +674,37 @@ extension InboxViewController { } } - private func open(message: Message, path: String, appContext: AppContextWithUser) { - let thread = MessageThread( - identifier: message.threadId, - snippet: nil, - path: path, - messages: [message] - ) - open(thread: thread, appContext: appContext) - } + private func fetchUpdatedInboxItem(identifier: MessageIdentifier) { + guard let index = inboxInput.firstIndex(where: { + switch $0.type { + case .thread(let threadId): + return threadId == identifier.threadId + case .message(let messageId): + return messageId == identifier.messageId + } + }) else { return } - private func open(thread: MessageThread, appContext: AppContextWithUser) { Task { - do { - let viewController = try await ThreadDetailsViewController( - appContext: appContext, - thread: thread, - onComposeMessageAction: { [weak self] action in - guard let self = self else { return } - - switch action { - case .update(let identifier), .sent(let identifier): - if let threadId = identifier.threadId { - if let index = self.inboxInput.firstIndex(where: { $0.wrappedThread?.identifier == threadId }) { - Task { - if let inboxItem = try await self.inboxDataProvider.fetchInboxItem( - identifier: Identifier(stringId: threadId), - path: self.path - ) { - self.inboxInput[index] = inboxItem - self.tableNode.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) - } - } - } - } - case .delete(let identifier): - print(identifier) - } - }, - completion: { [weak self] action, message in - self?.handleMessageOperation(message: message, action: action) - } - ) - navigationController?.pushViewController(viewController, animated: true) - } catch { - showAlert(message: error.errorMessage) + switch inboxInput[index].type { + case .thread(let threadId): + guard let inboxItem = try await inboxDataProvider.fetchInboxItem(identifier: threadId, path: path) + else { return } + + if inboxItem.messages(with: path).isEmpty { + self.inboxInput.remove(at: index) + self.tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + } else { + self.inboxInput[index] = inboxItem + self.tableNode.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + } + case .message(let messageId): + break } } } // MARK: Operation - private func handleMessageOperation(message: InboxRenderable, action: MessageAction) { + private func handleMessageOperation(message: InboxItem, action: MessageAction) { guard let indexToUpdate = getUpdatedIndex(for: message) else { return } @@ -737,3 +727,14 @@ extension InboxViewController { } } } + +/* +open draft: + - update + - fetch updated thread + - sent + - fetch updated thread and check if drafts there + - delete + - fetch updated thread + + */ diff --git a/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift b/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift index 6ca316d3c..8e1b31a6f 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift @@ -10,7 +10,7 @@ import FlowCryptUI import UIKit extension InboxCellNode.Input { - init(_ element: InboxRenderable) { + init(_ element: InboxItem) { let email = element.title let date = element.dateString let msg = element.subtitle @@ -32,8 +32,8 @@ extension InboxCellNode.Input { self.init( emailText: email, countText: { - guard element.messageCount > 1 else { return nil } - let count = element.messageCount > 99 ? "99+" : String(element.messageCount) + guard element.messages.count > 1 else { return nil } + let count = element.messages.count > 99 ? "99+" : String(element.messages.count) return NSAttributedString.text(from: "(\(count))", style: style, color: textColor) }(), dateText: NSAttributedString.text(from: date, style: style, color: dateColor), diff --git a/FlowCrypt/Controllers/Threads/MessageAction.swift b/FlowCrypt/Controllers/Threads/MessageAction.swift index 684aab5e5..bf424cc12 100644 --- a/FlowCrypt/Controllers/Threads/MessageAction.swift +++ b/FlowCrypt/Controllers/Threads/MessageAction.swift @@ -8,7 +8,7 @@ import Foundation -typealias MessageActionCompletion = (MessageAction, InboxRenderable) -> Void +typealias MessageActionCompletion = (MessageAction, InboxItem) -> Void enum MessageAction: Equatable { case moveToTrash, moveToInbox, archive, markAsRead(Bool), permanentlyDelete diff --git a/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift b/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift index 983a3bb10..a6fd6d95e 100644 --- a/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift +++ b/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift @@ -30,11 +30,11 @@ extension MessageActionsHandler where Self: UIViewController { Logger.nested("MessageActions") } - func setupNavigationBar(thread: MessageThread) { + func setupNavigationBar(inboxItem: InboxItem) { Task { do { let path = try await trashFolderProvider.trashFolderPath - setupNavigationBarItems(thread: thread, trashFolderPath: path) + setupNavigationBarItems(inboxItem: inboxItem, trashFolderPath: path) } catch { // todo - handle? logger.logError("setupNavigationBar: \(error)") @@ -42,7 +42,7 @@ extension MessageActionsHandler where Self: UIViewController { } } - private func setupNavigationBarItems(thread: MessageThread, trashFolderPath: String?) { + private func setupNavigationBarItems(inboxItem: InboxItem, trashFolderPath: String?) { logger.logInfo("setup navigation bar with \(trashFolderPath ?? "N/A")") logger.logInfo("currentFolderPath \(currentFolderPath)") @@ -92,10 +92,10 @@ extension MessageActionsHandler where Self: UIViewController { default: // in any other folders items = [helpButton, trashButton, unreadButton] - if thread.isInbox { + if inboxItem.isInbox { logger.logInfo("inbox - helpButton, archiveButton, trashButton, unreadButton") items.insert(archiveButton, at: 1) - } else if thread.shouldShowMoveToInboxButton { + } else if inboxItem.shouldShowMoveToInboxButton { logger.logInfo("archive - helpButton, moveToInboxButton, trashButton, unreadButton") items.insert(moveToInboxButton, at: 1) } else { diff --git a/FlowCrypt/Controllers/Threads/MessageThread.swift b/FlowCrypt/Controllers/Threads/MessageThread.swift index ee1698e2c..5a4fdf856 100644 --- a/FlowCrypt/Controllers/Threads/MessageThread.swift +++ b/FlowCrypt/Controllers/Threads/MessageThread.swift @@ -18,50 +18,4 @@ struct MessageThread: Equatable { let snippet: String? let path: String var messages: [Message] - - var subject: String? { - messages - .compactMap(\.subject) - .first(where: { $0.isNotEmpty }) - } - - var labels: Set { - Set(messages.flatMap(\.labels)) - } - - var isInbox: Bool { - labels.contains(.inbox) - } - - var shouldShowMoveToInboxButton: Bool { - guard let firstMessageLabels = messages.first?.labels else { - return false - } - // Thread is treated as archived when labels don't contain `inbox` and first message label doesn't contain sent label - // https://github.com/FlowCrypt/flowcrypt-ios/pull/1769#discussion_r931874353 - return !isInbox && !firstMessageLabels.contains(.sent) - } - - var isRead: Bool { - !messages.contains(where: { !$0.isRead }) - } - - private func messages(with label: String?) -> [Message] { - guard let label = label else { return messages } - - let messageLabel = MessageLabel(gmailLabel: label) - return messages.filter { $0.labels.contains(messageLabel) } - } - - func latestMessageDate(with label: String?) -> Date { - messages(with: label).map(\.date).max() ?? .distantPast - } -} - -extension MessageThread { - mutating func update(labelsToAdd: [MessageLabel], labelsToRemove: [MessageLabel]) { - for index in messages.indices { - messages[index].update(labelsToAdd: labelsToAdd, labelsToRemove: labelsToRemove) - } - } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 482841ef8..65056faab 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -43,12 +43,12 @@ final class ThreadDetailsViewController: TableNodeViewController { private let messageService: MessageService private let messageOperationsProvider: MessageOperationsProvider private let threadOperationsProvider: MessagesThreadOperationsProvider - private var thread: MessageThread + private var inboxItem: InboxItem private var input: [ThreadDetailsViewController.Input] let trashFolderProvider: TrashFolderProviderType var currentFolderPath: String { - thread.path + inboxItem.folderPath } private let onComposeMessageAction: ((ComposeMessageAction) -> Void)? private let onComplete: MessageActionCompletion @@ -56,7 +56,7 @@ final class ThreadDetailsViewController: TableNodeViewController { init( appContext: AppContextWithUser, messageService: MessageService? = nil, - thread: MessageThread, + inboxItem: InboxItem, onComposeMessageAction: ((ComposeMessageAction) -> Void)?, completion: @escaping MessageActionCompletion ) async throws { @@ -83,10 +83,10 @@ final class ThreadDetailsViewController: TableNodeViewController { remoteFoldersProvider: try mailProvider.remoteFoldersProvider ) ) - self.thread = thread + self.inboxItem = inboxItem self.onComposeMessageAction = onComposeMessageAction self.onComplete = completion - self.input = thread.messages + self.input = inboxItem.messages .sorted(by: >) .map { Input(message: $0) } @@ -103,16 +103,16 @@ final class ThreadDetailsViewController: TableNodeViewController { node.delegate = self node.dataSource = self - setupNavigationBar(thread: thread) + setupNavigationBar(inboxItem: inboxItem) expandThreadMessageAndMarkAsRead() } private func expandThreadMessageAndMarkAsRead() { Task { - try await threadOperationsProvider.mark(thread: thread, asRead: true, in: currentFolderPath) + try await threadOperationsProvider.mark(id: inboxItem.threadId, asRead: true, in: inboxItem.folderPath) } let indexOfSectionToExpand = input.firstIndex(where: { !$0.rawMessage.isRead }) - ?? input.firstIndex(where: { !$0.rawMessage.isRead && !$0.rawMessage.isDraft }) + ?? input.lastIndex(where: { !$0.rawMessage.isDraft }) ?? input.count - 1 let indexPath = IndexPath(row: 0, section: indexOfSectionToExpand + 1) handleExpandTap(at: indexPath) @@ -218,7 +218,7 @@ extension ThreadDetailsViewController { let processedMessage = try await messageService.getAndProcess( identifier: messageId, - folder: thread.path, + folder: inboxItem.folderPath, onlyLocalKeys: false, userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager @@ -243,7 +243,7 @@ extension ThreadDetailsViewController { guard let identifier = messageIdentifier.messageId else { return } // todo - throw let processedMessage = try await messageService.getAndProcess( identifier: identifier, - folder: thread.path, + folder: inboxItem.folderPath, onlyLocalKeys: false, userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager @@ -420,7 +420,7 @@ extension ThreadDetailsViewController { do { var processedMessage = try await messageService.getAndProcess( identifier: message.identifier, - folder: thread.path, + folder: inboxItem.folderPath, onlyLocalKeys: true, userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager @@ -430,7 +430,7 @@ extension ThreadDetailsViewController { processedMessage.signature = .pending retryVerifyingSignatureWithRemotelyFetchedKeys( message: message, - folder: thread.path, + folder: inboxItem.folderPath, indexPath: indexPath ) } @@ -488,7 +488,7 @@ extension ThreadDetailsViewController { if let someError = error as NSError?, someError.code == Imap.Err.fetch.rawValue { // todo - the missing msg should be removed from the list in inbox view // reproduce: 1) load inbox 2) move msg to trash on another email client 3) open trashed message in inbox - showToast("Message not found in folder: \(thread.path)") + showToast("Message not found in folder: \(inboxItem.folderPath)") } else { showRetryAlert(message: error.errorMessage, onRetry: { [weak self] _ in self?.fetchDecryptAndRenderMsg(at: indexPath) @@ -596,7 +596,7 @@ extension ThreadDetailsViewController { do { let processedMessage = try await messageService.getAndProcess( identifier: message.identifier, - folder: thread.path, + folder: inboxItem.folderPath, onlyLocalKeys: false, userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager @@ -632,7 +632,7 @@ extension ThreadDetailsViewController: MessageActionsHandler { onComplete( action, - .init(thread: thread, folderPath: currentFolderPath) + inboxItem ) navigationController?.popViewController(animated: true) @@ -671,18 +671,18 @@ extension ThreadDetailsViewController: MessageActionsHandler { switch action { case .archive: - try await threadOperationsProvider.archive(thread: thread, in: currentFolderPath) + try await threadOperationsProvider.archive(messages: inboxItem.messages, in: inboxItem.folderPath) case .markAsRead(let isRead): guard !isRead else { return } Task { // Run mark as unread operation in another thread - try await threadOperationsProvider.mark(thread: thread, asRead: false, in: currentFolderPath) + try await threadOperationsProvider.mark(id: inboxItem.threadId, asRead: false, in: inboxItem.folderPath) } case .moveToTrash: - try await threadOperationsProvider.moveThreadToTrash(thread: thread) + try await threadOperationsProvider.moveThreadToTrash(id: inboxItem.threadId, labels: inboxItem.labels) case .moveToInbox: - try await threadOperationsProvider.moveThreadToInbox(thread: thread) + try await threadOperationsProvider.moveThreadToInbox(id: inboxItem.threadId) case .permanentlyDelete: - try await threadOperationsProvider.delete(thread: thread) + try await threadOperationsProvider.delete(id: inboxItem.threadId) } handle(action: action) @@ -712,7 +712,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { guard let self = self else { return ASCellNode() } guard indexPath.section > 0 else { - let subject = self.thread.subject ?? "no subject" + let subject = self.inboxItem.subject ?? "no subject" return MessageSubjectNode(subject.attributed(.medium(18))) } @@ -805,16 +805,22 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { actionButtonTitle: "delete".localized, actionStyle: .destructive, onAction: { [weak self] _ in + guard let self = self else { return } Task { - try await self?.messageOperationsProvider.deleteMessage( + try await self.messageOperationsProvider.deleteMessage( id: id, from: nil ) - guard let index = self?.input.firstIndex(where: { $0.rawMessage.identifier == id }) else { return } + let messageIdentifier = MessageIdentifier( + threadId: Identifier(stringId: self.inboxItem.threadId) + ) + self.onComposeMessageAction?(.delete(messageIdentifier)) + + guard let index = self.input.firstIndex(where: { $0.rawMessage.identifier == id }) else { return } - self?.input.remove(at: index) - self?.node.deleteSections([index + 1], with: .automatic) + self.input.remove(at: index) + self.node.deleteSections([index + 1], with: .automatic) } } ) @@ -836,7 +842,7 @@ extension ThreadDetailsViewController: NavigationChildController { logger.logInfo("Back button. Messages are all read") onComplete( .markAsRead(true), - .init(thread: thread, folderPath: currentFolderPath) + inboxItem ) navigationController?.popViewController(animated: true) } diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift index f8282a878..5d20a53be 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift @@ -89,20 +89,13 @@ extension GmailService: MessageOperationsProvider { ) } - func archiveBatchMessages(messages: [Message]) async throws { - try await batchUpdate( - messages: messages, - labelsToRemove: [.inbox] - ) - } - - private func batchUpdate( + func batchUpdate( messages: [Message], labelsToAdd: [MessageLabel] = [], labelsToRemove: [MessageLabel] = [] ) async throws { let request = GTLRGmail_BatchModifyMessagesRequest() - request.ids = messages.compactMap { $0.identifier.stringId } + request.ids = messages.compactMap(\.identifier.stringId) request.addLabelIds = labelsToAdd.map(\.value) request.removeLabelIds = labelsToRemove.map(\.value) let query = GTLRGmailQuery_UsersMessagesBatchModify.query( diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageIdentifier.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageIdentifier.swift index f46f7dee5..ee3d41cfc 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageIdentifier.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageIdentifier.swift @@ -10,7 +10,7 @@ import GoogleAPIClientForREST_Gmail struct MessageIdentifier { var draftId: Identifier? - var threadId: String? + var threadId: Identifier? var messageId: Identifier? var draftMessageId: Identifier? } @@ -18,7 +18,7 @@ struct MessageIdentifier { extension MessageIdentifier { init(gmailDraft: GTLRGmail_Draft) { self.draftId = Identifier(stringId: gmailDraft.identifier) - self.threadId = gmailDraft.message?.threadId + self.threadId = Identifier(stringId: gmailDraft.message?.threadId) self.messageId = Identifier(stringId: gmailDraft.message?.identifier) } } diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift b/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift index d524bbc23..56b27772b 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift @@ -12,31 +12,35 @@ import Foundation extension Imap: MessagesThreadOperationsProvider { private var error: Error { AppErr.general("Doesn't support yet") } - func mark(thread: MessageThread, asRead: Bool, in folder: String) async throws { + func mark(id: String?, asRead: Bool, in folder: String) async throws { throw error } - func delete(thread: MessageThread) async throws { + func delete(id: String?) async throws { throw error } - func moveThreadToTrash(thread: MessageThread) async throws { + func moveThreadToTrash(id: String?, labels: Set) async throws { throw error } - func moveThreadToInbox(thread: MessageThread) async throws { + func moveThreadToInbox(id: String?) async throws { throw error } - func markThreadAsUnread(thread: MessageThread, folder: String) async throws { + func markThreadAsUnread(id: String?, folder: String) async throws { throw error } - func markThreadAsRead(thread: MessageThread, folder: String) async throws { + func markThreadAsRead(id: String?, folder: String) async throws { throw error } - func archive(thread: MessageThread, in folder: String) async throws { + func mark(messagesIds: [Identifier], asRead: Bool, in folder: String) async throws { + throw error + } + + func archive(messages: [Message], in folder: String) async throws { throw error } } diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift index 0d3e8c556..64b3f3bf0 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift @@ -9,25 +9,28 @@ import GoogleAPIClientForREST_Gmail protocol MessagesThreadOperationsProvider { - func mark(thread: MessageThread, asRead: Bool, in folder: String) async throws - func delete(thread: MessageThread) async throws - func moveThreadToTrash(thread: MessageThread) async throws - func moveThreadToInbox(thread: MessageThread) async throws - func markThreadAsUnread(thread: MessageThread, folder: String) async throws - func markThreadAsRead(thread: MessageThread, folder: String) async throws - func archive(thread: MessageThread, in folder: String) async throws + func mark(id: String?, asRead: Bool, in folder: String) async throws + func delete(id: String?) async throws + func moveThreadToTrash(id: String?, labels: Set) async throws + func moveThreadToInbox(id: String?) async throws + func markThreadAsUnread(id: String?, folder: String) async throws + func mark(messagesIds: [Identifier], asRead: Bool, in folder: String) async throws + func archive(messages: [Message], in folder: String) async throws } extension GmailService: MessagesThreadOperationsProvider { - func delete(thread: MessageThread) async throws { + func mark(id: String?, asRead: Bool, in folder: String) async throws { + } + + func delete(id: String?) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - guard let identifier = thread.identifier else { + guard let id = id else { return continuation.resume(throwing: GmailServiceError.missingMessageInfo("id")) } let query = GTLRGmailQuery_UsersThreadsDelete.query( withUserId: .me, - identifier: identifier + identifier: id ) self.gmailService.executeQuery(query) { _, _, error in @@ -39,30 +42,30 @@ extension GmailService: MessagesThreadOperationsProvider { } } - func moveThreadToTrash(thread: MessageThread) async throws { - let labelsToRemove = [MessageLabel.inbox, MessageLabel.sent].filter { thread.labels.contains($0) } - try await update(thread: thread, labelsToAdd: [.trash], labelsToRemove: labelsToRemove) + func moveThreadToTrash(id: String?, labels: Set) async throws { + let labelsToRemove = [MessageLabel.inbox, MessageLabel.sent].filter { labels.contains($0) } + try await update(id: id, labelsToAdd: [.trash], labelsToRemove: labelsToRemove) } - func moveThreadToInbox(thread: MessageThread) async throws { - try await update(thread: thread, labelsToAdd: [.inbox], labelsToRemove: [.trash]) + func moveThreadToInbox(id: String?) async throws { + try await update(id: id, labelsToAdd: [.inbox], labelsToRemove: [.trash]) } - func markThreadAsUnread(thread: MessageThread, folder: String) async throws { - try await update(thread: thread, labelsToAdd: [.unread]) + func markThreadAsUnread(id: String?, folder: String) async throws { + try await update(id: id, labelsToAdd: [.unread]) } - func markThreadAsRead(thread: MessageThread, folder: String) async throws { - try await update(thread: thread, labelsToRemove: [.unread]) + func markThreadAsRead(id: String?, folder: String) async throws { + try await update(id: id, labelsToRemove: [.unread]) } - func mark(thread: MessageThread, asRead: Bool, in folder: String) async throws { + func mark(messagesIds: [Identifier], asRead: Bool, in folder: String) async throws { try await withThrowingTaskGroup(of: Void.self) { taskGroup in - for message in thread.messages { + for id in messagesIds { taskGroup.addTask { asRead - ? try await self.markAsRead(id: message.identifier, folder: folder) - : try await self.markAsUnread(id: message.identifier, folder: folder) + ? try await self.markAsRead(id: id, folder: folder) + : try await self.markAsUnread(id: id, folder: folder) } } @@ -70,19 +73,22 @@ extension GmailService: MessagesThreadOperationsProvider { } } - func archive(thread: MessageThread, in folder: String) async throws { + func archive(messages: [Message], in folder: String) async throws { // manually updated each message rather than using update(thread:...) method // https://github.com/FlowCrypt/flowcrypt-ios/pull/1769#discussion_r932964129 - try await archiveBatchMessages(messages: thread.messages) + try await batchUpdate( + messages: messages, + labelsToRemove: [.inbox] + ) } private func update( - thread: MessageThread, + id: String?, labelsToAdd: [MessageLabel] = [], labelsToRemove: [MessageLabel] = [] ) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - guard let identifier = thread.identifier else { + guard let id = id else { return continuation.resume(throwing: GmailServiceError.missingMessageInfo("id")) } @@ -93,7 +99,7 @@ extension GmailService: MessagesThreadOperationsProvider { let query = GTLRGmailQuery_UsersThreadsModify.query( withObject: request, userId: .me, - identifier: identifier + identifier: id ) self.gmailService.executeQuery(query) { _, _, error in From ff002693157e9b9ca6b95f94cf4b12eef0e992fd Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 26 Sep 2022 16:54:51 +0300 Subject: [PATCH 24/56] fix drafts list reload --- Core/package-lock.json | 120 ++++++++-------- .../Inbox/InboxViewController.swift | 67 +++++---- .../Controllers/Threads/MessageThread.swift | 1 - .../Threads/MessagesThreadProvider.swift | 1 - package-lock.json | 131 +++++++++++------- 5 files changed, 170 insertions(+), 150 deletions(-) diff --git a/Core/package-lock.json b/Core/package-lock.json index 1d2257e37..879fe3de0 100644 --- a/Core/package-lock.json +++ b/Core/package-lock.json @@ -197,9 +197,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.7.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz", - "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==", + "version": "18.7.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.21.tgz", + "integrity": "sha512-rLFzK5bhM0YPyCoTC8bolBjMk7bwnZ8qeZUBslBfjZQou2ssJdWslx9CZ8DGM+Dx7QXQiiTVZ/6QO6kwtHkZCA==", "dev": true }, "node_modules/@types/node-cleanup": { @@ -495,9 +495,9 @@ } }, "node_modules/ansi-styles": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", - "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.1.tgz", + "integrity": "sha512-qDOv24WjnYuL+wbwHdlsYZFy+cgPtrYw0Tn7GLORicQp9BkQLzrgI3Pm4VyR9ERZ41YTn7KlMPuL1n05WdZvmg==", "dev": true, "engines": { "node": ">=12" @@ -771,9 +771,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", - "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", "dev": true, "funding": [ { @@ -786,10 +786,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001370", - "electron-to-chromium": "^1.4.202", + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.5" + "update-browserslist-db": "^1.0.9" }, "bin": { "browserslist": "cli.js" @@ -841,9 +841,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001387", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001387.tgz", - "integrity": "sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA==", + "version": "1.0.30001412", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz", + "integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA==", "dev": true, "funding": [ { @@ -956,9 +956,9 @@ "dev": true }, "node_modules/ci-info": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", - "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.4.0.tgz", + "integrity": "sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug==", "dev": true }, "node_modules/ci-parallel-vars": { @@ -1464,9 +1464,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.237", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.237.tgz", - "integrity": "sha512-vxVyGJcsgArNOVUJcXm+7iY3PJAfmSapEszQD1HbyPLl0qoCmNQ1o/EX3RI7Et5/88In9oLxX3SGF8J3orkUgA==", + "version": "1.4.262", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.262.tgz", + "integrity": "sha512-Ckn5haqmGh/xS8IbcgK3dnwAVnhDyo/WQnklWn6yaMucYTq7NNxwlGE8ElzEOnonzRLzUCo2Ot3vUb2GYUF2Hw==", "dev": true }, "node_modules/emittery": { @@ -1663,9 +1663,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -2196,9 +2196,9 @@ "dev": true }, "node_modules/is-unicode-supported": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.2.0.tgz", - "integrity": "sha512-wH+U77omcRzevfIG8dDhTS0V9zZyweakfD01FULl97+0EHiJTTZtJqxPSkIIo/SDPv/i07k/C9jAPY+jwLLeUQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, "engines": { "node": ">=12" @@ -3600,9 +3600,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", - "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", + "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", "dev": true, "funding": [ { @@ -4195,9 +4195,9 @@ "dev": true }, "@types/node": { - "version": "18.7.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz", - "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==", + "version": "18.7.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.21.tgz", + "integrity": "sha512-rLFzK5bhM0YPyCoTC8bolBjMk7bwnZ8qeZUBslBfjZQou2ssJdWslx9CZ8DGM+Dx7QXQiiTVZ/6QO6kwtHkZCA==", "dev": true }, "@types/node-cleanup": { @@ -4451,9 +4451,9 @@ "dev": true }, "ansi-styles": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", - "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.1.tgz", + "integrity": "sha512-qDOv24WjnYuL+wbwHdlsYZFy+cgPtrYw0Tn7GLORicQp9BkQLzrgI3Pm4VyR9ERZ41YTn7KlMPuL1n05WdZvmg==", "dev": true }, "anymatch": { @@ -4660,15 +4660,15 @@ } }, "browserslist": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", - "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001370", - "electron-to-chromium": "^1.4.202", + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.5" + "update-browserslist-db": "^1.0.9" } }, "buffer": { @@ -4694,9 +4694,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001387", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001387.tgz", - "integrity": "sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA==", + "version": "1.0.30001412", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz", + "integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA==", "dev": true }, "caseless": { @@ -4770,9 +4770,9 @@ "dev": true }, "ci-info": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", - "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.4.0.tgz", + "integrity": "sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug==", "dev": true }, "ci-parallel-vars": { @@ -5161,9 +5161,9 @@ } }, "electron-to-chromium": { - "version": "1.4.237", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.237.tgz", - "integrity": "sha512-vxVyGJcsgArNOVUJcXm+7iY3PJAfmSapEszQD1HbyPLl0qoCmNQ1o/EX3RI7Et5/88In9oLxX3SGF8J3orkUgA==", + "version": "1.4.262", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.262.tgz", + "integrity": "sha512-Ckn5haqmGh/xS8IbcgK3dnwAVnhDyo/WQnklWn6yaMucYTq7NNxwlGE8ElzEOnonzRLzUCo2Ot3vUb2GYUF2Hw==", "dev": true }, "emittery": { @@ -5304,9 +5304,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -5680,9 +5680,9 @@ "dev": true }, "is-unicode-supported": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.2.0.tgz", - "integrity": "sha512-wH+U77omcRzevfIG8dDhTS0V9zZyweakfD01FULl97+0EHiJTTZtJqxPSkIIo/SDPv/i07k/C9jAPY+jwLLeUQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true }, "isexe": { @@ -6645,9 +6645,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", - "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", + "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", "dev": true, "requires": { "escalade": "^3.1.1", diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 32b202704..645a7ddf2 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -396,7 +396,15 @@ extension InboxViewController { Task { do { TapTicFeedback.generate(.light) - let composeVc = try await ComposeViewController(appContext: appContext) + let composeVc = try await ComposeViewController( + appContext: appContext, + handleAction: { [weak self] action in + switch action { + case .update(let identifier), .sent(let identifier), .delete(let identifier): + self?.fetchUpdatedInboxItem(identifier: identifier) + } + } + ) navigationController?.pushViewController(composeVc, animated: true) } catch { showAlert(message: error.localizedDescription) @@ -638,33 +646,10 @@ extension InboxViewController { handleAction: { [weak self] action in guard let self = self else { return } -// switch action { -// case .update(let identifier): -// // todo -// break -// case .sent(let identifier): -// guard let index = self.inboxInput.firstIndex(where: { -// if let threadId = $0.wrappedThread?.identifier { -// return threadId == identifier.threadId?.stringId -// } else if let messageId = $0.wrappedMessage?.identifier { -// return messageId == identifier.messageId -// } -// return false -// }) else { return } -// -// case .delete(let identifier): -// guard let index = self.inboxInput.firstIndex(where: { -// if let threadId = $0.wrappedThread?.identifier { -// return threadId == identifier.threadId?.stringId -// } else if let messageId = $0.wrappedMessage?.identifier { -// return messageId == identifier.messageId -// } -// return false -// }) else { return } -// -// self.inboxInput.remove(at: index) -// self.tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) -// } + switch action { + case .update(let identifier), .sent(let identifier), .delete(let identifier): + self.fetchUpdatedInboxItem(identifier: identifier) + } } ) navigationController?.pushViewController(controller, animated: true) @@ -675,16 +660,26 @@ extension InboxViewController { } private func fetchUpdatedInboxItem(identifier: MessageIdentifier) { - guard let index = inboxInput.firstIndex(where: { - switch $0.type { - case .thread(let threadId): - return threadId == identifier.threadId - case .message(let messageId): - return messageId == identifier.messageId + Task { + guard let index = inboxInput.firstIndex(where: { + switch $0.type { + case .thread(let threadId): + return threadId == identifier.threadId + case .message(let messageId): + return messageId == identifier.messageId + } + }) else { + if let threadId = identifier.threadId { + if let inboxItem = try await inboxDataProvider.fetchInboxItem(identifier: threadId, path: path) { + if !inboxItem.messages(with: path).isEmpty { + self.inboxInput.insert(inboxItem, at: 0) + self.tableNode.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic) + } + } + } + return } - }) else { return } - Task { switch inboxInput[index].type { case .thread(let threadId): guard let inboxItem = try await inboxDataProvider.fetchInboxItem(identifier: threadId, path: path) diff --git a/FlowCrypt/Controllers/Threads/MessageThread.swift b/FlowCrypt/Controllers/Threads/MessageThread.swift index 5a4fdf856..7650f3c88 100644 --- a/FlowCrypt/Controllers/Threads/MessageThread.swift +++ b/FlowCrypt/Controllers/Threads/MessageThread.swift @@ -16,6 +16,5 @@ struct MessageThreadContext { struct MessageThread: Equatable { let identifier: String? let snippet: String? - let path: String var messages: [Message] } diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift index 647b30a9d..cca956aff 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift @@ -77,7 +77,6 @@ extension GmailService: MessagesThreadProvider { let thread = MessageThread( identifier: gmailThread.identifier, snippet: gmailThread.snippet, - path: path, messages: messages ) return continuation.resume(returning: thread) diff --git a/package-lock.json b/package-lock.json index 76ccee176..b618f1e99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,9 +60,9 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", - "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz", + "integrity": "sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -736,22 +736,22 @@ "dev": true }, "node_modules/es-abstract": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.2.tgz", - "integrity": "sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz", + "integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.2", + "get-intrinsic": "^1.1.3", "get-symbol-description": "^1.0.0", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-symbols": "^1.0.3", "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", + "is-callable": "^1.2.6", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", @@ -761,6 +761,7 @@ "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", "string.prototype.trimend": "^1.0.5", "string.prototype.trimstart": "^1.0.5", "unbox-primitive": "^1.0.2" @@ -1191,9 +1192,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -1338,9 +1339,9 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", "dev": true, "dependencies": { "function-bind": "^1.1.1", @@ -1656,9 +1657,9 @@ } }, "node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "engines": { "node": ">= 0.4" @@ -2659,14 +2660,28 @@ } }, "node_modules/rxjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", - "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", "dev": true, "dependencies": { "tslib": "^2.1.0" } }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -2749,9 +2764,9 @@ } }, "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", - "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.1.tgz", + "integrity": "sha512-qDOv24WjnYuL+wbwHdlsYZFy+cgPtrYw0Tn7GLORicQp9BkQLzrgI3Pm4VyR9ERZ41YTn7KlMPuL1n05WdZvmg==", "dev": true, "engines": { "node": ">=12" @@ -3020,9 +3035,9 @@ } }, "node_modules/typescript": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz", - "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", "dev": true, "peer": true, "bin": { @@ -3207,9 +3222,9 @@ } }, "@humanwhocodes/config-array": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", - "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz", + "integrity": "sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -3660,22 +3675,22 @@ "dev": true }, "es-abstract": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.2.tgz", - "integrity": "sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz", + "integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==", "dev": true, "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.2", + "get-intrinsic": "^1.1.3", "get-symbol-description": "^1.0.0", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-symbols": "^1.0.3", "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", + "is-callable": "^1.2.6", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", @@ -3685,6 +3700,7 @@ "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", "string.prototype.trimend": "^1.0.5", "string.prototype.trimstart": "^1.0.5", "unbox-primitive": "^1.0.2" @@ -4018,9 +4034,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -4137,9 +4153,9 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", "dev": true, "requires": { "function-bind": "^1.1.1", @@ -4353,9 +4369,9 @@ } }, "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true }, "is-core-module": { @@ -5033,14 +5049,25 @@ } }, "rxjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", - "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", "dev": true, "requires": { "tslib": "^2.1.0" } }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -5099,9 +5126,9 @@ }, "dependencies": { "ansi-styles": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", - "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.1.tgz", + "integrity": "sha512-qDOv24WjnYuL+wbwHdlsYZFy+cgPtrYw0Tn7GLORicQp9BkQLzrgI3Pm4VyR9ERZ41YTn7KlMPuL1n05WdZvmg==", "dev": true } } @@ -5298,9 +5325,9 @@ "dev": true }, "typescript": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz", - "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", "dev": true, "peer": true }, From 47fb96c940cf22819676f137724fa894ff10f2cc Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 28 Sep 2022 14:04:14 +0300 Subject: [PATCH 25/56] add ui test for drafts --- Core/package-lock.json | 50 ++--- Core/package.json | 2 +- .../Compose/ComposeViewController.swift | 13 -- .../Compose/ComposeViewDecorator.swift | 2 +- .../ComposeViewController+Setup.swift | 3 +- .../Threads/ThreadDetailsViewController.swift | 3 + FlowCrypt/Resources/flowcrypt-ios-prod.js.txt | 8 + FlowCryptUI/Cell Nodes/LabelCellNode.swift | 4 + .../Cell Nodes/MessageActionCellNode.swift | 15 +- appium/api-mocks/apis/google/google-data.ts | 30 ++- .../api-mocks/apis/google/google-endpoints.ts | 94 +++++---- appium/tests/screenobjects/email.screen.ts | 19 ++ appium/tests/screenobjects/menu-bar.screen.ts | 9 + .../tests/screenobjects/new-message.screen.ts | 27 +++ .../CheckDraftFunctionality.spec.ts | 70 +++++++ package-lock.json | 190 +++++++++--------- package.json | 4 +- 17 files changed, 350 insertions(+), 193 deletions(-) create mode 100644 appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts diff --git a/Core/package-lock.json b/Core/package-lock.json index 879fe3de0..f4154be2e 100644 --- a/Core/package-lock.json +++ b/Core/package-lock.json @@ -12,7 +12,7 @@ "@openpgp/web-stream-tools": "^0.0.11", "encoding-japanese": "^2.0.0", "openpgp": "5.5.0", - "sanitize-html": "2.7.1", + "sanitize-html": "2.7.2", "zxcvbn": "4.4.2" }, "devDependencies": { @@ -197,9 +197,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.7.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.21.tgz", - "integrity": "sha512-rLFzK5bhM0YPyCoTC8bolBjMk7bwnZ8qeZUBslBfjZQou2ssJdWslx9CZ8DGM+Dx7QXQiiTVZ/6QO6kwtHkZCA==", + "version": "18.7.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", + "integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==", "dev": true }, "node_modules/@types/node-cleanup": { @@ -1464,9 +1464,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.262", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.262.tgz", - "integrity": "sha512-Ckn5haqmGh/xS8IbcgK3dnwAVnhDyo/WQnklWn6yaMucYTq7NNxwlGE8ElzEOnonzRLzUCo2Ot3vUb2GYUF2Hw==", + "version": "1.4.265", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.265.tgz", + "integrity": "sha512-38KaYBNs0oCzWCpr6j7fY/W9vF0vSp4tKFIshQTgdZMhUpkxgotkQgjJP6iGMdmlsgMs3i0/Hkko4UXLTrkYVQ==", "dev": true }, "node_modules/emittery": { @@ -3146,9 +3146,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sanitize-html": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.1.tgz", - "integrity": "sha512-oOpe8l4J8CaBk++2haoN5yNI5beekjuHv3JRPKUx/7h40Rdr85pemn4NkvUB3TcBP7yjat574sPlcMAyv4UQig==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.2.tgz", + "integrity": "sha512-DggSTe7MviO+K4YTCwprG6W1vsG+IIX67yp/QY55yQqKCJYSWzCA1rZbaXzkjoKeL9+jqwm56wD6srYLtUNivg==", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -3587,9 +3587,9 @@ } }, "node_modules/typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -4195,9 +4195,9 @@ "dev": true }, "@types/node": { - "version": "18.7.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.21.tgz", - "integrity": "sha512-rLFzK5bhM0YPyCoTC8bolBjMk7bwnZ8qeZUBslBfjZQou2ssJdWslx9CZ8DGM+Dx7QXQiiTVZ/6QO6kwtHkZCA==", + "version": "18.7.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", + "integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==", "dev": true }, "@types/node-cleanup": { @@ -5161,9 +5161,9 @@ } }, "electron-to-chromium": { - "version": "1.4.262", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.262.tgz", - "integrity": "sha512-Ckn5haqmGh/xS8IbcgK3dnwAVnhDyo/WQnklWn6yaMucYTq7NNxwlGE8ElzEOnonzRLzUCo2Ot3vUb2GYUF2Hw==", + "version": "1.4.265", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.265.tgz", + "integrity": "sha512-38KaYBNs0oCzWCpr6j7fY/W9vF0vSp4tKFIshQTgdZMhUpkxgotkQgjJP6iGMdmlsgMs3i0/Hkko4UXLTrkYVQ==", "dev": true }, "emittery": { @@ -6338,9 +6338,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sanitize-html": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.1.tgz", - "integrity": "sha512-oOpe8l4J8CaBk++2haoN5yNI5beekjuHv3JRPKUx/7h40Rdr85pemn4NkvUB3TcBP7yjat574sPlcMAyv4UQig==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.2.tgz", + "integrity": "sha512-DggSTe7MviO+K4YTCwprG6W1vsG+IIX67yp/QY55yQqKCJYSWzCA1rZbaXzkjoKeL9+jqwm56wD6srYLtUNivg==", "requires": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -6639,9 +6639,9 @@ "dev": true }, "typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true }, "update-browserslist-db": { diff --git a/Core/package.json b/Core/package.json index df3d045d0..f93b9de96 100644 --- a/Core/package.json +++ b/Core/package.json @@ -6,7 +6,7 @@ "@openpgp/web-stream-tools": "^0.0.11", "encoding-japanese": "^2.0.0", "openpgp": "5.5.0", - "sanitize-html": "2.7.1", + "sanitize-html": "2.7.2", "zxcvbn": "4.4.2" }, "devDependencies": { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 760ff0f7e..ca56ce272 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -261,18 +261,5 @@ extension ComposeViewController: FilesManagerPresenter {} /* - show empty view as inbox table header - - reload drafts list when going back from compose or thread screen - check drafts for forward and reply all */ - -/* ui test -- open compose -- create draft - - go back - - go to drafts folder - - check if folder there - - go to inbox - - open existing thread - - create reply - - check draft - */ diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 7cd78d3e1..3ecea0bdc 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -131,7 +131,7 @@ struct ComposeViewDecorator { messageActionInput( text: "compose_draft_passphrase_placeholder".localized, color: .warningColor, - imageName: "lock" + imageName: "square.and.pencil" ) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index ba408acce..0a8f9267d 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -13,7 +13,8 @@ import FlowCryptUI extension ComposeViewController { func setupNavigationBar() { let deleteButton = NavigationBarItemsView.Input( - image: UIImage(systemName: "trash") + image: UIImage(systemName: "trash"), + accessibilityId: "aid-compose-delete" ) { [weak self] in self?.handleTrashTap() } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 65056faab..e68ab330b 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -790,6 +790,9 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { input: .init( title: "compose_draft".localized.attributed(color: .red), text: body.removingMailThreadQuote().attributed(color: .secondaryLabel), + accessibilityIdentifier: "aid-draft-body-\(messageIndex)", + labelAccessibilityIdentifier: "aid-draft-label-\(messageIndex)", + buttonAccessibilityIdentifier: "aid-draft-delete-button-\(messageIndex)", actionButtonImageName: "trash", action: { [weak self] in self?.deleteDraft(id: data.rawMessage.identifier) diff --git a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt index 6bd27082d..4f94ba099 100644 --- a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt +++ b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt @@ -40699,6 +40699,14 @@ function sanitizeHtml(html, options, _recursing) { // Do not crash on bad markup return; } + + if (frame.tag !== name) { + // Another case of bad markup. + // Push to stack, so that it will be used in future closing tags. + stack.push(frame); + return; + } + skipText = options.enforceHtmlBoundary ? name === 'html' : false; depth--; const skip = skipMap[depth]; diff --git a/FlowCryptUI/Cell Nodes/LabelCellNode.swift b/FlowCryptUI/Cell Nodes/LabelCellNode.swift index 6ba529fd6..2a91d080d 100644 --- a/FlowCryptUI/Cell Nodes/LabelCellNode.swift +++ b/FlowCryptUI/Cell Nodes/LabelCellNode.swift @@ -16,6 +16,7 @@ public final class LabelCellNode: CellNode { let spacing: CGFloat let accessibilityIdentifier: String? let labelAccessibilityIdentifier: String? + let buttonAccessibilityIdentifier: String? let actionButtonImageName: String? let action: (() -> Void)? @@ -26,6 +27,7 @@ public final class LabelCellNode: CellNode { spacing: CGFloat = 4, accessibilityIdentifier: String? = nil, labelAccessibilityIdentifier: String? = nil, + buttonAccessibilityIdentifier: String? = nil, actionButtonImageName: String? = nil, action: (() -> Void)? = nil ) { @@ -35,6 +37,7 @@ public final class LabelCellNode: CellNode { self.spacing = spacing self.accessibilityIdentifier = accessibilityIdentifier self.labelAccessibilityIdentifier = labelAccessibilityIdentifier + self.buttonAccessibilityIdentifier = buttonAccessibilityIdentifier self.actionButtonImageName = actionButtonImageName self.action = action } @@ -59,6 +62,7 @@ public final class LabelCellNode: CellNode { actionButtonNode.addTarget(self, action: #selector(onActionButtonTap), forControlEvents: .touchUpInside) if let imageName = input.actionButtonImageName { + actionButtonNode.accessibilityIdentifier = input.buttonAccessibilityIdentifier actionButtonNode.setImage(UIImage(systemName: imageName)?.tinted(.secondaryLabel), for: .normal) } } diff --git a/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift b/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift index eb9dde190..741771897 100644 --- a/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift @@ -54,22 +54,11 @@ public final class MessageActionCellNode: CellNode { } override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { - buttonNode.style.flexShrink = 1.0 - - let spacer = ASLayoutSpec() - spacer.style.flexGrow = 1.0 - - let spec = ASStackLayoutSpec( - direction: .horizontal, - spacing: 4, - justifyContent: .start, - alignItems: .start, - children: [buttonNode, spacer] - ) + buttonNode.style.flexGrow = 1.0 return ASInsetLayoutSpec( insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), - child: spec + child: buttonNode ) } diff --git a/appium/api-mocks/apis/google/google-data.ts b/appium/api-mocks/apis/google/google-data.ts index 000f49f18..0b8b02755 100644 --- a/appium/api-mocks/apis/google/google-data.ts +++ b/appium/api-mocks/apis/google/google-data.ts @@ -25,16 +25,18 @@ export class GmailMsg { public historyId: string; public sizeEstimate?: number; public threadId: string | null; + public draftId?: string | null; public payload?: GmailMsg$payload; public internalDate?: number | string; public labelIds?: GmailMsg$labelId[]; public snippet?: string; public raw?: string; - constructor(msg: { id: string, labelIds?: GmailMsg$labelId[], raw: string, payload?: GmailMsg$payload, mimeMsg: ParsedMail, threadId?: string | null }) { + constructor(msg: { id: string, labelIds?: GmailMsg$labelId[], raw: string, payload?: GmailMsg$payload, mimeMsg: ParsedMail, threadId?: string | null, draftId?: string | null }) { this.id = msg.id; this.historyId = msg.id; this.threadId = msg.threadId ?? msg.id; + this.draftId = msg.draftId; this.labelIds = msg.labelIds; this.raw = msg.raw; this.sizeEstimate = Buffer.byteLength(msg.raw, "utf-8"); @@ -210,6 +212,11 @@ export class GoogleData { return (subjectHeader && subjectHeader.value) || ''; }; + private static msgId = (m: GmailMsg): string => { + const msgIdHeader = m.payload && m.payload.headers && m.payload.headers.find(h => h.name.toLowerCase() === 'message-id'); + return (msgIdHeader && msgIdHeader.value) || ''; + }; + private static parseAcctMessages = async (acct: GoogleMockAccountEmail, config?: GoogleConfig) => { if (config?.accounts[acct]?.messages) { const dir = GoogleData.exportedMsgsPath; @@ -269,7 +276,7 @@ export class GoogleData { } public getMessage = (id: string): GmailMsg | undefined => { - return DATA[this.acct].messages.find(m => m.id === id); + return this.getMessagesAndDrafts().find(m => m.id === id); }; public getMessageBySubject = (subject: string): GmailMsg | undefined => { @@ -311,10 +318,14 @@ export class GoogleData { public getMessages = (labelIds: string[] = [], query?: string) => { const subject = (query?.match(/subject: '([^"]+)'/) || [])[1]?.trim().toLowerCase(); + const rfc822Msgid = (query?.match(/rfc822msgid:([^"]+)/) || [])[1]?.trim(); return DATA[this.acct].messages.filter(m => { if (subject && !GoogleData.msgSubject(m).toLowerCase().includes(subject)) { return false; } + if (rfc822Msgid && GoogleData.msgId(m) !== rfc822Msgid) { + return false; + } if (labelIds && !m.labelIds?.some(l => labelIds.includes(l))) { return false; } @@ -342,14 +353,19 @@ export class GoogleData { DATA[this.acct].messages = DATA[this.acct].messages.filter(m => !ids.includes(m.id)); } - public addDraft = (id: string, raw: string, mimeMsg: ParsedMail) => { - const draft = new GmailMsg({ labelIds: ['DRAFT'], id, raw, mimeMsg }); - const index = DATA[this.acct].drafts.findIndex(d => d.id === draft.id); + public addDraft = (raw: string, mimeMsg: ParsedMail, id?: string, threadId?: string) => { + const draftId = id ?? `draft_id_${lousyRandom()}`; + const msgId = `msg_id_${lousyRandom()}`; + const draft = new GmailMsg({ labelIds: ['DRAFT'], id: msgId, raw, mimeMsg, threadId: threadId, draftId: draftId }); + const index = DATA[this.acct].messages.findIndex(d => d.draftId === draftId); + if (index === -1) { - DATA[this.acct].drafts.push(draft); + DATA[this.acct].messages.push(draft); } else { - DATA[this.acct].drafts[index] = draft; + DATA[this.acct].messages[index] = draft; } + + return draft; }; public getDraft = (id: string): GmailMsg | undefined => { diff --git a/appium/api-mocks/apis/google/google-endpoints.ts b/appium/api-mocks/apis/google/google-endpoints.ts index d9adb8e04..f9bd5747b 100644 --- a/appium/api-mocks/apis/google/google-endpoints.ts +++ b/appium/api-mocks/apis/google/google-endpoints.ts @@ -8,7 +8,7 @@ import { isDelete, isGet, isPost, isPut, parseResourceId } from '../../lib/mock- import { oauth } from '../../lib/oauth'; import { GoogleMockAccountEmail } from './google-messages'; -// type DraftSaveModel = { message: { raw: string, threadId: string } }; +type DraftSaveModel = { message: { raw: string, threadId: string }, id?: string }; type LabelsModifyModel = { addLabelIds: string[], removeLabelIds: string[] } interface BatchModifyInterface { ids: string[]; @@ -121,6 +121,11 @@ export const getMockGoogleEndpoints = ( return GoogleData.fmtMsg(msg, parsedReq.query.format); } throw new HttpErr(`MOCK Message not found for ${acct}: ${id}`, Status.NOT_FOUND); + } else if (isDelete(req)) { + const id = parseResourceId(req.url!); + const data = await GoogleData.withInitializedData(acct, googleConfig); + data.deleteMessages([id]); + return {} } throw new HttpErr(`Method not implemented for ${req.url}: ${req.method}`); }, @@ -170,29 +175,44 @@ export const getMockGoogleEndpoints = ( } return {} }, - '/gmail/v1/users/me/drafts': async () => { - return {} - // if (isPost(req)) { - // const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); - // const body = parsedReq.body as DraftSaveModel; - // if (body && body.message && body.message.raw && typeof body.message.raw === 'string') { - // if (body.message.threadId && !(await GoogleData.withInitializedData(acct, googleConfig)).getThreads().find(t => t.id === body.message.threadId)) { - // throw new HttpErr('The thread you are replying to not found', 404); - // } - // const decoded = await Parse.convertBase64ToMimeMsg(body.message.raw); - // if (!decoded.text?.startsWith('[flowcrypt:') && !decoded.text?.startsWith('(saving of this draft was interrupted - to decrypt it, send it to yourself)')) { - // throw new Error(`The "flowcrypt" draft prefix was not found in the draft. Instead starts with: ${decoded.text?.substring(0, 100)}`); - // } - // return { - // id: 'mockfakedraftsave', message: { - // id: 'mockfakedmessageraftsave', - // labelIds: ['DRAFT'], - // threadId: body.message.threadId - // } - // }; - // } - // } - // throw new HttpErr(`Method not implemented for ${req.url}: ${req.method}`); + '/gmail/v1/users/me/drafts': async (parsedReq, req) => { + if (isPost(req)) { + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); + const body = parsedReq.body as DraftSaveModel; + const data = await GoogleData.withInitializedData(acct, googleConfig); + if (body && body.message && body.message.raw && typeof body.message.raw === 'string') { + if (body.message.threadId && !(data.getThreads().find(t => t.id === body.message.threadId))) { + throw new HttpErr('The thread you are replying to not found', 404); + } + const decoded = await Parse.convertBase64ToMimeMsg(body.message.raw); + // if (!decoded.text?.startsWith('[flowcrypt:') && !decoded.text?.startsWith('(saving of this draft was interrupted - to decrypt it, send it to yourself)')) { + // throw new Error(`The "flowcrypt" draft prefix was not found in the draft. Instead starts with: ${decoded.text?.substring(0, 100)}`); + // } + + const draft = data.addDraft(body.message.raw, decoded, undefined, body.message.threadId); + + return { + id: draft.draftId, message: { + id: draft.id, + labelIds: ['DRAFT'], + threadId: draft.threadId + } + }; + } + } else if (isGet(req)) { + const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); + const data = await GoogleData.withInitializedData(acct, googleConfig); + const message = data.getMessages(['DRAFT'], parsedReq.query.q)[0]; + return { + drafts: [ + { + id: message.draftId, + message: message + } + ] + }; + } + throw new HttpErr(`Method not implemented for ${req.url}: ${req.method}`); }, '/gmail/v1/users/me/drafts/?': async (parsedReq, req) => { const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); @@ -205,20 +225,24 @@ export const getMockGoogleEndpoints = ( } throw new HttpErr(`MOCK draft not found for ${acct} (draftId: ${id})`, Status.NOT_FOUND); } else if (isPut(req)) { - const raw = (parsedReq.body as { message?: { raw?: string } })?.message?.raw; + const body = parsedReq.body as DraftSaveModel; + const raw = body.message?.raw; if (!raw) { throw new Error('mock Draft PUT without raw data'); } - const mimeMsg = await Parse.convertBase64ToMimeMsg(raw); - if ((mimeMsg.subject || '').includes('saving and rendering a draft with image')) { - const data = (await GoogleData.withInitializedData(acct, googleConfig)); - data.addDraft('draft_with_image', raw, mimeMsg); - } - if ((mimeMsg.subject || '').includes('RTL')) { - const data = await GoogleData.withInitializedData(acct, googleConfig); - data.addDraft(`draft_with_rtl_text_${mimeMsg.subject?.includes('rich text') ? 'rich' : 'plain'}`, raw, mimeMsg); - } - return {}; + const decoded = await Parse.convertBase64ToMimeMsg(raw); + + const data = (await GoogleData.withInitializedData(acct, googleConfig)); + const draft = data.addDraft(raw, decoded, body.id, body.message?.threadId) + + // const mimeMsg = await Parse.convertBase64ToMimeMsg(raw); + return { + id: draft.draftId, message: { + id: draft.id, + labelIds: ['DRAFT'], + threadId: draft.threadId + } + }; } else if (isDelete(req)) { return {}; } diff --git a/appium/tests/screenobjects/email.screen.ts b/appium/tests/screenobjects/email.screen.ts index 2cc9d2530..793c92d9d 100644 --- a/appium/tests/screenobjects/email.screen.ts +++ b/appium/tests/screenobjects/email.screen.ts @@ -292,6 +292,25 @@ class EmailScreen extends BaseScreen { const text = await el.getText(); expect(text.includes(value)).toBeTrue(); } + + draftBody = async (index: number) => { + return $(`~aid-draft-body-${index}`); + } + + checkDraft = async (text: string, index: number) => { + const draftBodyEl = await this.draftBody(index); + expect(await draftBodyEl.getValue()).toEqual(text); + } + + openDraft = async (index: number) => { + await ElementHelper.waitAndClick(await this.draftBody(index)); + } + + deleteDraft = async (index: number) => { + await ElementHelper.waitAndClick(await $(`~aid-draft-delete-button-${index}`)); + await this.confirmDelete(); + await ElementHelper.waitElementInvisible(await this.draftBody(index)); + } } export default new EmailScreen(); diff --git a/appium/tests/screenobjects/menu-bar.screen.ts b/appium/tests/screenobjects/menu-bar.screen.ts index 095739e63..78312aae1 100644 --- a/appium/tests/screenobjects/menu-bar.screen.ts +++ b/appium/tests/screenobjects/menu-bar.screen.ts @@ -9,6 +9,7 @@ const SELECTORS = { INBOX_BTN: '~aid-menu-bar-item-inbox', SENT_BTN: '~aid-menu-bar-item-sent', TRASH_BTN: '~aid-menu-bar-item-trash', + DRAFTS_BTN: '~aid-menu-bar-item-drafts', ALL_MAIL_BTN: '~aid-menu-bar-item-all-mail', ADD_ACCOUNT_BUTTON: '~aid-add-account-btn' }; @@ -42,6 +43,10 @@ class MenuBarScreen extends BaseScreen { return $(SELECTORS.TRASH_BTN) } + get draftsButton() { + return $(SELECTORS.DRAFTS_BTN) + } + get allMailButton() { return $(SELECTORS.ALL_MAIL_BTN) } @@ -97,6 +102,10 @@ class MenuBarScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.trashButton); } + clickDraftsButton = async () => { + await ElementHelper.waitAndClick(await this.draftsButton); + } + clickAllMailButton = async () => { await ElementHelper.waitAndClick(await this.allMailButton); } diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index fafefae7e..20add91de 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -18,7 +18,9 @@ const SELECTORS = { SET_PASSWORD_BUTTON: '~Set', CANCEL_BUTTON: '~Cancel', BACK_BUTTON: '~aid-back-button', + DELETE_BUTTON: '~aid-compose-delete', SEND_BUTTON: '~aid-compose-send', + CONFIRM_DELETING: '~Delete', MESSAGE_PASSWORD_MODAL: '~aid-message-password-modal', MESSAGE_PASSWORD_TEXTFIELD: '~aid-message-password-textfield', ALERT: "-ios predicate string:type == 'XCUIElementTypeAlert'", @@ -84,10 +86,18 @@ class NewMessageScreen extends BaseScreen { return $(SELECTORS.BACK_BUTTON); } + get deleteButton() { + return $(SELECTORS.DELETE_BUTTON); + } + get sendButton() { return $(SELECTORS.SEND_BUTTON); } + get confirmDeletingButton() { + return $(SELECTORS.CONFIRM_DELETING) + } + get passwordCell() { return $(SELECTORS.PASSWORD_CELL); } @@ -364,10 +374,18 @@ class NewMessageScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.backButton); } + clickDeleteButton = async () => { + await ElementHelper.waitAndClick(await this.deleteButton); + } + clickSendButton = async () => { await ElementHelper.waitAndClick(await this.sendButton); } + confirmDelete = async () => { + await ElementHelper.waitAndClick(await this.confirmDeletingButton); + } + clickToggleRecipientsButton = async () => { await browser.pause(500); await ElementHelper.waitAndClick(await this.toggleRecipientsButton); @@ -400,6 +418,15 @@ class NewMessageScreen extends BaseScreen { await (await this.passwordTextField).setValue(password); await this.clickSetPasswordButton(); } + + clickComposeMessage = async () => { + await ElementHelper.waitAndClick(await this.composeSecurityMessage); + } + + addMessageText = async (text: string) => { + const messageEl = await this.composeSecurityMessage; + await messageEl.sendKeys([text]); + } } export default new NewMessageScreen(); diff --git a/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts b/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts new file mode 100644 index 000000000..50622ffd9 --- /dev/null +++ b/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts @@ -0,0 +1,70 @@ +import { MockApi } from 'api-mocks/mock'; +import { MockApiConfig } from 'api-mocks/mock-config'; +import { + EmailScreen, + MailFolderScreen, MenuBarScreen, NewMessageScreen, + SetupKeyScreen, + SplashScreen +} from '../../../screenobjects/all-screens'; + +describe('COMPOSE EMAIL: ', () => { + + it('check drafts functionality', async () => { + const mockApi = new MockApi(); + const subject = 'Test 1'; + + mockApi.fesConfig = MockApiConfig.defaultEnterpriseFesConfiguration; + mockApi.ekmConfig = MockApiConfig.defaultEnterpriseEkmConfiguration; + mockApi.addGoogleAccount('e2e.enterprise.test@flowcrypt.com', { + messages: [subject], + }); + + const draftText1 = 'Draft text'; + const updatedDraftText = 'Some new text'; + const draftText2 = 'Another draft'; + + await mockApi.withMockedApis(async () => { + await SplashScreen.mockLogin(); + await SetupKeyScreen.setPassPhrase(); + await MailFolderScreen.checkInboxScreen(); + await MailFolderScreen.clickOnEmailBySubject(subject); + + await EmailScreen.clickReplyButton(); + await NewMessageScreen.checkMessageFieldFocus(); + await NewMessageScreen.addMessageText(draftText1); + await NewMessageScreen.clickBackButton(); + + await EmailScreen.clickReplyButton(); + await NewMessageScreen.checkMessageFieldFocus(); + await NewMessageScreen.addMessageText(draftText2); + await NewMessageScreen.clickBackButton(); + + await EmailScreen.checkDraft(draftText1, 1); + await EmailScreen.checkDraft(draftText2, 2); + + await EmailScreen.openDraft(1); + + await NewMessageScreen.setComposeSecurityMessage(updatedDraftText); + await NewMessageScreen.clickBackButton(); + + await EmailScreen.checkDraft(updatedDraftText, 1); + await EmailScreen.checkDraft(draftText2, 2); + + await EmailScreen.deleteDraft(1); + await EmailScreen.clickBackButton(); + + await MenuBarScreen.clickMenuBtn(); + await MenuBarScreen.clickDraftsButton(); + + await MailFolderScreen.clickOnEmailBySubject(subject); + await EmailScreen.checkDraft(draftText2, 1); + await EmailScreen.openDraft(1); + + await NewMessageScreen.clickDeleteButton(); + await NewMessageScreen.confirmDelete(); + }); + }); +}); + +// check passphrase modal after app restart +// check compose new draft is added to drafts folder \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b618f1e99..9dc176c7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.38.0", "@typescript-eslint/parser": "^5.38.0", - "eslint": "8.23.1", + "eslint": "8.24.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.3.6", @@ -150,14 +150,14 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.0.tgz", - "integrity": "sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", + "integrity": "sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.38.0", - "@typescript-eslint/type-utils": "5.38.0", - "@typescript-eslint/utils": "5.38.0", + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/type-utils": "5.38.1", + "@typescript-eslint/utils": "5.38.1", "debug": "^4.3.4", "ignore": "^5.2.0", "regexpp": "^3.2.0", @@ -182,14 +182,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.0.tgz", - "integrity": "sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.1.tgz", + "integrity": "sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.38.0", - "@typescript-eslint/types": "5.38.0", - "@typescript-eslint/typescript-estree": "5.38.0", + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/typescript-estree": "5.38.1", "debug": "^4.3.4" }, "engines": { @@ -209,13 +209,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.0.tgz", - "integrity": "sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.1.tgz", + "integrity": "sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.38.0", - "@typescript-eslint/visitor-keys": "5.38.0" + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/visitor-keys": "5.38.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -226,13 +226,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.0.tgz", - "integrity": "sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.1.tgz", + "integrity": "sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.38.0", - "@typescript-eslint/utils": "5.38.0", + "@typescript-eslint/typescript-estree": "5.38.1", + "@typescript-eslint/utils": "5.38.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -253,9 +253,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.0.tgz", - "integrity": "sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.1.tgz", + "integrity": "sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -266,13 +266,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.0.tgz", - "integrity": "sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.1.tgz", + "integrity": "sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.38.0", - "@typescript-eslint/visitor-keys": "5.38.0", + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/visitor-keys": "5.38.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -293,15 +293,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.0.tgz", - "integrity": "sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.1.tgz", + "integrity": "sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.38.0", - "@typescript-eslint/types": "5.38.0", - "@typescript-eslint/typescript-estree": "5.38.0", + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/typescript-estree": "5.38.1", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, @@ -317,12 +317,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.0.tgz", - "integrity": "sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.1.tgz", + "integrity": "sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.38.0", + "@typescript-eslint/types": "5.38.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -812,13 +812,13 @@ } }, "node_modules/eslint": { - "version": "8.23.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz", - "integrity": "sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", + "integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.2", - "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/config-array": "^0.10.5", "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "@humanwhocodes/module-importer": "^1.0.1", "ajv": "^6.10.0", @@ -3035,9 +3035,9 @@ } }, "node_modules/typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true, "peer": true, "bin": { @@ -3289,14 +3289,14 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.0.tgz", - "integrity": "sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", + "integrity": "sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.38.0", - "@typescript-eslint/type-utils": "5.38.0", - "@typescript-eslint/utils": "5.38.0", + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/type-utils": "5.38.1", + "@typescript-eslint/utils": "5.38.1", "debug": "^4.3.4", "ignore": "^5.2.0", "regexpp": "^3.2.0", @@ -3305,53 +3305,53 @@ } }, "@typescript-eslint/parser": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.0.tgz", - "integrity": "sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.1.tgz", + "integrity": "sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.38.0", - "@typescript-eslint/types": "5.38.0", - "@typescript-eslint/typescript-estree": "5.38.0", + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/typescript-estree": "5.38.1", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.0.tgz", - "integrity": "sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.1.tgz", + "integrity": "sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.38.0", - "@typescript-eslint/visitor-keys": "5.38.0" + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/visitor-keys": "5.38.1" } }, "@typescript-eslint/type-utils": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.0.tgz", - "integrity": "sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.1.tgz", + "integrity": "sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.38.0", - "@typescript-eslint/utils": "5.38.0", + "@typescript-eslint/typescript-estree": "5.38.1", + "@typescript-eslint/utils": "5.38.1", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.0.tgz", - "integrity": "sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.1.tgz", + "integrity": "sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.0.tgz", - "integrity": "sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.1.tgz", + "integrity": "sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g==", "dev": true, "requires": { - "@typescript-eslint/types": "5.38.0", - "@typescript-eslint/visitor-keys": "5.38.0", + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/visitor-keys": "5.38.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3360,26 +3360,26 @@ } }, "@typescript-eslint/utils": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.0.tgz", - "integrity": "sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.1.tgz", + "integrity": "sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.38.0", - "@typescript-eslint/types": "5.38.0", - "@typescript-eslint/typescript-estree": "5.38.0", + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/typescript-estree": "5.38.1", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" } }, "@typescript-eslint/visitor-keys": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.0.tgz", - "integrity": "sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.1.tgz", + "integrity": "sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.38.0", + "@typescript-eslint/types": "5.38.1", "eslint-visitor-keys": "^3.3.0" } }, @@ -3733,13 +3733,13 @@ "dev": true }, "eslint": { - "version": "8.23.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz", - "integrity": "sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", + "integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.2", - "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/config-array": "^0.10.5", "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "@humanwhocodes/module-importer": "^1.0.1", "ajv": "^6.10.0", @@ -5325,9 +5325,9 @@ "dev": true }, "typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true, "peer": true }, diff --git a/package.json b/package.json index 4282aad9a..22b37b5c0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.38.0", "@typescript-eslint/parser": "^5.38.0", - "eslint": "8.23.1", + "eslint": "8.24.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.3.6", @@ -36,4 +36,4 @@ "npx eslint" ] } -} +} \ No newline at end of file From 5c59026140dbf0aee19e357f2ec00acbe45741ca Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 29 Sep 2022 11:31:57 +0300 Subject: [PATCH 26/56] update drafts ui test --- .../Compose/ComposeViewDecorator.swift | 17 ++++--- .../ComposeViewController+ErrorHandling.swift | 8 +++- .../ComposeViewController+Setup.swift | 2 + .../Backups Scene/BackupViewDecorator.swift | 3 +- .../Controllers/Threads/AlertsFactory.swift | 3 +- FlowCryptCommon/Trace.swift | 2 +- .../Cell Nodes/MessageActionCellNode.swift | 13 ++++-- appium/api-mocks/apis/google/google-data.ts | 1 + .../tests/screenobjects/mail-folder.screen.ts | 5 +++ .../tests/screenobjects/new-message.screen.ts | 37 ++++++++++++--- .../CheckDraftFunctionality.spec.ts | 45 ++++++++++++++++--- 11 files changed, 110 insertions(+), 26 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 3ecea0bdc..0cfc552d9 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -118,7 +118,7 @@ struct ComposeViewDecorator { let from = info.sender?.formatted ?? "unknown sender" - let text: String = "\n\n" + let text = "\n\n" + "compose_quote_from".localizeWithArguments(date, time, from) + "\n" @@ -131,7 +131,8 @@ struct ComposeViewDecorator { messageActionInput( text: "compose_draft_passphrase_placeholder".localized, color: .warningColor, - imageName: "square.and.pencil" + imageName: "square.and.pencil", + accessibilityIdentifier: "aid-message-passphrase-cell" ) } @@ -139,7 +140,8 @@ struct ComposeViewDecorator { messageActionInput( text: "compose_password_placeholder".localized, color: .warningColor, - imageName: "lock" + imageName: "lock", + accessibilityIdentifier: "aid-message-password-cell" ) } @@ -147,7 +149,8 @@ struct ComposeViewDecorator { messageActionInput( text: "compose_password_set_message".localized, color: .main, - imageName: "checkmark.circle" + imageName: "checkmark.circle", + accessibilityIdentifier: "aid-message-password-cell" ) } @@ -191,12 +194,14 @@ struct ComposeViewDecorator { private func messageActionInput( text: String, color: UIColor, - imageName: String + imageName: String, + accessibilityIdentifier: String? ) -> MessageActionCellNode.Input { .init( text: text.attributed(.regular(14), color: color), color: color, - image: UIImage(systemName: imageName)?.tinted(color) + image: UIImage(systemName: imageName)?.tinted(color), + accessibilityIdentifier: accessibilityIdentifier ) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift index c82f59eb9..b65b78da8 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift @@ -29,11 +29,16 @@ extension ComposeViewController { passPhrase, for: signingKey ) + // TODO: make more readable if matched { self.signingKeyWithMissingPassphrase = nil if isDraft { if self.didFinishSetup { - self.saveDraftIfNeeded() + if withDiscard { + self.handleBackButtonTap() + } else { + self.saveDraftIfNeeded() + } } else { self.fillDataFromInput() } @@ -84,6 +89,7 @@ extension ComposeViewController { private func reEnableSendButton() { UIApplication.shared.isIdleTimerDisabled = false + startDraftTimer() hideSpinner() navigationItem.rightBarButtonItem?.isEnabled = true } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 0a8f9267d..3485ac33e 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -153,6 +153,8 @@ extension ComposeViewController { // MARK: - NavigationChildController extension ComposeViewController: NavigationChildController { func handleBackButtonTap() { + stopDraftTimer() + if let keyPair = signingKeyWithMissingPassphrase { requestMissingPassPhraseWithModal(for: keyPair, isDraft: true, withDiscard: true) } else { diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewDecorator.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewDecorator.swift index f5aa923a3..46aa00eb0 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewDecorator.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewDecorator.swift @@ -9,8 +9,7 @@ import UIKit struct BackupViewDecorator { - let sceneTitle: String = "backup_screen_title" - .localized + let sceneTitle = "backup_screen_title".localized func buttonTitle(for state: BackupViewController.State) -> NSAttributedString { (state.hasAnyBackups ? "backup_screen_found_action" : "backup_screen_not_found_action") diff --git a/FlowCrypt/Controllers/Threads/AlertsFactory.swift b/FlowCrypt/Controllers/Threads/AlertsFactory.swift index a30854eed..e2bc9a188 100644 --- a/FlowCrypt/Controllers/Threads/AlertsFactory.swift +++ b/FlowCrypt/Controllers/Threads/AlertsFactory.swift @@ -35,9 +35,10 @@ class AlertsFactory { alert.addTextField { [weak self] tf in tf.isSecureTextEntry = true tf.delegate = self?.textFieldDelegate + tf.accessibilityIdentifier = "aid-message-passphrase-textfield" } let saveAction = UIAlertAction( - title: "ok".localized, + title: "save".localized, style: .default ) { _ in guard let textField = alert.textFields?.first, diff --git a/FlowCryptCommon/Trace.swift b/FlowCryptCommon/Trace.swift index 0820317e8..018ad20a7 100644 --- a/FlowCryptCommon/Trace.swift +++ b/FlowCryptCommon/Trace.swift @@ -29,7 +29,7 @@ public final class Trace { public func finish(roundedTo: Int = 3) -> String { let resultValue = result() - let timeValue: String = resultValue <= 1 ? " ms" : " sec" + let timeValue = resultValue <= 1 ? " ms" : " sec" return resultValue.roundedString(toPlace: roundedTo) + timeValue } diff --git a/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift b/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift index 741771897..f439ec62b 100644 --- a/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageActionCellNode.swift @@ -14,13 +14,18 @@ public final class MessageActionCellNode: CellNode { let text: NSAttributedString? let color: UIColor let image: UIImage? + let accessibilityIdentifier: String? - public init(text: NSAttributedString?, - color: UIColor, - image: UIImage?) { + public init( + text: NSAttributedString?, + color: UIColor, + image: UIImage?, + accessibilityIdentifier: String? + ) { self.text = text self.color = color self.image = image + self.accessibilityIdentifier = accessibilityIdentifier } } @@ -46,7 +51,7 @@ public final class MessageActionCellNode: CellNode { buttonNode.borderWidth = 1 buttonNode.cornerRadius = 6 buttonNode.contentHorizontalAlignment = .left - buttonNode.accessibilityIdentifier = "aid-message-password-cell" + buttonNode.accessibilityIdentifier = input.accessibilityIdentifier buttonNode.setAttributedTitle(input.text, for: .normal) buttonNode.setImage(input.image, for: .normal) diff --git a/appium/api-mocks/apis/google/google-data.ts b/appium/api-mocks/apis/google/google-data.ts index 0b8b02755..ac3335d72 100644 --- a/appium/api-mocks/apis/google/google-data.ts +++ b/appium/api-mocks/apis/google/google-data.ts @@ -342,6 +342,7 @@ export class GoogleData { const rawBase64 = Buffer.from(decodedRaw).toString('base64'); const msg = new GmailMsg({ labelIds: ['SENT'], id, raw: rawBase64, mimeMsg }); + DATA[this.acct].messages = DATA[this.acct].messages.filter(m => GoogleData.msgId(m) === mimeMsg.messageId); DATA[this.acct].messages.unshift(msg); }; diff --git a/appium/tests/screenobjects/mail-folder.screen.ts b/appium/tests/screenobjects/mail-folder.screen.ts index fa4597090..429a5224a 100644 --- a/appium/tests/screenobjects/mail-folder.screen.ts +++ b/appium/tests/screenobjects/mail-folder.screen.ts @@ -131,6 +131,11 @@ class MailFolderScreen extends BaseScreen { await ElementHelper.waitElementVisible(await this.helpBtn); } + checkIfFolderIsEmpty = async () => { + const emailCount = await this.getEmailCount(); + return emailCount === 0; + } + emptyFolder = async () => { await ElementHelper.waitAndClick(await this.emptyFolderBtn); await BaseScreen.clickConfirmButton(); diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index 20add91de..48a2756ba 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -11,17 +11,19 @@ const SELECTORS = { COMPOSE_SECURITY_MESSAGE: '~aid-message-text-view', RECIPIENTS_LIST: '~aid-recipients-list', PASSWORD_CELL: '~aid-message-password-cell', + PASSPHRASE_CELL: '~aid-message-passphrase-cell', ATTACHMENT_CELL: '~aid-attachment-cell-0', ATTACHMENT_NAME_LABEL: '~aid-attachment-title-label-0', DELETE_ATTACHMENT_BUTTON: '~aid-attachment-delete-button-0', RETURN_BUTTON: '~Return', SET_PASSWORD_BUTTON: '~Set', + SAVE_PASSPHRASE_BUTTON: '~Save', CANCEL_BUTTON: '~Cancel', BACK_BUTTON: '~aid-back-button', DELETE_BUTTON: '~aid-compose-delete', SEND_BUTTON: '~aid-compose-send', CONFIRM_DELETING: '~Delete', - MESSAGE_PASSWORD_MODAL: '~aid-message-password-modal', + MESSAGE_PASSPHRASE_TEXTFIELD: '~aid-message-passphrase-textfield', MESSAGE_PASSWORD_TEXTFIELD: '~aid-message-password-textfield', ALERT: "-ios predicate string:type == 'XCUIElementTypeAlert'", RECIPIENT_POPUP_EMAIL_NODE: '~aid-recipient-popup-email-node', @@ -98,18 +100,26 @@ class NewMessageScreen extends BaseScreen { return $(SELECTORS.CONFIRM_DELETING) } - get passwordCell() { - return $(SELECTORS.PASSWORD_CELL); + get passphraseCell() { + return $(SELECTORS.PASSPHRASE_CELL); } - get passwordModal() { - return $(SELECTORS.MESSAGE_PASSWORD_MODAL); + get passwordCell() { + return $(SELECTORS.PASSWORD_CELL); } get currentModal() { return $(SELECTORS.ALERT); } + get passphraseTextField() { + return $(SELECTORS.MESSAGE_PASSPHRASE_TEXTFIELD); + } + + get savePassphraseButton() { + return $(SELECTORS.SAVE_PASSPHRASE_BUTTON); + } + get passwordTextField() { return $(SELECTORS.MESSAGE_PASSWORD_TEXTFIELD); } @@ -395,10 +405,22 @@ class NewMessageScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.setPasswordButton); } + clickSavePassphraseButton = async () => { + await ElementHelper.waitAndClick(await this.savePassphraseButton); + } + clickCancelButton = async () => { await ElementHelper.waitAndClick(await this.cancelButton); } + checkPassphraseCellIsVisible = async () => { + await ElementHelper.waitElementVisible(await this.passphraseCell); + } + + clickPassphraseCell = async () => { + await ElementHelper.waitAndClick(await this.passphraseCell); + } + checkSetPasswordButton = async (isEnabled: boolean) => { const el = await this.setPasswordButton; expect(await el.isEnabled()).toBe(isEnabled); @@ -419,6 +441,11 @@ class NewMessageScreen extends BaseScreen { await this.clickSetPasswordButton(); } + setMessagePassphrase = async (passphrase: string) => { + await (await this.passphraseTextField).setValue(passphrase); + await this.clickSavePassphraseButton(); + } + clickComposeMessage = async () => { await ElementHelper.waitAndClick(await this.composeSecurityMessage); } diff --git a/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts b/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts index 50622ffd9..0e1861940 100644 --- a/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts +++ b/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts @@ -1,5 +1,8 @@ import { MockApi } from 'api-mocks/mock'; import { MockApiConfig } from 'api-mocks/mock-config'; +import { MockUserList } from 'api-mocks/mock-data'; +import { CommonData } from 'tests/data'; +import AppiumHelper from 'tests/helpers/AppiumHelper'; import { EmailScreen, MailFolderScreen, MenuBarScreen, NewMessageScreen, @@ -11,17 +14,27 @@ describe('COMPOSE EMAIL: ', () => { it('check drafts functionality', async () => { const mockApi = new MockApi(); + + const recipient = MockUserList.robot; const subject = 'Test 1'; + const draftSubject = "Draft subject"; + const draftText1 = 'Draft text'; + const updatedDraftText = 'Some new text'; + const draftText2 = 'Another draft'; mockApi.fesConfig = MockApiConfig.defaultEnterpriseFesConfiguration; mockApi.ekmConfig = MockApiConfig.defaultEnterpriseEkmConfiguration; mockApi.addGoogleAccount('e2e.enterprise.test@flowcrypt.com', { messages: [subject], }); + mockApi.attesterConfig = { + servedPubkeys: { + [MockUserList.robot.email]: MockUserList.robot.pub! + } + }; - const draftText1 = 'Draft text'; - const updatedDraftText = 'Some new text'; - const draftText2 = 'Another draft'; + const processArgs = CommonData.mockProcessArgs; + const passPhrase = CommonData.account.passPhrase; await mockApi.withMockedApis(async () => { await SplashScreen.mockLogin(); @@ -41,7 +54,6 @@ describe('COMPOSE EMAIL: ', () => { await EmailScreen.checkDraft(draftText1, 1); await EmailScreen.checkDraft(draftText2, 2); - await EmailScreen.openDraft(1); await NewMessageScreen.setComposeSecurityMessage(updatedDraftText); @@ -49,7 +61,6 @@ describe('COMPOSE EMAIL: ', () => { await EmailScreen.checkDraft(updatedDraftText, 1); await EmailScreen.checkDraft(draftText2, 2); - await EmailScreen.deleteDraft(1); await EmailScreen.clickBackButton(); @@ -62,9 +73,31 @@ describe('COMPOSE EMAIL: ', () => { await NewMessageScreen.clickDeleteButton(); await NewMessageScreen.confirmDelete(); + + await AppiumHelper.restartApp(processArgs); + + await MailFolderScreen.checkInboxScreen(); + await MailFolderScreen.clickCreateEmail(); + await NewMessageScreen.composeEmail(recipient.email, draftSubject, draftText1); + await NewMessageScreen.checkPassphraseCellIsVisible(); + await NewMessageScreen.clickBackButton(); + await NewMessageScreen.setMessagePassphrase(passPhrase); + + await MenuBarScreen.clickMenuBtn(); + await MenuBarScreen.clickDraftsButton(); + await MailFolderScreen.clickOnEmailBySubject(draftSubject); + await NewMessageScreen.clickSendButton(); + await NewMessageScreen.clickBackButton(); + await MailFolderScreen.checkIfFolderIsEmpty(); + await MenuBarScreen.clickMenuBtn(); + await MenuBarScreen.clickSentButton(); + await MailFolderScreen.clickOnEmailBySubject(draftSubject); + + await browser.pause(600000); }); }); }); // check passphrase modal after app restart -// check compose new draft is added to drafts folder \ No newline at end of file +// check compose new draft is added to drafts folder +// check sending message from drafts \ No newline at end of file From 835c084ee19a94e6b699b55be0b35b2b62d079a6 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 29 Sep 2022 16:07:57 +0300 Subject: [PATCH 27/56] only encrypt drafts, without signing --- BuildTools/Package.swift | 2 +- .../Compose/ComposeViewController.swift | 3 +-- .../ComposeViewController+Drafts.swift | 14 +++++----- .../ComposeViewController+ErrorHandling.swift | 1 - .../ComposeViewController+MessageSend.swift | 2 +- .../ComposeViewController+Nodes.swift | 13 --------- .../ComposeViewController+Setup.swift | 27 ++++++++----------- .../ComposeViewController+TableView.swift | 4 --- .../Controllers/Threads/AlertsFactory.swift | 2 +- .../Threads/ThreadDetailsViewController.swift | 2 +- .../ComposeMessageService.swift | 2 +- .../tests/screenobjects/new-message.screen.ts | 27 ------------------- .../CheckDraftFunctionality.spec.ts | 17 +++--------- 13 files changed, 29 insertions(+), 87 deletions(-) diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift index a935ad029..412fc7b85 100644 --- a/BuildTools/Package.swift +++ b/BuildTools/Package.swift @@ -6,7 +6,7 @@ let package = Package( name: "BuildTools", platforms: [.macOS(.v10_11)], dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.49.18"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.0"), ], targets: [.target(name: "BuildTools", path: "")] ) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index ca56ce272..ef9cbb478 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -73,7 +73,6 @@ final class ComposeViewController: TableNodeViewController { weak var saveDraftTimer: Timer? var composedLatestDraft: ComposedDraft? - var signingKeyWithMissingPassphrase: Keypair? var messagePasswordAlertController: UIAlertController? lazy var alertsFactory = AlertsFactory() @@ -185,7 +184,7 @@ final class ComposeViewController: TableNodeViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) node.view.endEditing(true) - stopDraftTimer() + stopDraftTimer(withSave: false) navigationController?.interactivePopGestureRecognizer?.isEnabled = true } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 6c109eaf7..47b9c1651 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -15,10 +15,15 @@ extension ComposeViewController { saveDraftTimer?.fire() } - @objc func stopDraftTimer() { + @objc func stopDraftTimer(withSave: Bool = true) { + guard saveDraftTimer != nil else { return } + saveDraftTimer?.invalidate() saveDraftTimer = nil - saveDraftIfNeeded() + + if withSave { + saveDraftIfNeeded() + } } private func createDraft() -> ComposedDraft? { @@ -57,10 +62,7 @@ extension ComposeViewController { composedLatestDraft = draft completion?(nil) } catch { - if case .missingPassPhrase(let keyPair) = error as? ComposeMessageError { - signingKeyWithMissingPassphrase = keyPair - reload(sections: [.passphrase]) - } else if !(error is MessageValidationError) { + if !(error is MessageValidationError) { // no need to save or notify user if validation error // for other errors show toast // todo - should make sure that the toast doesn't hide the keyboard. Also should be toasted on top when keyboard open? diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift index b65b78da8..b3d493eec 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift @@ -31,7 +31,6 @@ extension ComposeViewController { ) // TODO: make more readable if matched { - self.signingKeyWithMissingPassphrase = nil if isDraft { if self.didFinishSetup { if withDiscard { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index c00e300a3..fde1de808 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -13,7 +13,7 @@ import FlowCryptUI extension ComposeViewController { func sendMessage() async throws { view.endEditing(true) - stopDraftTimer() + stopDraftTimer(withSave: false) navigationItem.rightBarButtonItem?.isEnabled = false let spinnerTitle = contextToSend.attachments.isEmpty ? "sending_title" : "encrypting_title" diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index fd41a2146..963f9e5f1 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -118,19 +118,6 @@ extension ComposeViewController { reload(sections: [.recipients(.from)]) } - func messagePassPhraseNode() -> ASCellNode { - MessageActionCellNode( - input: decorator.styledMessagePassPhraseInput(), - action: { [weak self] in - guard let self = self, - let keyPair = self.signingKeyWithMissingPassphrase - else { return } - - self.requestMissingPassPhraseWithModal(for: keyPair, isDraft: true) - } - ) - } - func messagePasswordNode() -> ASCellNode { let input = contextToSend.hasMessagePassword ? decorator.styledFilledMessagePasswordInput() diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 3485ac33e..c1d153783 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -153,24 +153,19 @@ extension ComposeViewController { // MARK: - NavigationChildController extension ComposeViewController: NavigationChildController { func handleBackButtonTap() { - stopDraftTimer() + stopDraftTimer(withSave: false) - if let keyPair = signingKeyWithMissingPassphrase { - requestMissingPassPhraseWithModal(for: keyPair, isDraft: true, withDiscard: true) - } else { - saveDraftIfNeeded(withAlert: true) { [weak self] error in - guard let self = self else { return } - if case .missingPassPhrase(let keyPair) = error as? ComposeMessageError { - self.requestMissingPassPhraseWithModal(for: keyPair, isDraft: true, withDiscard: true) - } else if let error = error { - self.handle(error: error) - } else { - if var messageIdentifier = self.composeMessageService.messageIdentifier { - messageIdentifier.draftMessageId = self.input.type.info?.id - self.handleAction?(.update(messageIdentifier)) - } - self.navigationController?.popViewController(animated: true) + saveDraftIfNeeded(withAlert: true) { [weak self] error in + guard let self = self else { return } + + if let error = error { + self.handle(error: error) + } else { + if var messageIdentifier = self.composeMessageService.messageIdentifier { + messageIdentifier.draftMessageId = self.input.type.info?.id + self.handleAction?(.update(messageIdentifier)) } + self.navigationController?.popViewController(animated: true) } } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift index ff2f7a024..36eba7678 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift @@ -20,8 +20,6 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { guard let sectionItem = sectionsList[safe: section] else { return 0 } switch (state, sectionItem) { - case (.main, .passphrase): - return signingKeyWithMissingPassphrase != nil ? 1 : 0 case (.main, .recipientsLabel): return shouldShowEmailRecipientsLabel ? 1 : 0 case (.main, .recipients(.to)): @@ -62,8 +60,6 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { return self.recipientsNode(type: recipientType) case (.main, .recipientsLabel): return self.recipientTextNode() - case (.main, .passphrase): - return self.messagePassPhraseNode() case (.main, .password): return self.messagePasswordNode() case (.main, .compose): diff --git a/FlowCrypt/Controllers/Threads/AlertsFactory.swift b/FlowCrypt/Controllers/Threads/AlertsFactory.swift index e2bc9a188..2ec429189 100644 --- a/FlowCrypt/Controllers/Threads/AlertsFactory.swift +++ b/FlowCrypt/Controllers/Threads/AlertsFactory.swift @@ -38,7 +38,7 @@ class AlertsFactory { tf.accessibilityIdentifier = "aid-message-passphrase-textfield" } let saveAction = UIAlertAction( - title: "save".localized, + title: "ok".localized, style: .default ) { _ in guard let textField = alert.textFields?.first, diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index e68ab330b..cebb1aa5c 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -460,7 +460,7 @@ extension ThreadDetailsViewController { withDuration: 0.2, animations: { if indexPath.section < self.node.numberOfSections { - self.node.reloadSections([indexPath.section], with: .automatic) + self.node.reloadSections([indexPath.section], with: .automatic) } else { self.node.insertSections([indexPath.section], with: .automatic) } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 14d8f4b3e..11e92f1e5 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -155,7 +155,7 @@ final class ComposeMessageService { ignoreErrors: isDraft ) - let signingPrv = try await prepareSigningKey(senderEmail: contextToSend.sender) + let signingPrv = isDraft ? nil : try await prepareSigningKey(senderEmail: contextToSend.sender) return SendableMsg( text: contextToSend.message ?? "", diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index 48a2756ba..a4d4b994e 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -11,13 +11,11 @@ const SELECTORS = { COMPOSE_SECURITY_MESSAGE: '~aid-message-text-view', RECIPIENTS_LIST: '~aid-recipients-list', PASSWORD_CELL: '~aid-message-password-cell', - PASSPHRASE_CELL: '~aid-message-passphrase-cell', ATTACHMENT_CELL: '~aid-attachment-cell-0', ATTACHMENT_NAME_LABEL: '~aid-attachment-title-label-0', DELETE_ATTACHMENT_BUTTON: '~aid-attachment-delete-button-0', RETURN_BUTTON: '~Return', SET_PASSWORD_BUTTON: '~Set', - SAVE_PASSPHRASE_BUTTON: '~Save', CANCEL_BUTTON: '~Cancel', BACK_BUTTON: '~aid-back-button', DELETE_BUTTON: '~aid-compose-delete', @@ -100,10 +98,6 @@ class NewMessageScreen extends BaseScreen { return $(SELECTORS.CONFIRM_DELETING) } - get passphraseCell() { - return $(SELECTORS.PASSPHRASE_CELL); - } - get passwordCell() { return $(SELECTORS.PASSWORD_CELL); } @@ -116,10 +110,6 @@ class NewMessageScreen extends BaseScreen { return $(SELECTORS.MESSAGE_PASSPHRASE_TEXTFIELD); } - get savePassphraseButton() { - return $(SELECTORS.SAVE_PASSPHRASE_BUTTON); - } - get passwordTextField() { return $(SELECTORS.MESSAGE_PASSWORD_TEXTFIELD); } @@ -405,22 +395,10 @@ class NewMessageScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.setPasswordButton); } - clickSavePassphraseButton = async () => { - await ElementHelper.waitAndClick(await this.savePassphraseButton); - } - clickCancelButton = async () => { await ElementHelper.waitAndClick(await this.cancelButton); } - checkPassphraseCellIsVisible = async () => { - await ElementHelper.waitElementVisible(await this.passphraseCell); - } - - clickPassphraseCell = async () => { - await ElementHelper.waitAndClick(await this.passphraseCell); - } - checkSetPasswordButton = async (isEnabled: boolean) => { const el = await this.setPasswordButton; expect(await el.isEnabled()).toBe(isEnabled); @@ -441,11 +419,6 @@ class NewMessageScreen extends BaseScreen { await this.clickSetPasswordButton(); } - setMessagePassphrase = async (passphrase: string) => { - await (await this.passphraseTextField).setValue(passphrase); - await this.clickSavePassphraseButton(); - } - clickComposeMessage = async () => { await ElementHelper.waitAndClick(await this.composeSecurityMessage); } diff --git a/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts b/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts index 0e1861940..cbfa8b3a3 100644 --- a/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts +++ b/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts @@ -1,8 +1,6 @@ import { MockApi } from 'api-mocks/mock'; import { MockApiConfig } from 'api-mocks/mock-config'; import { MockUserList } from 'api-mocks/mock-data'; -import { CommonData } from 'tests/data'; -import AppiumHelper from 'tests/helpers/AppiumHelper'; import { EmailScreen, MailFolderScreen, MenuBarScreen, NewMessageScreen, @@ -33,9 +31,6 @@ describe('COMPOSE EMAIL: ', () => { } }; - const processArgs = CommonData.mockProcessArgs; - const passPhrase = CommonData.account.passPhrase; - await mockApi.withMockedApis(async () => { await SplashScreen.mockLogin(); await SetupKeyScreen.setPassPhrase(); @@ -74,14 +69,14 @@ describe('COMPOSE EMAIL: ', () => { await NewMessageScreen.clickDeleteButton(); await NewMessageScreen.confirmDelete(); - await AppiumHelper.restartApp(processArgs); + await EmailScreen.clickBackButton(); + await MenuBarScreen.clickMenuBtn(); + await MenuBarScreen.clickInboxButton(); await MailFolderScreen.checkInboxScreen(); await MailFolderScreen.clickCreateEmail(); await NewMessageScreen.composeEmail(recipient.email, draftSubject, draftText1); - await NewMessageScreen.checkPassphraseCellIsVisible(); await NewMessageScreen.clickBackButton(); - await NewMessageScreen.setMessagePassphrase(passPhrase); await MenuBarScreen.clickMenuBtn(); await MenuBarScreen.clickDraftsButton(); @@ -96,8 +91,4 @@ describe('COMPOSE EMAIL: ', () => { await browser.pause(600000); }); }); -}); - -// check passphrase modal after app restart -// check compose new draft is added to drafts folder -// check sending message from drafts \ No newline at end of file +}); \ No newline at end of file From 8370b848cfef12a164ff16a84aa231ccb7a60631 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 30 Sep 2022 15:34:26 +0300 Subject: [PATCH 28/56] code cleanup --- .../Compose/ComposeViewController.swift | 8 ++--- .../Compose/ComposeViewDecorator.swift | 9 ------ .../ComposeViewController+ErrorHandling.swift | 1 - .../ComposeViewController+State.swift | 4 +-- .../ComposeViewController+TableView.swift | 2 +- .../ComposeViewController+TapActions.swift | 32 +++++++++---------- .../Controllers/Inbox/InboxProviders.swift | 17 +++------- .../Inbox/InboxViewController.swift | 32 ++++++------------- .../ComposeMessageService.swift | 9 ++---- 9 files changed, 38 insertions(+), 76 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index ef9cbb478..34bc8a92c 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -29,7 +29,7 @@ final class ComposeViewController: TableNodeViewController { } enum Section: Hashable { - case passphrase, recipientsLabel, recipients(RecipientType), password, compose, attachments, searchResults, contacts + case recipientsLabel, recipients(RecipientType), password, compose, attachments, searchResults, contacts static var recipientsSections: [Section] { RecipientType.allCases.map { Self.recipients($0) } @@ -259,6 +259,6 @@ final class ComposeViewController: TableNodeViewController { extension ComposeViewController: FilesManagerPresenter {} /* - - show empty view as inbox table header - - check drafts for forward and reply all -*/ + - improve InboxItem 'var title' + - improve fetchUpdatedInboxItem + */ diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 0cfc552d9..949149d26 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -127,15 +127,6 @@ struct ComposeViewDecorator { return (text + message).attributed(.regular(17)) } - func styledMessagePassPhraseInput() -> MessageActionCellNode.Input { - messageActionInput( - text: "compose_draft_passphrase_placeholder".localized, - color: .warningColor, - imageName: "square.and.pencil", - accessibilityIdentifier: "aid-message-passphrase-cell" - ) - } - func styledEmptyMessagePasswordInput() -> MessageActionCellNode.Input { messageActionInput( text: "compose_password_placeholder".localized, diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift index b3d493eec..4e3181d76 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift @@ -44,7 +44,6 @@ extension ComposeViewController { } else { self.handleSendTap() } - self.reload(sections: [.passphrase]) } else { self.handle(error: ComposeMessageError.passPhraseNoMatch) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift index 93bc41eb3..311624639 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+State.swift @@ -20,11 +20,11 @@ extension ComposeViewController { switch state { case .main: - sectionsList = [.passphrase] + Section.recipientsSections + [.recipientsLabel, .password, .compose, .attachments] + sectionsList = Section.recipientsSections + [.recipientsLabel, .password, .compose, .attachments] node.reloadData() case .searchEmails: let previousSectionsCount = sectionsList.count - sectionsList = [.passphrase] + Section.recipientsSections + [.searchResults, .contacts] + sectionsList = Section.recipientsSections + [.searchResults, .contacts] let deletedSectionsCount = previousSectionsCount - sectionsList.count diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift index 36eba7678..875bf6d4f 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift @@ -56,7 +56,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case (_, .recipients(.from)): return self.fromCellNode() case (_, .recipients(.to)), (_, .recipients(.cc)), (_, .recipients(.bcc)): - let recipientType = RecipientType.allCases[indexPath.section - 1] + let recipientType = RecipientType.allCases[indexPath.section] return self.recipientsNode(type: recipientType) case (.main, .recipientsLabel): return self.recipientTextNode() diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index bd320f265..a0294d40e 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -37,27 +37,25 @@ extension ComposeViewController { actionButtonTitle: "delete".localized, actionStyle: .destructive, onAction: { [weak self] _ in - guard let self = self else { return } - Task { - do { - let messageId = self.input.type.info?.id - try await self.composeMessageService.deleteDraft(messageId: messageId) + self?.deleteDraft() + } + ) + } - if let messageId = messageId { - let identifier = MessageIdentifier( - threadId: Identifier(stringId: self.input.type.info?.threadId), - messageId: messageId - ) - self.handleAction?(.delete(identifier)) - } + private func deleteDraft() { + Task { + do { + try await composeMessageService.deleteDraft() - self.navigationController?.popViewController(animated: true) - } catch { - self.handle(error: error) - } + if let messageIdentifier = composeMessageService.messageIdentifier { + handleAction?(.delete(messageIdentifier)) } + + navigationController?.popViewController(animated: true) + } catch { + handle(error: error) } - ) + } } @objc func handleTableTap() { diff --git a/FlowCrypt/Controllers/Inbox/InboxProviders.swift b/FlowCrypt/Controllers/Inbox/InboxProviders.swift index 93aeb3ccc..c325f5672 100644 --- a/FlowCrypt/Controllers/Inbox/InboxProviders.swift +++ b/FlowCrypt/Controllers/Inbox/InboxProviders.swift @@ -29,26 +29,17 @@ class InboxMessageThreadsProvider: InboxDataProvider { func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxItem? { guard let id = identifier.stringId else { return nil } let thread = try await provider.fetchThread(identifier: id, path: path) - return InboxItem( - messages: thread.messages, - folderPath: path, - type: .thread(identifier) - ) + return InboxItem(thread: thread, folderPath: path) } func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { let result = try await provider.fetchThreads(using: context) let inboxData = result.threads - .map { - InboxItem( - thread: $0, - folderPath: context.folderPath - ) - } - .sorted(by: { + .map { InboxItem(thread: $0, folderPath: context.folderPath) } + .sorted { $0.latestMessageDate(with: context.folderPath) > $1.latestMessageDate(with: context.folderPath) - }) + } let inboxContext = InboxContext( data: inboxData, diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 645a7ddf2..63eb2530d 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -644,11 +644,9 @@ extension InboxViewController { appContext: appContext, input: .init(type: .draft(draftInfo)), handleAction: { [weak self] action in - guard let self = self else { return } - switch action { case .update(let identifier), .sent(let identifier), .delete(let identifier): - self.fetchUpdatedInboxItem(identifier: identifier) + self?.fetchUpdatedInboxItem(identifier: identifier) } } ) @@ -682,16 +680,15 @@ extension InboxViewController { switch inboxInput[index].type { case .thread(let threadId): - guard let inboxItem = try await inboxDataProvider.fetchInboxItem(identifier: threadId, path: path) - else { return } - - if inboxItem.messages(with: path).isEmpty { - self.inboxInput.remove(at: index) - self.tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) - } else { - self.inboxInput[index] = inboxItem - self.tableNode.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + guard let inboxItem = try? await inboxDataProvider.fetchInboxItem(identifier: threadId, path: path), + !inboxItem.messages(with: path).isEmpty + else { + inboxInput.remove(at: index) + tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + return } + inboxInput[index] = inboxItem + tableNode.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) case .message(let messageId): break } @@ -722,14 +719,3 @@ extension InboxViewController { } } } - -/* -open draft: - - update - - fetch updated thread - - sent - - fetch updated thread and check if drafts there - - delete - - fetch updated thread - - */ diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 11e92f1e5..56c568147 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -245,12 +245,9 @@ final class ComposeMessageService { } } - func deleteDraft(messageId: Identifier?) async throws { - if let draftId = messageIdentifier?.draftId { - try await draftGateway?.deleteDraft(with: draftId) - } else if let messageId = messageId { - try await messageOperationsProvider.deleteMessage(id: messageId, from: nil) - } + func deleteDraft() async throws { + guard let draftId = messageIdentifier?.draftId else { return } + try await draftGateway?.deleteDraft(with: draftId) } // MARK: - Encrypt and Send From dd922f4d5c66a3c16d9415eaaec9f0b3aae5b351 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 30 Sep 2022 21:51:56 +0300 Subject: [PATCH 29/56] code cleanup --- .../Compose/ComposeViewController.swift | 3 +- .../ComposeViewController+Setup.swift | 79 ++++++----- .../Inbox/InboxViewController.swift | 52 ++++--- .../Threads/ThreadDetailsViewController.swift | 130 +++++++++++------- .../Gmail+MessageOperations.swift | 4 +- .../Threads/Imap+ThreadOperations.swift | 10 +- .../MessagesThreadOperationsProvider.swift | 14 +- 7 files changed, 163 insertions(+), 129 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 34bc8a92c..64c1b77fa 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -259,6 +259,5 @@ final class ComposeViewController: TableNodeViewController { extension ComposeViewController: FilesManagerPresenter {} /* - - improve InboxItem 'var title' - - improve fetchUpdatedInboxItem + - split decryptAndProcess in MessageService for parsing drafts */ diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index c1d153783..96ec1c9e9 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -67,7 +67,19 @@ extension ComposeViewController { } contextToSend.subject = info.subject + addRecipients(from: info) + if input.isPgp { + decodeDraft(from: info) + } else { + if case .draft = input.type { + contextToSend.message = input.text + } + reload(sections: Section.recipientsSections) + } + } + + private func addRecipients(from info: ComposeMessageInput.MessageQuoteInfo) { for recipient in info.recipients { add(recipient: recipient, type: .to) } @@ -83,45 +95,40 @@ extension ComposeViewController { if info.ccRecipients.isNotEmpty || info.bccRecipients.isNotEmpty { shouldShowAllRecipientTypes.toggle() } + } - if input.isPgp { - let message = Message( - identifier: .random, - date: info.sentDate, - sender: info.sender, - subject: info.subject, - size: nil, - labels: [], - attachmentIds: [], - body: .init(text: info.text, html: nil) - ) - Task { - do { - let processedMessage = try await messageService.decryptAndProcess( - message: message, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager - ) - contextToSend.message = processedMessage.text - setupTextNode() - reload(sections: [.compose]) - didFinishSetup = true - } catch { - if case .missingPassPhrase(let keyPair) = error as? MessageServiceError, let keyPair = keyPair { - requestMissingPassPhraseWithModal(for: keyPair, isDraft: true) - return - } else { - handle(error: error) - } + private func decodeDraft(from info: ComposeMessageInput.MessageQuoteInfo) { + let message = Message( + identifier: .random, + date: info.sentDate, + sender: info.sender, + subject: info.subject, + size: nil, + labels: [], + attachmentIds: [], + body: .init(text: info.text, html: nil) + ) + + Task { + do { + let processedMessage = try await messageService.decryptAndProcess( + message: message, + onlyLocalKeys: false, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + contextToSend.message = processedMessage.text + setupTextNode() + reload(sections: [.compose]) + didFinishSetup = true + } catch { + if case .missingPassPhrase(let keyPair) = error as? MessageServiceError, let keyPair = keyPair { + requestMissingPassPhraseWithModal(for: keyPair, isDraft: true) + return + } else { + handle(error: error) } } - } else { - if case .draft = input.type { - contextToSend.message = input.text - } - reload(sections: Section.recipientsSections) - didFinishSetup = true } } diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 63eb2530d..7922ac73c 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -658,26 +658,39 @@ extension InboxViewController { } private func fetchUpdatedInboxItem(identifier: MessageIdentifier) { - Task { - guard let index = inboxInput.firstIndex(where: { - switch $0.type { - case .thread(let threadId): - return threadId == identifier.threadId - case .message(let messageId): - return messageId == identifier.messageId - } - }) else { - if let threadId = identifier.threadId { - if let inboxItem = try await inboxDataProvider.fetchInboxItem(identifier: threadId, path: path) { - if !inboxItem.messages(with: path).isEmpty { - self.inboxInput.insert(inboxItem, at: 0) - self.tableNode.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic) - } - } - } - return + guard let index = findInboxItemIndex(identifier: identifier) else { + addInboxItem(identifier: identifier) + return + } + + updateInboxItem(at: index) + } + + private func findInboxItemIndex(identifier: MessageIdentifier) -> Int? { + return inboxInput.firstIndex(where: { + switch $0.type { + case .thread(let threadId): + return threadId == identifier.threadId + case .message(let messageId): + return messageId == identifier.messageId } + }) + } + + private func addInboxItem(identifier: MessageIdentifier) { + Task { + guard let threadId = identifier.threadId, + let inboxItem = try await inboxDataProvider.fetchInboxItem(identifier: threadId, path: path), + !inboxItem.messages(with: path).isEmpty + else { return } + inboxInput.insert(inboxItem, at: 0) + tableNode.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic) + } + } + + private func updateInboxItem(at index: Int) { + Task { switch inboxInput[index].type { case .thread(let threadId): guard let inboxItem = try? await inboxDataProvider.fetchInboxItem(identifier: threadId, path: path), @@ -689,7 +702,8 @@ extension InboxViewController { } inboxInput[index] = inboxItem tableNode.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) - case .message(let messageId): + case .message: + // used only with imap, can be implemented later break } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index cebb1aa5c..ae1cb9d55 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -109,7 +109,11 @@ final class ThreadDetailsViewController: TableNodeViewController { private func expandThreadMessageAndMarkAsRead() { Task { - try await threadOperationsProvider.mark(id: inboxItem.threadId, asRead: true, in: inboxItem.folderPath) + try await threadOperationsProvider.mark( + messagesIds: inboxItem.messages.map(\.identifier), + asRead: true, + in: inboxItem.folderPath + ) } let indexOfSectionToExpand = input.firstIndex(where: { !$0.rawMessage.isRead }) ?? input.lastIndex(where: { !$0.rawMessage.isDraft }) @@ -212,54 +216,79 @@ extension ThreadDetailsViewController { onComposeMessageAction?(action) switch action { - case .update(let messageIdentifier): - Task { - guard let messageId = messageIdentifier.messageId else { return } + case .update(let identifier): + updateMessage(identifier: identifier) + case .sent(let identifier): + handleSentMessage(identifier: identifier) + case .delete(let identifier): + deleteMessage(identifier: identifier) + } + } - let processedMessage = try await messageService.getAndProcess( - identifier: messageId, - folder: inboxItem.folderPath, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager - ) + private func updateMessage(identifier: MessageIdentifier) { + Task { + guard let messageId = identifier.messageId else { return } - let indexPath: IndexPath - if let index = input.firstIndex(where: { $0.rawMessage.identifier == messageIdentifier.draftMessageId }) { - indexPath = IndexPath(row: 0, section: index + 1) - } else { - indexPath = IndexPath(row: 0, section: input.count + 1) - } + let processedMessage = try await getAndProcessMessage( + identifier: messageId, + folder: inboxItem.folderPath + ) - self.handle(processedMessage: processedMessage, at: indexPath) + let section: Int + if let index = input.firstIndex(where: { $0.rawMessage.identifier == identifier.draftMessageId }) { + section = index + 1 + } else { + section = input.count + 1 } - case .sent(let messageIdentifier): - Task { - if let draftId = messageIdentifier.draftId, let index = input.firstIndex(where: { $0.rawMessage.identifier == draftId }) { - input.remove(at: index) - node.deleteSections([index + 1], with: .automatic) - } - guard let identifier = messageIdentifier.messageId else { return } // todo - throw - let processedMessage = try await messageService.getAndProcess( - identifier: identifier, - folder: inboxItem.folderPath, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager - ) - let indexPath = IndexPath(row: 0, section: self.input.count + 1) - self.handle(processedMessage: processedMessage, at: indexPath) + handle(processedMessage: processedMessage, at: IndexPath(row: 0, section: section)) + } + } + + private func handleSentMessage(identifier: MessageIdentifier) { + Task { + if let draftId = identifier.draftId, + let index = input.firstIndex(where: { $0.rawMessage.identifier == draftId }) { + input.remove(at: index) + node.deleteSections([index + 1], with: .automatic) } - case .delete(let messageIdentifier): - guard let index = input.firstIndex(where: { $0.rawMessage.identifier == messageIdentifier.messageId }) - else { return } - input.remove(at: index) - node.deleteSections([index + 1], with: .automatic) + guard let messageId = identifier.messageId else { return } + + let processedMessage = try await getAndProcessMessage( + identifier: messageId, + folder: inboxItem.folderPath + ) + let indexPath = IndexPath(row: 0, section: input.count + 1) + handle(processedMessage: processedMessage, at: indexPath) } } + private func deleteMessage(identifier: MessageIdentifier) { + guard let messageId = identifier.messageId, + let index = input.firstIndex(where: { + $0.rawMessage.identifier == messageId + }) + else { return } + + input.remove(at: index) + node.deleteSections([index + 1], with: .automatic) + } + + private func getAndProcessMessage( + identifier: Identifier, + folder: String, + onlyLocalKeys: Bool = false + ) async throws -> ProcessedMessage { + return try await messageService.getAndProcess( + identifier: identifier, + folder: folder, + onlyLocalKeys: onlyLocalKeys, + userEmail: appContext.user.email, + isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + ) + } + private func createComposeNewMessageAlertAction(at indexPath: IndexPath, type: MessageQuoteType) -> UIAlertAction { let action = UIAlertAction( title: type.actionLabel, @@ -418,12 +447,10 @@ extension ThreadDetailsViewController { Task { do { - var processedMessage = try await messageService.getAndProcess( + var processedMessage = try await getAndProcessMessage( identifier: message.identifier, folder: inboxItem.folderPath, - onlyLocalKeys: true, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + onlyLocalKeys: true ) if case .missingPubkey = processedMessage.signature { @@ -594,12 +621,9 @@ extension ThreadDetailsViewController { ) { Task { do { - let processedMessage = try await messageService.getAndProcess( + let processedMessage = try await getAndProcessMessage( identifier: message.identifier, - folder: inboxItem.folderPath, - onlyLocalKeys: false, - userEmail: appContext.user.email, - isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager + folder: inboxItem.folderPath ) handle(processedMessage: processedMessage, at: indexPath) } catch { @@ -671,11 +695,17 @@ extension ThreadDetailsViewController: MessageActionsHandler { switch action { case .archive: - try await threadOperationsProvider.archive(messages: inboxItem.messages, in: inboxItem.folderPath) + try await threadOperationsProvider.archive( + messagesIds: inboxItem.messages.map(\.identifier), + in: inboxItem.folderPath + ) case .markAsRead(let isRead): guard !isRead else { return } Task { // Run mark as unread operation in another thread - try await threadOperationsProvider.mark(id: inboxItem.threadId, asRead: false, in: inboxItem.folderPath) + try await threadOperationsProvider.markThreadAsUnread( + id: inboxItem.threadId, + folder: inboxItem.folderPath + ) } case .moveToTrash: try await threadOperationsProvider.moveThreadToTrash(id: inboxItem.threadId, labels: inboxItem.labels) diff --git a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift index 5d20a53be..4264d8250 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessageOperations Provider/Gmail+MessageOperations.swift @@ -90,12 +90,12 @@ extension GmailService: MessageOperationsProvider { } func batchUpdate( - messages: [Message], + messagesIds: [Identifier], labelsToAdd: [MessageLabel] = [], labelsToRemove: [MessageLabel] = [] ) async throws { let request = GTLRGmail_BatchModifyMessagesRequest() - request.ids = messages.compactMap(\.identifier.stringId) + request.ids = messagesIds.compactMap(\.stringId) request.addLabelIds = labelsToAdd.map(\.value) request.removeLabelIds = labelsToRemove.map(\.value) let query = GTLRGmailQuery_UsersMessagesBatchModify.query( diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift b/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift index 56b27772b..121c53044 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/Imap+ThreadOperations.swift @@ -12,10 +12,6 @@ import Foundation extension Imap: MessagesThreadOperationsProvider { private var error: Error { AppErr.general("Doesn't support yet") } - func mark(id: String?, asRead: Bool, in folder: String) async throws { - throw error - } - func delete(id: String?) async throws { throw error } @@ -32,15 +28,11 @@ extension Imap: MessagesThreadOperationsProvider { throw error } - func markThreadAsRead(id: String?, folder: String) async throws { - throw error - } - func mark(messagesIds: [Identifier], asRead: Bool, in folder: String) async throws { throw error } - func archive(messages: [Message], in folder: String) async throws { + func archive(messagesIds: [Identifier], in folder: String) async throws { throw error } } diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift index 64b3f3bf0..45460e219 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadOperationsProvider.swift @@ -9,19 +9,15 @@ import GoogleAPIClientForREST_Gmail protocol MessagesThreadOperationsProvider { - func mark(id: String?, asRead: Bool, in folder: String) async throws func delete(id: String?) async throws func moveThreadToTrash(id: String?, labels: Set) async throws func moveThreadToInbox(id: String?) async throws func markThreadAsUnread(id: String?, folder: String) async throws func mark(messagesIds: [Identifier], asRead: Bool, in folder: String) async throws - func archive(messages: [Message], in folder: String) async throws + func archive(messagesIds: [Identifier], in folder: String) async throws } extension GmailService: MessagesThreadOperationsProvider { - func mark(id: String?, asRead: Bool, in folder: String) async throws { - } - func delete(id: String?) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in guard let id = id else { @@ -55,10 +51,6 @@ extension GmailService: MessagesThreadOperationsProvider { try await update(id: id, labelsToAdd: [.unread]) } - func markThreadAsRead(id: String?, folder: String) async throws { - try await update(id: id, labelsToRemove: [.unread]) - } - func mark(messagesIds: [Identifier], asRead: Bool, in folder: String) async throws { try await withThrowingTaskGroup(of: Void.self) { taskGroup in for id in messagesIds { @@ -73,11 +65,11 @@ extension GmailService: MessagesThreadOperationsProvider { } } - func archive(messages: [Message], in folder: String) async throws { + func archive(messagesIds: [Identifier], in folder: String) async throws { // manually updated each message rather than using update(thread:...) method // https://github.com/FlowCrypt/flowcrypt-ios/pull/1769#discussion_r932964129 try await batchUpdate( - messages: messages, + messagesIds: messagesIds, labelsToRemove: [.inbox] ) } From cfc31036a26845c5d345cebf18254909118ec731 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 30 Sep 2022 22:27:44 +0300 Subject: [PATCH 30/56] fix drafts test --- .../Compose/Extensions/ComposeViewController+Setup.swift | 1 + .../specs/mock/composeEmail/CheckDraftFunctionality.spec.ts | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 96ec1c9e9..7040ce931 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -76,6 +76,7 @@ extension ComposeViewController { contextToSend.message = input.text } reload(sections: Section.recipientsSections) + didFinishSetup = true } } diff --git a/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts b/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts index cbfa8b3a3..04caf61be 100644 --- a/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts +++ b/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts @@ -82,13 +82,10 @@ describe('COMPOSE EMAIL: ', () => { await MenuBarScreen.clickDraftsButton(); await MailFolderScreen.clickOnEmailBySubject(draftSubject); await NewMessageScreen.clickSendButton(); - await NewMessageScreen.clickBackButton(); await MailFolderScreen.checkIfFolderIsEmpty(); await MenuBarScreen.clickMenuBtn(); await MenuBarScreen.clickSentButton(); await MailFolderScreen.clickOnEmailBySubject(draftSubject); - - await browser.pause(600000); }); }); }); \ No newline at end of file From 852d67250fa3f3da19dc8331e843e03791ad90ba Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 3 Oct 2022 22:14:51 +0300 Subject: [PATCH 31/56] drafts test update --- .../Compose/ComposeViewController.swift | 2 +- .../ComposeViewController+Setup.swift | 4 - .../Threads/ThreadDetailsViewController.swift | 7 +- .../Message Provider/MessageService.swift | 45 +- FlowCrypt/Resources/flowcrypt-ios-prod.js.txt | 888 +++++++----------- .../api-mocks/apis/google/google-endpoints.ts | 3 +- appium/tests/screenobjects/email.screen.ts | 1 + 7 files changed, 390 insertions(+), 560 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 64c1b77fa..418cc9375 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -174,7 +174,7 @@ final class ComposeViewController: TableNodeViewController { setupUI() setupNavigationBar() - setupNodes() + setupSubjectNode() observeKeyboardNotifications() observerAppStates() observeComposeUpdates() diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 7040ce931..87d30c720 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -132,10 +132,6 @@ extension ComposeViewController { } } } - - func setupNodes() { - setupSubjectNode() - } } // MARK: - Search diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index ae1cb9d55..eedf75921 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -600,12 +600,13 @@ extension ThreadDetailsViewController { guard data.rawMessage.isDraft && data.rawMessage.isPgp && data.processedMessage == nil else { continue } let indexPath = IndexPath(row: 0, section: index + 1) do { - let processedMessage = try await messageService.decryptAndProcess( - message: data.rawMessage, - onlyLocalKeys: false, + let decryptedText = try await messageService.decrypt( + text: data.rawMessage.body.text, userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) + + let processedMessage = ProcessedMessage(message: data.rawMessage, text: decryptedText, type: .plain, attachments: []) handle(processedMessage: processedMessage, at: indexPath) } catch { handle(error: error, at: indexPath) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 48cb71da7..91c1ea65e 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -109,19 +109,50 @@ final class MessageService { } } - func decryptAndProcess( - message: Message, - onlyLocalKeys: Bool, - userEmail: String, - isUsingKeyManager: Bool - ) async throws -> ProcessedMessage { - let keys = try await keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: userEmail) + private func getKeypairs(email: String, isUsingKeyManager: Bool) async throws -> [Keypair] { + let keys = try await keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: email) + guard keys.isNotEmpty else { if isUsingKeyManager { throw MessageServiceError.emptyKeysForEKM } throw MessageServiceError.emptyKeys } + + return keys + } + + func decrypt( + text: String, + userEmail: String, + isUsingKeyManager: Bool + ) async throws -> String { + let keys = try await getKeypairs(email: userEmail, isUsingKeyManager: isUsingKeyManager) + + let decrypted = try await core.parseDecryptMsg( + encrypted: text.data(), + keys: keys, + msgPwd: nil, + isMime: false, + verificationPubKeys: [] + ) + + guard !hasMsgBlockThatNeedsPassPhrase(decrypted) else { + let keyPair = keys.first(where: { $0.passphrase == nil }) + throw MessageServiceError.missingPassPhrase(keyPair) + } + + return decrypted.text + } + + func decryptAndProcess( + message: Message, + onlyLocalKeys: Bool, + userEmail: String, + isUsingKeyManager: Bool + ) async throws -> ProcessedMessage { + let keys = try await getKeypairs(email: userEmail, isUsingKeyManager: isUsingKeyManager) + let verificationPubKeys = try await fetchVerificationPubKeys( for: message.sender, onlyLocal: onlyLocalKeys diff --git a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt index 4f94ba099..45b63755f 100644 --- a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt +++ b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt @@ -20636,7 +20636,7 @@ var time_estimates;time_estimates={estimate_attack_times:function(e){var t,n,s,o /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ -/***/ 110: +/***/ 111: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -20644,26 +20644,26 @@ var time_estimates;time_estimates={estimate_attack_times:function(e){var t,n,s,o global.dereq_asn1 = exports; -global.dereq_asn1.bignum = __webpack_require__(111); +global.dereq_asn1.bignum = __webpack_require__(112); -global.dereq_asn1.define = (__webpack_require__(113).define); -global.dereq_asn1.base = __webpack_require__(127); -global.dereq_asn1.constants = __webpack_require__(128); -global.dereq_asn1.decoders = __webpack_require__(124); -global.dereq_asn1.encoders = __webpack_require__(114); +global.dereq_asn1.define = (__webpack_require__(114).define); +global.dereq_asn1.base = __webpack_require__(128); +global.dereq_asn1.constants = __webpack_require__(129); +global.dereq_asn1.decoders = __webpack_require__(125); +global.dereq_asn1.encoders = __webpack_require__(115); /***/ }), -/***/ 113: +/***/ 114: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; -const encoders = __webpack_require__(114); -const decoders = __webpack_require__(124); -const inherits = __webpack_require__(116); +const encoders = __webpack_require__(115); +const decoders = __webpack_require__(125); +const inherits = __webpack_require__(117); const api = exports; @@ -20720,15 +20720,15 @@ Entity.prototype.encode = function encode(data, enc, /* internal */ reporter) { /***/ }), -/***/ 120: +/***/ 121: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); -const Reporter = (__webpack_require__(119).Reporter); -const Buffer = (__webpack_require__(117).Buffer); +const inherits = __webpack_require__(117); +const Reporter = (__webpack_require__(120).Reporter); +const Buffer = (__webpack_require__(118).Buffer); function DecoderBuffer(base, options) { Reporter.call(this, options); @@ -20881,7 +20881,7 @@ EncoderBuffer.prototype.join = function join(out, offset) { /***/ }), -/***/ 127: +/***/ 128: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -20889,24 +20889,24 @@ EncoderBuffer.prototype.join = function join(out, offset) { const base = exports; -base.Reporter = (__webpack_require__(119).Reporter); -base.DecoderBuffer = (__webpack_require__(120).DecoderBuffer); -base.EncoderBuffer = (__webpack_require__(120).EncoderBuffer); -base.Node = __webpack_require__(118); +base.Reporter = (__webpack_require__(120).Reporter); +base.DecoderBuffer = (__webpack_require__(121).DecoderBuffer); +base.EncoderBuffer = (__webpack_require__(121).EncoderBuffer); +base.Node = __webpack_require__(119); /***/ }), -/***/ 118: +/***/ 119: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -const Reporter = (__webpack_require__(119).Reporter); -const EncoderBuffer = (__webpack_require__(120).EncoderBuffer); -const DecoderBuffer = (__webpack_require__(120).DecoderBuffer); -const assert = __webpack_require__(121); +const Reporter = (__webpack_require__(120).Reporter); +const EncoderBuffer = (__webpack_require__(121).EncoderBuffer); +const DecoderBuffer = (__webpack_require__(121).DecoderBuffer); +const assert = __webpack_require__(122); // Supported tags const tags = [ @@ -21543,13 +21543,13 @@ Node.prototype._isPrintstr = function isPrintstr(str) { /***/ }), -/***/ 119: +/***/ 120: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); +const inherits = __webpack_require__(117); function Reporter(options) { this._reporterState = { @@ -21674,7 +21674,7 @@ ReporterError.prototype.rethrow = function rethrow(msg) { /***/ }), -/***/ 122: +/***/ 123: /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -21740,7 +21740,7 @@ exports.tagByName = reverse(exports.tag); /***/ }), -/***/ 128: +/***/ 129: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -21764,25 +21764,25 @@ constants._reverse = function reverse(map) { return res; }; -constants.der = __webpack_require__(122); +constants.der = __webpack_require__(123); /***/ }), -/***/ 125: +/***/ 126: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); +const inherits = __webpack_require__(117); -const bignum = __webpack_require__(111); -const DecoderBuffer = (__webpack_require__(120).DecoderBuffer); -const Node = __webpack_require__(118); +const bignum = __webpack_require__(112); +const DecoderBuffer = (__webpack_require__(121).DecoderBuffer); +const Node = __webpack_require__(119); // Import DER constants -const der = __webpack_require__(122); +const der = __webpack_require__(123); function DERDecoder(entity) { this.enc = 'der'; @@ -22112,7 +22112,7 @@ function derDecodeLen(buf, primitive, fail) { /***/ }), -/***/ 124: +/***/ 125: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -22120,22 +22120,22 @@ function derDecodeLen(buf, primitive, fail) { const decoders = exports; -decoders.der = __webpack_require__(125); -decoders.pem = __webpack_require__(126); +decoders.der = __webpack_require__(126); +decoders.pem = __webpack_require__(127); /***/ }), -/***/ 126: +/***/ 127: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); -const Buffer = (__webpack_require__(117).Buffer); +const inherits = __webpack_require__(117); +const Buffer = (__webpack_require__(118).Buffer); -const DERDecoder = __webpack_require__(125); +const DERDecoder = __webpack_require__(126); function PEMDecoder(entity) { DERDecoder.call(this, entity); @@ -22185,18 +22185,18 @@ PEMDecoder.prototype.decode = function decode(data, options) { /***/ }), -/***/ 115: +/***/ 116: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); -const Buffer = (__webpack_require__(117).Buffer); -const Node = __webpack_require__(118); +const inherits = __webpack_require__(117); +const Buffer = (__webpack_require__(118).Buffer); +const Node = __webpack_require__(119); // Import DER constants -const der = __webpack_require__(122); +const der = __webpack_require__(123); function DEREncoder(entity) { this.enc = 'der'; @@ -22488,7 +22488,7 @@ function encodeTag(tag, primitive, cls, reporter) { /***/ }), -/***/ 114: +/***/ 115: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -22496,21 +22496,21 @@ function encodeTag(tag, primitive, cls, reporter) { const encoders = exports; -encoders.der = __webpack_require__(115); -encoders.pem = __webpack_require__(123); +encoders.der = __webpack_require__(116); +encoders.pem = __webpack_require__(124); /***/ }), -/***/ 123: +/***/ 124: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -const inherits = __webpack_require__(116); +const inherits = __webpack_require__(117); -const DEREncoder = __webpack_require__(115); +const DEREncoder = __webpack_require__(116); function PEMEncoder(entity) { DEREncoder.call(this, entity); @@ -22691,7 +22691,7 @@ function fromByteArray (uint8) { /***/ }), -/***/ 111: +/***/ 112: /***/ (function(module, __unused_webpack_exports, __webpack_require__) { /* module decorator */ module = __webpack_require__.nmd(module); @@ -22750,7 +22750,7 @@ function fromByteArray (uint8) { if (typeof window !== 'undefined' && typeof window.Buffer !== 'undefined') { Buffer = window.Buffer; } else { - Buffer = (__webpack_require__(112).Buffer); + Buffer = (__webpack_require__(113).Buffer); } } catch (e) { } @@ -28351,7 +28351,7 @@ exports.write = function (buffer, value, offset, isLE, mLen, nBytes) { /***/ }), -/***/ 116: +/***/ 117: /***/ ((module) => { if (typeof Object.create === 'function') { @@ -28385,7 +28385,7 @@ if (typeof Object.create === 'function') { /***/ }), -/***/ 121: +/***/ 122: /***/ ((module) => { module.exports = assert; @@ -28403,7 +28403,7 @@ assert.equal = function assertEqual(l, r, msg) { /***/ }), -/***/ 117: +/***/ 118: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; @@ -28488,7 +28488,7 @@ module.exports = safer /***/ }), -/***/ 112: +/***/ 113: /***/ (() => { /* (ignored) */ @@ -28539,7 +28539,7 @@ module.exports = safer /******/ // startup /******/ // Load entry module and return exports /******/ // This entry module is referenced by other modules so it can't be inlined -/******/ var __webpack_exports__ = __webpack_require__(110); +/******/ var __webpack_exports__ = __webpack_require__(111); /******/ module.exports = __webpack_exports__; /******/ /******/ })() @@ -28555,20 +28555,20 @@ module.exports = safer Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getSigningPrv = exports.Endpoints = void 0; const format_output_1 = __webpack_require__(2); -const pgp_msg_1 = __webpack_require__(107); -const pgp_key_1 = __webpack_require__(102); +const pgp_msg_1 = __webpack_require__(108); +const pgp_key_1 = __webpack_require__(103); const mime_1 = __webpack_require__(22); -const att_1 = __webpack_require__(32); +const att_1 = __webpack_require__(33); const buf_1 = __webpack_require__(4); -const msg_block_parser_1 = __webpack_require__(34); -const pgp_password_1 = __webpack_require__(108); -const store_1 = __webpack_require__(103); +const msg_block_parser_1 = __webpack_require__(35); +const pgp_password_1 = __webpack_require__(109); +const store_1 = __webpack_require__(104); const common_1 = __webpack_require__(23); -const const_1 = __webpack_require__(106); -const validate_input_1 = __webpack_require__(109); -const xss_1 = __webpack_require__(35); -const const_2 = __webpack_require__(106); -const openpgp_1 = __webpack_require__(101); +const const_1 = __webpack_require__(107); +const validate_input_1 = __webpack_require__(110); +const xss_1 = __webpack_require__(36); +const const_2 = __webpack_require__(107); +const openpgp_1 = __webpack_require__(102); class Endpoints { constructor() { this.version = async () => { @@ -28921,7 +28921,7 @@ const msg_block_1 = __webpack_require__(3); const buf_1 = __webpack_require__(4); const mime_1 = __webpack_require__(22); const common_1 = __webpack_require__(23); -const xss_1 = __webpack_require__(35); +const xss_1 = __webpack_require__(36); const isContentBlock = (t) => { return t === 'plainText' || t === 'decryptedText' || t === 'plainHtml' || t === 'decryptedHtml' || t === 'signedMsg' || t === 'verifiedMsg'; @@ -37915,12 +37915,12 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Mime = void 0; const common_1 = __webpack_require__(23); const require_1 = __webpack_require__(24); -const att_1 = __webpack_require__(32); +const att_1 = __webpack_require__(33); const buf_1 = __webpack_require__(4); -const catch_1 = __webpack_require__(33); +const catch_1 = __webpack_require__(34); const msg_block_1 = __webpack_require__(3); -const msg_block_parser_1 = __webpack_require__(34); -const pgp_armor_1 = __webpack_require__(100); +const msg_block_parser_1 = __webpack_require__(35); +const pgp_armor_1 = __webpack_require__(101); const util_1 = __webpack_require__(5); const MimeParser = (0, require_1.requireMimeParser)(); const MimeBuilder = (0, require_1.requireMimeBuilder)(); @@ -38557,7 +38557,7 @@ const requireStreamReadToEnd = async () => { const runtime = globalThis.process?.release?.name || 'not node'; return runtime === 'not node' ? (await Promise.resolve().then(() => __webpack_require__(25))).readToEnd - : (__webpack_require__(31).readToEnd); + : (__webpack_require__(32).readToEnd); }; exports.requireStreamReadToEnd = requireStreamReadToEnd; const requireMimeParser = () => { @@ -38582,9 +38582,6 @@ exports.requireIso88592 = requireIso88592; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "ArrayStream": () => (/* reexport safe */ _writer__WEBPACK_IMPORTED_MODULE_3__.ArrayStream), -/* harmony export */ "ReadableStream": () => (/* binding */ ReadableStream), -/* harmony export */ "TransformStream": () => (/* binding */ TransformStream), -/* harmony export */ "WritableStream": () => (/* binding */ WritableStream), /* harmony export */ "cancel": () => (/* binding */ cancel), /* harmony export */ "clone": () => (/* binding */ clone), /* harmony export */ "concat": () => (/* binding */ concat), @@ -38596,15 +38593,12 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ "isArrayStream": () => (/* reexport safe */ _util__WEBPACK_IMPORTED_MODULE_0__.isArrayStream), /* harmony export */ "isStream": () => (/* reexport safe */ _util__WEBPACK_IMPORTED_MODULE_0__.isStream), /* harmony export */ "isUint8Array": () => (/* reexport safe */ _util__WEBPACK_IMPORTED_MODULE_0__.isUint8Array), -/* harmony export */ "loadStreamsPonyfill": () => (/* binding */ loadStreamsPonyfill), /* harmony export */ "nodeToWeb": () => (/* reexport safe */ _node_conversions__WEBPACK_IMPORTED_MODULE_1__.nodeToWeb), /* harmony export */ "parse": () => (/* binding */ parse), /* harmony export */ "passiveClone": () => (/* binding */ passiveClone), /* harmony export */ "pipe": () => (/* binding */ pipe), /* harmony export */ "readToEnd": () => (/* binding */ readToEnd), /* harmony export */ "slice": () => (/* binding */ slice), -/* harmony export */ "toNativeReadable": () => (/* binding */ toNativeReadable), -/* harmony export */ "toPonyfillReadable": () => (/* binding */ toPonyfillReadable), /* harmony export */ "toStream": () => (/* binding */ toStream), /* harmony export */ "transform": () => (/* binding */ transform), /* harmony export */ "transformPair": () => (/* binding */ transformPair), @@ -38613,38 +38607,14 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ }); /* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(26); /* harmony import */ var _node_conversions__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(29); -/* harmony import */ var _reader__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(30); +/* harmony import */ var _reader__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31); /* harmony import */ var _writer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(27); -let { ReadableStream, WritableStream, TransformStream } = globalThis; - -let toPonyfillReadable, toNativeReadable; - -async function loadStreamsPonyfill() { - if (TransformStream) { - return; - } - - const [ponyfill, adapter] = await Promise.all([ - __webpack_require__.e(/* import() */ 3).then(__webpack_require__.bind(__webpack_require__, 129)), - __webpack_require__.e(/* import() */ 4).then(__webpack_require__.bind(__webpack_require__, 130)) - ]); - - ({ ReadableStream, WritableStream, TransformStream } = ponyfill); - - const { createReadableStreamWrapper } = adapter; - - if (globalThis.ReadableStream && ReadableStream !== globalThis.ReadableStream) { - toPonyfillReadable = createReadableStreamWrapper(ReadableStream); - toNativeReadable = createReadableStreamWrapper(globalThis.ReadableStream); - } -} - -const NodeBuffer = _util__WEBPACK_IMPORTED_MODULE_0__.isNode && (__webpack_require__(6).Buffer); +const NodeBuffer = _util__WEBPACK_IMPORTED_MODULE_0__.isNode && (__webpack_require__(30).Buffer); /** * Convert data to Stream @@ -38656,9 +38626,6 @@ function toStream(input) { if (streamType === 'node') { return (0,_node_conversions__WEBPACK_IMPORTED_MODULE_1__.nodeToWeb)(input); } - if (streamType === 'web' && toPonyfillReadable) { - return toPonyfillReadable(input); - } if (streamType) { return input; } @@ -38717,7 +38684,7 @@ function concat(list) { */ function concatStream(list) { list = list.map(toStream); - const transform = transformWithCancel(async function (reason) { + const transform = transformWithCancel(async function(reason) { await Promise.all(transforms.map(stream => cancel(stream, reason))); }); let prev = Promise.resolve(); @@ -38794,7 +38761,7 @@ async function pipe(input, target, { preventAbort, preventCancel }); - } catch (e) { } + } catch(e) {} return; } input = toArrayStream(input); @@ -38852,9 +38819,9 @@ function transformWithCancel(cancel) { } }, cancel - }, { highWaterMark: 0 }), + }, {highWaterMark: 0}), writable: new WritableStream({ - write: async function (chunk) { + write: async function(chunk) { outputController.enqueue(chunk); if (!pulled) { await new Promise(resolve => { @@ -38904,7 +38871,7 @@ function transform(input, process = () => undefined, finish = () => undefined) { try { const result = await process(value); if (result !== undefined) controller.enqueue(result); - } catch (e) { + } catch(e) { controller.error(e); } }, @@ -38912,7 +38879,7 @@ function transform(input, process = () => undefined, finish = () => undefined) { try { const result = await finish(); if (result !== undefined) controller.enqueue(result); - } catch (e) { + } catch(e) { controller.error(e); } } @@ -38944,7 +38911,7 @@ function transformPair(input, fn) { const pipeDonePromise = pipe(input, incoming.writable); - const outgoing = transformWithCancel(async function (reason) { + const outgoing = transformWithCancel(async function(reason) { incomingTransformController.error(reason); await pipeDonePromise; await new Promise(setTimeout); @@ -39042,14 +39009,14 @@ function passiveClone(input) { await writer.ready; const { done, value } = await reader.read(); if (done) { - try { controller.close(); } catch (e) { } + try { controller.close(); } catch(e) {} await writer.close(); return; } - try { controller.enqueue(value); } catch (e) { } + try { controller.enqueue(value); } catch(e) {} await writer.write(value); } - } catch (e) { + } catch(e) { controller.error(e); await writer.abort(e); } @@ -39087,7 +39054,7 @@ function overwrite(input, clone) { * @param {ReadableStream|Uint8array|String} input * @returns {ReadableStream|Uint8array|String} clone */ -function slice(input, begin = 0, end = Infinity) { +function slice(input, begin=0, end=Infinity) { if ((0,_util__WEBPACK_IMPORTED_MODULE_0__.isArrayStream)(input)) { throw new Error('Not implemented'); } @@ -39146,7 +39113,7 @@ function slice(input, begin = 0, end = Infinity) { * @returns {Promise} the return value of join() * @async */ -async function readToEnd(input, join = concat) { +async function readToEnd(input, join=concat) { if ((0,_util__WEBPACK_IMPORTED_MODULE_0__.isArrayStream)(input)) { return input.readToEnd(join); } @@ -39207,14 +39174,12 @@ function fromAsync(fn) { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "concatUint8Array": () => (/* binding */ concatUint8Array), -/* harmony export */ "isArrayStream": () => (/* reexport safe */ _writer__WEBPACK_IMPORTED_MODULE_1__.isArrayStream), +/* harmony export */ "isArrayStream": () => (/* reexport safe */ _writer__WEBPACK_IMPORTED_MODULE_0__.isArrayStream), /* harmony export */ "isNode": () => (/* binding */ isNode), /* harmony export */ "isStream": () => (/* binding */ isStream), /* harmony export */ "isUint8Array": () => (/* binding */ isUint8Array) /* harmony export */ }); -/* harmony import */ var _streams__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(25); -/* harmony import */ var _writer__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(27); - +/* harmony import */ var _writer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(27); const isNode = typeof globalThis.process === 'object' && @@ -39225,18 +39190,15 @@ const NodeReadableStream = isNode && (__webpack_require__(28).Readable); /** * Check whether data is a Stream, and if so of which type * @param {Any} input data to check - * @returns {'web'|'ponyfill'|'node'|'array'|'web-like'|false} + * @returns {'web'|'node'|'array'|'web-like'|false} */ function isStream(input) { - if ((0,_writer__WEBPACK_IMPORTED_MODULE_1__.isArrayStream)(input)) { + if ((0,_writer__WEBPACK_IMPORTED_MODULE_0__.isArrayStream)(input)) { return 'array'; } if (globalThis.ReadableStream && globalThis.ReadableStream.prototype.isPrototypeOf(input)) { return 'web'; } - if (_streams__WEBPACK_IMPORTED_MODULE_0__.ReadableStream && _streams__WEBPACK_IMPORTED_MODULE_0__.ReadableStream.prototype.isPrototypeOf(input)) { - return 'ponyfill'; - } if (NodeReadableStream && NodeReadableStream.prototype.isPrototypeOf(input)) { return 'node'; } @@ -39431,7 +39393,7 @@ __webpack_require__.r(__webpack_exports__); -const NodeBuffer = _util__WEBPACK_IMPORTED_MODULE_0__.isNode && (__webpack_require__(6).Buffer); +const NodeBuffer = _util__WEBPACK_IMPORTED_MODULE_0__.isNode && (__webpack_require__(30).Buffer); const NodeReadableStream = _util__WEBPACK_IMPORTED_MODULE_0__.isNode && (__webpack_require__(28).Readable); /** @@ -39451,7 +39413,7 @@ if (NodeReadableStream) { */ nodeToWeb = function(nodeStream) { let canceled = false; - return new _streams__WEBPACK_IMPORTED_MODULE_1__.ReadableStream({ + return new ReadableStream({ start(controller) { nodeStream.pause(); nodeStream.on('data', chunk => { @@ -39529,6 +39491,12 @@ if (NodeReadableStream) { /***/ }), /* 30 */ +/***/ (() => { + +/* (ignored) */ + +/***/ }), +/* 31 */ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; @@ -39745,14 +39713,14 @@ Reader.prototype.readToEnd = async function(join=_streams__WEBPACK_IMPORTED_MODU /***/ }), -/* 31 */ +/* 32 */ /***/ ((module) => { "use strict"; module.exports = require("../../bundles/raw/web-stream-tools"); /***/ }), -/* 32 */ +/* 33 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -39858,7 +39826,7 @@ Att.keyinfoAsPubkeyAtt = (ki) => { /***/ }), -/* 33 */ +/* 34 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -39877,7 +39845,7 @@ Catch.report = (name, details) => { /***/ }), -/* 34 */ +/* 35 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -39886,13 +39854,13 @@ var _a; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.MsgBlockParser = void 0; const msg_block_1 = __webpack_require__(3); -const xss_1 = __webpack_require__(35); +const xss_1 = __webpack_require__(36); const buf_1 = __webpack_require__(4); -const catch_1 = __webpack_require__(33); +const catch_1 = __webpack_require__(34); const mime_1 = __webpack_require__(22); -const pgp_armor_1 = __webpack_require__(100); -const pgp_key_1 = __webpack_require__(102); -const pgp_msg_1 = __webpack_require__(107); +const pgp_armor_1 = __webpack_require__(101); +const pgp_key_1 = __webpack_require__(103); +const pgp_msg_1 = __webpack_require__(108); const common_1 = __webpack_require__(23); class MsgBlockParser { } @@ -40040,11 +40008,11 @@ MsgBlockParser.pushArmoredPubkeysToBlocks = async (armoredPubkeys, blocks) => { /***/ }), -/* 35 */ +/* 36 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; -/* provided dependency */ var dereq_sanitize_html = __webpack_require__(36); +/* provided dependency */ var dereq_sanitize_html = __webpack_require__(37); Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Xss = void 0; @@ -40173,15 +40141,15 @@ Xss.htmlUnescape = (str) => { /***/ }), -/* 36 */ +/* 37 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { -const htmlparser = __webpack_require__(37); -const escapeStringRegexp = __webpack_require__(62); -const { isPlainObject } = __webpack_require__(63); -const deepmerge = __webpack_require__(64); -const parseSrcset = __webpack_require__(65); -const { parse: postcssParse } = __webpack_require__(66); +const htmlparser = __webpack_require__(38); +const escapeStringRegexp = __webpack_require__(63); +const { isPlainObject } = __webpack_require__(64); +const deepmerge = __webpack_require__(65); +const parseSrcset = __webpack_require__(66); +const { parse: postcssParse } = __webpack_require__(67); // Tags that can conceivably represent stand-alone media. const mediaTags = [ 'img', 'audio', 'video', 'picture', 'svg', @@ -41019,7 +40987,7 @@ sanitizeHtml.simpleTransform = function(newTagName, newAttribs, merge) { /***/ }), -/* 37 */ +/* 38 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -41051,9 +41019,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.RssHandler = exports.DefaultHandler = exports.DomUtils = exports.ElementType = exports.Tokenizer = exports.createDomStream = exports.parseDOM = exports.parseDocument = exports.DomHandler = exports.Parser = void 0; -var Parser_1 = __webpack_require__(38); +var Parser_1 = __webpack_require__(39); Object.defineProperty(exports, "Parser", ({ enumerable: true, get: function () { return Parser_1.Parser; } })); -var domhandler_1 = __webpack_require__(45); +var domhandler_1 = __webpack_require__(46); Object.defineProperty(exports, "DomHandler", ({ enumerable: true, get: function () { return domhandler_1.DomHandler; } })); Object.defineProperty(exports, "DefaultHandler", ({ enumerable: true, get: function () { return domhandler_1.DomHandler; } })); // Helper methods @@ -41095,22 +41063,22 @@ function createDomStream(cb, options, elementCb) { return new Parser_1.Parser(handler, options); } exports.createDomStream = createDomStream; -var Tokenizer_1 = __webpack_require__(39); +var Tokenizer_1 = __webpack_require__(40); Object.defineProperty(exports, "Tokenizer", ({ enumerable: true, get: function () { return __importDefault(Tokenizer_1).default; } })); -var ElementType = __importStar(__webpack_require__(46)); +var ElementType = __importStar(__webpack_require__(47)); exports.ElementType = ElementType; /* * All of the following exports exist for backwards-compatibility. * They should probably be removed eventually. */ -__exportStar(__webpack_require__(48), exports); -exports.DomUtils = __importStar(__webpack_require__(49)); -var FeedHandler_1 = __webpack_require__(48); +__exportStar(__webpack_require__(49), exports); +exports.DomUtils = __importStar(__webpack_require__(50)); +var FeedHandler_1 = __webpack_require__(49); Object.defineProperty(exports, "RssHandler", ({ enumerable: true, get: function () { return FeedHandler_1.FeedHandler; } })); /***/ }), -/* 38 */ +/* 39 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -41120,7 +41088,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Parser = void 0; -var Tokenizer_1 = __importDefault(__webpack_require__(39)); +var Tokenizer_1 = __importDefault(__webpack_require__(40)); var formTags = new Set([ "input", "option", @@ -41498,7 +41466,7 @@ exports.Parser = Parser; /***/ }), -/* 39 */ +/* 40 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -41507,10 +41475,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -var decode_codepoint_1 = __importDefault(__webpack_require__(40)); -var entities_json_1 = __importDefault(__webpack_require__(42)); -var legacy_json_1 = __importDefault(__webpack_require__(43)); -var xml_json_1 = __importDefault(__webpack_require__(44)); +var decode_codepoint_1 = __importDefault(__webpack_require__(41)); +var entities_json_1 = __importDefault(__webpack_require__(43)); +var legacy_json_1 = __importDefault(__webpack_require__(44)); +var xml_json_1 = __importDefault(__webpack_require__(45)); function whitespace(c) { return c === " " || c === "\n" || c === "\t" || c === "\f" || c === "\r"; } @@ -42414,7 +42382,7 @@ exports["default"] = Tokenizer; /***/ }), -/* 40 */ +/* 41 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -42423,7 +42391,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -var decode_json_1 = __importDefault(__webpack_require__(41)); +var decode_json_1 = __importDefault(__webpack_require__(42)); // Adapted from https://github.com/mathiasbynens/he/blob/master/src/he.js#L94-L119 var fromCodePoint = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -42451,35 +42419,35 @@ exports["default"] = decodeCodePoint; /***/ }), -/* 41 */ +/* 42 */ /***/ ((module) => { "use strict"; module.exports = JSON.parse('{"0":65533,"128":8364,"130":8218,"131":402,"132":8222,"133":8230,"134":8224,"135":8225,"136":710,"137":8240,"138":352,"139":8249,"140":338,"142":381,"145":8216,"146":8217,"147":8220,"148":8221,"149":8226,"150":8211,"151":8212,"152":732,"153":8482,"154":353,"155":8250,"156":339,"158":382,"159":376}'); /***/ }), -/* 42 */ +/* 43 */ /***/ ((module) => { "use strict"; module.exports = JSON.parse('{"Aacute":"Á","aacute":"á","Abreve":"Ă","abreve":"ă","ac":"∾","acd":"∿","acE":"∾̳","Acirc":"Â","acirc":"â","acute":"´","Acy":"А","acy":"а","AElig":"Æ","aelig":"æ","af":"⁡","Afr":"𝔄","afr":"𝔞","Agrave":"À","agrave":"à","alefsym":"ℵ","aleph":"ℵ","Alpha":"Α","alpha":"α","Amacr":"Ā","amacr":"ā","amalg":"⨿","amp":"&","AMP":"&","andand":"⩕","And":"⩓","and":"∧","andd":"⩜","andslope":"⩘","andv":"⩚","ang":"∠","ange":"⦤","angle":"∠","angmsdaa":"⦨","angmsdab":"⦩","angmsdac":"⦪","angmsdad":"⦫","angmsdae":"⦬","angmsdaf":"⦭","angmsdag":"⦮","angmsdah":"⦯","angmsd":"∡","angrt":"∟","angrtvb":"⊾","angrtvbd":"⦝","angsph":"∢","angst":"Å","angzarr":"⍼","Aogon":"Ą","aogon":"ą","Aopf":"𝔸","aopf":"𝕒","apacir":"⩯","ap":"≈","apE":"⩰","ape":"≊","apid":"≋","apos":"\'","ApplyFunction":"⁡","approx":"≈","approxeq":"≊","Aring":"Å","aring":"å","Ascr":"𝒜","ascr":"𝒶","Assign":"≔","ast":"*","asymp":"≈","asympeq":"≍","Atilde":"Ã","atilde":"ã","Auml":"Ä","auml":"ä","awconint":"∳","awint":"⨑","backcong":"≌","backepsilon":"϶","backprime":"‵","backsim":"∽","backsimeq":"⋍","Backslash":"∖","Barv":"⫧","barvee":"⊽","barwed":"⌅","Barwed":"⌆","barwedge":"⌅","bbrk":"⎵","bbrktbrk":"⎶","bcong":"≌","Bcy":"Б","bcy":"б","bdquo":"„","becaus":"∵","because":"∵","Because":"∵","bemptyv":"⦰","bepsi":"϶","bernou":"ℬ","Bernoullis":"ℬ","Beta":"Β","beta":"β","beth":"ℶ","between":"≬","Bfr":"𝔅","bfr":"𝔟","bigcap":"⋂","bigcirc":"◯","bigcup":"⋃","bigodot":"⨀","bigoplus":"⨁","bigotimes":"⨂","bigsqcup":"⨆","bigstar":"★","bigtriangledown":"▽","bigtriangleup":"△","biguplus":"⨄","bigvee":"⋁","bigwedge":"⋀","bkarow":"⤍","blacklozenge":"⧫","blacksquare":"▪","blacktriangle":"▴","blacktriangledown":"▾","blacktriangleleft":"◂","blacktriangleright":"▸","blank":"␣","blk12":"▒","blk14":"░","blk34":"▓","block":"█","bne":"=⃥","bnequiv":"≡⃥","bNot":"⫭","bnot":"⌐","Bopf":"𝔹","bopf":"𝕓","bot":"⊥","bottom":"⊥","bowtie":"⋈","boxbox":"⧉","boxdl":"┐","boxdL":"╕","boxDl":"╖","boxDL":"╗","boxdr":"┌","boxdR":"╒","boxDr":"╓","boxDR":"╔","boxh":"─","boxH":"═","boxhd":"┬","boxHd":"╤","boxhD":"╥","boxHD":"╦","boxhu":"┴","boxHu":"╧","boxhU":"╨","boxHU":"╩","boxminus":"⊟","boxplus":"⊞","boxtimes":"⊠","boxul":"┘","boxuL":"╛","boxUl":"╜","boxUL":"╝","boxur":"└","boxuR":"╘","boxUr":"╙","boxUR":"╚","boxv":"│","boxV":"║","boxvh":"┼","boxvH":"╪","boxVh":"╫","boxVH":"╬","boxvl":"┤","boxvL":"╡","boxVl":"╢","boxVL":"╣","boxvr":"├","boxvR":"╞","boxVr":"╟","boxVR":"╠","bprime":"‵","breve":"˘","Breve":"˘","brvbar":"¦","bscr":"𝒷","Bscr":"ℬ","bsemi":"⁏","bsim":"∽","bsime":"⋍","bsolb":"⧅","bsol":"\\\\","bsolhsub":"⟈","bull":"•","bullet":"•","bump":"≎","bumpE":"⪮","bumpe":"≏","Bumpeq":"≎","bumpeq":"≏","Cacute":"Ć","cacute":"ć","capand":"⩄","capbrcup":"⩉","capcap":"⩋","cap":"∩","Cap":"⋒","capcup":"⩇","capdot":"⩀","CapitalDifferentialD":"ⅅ","caps":"∩︀","caret":"⁁","caron":"ˇ","Cayleys":"ℭ","ccaps":"⩍","Ccaron":"Č","ccaron":"č","Ccedil":"Ç","ccedil":"ç","Ccirc":"Ĉ","ccirc":"ĉ","Cconint":"∰","ccups":"⩌","ccupssm":"⩐","Cdot":"Ċ","cdot":"ċ","cedil":"¸","Cedilla":"¸","cemptyv":"⦲","cent":"¢","centerdot":"·","CenterDot":"·","cfr":"𝔠","Cfr":"ℭ","CHcy":"Ч","chcy":"ч","check":"✓","checkmark":"✓","Chi":"Χ","chi":"χ","circ":"ˆ","circeq":"≗","circlearrowleft":"↺","circlearrowright":"↻","circledast":"⊛","circledcirc":"⊚","circleddash":"⊝","CircleDot":"⊙","circledR":"®","circledS":"Ⓢ","CircleMinus":"⊖","CirclePlus":"⊕","CircleTimes":"⊗","cir":"○","cirE":"⧃","cire":"≗","cirfnint":"⨐","cirmid":"⫯","cirscir":"⧂","ClockwiseContourIntegral":"∲","CloseCurlyDoubleQuote":"”","CloseCurlyQuote":"’","clubs":"♣","clubsuit":"♣","colon":":","Colon":"∷","Colone":"⩴","colone":"≔","coloneq":"≔","comma":",","commat":"@","comp":"∁","compfn":"∘","complement":"∁","complexes":"ℂ","cong":"≅","congdot":"⩭","Congruent":"≡","conint":"∮","Conint":"∯","ContourIntegral":"∮","copf":"𝕔","Copf":"ℂ","coprod":"∐","Coproduct":"∐","copy":"©","COPY":"©","copysr":"℗","CounterClockwiseContourIntegral":"∳","crarr":"↵","cross":"✗","Cross":"⨯","Cscr":"𝒞","cscr":"𝒸","csub":"⫏","csube":"⫑","csup":"⫐","csupe":"⫒","ctdot":"⋯","cudarrl":"⤸","cudarrr":"⤵","cuepr":"⋞","cuesc":"⋟","cularr":"↶","cularrp":"⤽","cupbrcap":"⩈","cupcap":"⩆","CupCap":"≍","cup":"∪","Cup":"⋓","cupcup":"⩊","cupdot":"⊍","cupor":"⩅","cups":"∪︀","curarr":"↷","curarrm":"⤼","curlyeqprec":"⋞","curlyeqsucc":"⋟","curlyvee":"⋎","curlywedge":"⋏","curren":"¤","curvearrowleft":"↶","curvearrowright":"↷","cuvee":"⋎","cuwed":"⋏","cwconint":"∲","cwint":"∱","cylcty":"⌭","dagger":"†","Dagger":"‡","daleth":"ℸ","darr":"↓","Darr":"↡","dArr":"⇓","dash":"‐","Dashv":"⫤","dashv":"⊣","dbkarow":"⤏","dblac":"˝","Dcaron":"Ď","dcaron":"ď","Dcy":"Д","dcy":"д","ddagger":"‡","ddarr":"⇊","DD":"ⅅ","dd":"ⅆ","DDotrahd":"⤑","ddotseq":"⩷","deg":"°","Del":"∇","Delta":"Δ","delta":"δ","demptyv":"⦱","dfisht":"⥿","Dfr":"𝔇","dfr":"𝔡","dHar":"⥥","dharl":"⇃","dharr":"⇂","DiacriticalAcute":"´","DiacriticalDot":"˙","DiacriticalDoubleAcute":"˝","DiacriticalGrave":"`","DiacriticalTilde":"˜","diam":"⋄","diamond":"⋄","Diamond":"⋄","diamondsuit":"♦","diams":"♦","die":"¨","DifferentialD":"ⅆ","digamma":"ϝ","disin":"⋲","div":"÷","divide":"÷","divideontimes":"⋇","divonx":"⋇","DJcy":"Ђ","djcy":"ђ","dlcorn":"⌞","dlcrop":"⌍","dollar":"$","Dopf":"𝔻","dopf":"𝕕","Dot":"¨","dot":"˙","DotDot":"⃜","doteq":"≐","doteqdot":"≑","DotEqual":"≐","dotminus":"∸","dotplus":"∔","dotsquare":"⊡","doublebarwedge":"⌆","DoubleContourIntegral":"∯","DoubleDot":"¨","DoubleDownArrow":"⇓","DoubleLeftArrow":"⇐","DoubleLeftRightArrow":"⇔","DoubleLeftTee":"⫤","DoubleLongLeftArrow":"⟸","DoubleLongLeftRightArrow":"⟺","DoubleLongRightArrow":"⟹","DoubleRightArrow":"⇒","DoubleRightTee":"⊨","DoubleUpArrow":"⇑","DoubleUpDownArrow":"⇕","DoubleVerticalBar":"∥","DownArrowBar":"⤓","downarrow":"↓","DownArrow":"↓","Downarrow":"⇓","DownArrowUpArrow":"⇵","DownBreve":"̑","downdownarrows":"⇊","downharpoonleft":"⇃","downharpoonright":"⇂","DownLeftRightVector":"⥐","DownLeftTeeVector":"⥞","DownLeftVectorBar":"⥖","DownLeftVector":"↽","DownRightTeeVector":"⥟","DownRightVectorBar":"⥗","DownRightVector":"⇁","DownTeeArrow":"↧","DownTee":"⊤","drbkarow":"⤐","drcorn":"⌟","drcrop":"⌌","Dscr":"𝒟","dscr":"𝒹","DScy":"Ѕ","dscy":"ѕ","dsol":"⧶","Dstrok":"Đ","dstrok":"đ","dtdot":"⋱","dtri":"▿","dtrif":"▾","duarr":"⇵","duhar":"⥯","dwangle":"⦦","DZcy":"Џ","dzcy":"џ","dzigrarr":"⟿","Eacute":"É","eacute":"é","easter":"⩮","Ecaron":"Ě","ecaron":"ě","Ecirc":"Ê","ecirc":"ê","ecir":"≖","ecolon":"≕","Ecy":"Э","ecy":"э","eDDot":"⩷","Edot":"Ė","edot":"ė","eDot":"≑","ee":"ⅇ","efDot":"≒","Efr":"𝔈","efr":"𝔢","eg":"⪚","Egrave":"È","egrave":"è","egs":"⪖","egsdot":"⪘","el":"⪙","Element":"∈","elinters":"⏧","ell":"ℓ","els":"⪕","elsdot":"⪗","Emacr":"Ē","emacr":"ē","empty":"∅","emptyset":"∅","EmptySmallSquare":"◻","emptyv":"∅","EmptyVerySmallSquare":"▫","emsp13":" ","emsp14":" ","emsp":" ","ENG":"Ŋ","eng":"ŋ","ensp":" ","Eogon":"Ę","eogon":"ę","Eopf":"𝔼","eopf":"𝕖","epar":"⋕","eparsl":"⧣","eplus":"⩱","epsi":"ε","Epsilon":"Ε","epsilon":"ε","epsiv":"ϵ","eqcirc":"≖","eqcolon":"≕","eqsim":"≂","eqslantgtr":"⪖","eqslantless":"⪕","Equal":"⩵","equals":"=","EqualTilde":"≂","equest":"≟","Equilibrium":"⇌","equiv":"≡","equivDD":"⩸","eqvparsl":"⧥","erarr":"⥱","erDot":"≓","escr":"ℯ","Escr":"ℰ","esdot":"≐","Esim":"⩳","esim":"≂","Eta":"Η","eta":"η","ETH":"Ð","eth":"ð","Euml":"Ë","euml":"ë","euro":"€","excl":"!","exist":"∃","Exists":"∃","expectation":"ℰ","exponentiale":"ⅇ","ExponentialE":"ⅇ","fallingdotseq":"≒","Fcy":"Ф","fcy":"ф","female":"♀","ffilig":"ffi","fflig":"ff","ffllig":"ffl","Ffr":"𝔉","ffr":"𝔣","filig":"fi","FilledSmallSquare":"◼","FilledVerySmallSquare":"▪","fjlig":"fj","flat":"♭","fllig":"fl","fltns":"▱","fnof":"ƒ","Fopf":"𝔽","fopf":"𝕗","forall":"∀","ForAll":"∀","fork":"⋔","forkv":"⫙","Fouriertrf":"ℱ","fpartint":"⨍","frac12":"½","frac13":"⅓","frac14":"¼","frac15":"⅕","frac16":"⅙","frac18":"⅛","frac23":"⅔","frac25":"⅖","frac34":"¾","frac35":"⅗","frac38":"⅜","frac45":"⅘","frac56":"⅚","frac58":"⅝","frac78":"⅞","frasl":"⁄","frown":"⌢","fscr":"𝒻","Fscr":"ℱ","gacute":"ǵ","Gamma":"Γ","gamma":"γ","Gammad":"Ϝ","gammad":"ϝ","gap":"⪆","Gbreve":"Ğ","gbreve":"ğ","Gcedil":"Ģ","Gcirc":"Ĝ","gcirc":"ĝ","Gcy":"Г","gcy":"г","Gdot":"Ġ","gdot":"ġ","ge":"≥","gE":"≧","gEl":"⪌","gel":"⋛","geq":"≥","geqq":"≧","geqslant":"⩾","gescc":"⪩","ges":"⩾","gesdot":"⪀","gesdoto":"⪂","gesdotol":"⪄","gesl":"⋛︀","gesles":"⪔","Gfr":"𝔊","gfr":"𝔤","gg":"≫","Gg":"⋙","ggg":"⋙","gimel":"ℷ","GJcy":"Ѓ","gjcy":"ѓ","gla":"⪥","gl":"≷","glE":"⪒","glj":"⪤","gnap":"⪊","gnapprox":"⪊","gne":"⪈","gnE":"≩","gneq":"⪈","gneqq":"≩","gnsim":"⋧","Gopf":"𝔾","gopf":"𝕘","grave":"`","GreaterEqual":"≥","GreaterEqualLess":"⋛","GreaterFullEqual":"≧","GreaterGreater":"⪢","GreaterLess":"≷","GreaterSlantEqual":"⩾","GreaterTilde":"≳","Gscr":"𝒢","gscr":"ℊ","gsim":"≳","gsime":"⪎","gsiml":"⪐","gtcc":"⪧","gtcir":"⩺","gt":">","GT":">","Gt":"≫","gtdot":"⋗","gtlPar":"⦕","gtquest":"⩼","gtrapprox":"⪆","gtrarr":"⥸","gtrdot":"⋗","gtreqless":"⋛","gtreqqless":"⪌","gtrless":"≷","gtrsim":"≳","gvertneqq":"≩︀","gvnE":"≩︀","Hacek":"ˇ","hairsp":" ","half":"½","hamilt":"ℋ","HARDcy":"Ъ","hardcy":"ъ","harrcir":"⥈","harr":"↔","hArr":"⇔","harrw":"↭","Hat":"^","hbar":"ℏ","Hcirc":"Ĥ","hcirc":"ĥ","hearts":"♥","heartsuit":"♥","hellip":"…","hercon":"⊹","hfr":"𝔥","Hfr":"ℌ","HilbertSpace":"ℋ","hksearow":"⤥","hkswarow":"⤦","hoarr":"⇿","homtht":"∻","hookleftarrow":"↩","hookrightarrow":"↪","hopf":"𝕙","Hopf":"ℍ","horbar":"―","HorizontalLine":"─","hscr":"𝒽","Hscr":"ℋ","hslash":"ℏ","Hstrok":"Ħ","hstrok":"ħ","HumpDownHump":"≎","HumpEqual":"≏","hybull":"⁃","hyphen":"‐","Iacute":"Í","iacute":"í","ic":"⁣","Icirc":"Î","icirc":"î","Icy":"И","icy":"и","Idot":"İ","IEcy":"Е","iecy":"е","iexcl":"¡","iff":"⇔","ifr":"𝔦","Ifr":"ℑ","Igrave":"Ì","igrave":"ì","ii":"ⅈ","iiiint":"⨌","iiint":"∭","iinfin":"⧜","iiota":"℩","IJlig":"IJ","ijlig":"ij","Imacr":"Ī","imacr":"ī","image":"ℑ","ImaginaryI":"ⅈ","imagline":"ℐ","imagpart":"ℑ","imath":"ı","Im":"ℑ","imof":"⊷","imped":"Ƶ","Implies":"⇒","incare":"℅","in":"∈","infin":"∞","infintie":"⧝","inodot":"ı","intcal":"⊺","int":"∫","Int":"∬","integers":"ℤ","Integral":"∫","intercal":"⊺","Intersection":"⋂","intlarhk":"⨗","intprod":"⨼","InvisibleComma":"⁣","InvisibleTimes":"⁢","IOcy":"Ё","iocy":"ё","Iogon":"Į","iogon":"į","Iopf":"𝕀","iopf":"𝕚","Iota":"Ι","iota":"ι","iprod":"⨼","iquest":"¿","iscr":"𝒾","Iscr":"ℐ","isin":"∈","isindot":"⋵","isinE":"⋹","isins":"⋴","isinsv":"⋳","isinv":"∈","it":"⁢","Itilde":"Ĩ","itilde":"ĩ","Iukcy":"І","iukcy":"і","Iuml":"Ï","iuml":"ï","Jcirc":"Ĵ","jcirc":"ĵ","Jcy":"Й","jcy":"й","Jfr":"𝔍","jfr":"𝔧","jmath":"ȷ","Jopf":"𝕁","jopf":"𝕛","Jscr":"𝒥","jscr":"𝒿","Jsercy":"Ј","jsercy":"ј","Jukcy":"Є","jukcy":"є","Kappa":"Κ","kappa":"κ","kappav":"ϰ","Kcedil":"Ķ","kcedil":"ķ","Kcy":"К","kcy":"к","Kfr":"𝔎","kfr":"𝔨","kgreen":"ĸ","KHcy":"Х","khcy":"х","KJcy":"Ќ","kjcy":"ќ","Kopf":"𝕂","kopf":"𝕜","Kscr":"𝒦","kscr":"𝓀","lAarr":"⇚","Lacute":"Ĺ","lacute":"ĺ","laemptyv":"⦴","lagran":"ℒ","Lambda":"Λ","lambda":"λ","lang":"⟨","Lang":"⟪","langd":"⦑","langle":"⟨","lap":"⪅","Laplacetrf":"ℒ","laquo":"«","larrb":"⇤","larrbfs":"⤟","larr":"←","Larr":"↞","lArr":"⇐","larrfs":"⤝","larrhk":"↩","larrlp":"↫","larrpl":"⤹","larrsim":"⥳","larrtl":"↢","latail":"⤙","lAtail":"⤛","lat":"⪫","late":"⪭","lates":"⪭︀","lbarr":"⤌","lBarr":"⤎","lbbrk":"❲","lbrace":"{","lbrack":"[","lbrke":"⦋","lbrksld":"⦏","lbrkslu":"⦍","Lcaron":"Ľ","lcaron":"ľ","Lcedil":"Ļ","lcedil":"ļ","lceil":"⌈","lcub":"{","Lcy":"Л","lcy":"л","ldca":"⤶","ldquo":"“","ldquor":"„","ldrdhar":"⥧","ldrushar":"⥋","ldsh":"↲","le":"≤","lE":"≦","LeftAngleBracket":"⟨","LeftArrowBar":"⇤","leftarrow":"←","LeftArrow":"←","Leftarrow":"⇐","LeftArrowRightArrow":"⇆","leftarrowtail":"↢","LeftCeiling":"⌈","LeftDoubleBracket":"⟦","LeftDownTeeVector":"⥡","LeftDownVectorBar":"⥙","LeftDownVector":"⇃","LeftFloor":"⌊","leftharpoondown":"↽","leftharpoonup":"↼","leftleftarrows":"⇇","leftrightarrow":"↔","LeftRightArrow":"↔","Leftrightarrow":"⇔","leftrightarrows":"⇆","leftrightharpoons":"⇋","leftrightsquigarrow":"↭","LeftRightVector":"⥎","LeftTeeArrow":"↤","LeftTee":"⊣","LeftTeeVector":"⥚","leftthreetimes":"⋋","LeftTriangleBar":"⧏","LeftTriangle":"⊲","LeftTriangleEqual":"⊴","LeftUpDownVector":"⥑","LeftUpTeeVector":"⥠","LeftUpVectorBar":"⥘","LeftUpVector":"↿","LeftVectorBar":"⥒","LeftVector":"↼","lEg":"⪋","leg":"⋚","leq":"≤","leqq":"≦","leqslant":"⩽","lescc":"⪨","les":"⩽","lesdot":"⩿","lesdoto":"⪁","lesdotor":"⪃","lesg":"⋚︀","lesges":"⪓","lessapprox":"⪅","lessdot":"⋖","lesseqgtr":"⋚","lesseqqgtr":"⪋","LessEqualGreater":"⋚","LessFullEqual":"≦","LessGreater":"≶","lessgtr":"≶","LessLess":"⪡","lesssim":"≲","LessSlantEqual":"⩽","LessTilde":"≲","lfisht":"⥼","lfloor":"⌊","Lfr":"𝔏","lfr":"𝔩","lg":"≶","lgE":"⪑","lHar":"⥢","lhard":"↽","lharu":"↼","lharul":"⥪","lhblk":"▄","LJcy":"Љ","ljcy":"љ","llarr":"⇇","ll":"≪","Ll":"⋘","llcorner":"⌞","Lleftarrow":"⇚","llhard":"⥫","lltri":"◺","Lmidot":"Ŀ","lmidot":"ŀ","lmoustache":"⎰","lmoust":"⎰","lnap":"⪉","lnapprox":"⪉","lne":"⪇","lnE":"≨","lneq":"⪇","lneqq":"≨","lnsim":"⋦","loang":"⟬","loarr":"⇽","lobrk":"⟦","longleftarrow":"⟵","LongLeftArrow":"⟵","Longleftarrow":"⟸","longleftrightarrow":"⟷","LongLeftRightArrow":"⟷","Longleftrightarrow":"⟺","longmapsto":"⟼","longrightarrow":"⟶","LongRightArrow":"⟶","Longrightarrow":"⟹","looparrowleft":"↫","looparrowright":"↬","lopar":"⦅","Lopf":"𝕃","lopf":"𝕝","loplus":"⨭","lotimes":"⨴","lowast":"∗","lowbar":"_","LowerLeftArrow":"↙","LowerRightArrow":"↘","loz":"◊","lozenge":"◊","lozf":"⧫","lpar":"(","lparlt":"⦓","lrarr":"⇆","lrcorner":"⌟","lrhar":"⇋","lrhard":"⥭","lrm":"‎","lrtri":"⊿","lsaquo":"‹","lscr":"𝓁","Lscr":"ℒ","lsh":"↰","Lsh":"↰","lsim":"≲","lsime":"⪍","lsimg":"⪏","lsqb":"[","lsquo":"‘","lsquor":"‚","Lstrok":"Ł","lstrok":"ł","ltcc":"⪦","ltcir":"⩹","lt":"<","LT":"<","Lt":"≪","ltdot":"⋖","lthree":"⋋","ltimes":"⋉","ltlarr":"⥶","ltquest":"⩻","ltri":"◃","ltrie":"⊴","ltrif":"◂","ltrPar":"⦖","lurdshar":"⥊","luruhar":"⥦","lvertneqq":"≨︀","lvnE":"≨︀","macr":"¯","male":"♂","malt":"✠","maltese":"✠","Map":"⤅","map":"↦","mapsto":"↦","mapstodown":"↧","mapstoleft":"↤","mapstoup":"↥","marker":"▮","mcomma":"⨩","Mcy":"М","mcy":"м","mdash":"—","mDDot":"∺","measuredangle":"∡","MediumSpace":" ","Mellintrf":"ℳ","Mfr":"𝔐","mfr":"𝔪","mho":"℧","micro":"µ","midast":"*","midcir":"⫰","mid":"∣","middot":"·","minusb":"⊟","minus":"−","minusd":"∸","minusdu":"⨪","MinusPlus":"∓","mlcp":"⫛","mldr":"…","mnplus":"∓","models":"⊧","Mopf":"𝕄","mopf":"𝕞","mp":"∓","mscr":"𝓂","Mscr":"ℳ","mstpos":"∾","Mu":"Μ","mu":"μ","multimap":"⊸","mumap":"⊸","nabla":"∇","Nacute":"Ń","nacute":"ń","nang":"∠⃒","nap":"≉","napE":"⩰̸","napid":"≋̸","napos":"ʼn","napprox":"≉","natural":"♮","naturals":"ℕ","natur":"♮","nbsp":" ","nbump":"≎̸","nbumpe":"≏̸","ncap":"⩃","Ncaron":"Ň","ncaron":"ň","Ncedil":"Ņ","ncedil":"ņ","ncong":"≇","ncongdot":"⩭̸","ncup":"⩂","Ncy":"Н","ncy":"н","ndash":"–","nearhk":"⤤","nearr":"↗","neArr":"⇗","nearrow":"↗","ne":"≠","nedot":"≐̸","NegativeMediumSpace":"​","NegativeThickSpace":"​","NegativeThinSpace":"​","NegativeVeryThinSpace":"​","nequiv":"≢","nesear":"⤨","nesim":"≂̸","NestedGreaterGreater":"≫","NestedLessLess":"≪","NewLine":"\\n","nexist":"∄","nexists":"∄","Nfr":"𝔑","nfr":"𝔫","ngE":"≧̸","nge":"≱","ngeq":"≱","ngeqq":"≧̸","ngeqslant":"⩾̸","nges":"⩾̸","nGg":"⋙̸","ngsim":"≵","nGt":"≫⃒","ngt":"≯","ngtr":"≯","nGtv":"≫̸","nharr":"↮","nhArr":"⇎","nhpar":"⫲","ni":"∋","nis":"⋼","nisd":"⋺","niv":"∋","NJcy":"Њ","njcy":"њ","nlarr":"↚","nlArr":"⇍","nldr":"‥","nlE":"≦̸","nle":"≰","nleftarrow":"↚","nLeftarrow":"⇍","nleftrightarrow":"↮","nLeftrightarrow":"⇎","nleq":"≰","nleqq":"≦̸","nleqslant":"⩽̸","nles":"⩽̸","nless":"≮","nLl":"⋘̸","nlsim":"≴","nLt":"≪⃒","nlt":"≮","nltri":"⋪","nltrie":"⋬","nLtv":"≪̸","nmid":"∤","NoBreak":"⁠","NonBreakingSpace":" ","nopf":"𝕟","Nopf":"ℕ","Not":"⫬","not":"¬","NotCongruent":"≢","NotCupCap":"≭","NotDoubleVerticalBar":"∦","NotElement":"∉","NotEqual":"≠","NotEqualTilde":"≂̸","NotExists":"∄","NotGreater":"≯","NotGreaterEqual":"≱","NotGreaterFullEqual":"≧̸","NotGreaterGreater":"≫̸","NotGreaterLess":"≹","NotGreaterSlantEqual":"⩾̸","NotGreaterTilde":"≵","NotHumpDownHump":"≎̸","NotHumpEqual":"≏̸","notin":"∉","notindot":"⋵̸","notinE":"⋹̸","notinva":"∉","notinvb":"⋷","notinvc":"⋶","NotLeftTriangleBar":"⧏̸","NotLeftTriangle":"⋪","NotLeftTriangleEqual":"⋬","NotLess":"≮","NotLessEqual":"≰","NotLessGreater":"≸","NotLessLess":"≪̸","NotLessSlantEqual":"⩽̸","NotLessTilde":"≴","NotNestedGreaterGreater":"⪢̸","NotNestedLessLess":"⪡̸","notni":"∌","notniva":"∌","notnivb":"⋾","notnivc":"⋽","NotPrecedes":"⊀","NotPrecedesEqual":"⪯̸","NotPrecedesSlantEqual":"⋠","NotReverseElement":"∌","NotRightTriangleBar":"⧐̸","NotRightTriangle":"⋫","NotRightTriangleEqual":"⋭","NotSquareSubset":"⊏̸","NotSquareSubsetEqual":"⋢","NotSquareSuperset":"⊐̸","NotSquareSupersetEqual":"⋣","NotSubset":"⊂⃒","NotSubsetEqual":"⊈","NotSucceeds":"⊁","NotSucceedsEqual":"⪰̸","NotSucceedsSlantEqual":"⋡","NotSucceedsTilde":"≿̸","NotSuperset":"⊃⃒","NotSupersetEqual":"⊉","NotTilde":"≁","NotTildeEqual":"≄","NotTildeFullEqual":"≇","NotTildeTilde":"≉","NotVerticalBar":"∤","nparallel":"∦","npar":"∦","nparsl":"⫽⃥","npart":"∂̸","npolint":"⨔","npr":"⊀","nprcue":"⋠","nprec":"⊀","npreceq":"⪯̸","npre":"⪯̸","nrarrc":"⤳̸","nrarr":"↛","nrArr":"⇏","nrarrw":"↝̸","nrightarrow":"↛","nRightarrow":"⇏","nrtri":"⋫","nrtrie":"⋭","nsc":"⊁","nsccue":"⋡","nsce":"⪰̸","Nscr":"𝒩","nscr":"𝓃","nshortmid":"∤","nshortparallel":"∦","nsim":"≁","nsime":"≄","nsimeq":"≄","nsmid":"∤","nspar":"∦","nsqsube":"⋢","nsqsupe":"⋣","nsub":"⊄","nsubE":"⫅̸","nsube":"⊈","nsubset":"⊂⃒","nsubseteq":"⊈","nsubseteqq":"⫅̸","nsucc":"⊁","nsucceq":"⪰̸","nsup":"⊅","nsupE":"⫆̸","nsupe":"⊉","nsupset":"⊃⃒","nsupseteq":"⊉","nsupseteqq":"⫆̸","ntgl":"≹","Ntilde":"Ñ","ntilde":"ñ","ntlg":"≸","ntriangleleft":"⋪","ntrianglelefteq":"⋬","ntriangleright":"⋫","ntrianglerighteq":"⋭","Nu":"Ν","nu":"ν","num":"#","numero":"№","numsp":" ","nvap":"≍⃒","nvdash":"⊬","nvDash":"⊭","nVdash":"⊮","nVDash":"⊯","nvge":"≥⃒","nvgt":">⃒","nvHarr":"⤄","nvinfin":"⧞","nvlArr":"⤂","nvle":"≤⃒","nvlt":"<⃒","nvltrie":"⊴⃒","nvrArr":"⤃","nvrtrie":"⊵⃒","nvsim":"∼⃒","nwarhk":"⤣","nwarr":"↖","nwArr":"⇖","nwarrow":"↖","nwnear":"⤧","Oacute":"Ó","oacute":"ó","oast":"⊛","Ocirc":"Ô","ocirc":"ô","ocir":"⊚","Ocy":"О","ocy":"о","odash":"⊝","Odblac":"Ő","odblac":"ő","odiv":"⨸","odot":"⊙","odsold":"⦼","OElig":"Œ","oelig":"œ","ofcir":"⦿","Ofr":"𝔒","ofr":"𝔬","ogon":"˛","Ograve":"Ò","ograve":"ò","ogt":"⧁","ohbar":"⦵","ohm":"Ω","oint":"∮","olarr":"↺","olcir":"⦾","olcross":"⦻","oline":"‾","olt":"⧀","Omacr":"Ō","omacr":"ō","Omega":"Ω","omega":"ω","Omicron":"Ο","omicron":"ο","omid":"⦶","ominus":"⊖","Oopf":"𝕆","oopf":"𝕠","opar":"⦷","OpenCurlyDoubleQuote":"“","OpenCurlyQuote":"‘","operp":"⦹","oplus":"⊕","orarr":"↻","Or":"⩔","or":"∨","ord":"⩝","order":"ℴ","orderof":"ℴ","ordf":"ª","ordm":"º","origof":"⊶","oror":"⩖","orslope":"⩗","orv":"⩛","oS":"Ⓢ","Oscr":"𝒪","oscr":"ℴ","Oslash":"Ø","oslash":"ø","osol":"⊘","Otilde":"Õ","otilde":"õ","otimesas":"⨶","Otimes":"⨷","otimes":"⊗","Ouml":"Ö","ouml":"ö","ovbar":"⌽","OverBar":"‾","OverBrace":"⏞","OverBracket":"⎴","OverParenthesis":"⏜","para":"¶","parallel":"∥","par":"∥","parsim":"⫳","parsl":"⫽","part":"∂","PartialD":"∂","Pcy":"П","pcy":"п","percnt":"%","period":".","permil":"‰","perp":"⊥","pertenk":"‱","Pfr":"𝔓","pfr":"𝔭","Phi":"Φ","phi":"φ","phiv":"ϕ","phmmat":"ℳ","phone":"☎","Pi":"Π","pi":"π","pitchfork":"⋔","piv":"ϖ","planck":"ℏ","planckh":"ℎ","plankv":"ℏ","plusacir":"⨣","plusb":"⊞","pluscir":"⨢","plus":"+","plusdo":"∔","plusdu":"⨥","pluse":"⩲","PlusMinus":"±","plusmn":"±","plussim":"⨦","plustwo":"⨧","pm":"±","Poincareplane":"ℌ","pointint":"⨕","popf":"𝕡","Popf":"ℙ","pound":"£","prap":"⪷","Pr":"⪻","pr":"≺","prcue":"≼","precapprox":"⪷","prec":"≺","preccurlyeq":"≼","Precedes":"≺","PrecedesEqual":"⪯","PrecedesSlantEqual":"≼","PrecedesTilde":"≾","preceq":"⪯","precnapprox":"⪹","precneqq":"⪵","precnsim":"⋨","pre":"⪯","prE":"⪳","precsim":"≾","prime":"′","Prime":"″","primes":"ℙ","prnap":"⪹","prnE":"⪵","prnsim":"⋨","prod":"∏","Product":"∏","profalar":"⌮","profline":"⌒","profsurf":"⌓","prop":"∝","Proportional":"∝","Proportion":"∷","propto":"∝","prsim":"≾","prurel":"⊰","Pscr":"𝒫","pscr":"𝓅","Psi":"Ψ","psi":"ψ","puncsp":" ","Qfr":"𝔔","qfr":"𝔮","qint":"⨌","qopf":"𝕢","Qopf":"ℚ","qprime":"⁗","Qscr":"𝒬","qscr":"𝓆","quaternions":"ℍ","quatint":"⨖","quest":"?","questeq":"≟","quot":"\\"","QUOT":"\\"","rAarr":"⇛","race":"∽̱","Racute":"Ŕ","racute":"ŕ","radic":"√","raemptyv":"⦳","rang":"⟩","Rang":"⟫","rangd":"⦒","range":"⦥","rangle":"⟩","raquo":"»","rarrap":"⥵","rarrb":"⇥","rarrbfs":"⤠","rarrc":"⤳","rarr":"→","Rarr":"↠","rArr":"⇒","rarrfs":"⤞","rarrhk":"↪","rarrlp":"↬","rarrpl":"⥅","rarrsim":"⥴","Rarrtl":"⤖","rarrtl":"↣","rarrw":"↝","ratail":"⤚","rAtail":"⤜","ratio":"∶","rationals":"ℚ","rbarr":"⤍","rBarr":"⤏","RBarr":"⤐","rbbrk":"❳","rbrace":"}","rbrack":"]","rbrke":"⦌","rbrksld":"⦎","rbrkslu":"⦐","Rcaron":"Ř","rcaron":"ř","Rcedil":"Ŗ","rcedil":"ŗ","rceil":"⌉","rcub":"}","Rcy":"Р","rcy":"р","rdca":"⤷","rdldhar":"⥩","rdquo":"”","rdquor":"”","rdsh":"↳","real":"ℜ","realine":"ℛ","realpart":"ℜ","reals":"ℝ","Re":"ℜ","rect":"▭","reg":"®","REG":"®","ReverseElement":"∋","ReverseEquilibrium":"⇋","ReverseUpEquilibrium":"⥯","rfisht":"⥽","rfloor":"⌋","rfr":"𝔯","Rfr":"ℜ","rHar":"⥤","rhard":"⇁","rharu":"⇀","rharul":"⥬","Rho":"Ρ","rho":"ρ","rhov":"ϱ","RightAngleBracket":"⟩","RightArrowBar":"⇥","rightarrow":"→","RightArrow":"→","Rightarrow":"⇒","RightArrowLeftArrow":"⇄","rightarrowtail":"↣","RightCeiling":"⌉","RightDoubleBracket":"⟧","RightDownTeeVector":"⥝","RightDownVectorBar":"⥕","RightDownVector":"⇂","RightFloor":"⌋","rightharpoondown":"⇁","rightharpoonup":"⇀","rightleftarrows":"⇄","rightleftharpoons":"⇌","rightrightarrows":"⇉","rightsquigarrow":"↝","RightTeeArrow":"↦","RightTee":"⊢","RightTeeVector":"⥛","rightthreetimes":"⋌","RightTriangleBar":"⧐","RightTriangle":"⊳","RightTriangleEqual":"⊵","RightUpDownVector":"⥏","RightUpTeeVector":"⥜","RightUpVectorBar":"⥔","RightUpVector":"↾","RightVectorBar":"⥓","RightVector":"⇀","ring":"˚","risingdotseq":"≓","rlarr":"⇄","rlhar":"⇌","rlm":"‏","rmoustache":"⎱","rmoust":"⎱","rnmid":"⫮","roang":"⟭","roarr":"⇾","robrk":"⟧","ropar":"⦆","ropf":"𝕣","Ropf":"ℝ","roplus":"⨮","rotimes":"⨵","RoundImplies":"⥰","rpar":")","rpargt":"⦔","rppolint":"⨒","rrarr":"⇉","Rrightarrow":"⇛","rsaquo":"›","rscr":"𝓇","Rscr":"ℛ","rsh":"↱","Rsh":"↱","rsqb":"]","rsquo":"’","rsquor":"’","rthree":"⋌","rtimes":"⋊","rtri":"▹","rtrie":"⊵","rtrif":"▸","rtriltri":"⧎","RuleDelayed":"⧴","ruluhar":"⥨","rx":"℞","Sacute":"Ś","sacute":"ś","sbquo":"‚","scap":"⪸","Scaron":"Š","scaron":"š","Sc":"⪼","sc":"≻","sccue":"≽","sce":"⪰","scE":"⪴","Scedil":"Ş","scedil":"ş","Scirc":"Ŝ","scirc":"ŝ","scnap":"⪺","scnE":"⪶","scnsim":"⋩","scpolint":"⨓","scsim":"≿","Scy":"С","scy":"с","sdotb":"⊡","sdot":"⋅","sdote":"⩦","searhk":"⤥","searr":"↘","seArr":"⇘","searrow":"↘","sect":"§","semi":";","seswar":"⤩","setminus":"∖","setmn":"∖","sext":"✶","Sfr":"𝔖","sfr":"𝔰","sfrown":"⌢","sharp":"♯","SHCHcy":"Щ","shchcy":"щ","SHcy":"Ш","shcy":"ш","ShortDownArrow":"↓","ShortLeftArrow":"←","shortmid":"∣","shortparallel":"∥","ShortRightArrow":"→","ShortUpArrow":"↑","shy":"­","Sigma":"Σ","sigma":"σ","sigmaf":"ς","sigmav":"ς","sim":"∼","simdot":"⩪","sime":"≃","simeq":"≃","simg":"⪞","simgE":"⪠","siml":"⪝","simlE":"⪟","simne":"≆","simplus":"⨤","simrarr":"⥲","slarr":"←","SmallCircle":"∘","smallsetminus":"∖","smashp":"⨳","smeparsl":"⧤","smid":"∣","smile":"⌣","smt":"⪪","smte":"⪬","smtes":"⪬︀","SOFTcy":"Ь","softcy":"ь","solbar":"⌿","solb":"⧄","sol":"/","Sopf":"𝕊","sopf":"𝕤","spades":"♠","spadesuit":"♠","spar":"∥","sqcap":"⊓","sqcaps":"⊓︀","sqcup":"⊔","sqcups":"⊔︀","Sqrt":"√","sqsub":"⊏","sqsube":"⊑","sqsubset":"⊏","sqsubseteq":"⊑","sqsup":"⊐","sqsupe":"⊒","sqsupset":"⊐","sqsupseteq":"⊒","square":"□","Square":"□","SquareIntersection":"⊓","SquareSubset":"⊏","SquareSubsetEqual":"⊑","SquareSuperset":"⊐","SquareSupersetEqual":"⊒","SquareUnion":"⊔","squarf":"▪","squ":"□","squf":"▪","srarr":"→","Sscr":"𝒮","sscr":"𝓈","ssetmn":"∖","ssmile":"⌣","sstarf":"⋆","Star":"⋆","star":"☆","starf":"★","straightepsilon":"ϵ","straightphi":"ϕ","strns":"¯","sub":"⊂","Sub":"⋐","subdot":"⪽","subE":"⫅","sube":"⊆","subedot":"⫃","submult":"⫁","subnE":"⫋","subne":"⊊","subplus":"⪿","subrarr":"⥹","subset":"⊂","Subset":"⋐","subseteq":"⊆","subseteqq":"⫅","SubsetEqual":"⊆","subsetneq":"⊊","subsetneqq":"⫋","subsim":"⫇","subsub":"⫕","subsup":"⫓","succapprox":"⪸","succ":"≻","succcurlyeq":"≽","Succeeds":"≻","SucceedsEqual":"⪰","SucceedsSlantEqual":"≽","SucceedsTilde":"≿","succeq":"⪰","succnapprox":"⪺","succneqq":"⪶","succnsim":"⋩","succsim":"≿","SuchThat":"∋","sum":"∑","Sum":"∑","sung":"♪","sup1":"¹","sup2":"²","sup3":"³","sup":"⊃","Sup":"⋑","supdot":"⪾","supdsub":"⫘","supE":"⫆","supe":"⊇","supedot":"⫄","Superset":"⊃","SupersetEqual":"⊇","suphsol":"⟉","suphsub":"⫗","suplarr":"⥻","supmult":"⫂","supnE":"⫌","supne":"⊋","supplus":"⫀","supset":"⊃","Supset":"⋑","supseteq":"⊇","supseteqq":"⫆","supsetneq":"⊋","supsetneqq":"⫌","supsim":"⫈","supsub":"⫔","supsup":"⫖","swarhk":"⤦","swarr":"↙","swArr":"⇙","swarrow":"↙","swnwar":"⤪","szlig":"ß","Tab":"\\t","target":"⌖","Tau":"Τ","tau":"τ","tbrk":"⎴","Tcaron":"Ť","tcaron":"ť","Tcedil":"Ţ","tcedil":"ţ","Tcy":"Т","tcy":"т","tdot":"⃛","telrec":"⌕","Tfr":"𝔗","tfr":"𝔱","there4":"∴","therefore":"∴","Therefore":"∴","Theta":"Θ","theta":"θ","thetasym":"ϑ","thetav":"ϑ","thickapprox":"≈","thicksim":"∼","ThickSpace":"  ","ThinSpace":" ","thinsp":" ","thkap":"≈","thksim":"∼","THORN":"Þ","thorn":"þ","tilde":"˜","Tilde":"∼","TildeEqual":"≃","TildeFullEqual":"≅","TildeTilde":"≈","timesbar":"⨱","timesb":"⊠","times":"×","timesd":"⨰","tint":"∭","toea":"⤨","topbot":"⌶","topcir":"⫱","top":"⊤","Topf":"𝕋","topf":"𝕥","topfork":"⫚","tosa":"⤩","tprime":"‴","trade":"™","TRADE":"™","triangle":"▵","triangledown":"▿","triangleleft":"◃","trianglelefteq":"⊴","triangleq":"≜","triangleright":"▹","trianglerighteq":"⊵","tridot":"◬","trie":"≜","triminus":"⨺","TripleDot":"⃛","triplus":"⨹","trisb":"⧍","tritime":"⨻","trpezium":"⏢","Tscr":"𝒯","tscr":"𝓉","TScy":"Ц","tscy":"ц","TSHcy":"Ћ","tshcy":"ћ","Tstrok":"Ŧ","tstrok":"ŧ","twixt":"≬","twoheadleftarrow":"↞","twoheadrightarrow":"↠","Uacute":"Ú","uacute":"ú","uarr":"↑","Uarr":"↟","uArr":"⇑","Uarrocir":"⥉","Ubrcy":"Ў","ubrcy":"ў","Ubreve":"Ŭ","ubreve":"ŭ","Ucirc":"Û","ucirc":"û","Ucy":"У","ucy":"у","udarr":"⇅","Udblac":"Ű","udblac":"ű","udhar":"⥮","ufisht":"⥾","Ufr":"𝔘","ufr":"𝔲","Ugrave":"Ù","ugrave":"ù","uHar":"⥣","uharl":"↿","uharr":"↾","uhblk":"▀","ulcorn":"⌜","ulcorner":"⌜","ulcrop":"⌏","ultri":"◸","Umacr":"Ū","umacr":"ū","uml":"¨","UnderBar":"_","UnderBrace":"⏟","UnderBracket":"⎵","UnderParenthesis":"⏝","Union":"⋃","UnionPlus":"⊎","Uogon":"Ų","uogon":"ų","Uopf":"𝕌","uopf":"𝕦","UpArrowBar":"⤒","uparrow":"↑","UpArrow":"↑","Uparrow":"⇑","UpArrowDownArrow":"⇅","updownarrow":"↕","UpDownArrow":"↕","Updownarrow":"⇕","UpEquilibrium":"⥮","upharpoonleft":"↿","upharpoonright":"↾","uplus":"⊎","UpperLeftArrow":"↖","UpperRightArrow":"↗","upsi":"υ","Upsi":"ϒ","upsih":"ϒ","Upsilon":"Υ","upsilon":"υ","UpTeeArrow":"↥","UpTee":"⊥","upuparrows":"⇈","urcorn":"⌝","urcorner":"⌝","urcrop":"⌎","Uring":"Ů","uring":"ů","urtri":"◹","Uscr":"𝒰","uscr":"𝓊","utdot":"⋰","Utilde":"Ũ","utilde":"ũ","utri":"▵","utrif":"▴","uuarr":"⇈","Uuml":"Ü","uuml":"ü","uwangle":"⦧","vangrt":"⦜","varepsilon":"ϵ","varkappa":"ϰ","varnothing":"∅","varphi":"ϕ","varpi":"ϖ","varpropto":"∝","varr":"↕","vArr":"⇕","varrho":"ϱ","varsigma":"ς","varsubsetneq":"⊊︀","varsubsetneqq":"⫋︀","varsupsetneq":"⊋︀","varsupsetneqq":"⫌︀","vartheta":"ϑ","vartriangleleft":"⊲","vartriangleright":"⊳","vBar":"⫨","Vbar":"⫫","vBarv":"⫩","Vcy":"В","vcy":"в","vdash":"⊢","vDash":"⊨","Vdash":"⊩","VDash":"⊫","Vdashl":"⫦","veebar":"⊻","vee":"∨","Vee":"⋁","veeeq":"≚","vellip":"⋮","verbar":"|","Verbar":"‖","vert":"|","Vert":"‖","VerticalBar":"∣","VerticalLine":"|","VerticalSeparator":"❘","VerticalTilde":"≀","VeryThinSpace":" ","Vfr":"𝔙","vfr":"𝔳","vltri":"⊲","vnsub":"⊂⃒","vnsup":"⊃⃒","Vopf":"𝕍","vopf":"𝕧","vprop":"∝","vrtri":"⊳","Vscr":"𝒱","vscr":"𝓋","vsubnE":"⫋︀","vsubne":"⊊︀","vsupnE":"⫌︀","vsupne":"⊋︀","Vvdash":"⊪","vzigzag":"⦚","Wcirc":"Ŵ","wcirc":"ŵ","wedbar":"⩟","wedge":"∧","Wedge":"⋀","wedgeq":"≙","weierp":"℘","Wfr":"𝔚","wfr":"𝔴","Wopf":"𝕎","wopf":"𝕨","wp":"℘","wr":"≀","wreath":"≀","Wscr":"𝒲","wscr":"𝓌","xcap":"⋂","xcirc":"◯","xcup":"⋃","xdtri":"▽","Xfr":"𝔛","xfr":"𝔵","xharr":"⟷","xhArr":"⟺","Xi":"Ξ","xi":"ξ","xlarr":"⟵","xlArr":"⟸","xmap":"⟼","xnis":"⋻","xodot":"⨀","Xopf":"𝕏","xopf":"𝕩","xoplus":"⨁","xotime":"⨂","xrarr":"⟶","xrArr":"⟹","Xscr":"𝒳","xscr":"𝓍","xsqcup":"⨆","xuplus":"⨄","xutri":"△","xvee":"⋁","xwedge":"⋀","Yacute":"Ý","yacute":"ý","YAcy":"Я","yacy":"я","Ycirc":"Ŷ","ycirc":"ŷ","Ycy":"Ы","ycy":"ы","yen":"¥","Yfr":"𝔜","yfr":"𝔶","YIcy":"Ї","yicy":"ї","Yopf":"𝕐","yopf":"𝕪","Yscr":"𝒴","yscr":"𝓎","YUcy":"Ю","yucy":"ю","yuml":"ÿ","Yuml":"Ÿ","Zacute":"Ź","zacute":"ź","Zcaron":"Ž","zcaron":"ž","Zcy":"З","zcy":"з","Zdot":"Ż","zdot":"ż","zeetrf":"ℨ","ZeroWidthSpace":"​","Zeta":"Ζ","zeta":"ζ","zfr":"𝔷","Zfr":"ℨ","ZHcy":"Ж","zhcy":"ж","zigrarr":"⇝","zopf":"𝕫","Zopf":"ℤ","Zscr":"𝒵","zscr":"𝓏","zwj":"‍","zwnj":"‌"}'); /***/ }), -/* 43 */ +/* 44 */ /***/ ((module) => { "use strict"; module.exports = JSON.parse('{"Aacute":"Á","aacute":"á","Acirc":"Â","acirc":"â","acute":"´","AElig":"Æ","aelig":"æ","Agrave":"À","agrave":"à","amp":"&","AMP":"&","Aring":"Å","aring":"å","Atilde":"Ã","atilde":"ã","Auml":"Ä","auml":"ä","brvbar":"¦","Ccedil":"Ç","ccedil":"ç","cedil":"¸","cent":"¢","copy":"©","COPY":"©","curren":"¤","deg":"°","divide":"÷","Eacute":"É","eacute":"é","Ecirc":"Ê","ecirc":"ê","Egrave":"È","egrave":"è","ETH":"Ð","eth":"ð","Euml":"Ë","euml":"ë","frac12":"½","frac14":"¼","frac34":"¾","gt":">","GT":">","Iacute":"Í","iacute":"í","Icirc":"Î","icirc":"î","iexcl":"¡","Igrave":"Ì","igrave":"ì","iquest":"¿","Iuml":"Ï","iuml":"ï","laquo":"«","lt":"<","LT":"<","macr":"¯","micro":"µ","middot":"·","nbsp":" ","not":"¬","Ntilde":"Ñ","ntilde":"ñ","Oacute":"Ó","oacute":"ó","Ocirc":"Ô","ocirc":"ô","Ograve":"Ò","ograve":"ò","ordf":"ª","ordm":"º","Oslash":"Ø","oslash":"ø","Otilde":"Õ","otilde":"õ","Ouml":"Ö","ouml":"ö","para":"¶","plusmn":"±","pound":"£","quot":"\\"","QUOT":"\\"","raquo":"»","reg":"®","REG":"®","sect":"§","shy":"­","sup1":"¹","sup2":"²","sup3":"³","szlig":"ß","THORN":"Þ","thorn":"þ","times":"×","Uacute":"Ú","uacute":"ú","Ucirc":"Û","ucirc":"û","Ugrave":"Ù","ugrave":"ù","uml":"¨","Uuml":"Ü","uuml":"ü","Yacute":"Ý","yacute":"ý","yen":"¥","yuml":"ÿ"}'); /***/ }), -/* 44 */ +/* 45 */ /***/ ((module) => { "use strict"; module.exports = JSON.parse('{"amp":"&","apos":"\'","gt":">","lt":"<","quot":"\\""}'); /***/ }), -/* 45 */ +/* 46 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -42500,9 +42468,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.DomHandler = void 0; -var domelementtype_1 = __webpack_require__(46); -var node_1 = __webpack_require__(47); -__exportStar(__webpack_require__(47), exports); +var domelementtype_1 = __webpack_require__(47); +var node_1 = __webpack_require__(48); +__exportStar(__webpack_require__(48), exports); var reWhitespace = /\s+/g; // Default options var defaultOpts = { @@ -42662,7 +42630,7 @@ exports["default"] = DomHandler; /***/ }), -/* 46 */ +/* 47 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -42724,7 +42692,7 @@ exports.Doctype = ElementType.Doctype; /***/ }), -/* 47 */ +/* 48 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -42757,7 +42725,7 @@ var __assign = (this && this.__assign) || function () { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.cloneNode = exports.hasChildren = exports.isDocument = exports.isDirective = exports.isComment = exports.isText = exports.isCDATA = exports.isTag = exports.Element = exports.Document = exports.NodeWithChildren = exports.ProcessingInstruction = exports.Comment = exports.Text = exports.DataNode = exports.Node = void 0; -var domelementtype_1 = __webpack_require__(46); +var domelementtype_1 = __webpack_require__(47); var nodeTypes = new Map([ [domelementtype_1.ElementType.Tag, 1], [domelementtype_1.ElementType.Script, 1], @@ -43175,7 +43143,7 @@ function cloneChildren(childs) { /***/ }), -/* 48 */ +/* 49 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43219,9 +43187,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.parseFeed = exports.FeedHandler = void 0; -var domhandler_1 = __importDefault(__webpack_require__(45)); -var DomUtils = __importStar(__webpack_require__(49)); -var Parser_1 = __webpack_require__(38); +var domhandler_1 = __importDefault(__webpack_require__(46)); +var DomUtils = __importStar(__webpack_require__(50)); +var Parser_1 = __webpack_require__(39); var FeedItemMediaMedium; (function (FeedItemMediaMedium) { FeedItemMediaMedium[FeedItemMediaMedium["image"] = 0] = "image"; @@ -43417,7 +43385,7 @@ exports.parseFeed = parseFeed; /***/ }), -/* 49 */ +/* 50 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43434,15 +43402,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.hasChildren = exports.isDocument = exports.isComment = exports.isText = exports.isCDATA = exports.isTag = void 0; -__exportStar(__webpack_require__(50), exports); -__exportStar(__webpack_require__(56), exports); +__exportStar(__webpack_require__(51), exports); __exportStar(__webpack_require__(57), exports); __exportStar(__webpack_require__(58), exports); __exportStar(__webpack_require__(59), exports); __exportStar(__webpack_require__(60), exports); __exportStar(__webpack_require__(61), exports); +__exportStar(__webpack_require__(62), exports); /** @deprecated Use these methods from `domhandler` directly. */ -var domhandler_1 = __webpack_require__(45); +var domhandler_1 = __webpack_require__(46); Object.defineProperty(exports, "isTag", ({ enumerable: true, get: function () { return domhandler_1.isTag; } })); Object.defineProperty(exports, "isCDATA", ({ enumerable: true, get: function () { return domhandler_1.isCDATA; } })); Object.defineProperty(exports, "isText", ({ enumerable: true, get: function () { return domhandler_1.isText; } })); @@ -43452,7 +43420,7 @@ Object.defineProperty(exports, "hasChildren", ({ enumerable: true, get: function /***/ }), -/* 50 */ +/* 51 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43462,9 +43430,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.innerText = exports.textContent = exports.getText = exports.getInnerHTML = exports.getOuterHTML = void 0; -var domhandler_1 = __webpack_require__(45); -var dom_serializer_1 = __importDefault(__webpack_require__(51)); -var domelementtype_1 = __webpack_require__(46); +var domhandler_1 = __webpack_require__(46); +var dom_serializer_1 = __importDefault(__webpack_require__(52)); +var domelementtype_1 = __webpack_require__(47); /** * @param node Node to get the outer HTML of. * @param options Options for serialization. @@ -43545,7 +43513,7 @@ exports.innerText = innerText; /***/ }), -/* 51 */ +/* 52 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43584,15 +43552,15 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); /* * Module dependencies */ -var ElementType = __importStar(__webpack_require__(46)); -var entities_1 = __webpack_require__(52); +var ElementType = __importStar(__webpack_require__(47)); +var entities_1 = __webpack_require__(53); /** * Mixed-case SVG and MathML tags & attributes * recognized by the HTML parser. * * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign */ -var foreignNames_1 = __webpack_require__(55); +var foreignNames_1 = __webpack_require__(56); var unencodedElements = new Set([ "style", "script", @@ -43763,15 +43731,15 @@ function renderComment(elem) { /***/ }), -/* 52 */ +/* 53 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.decodeXMLStrict = exports.decodeHTML5Strict = exports.decodeHTML4Strict = exports.decodeHTML5 = exports.decodeHTML4 = exports.decodeHTMLStrict = exports.decodeHTML = exports.decodeXML = exports.encodeHTML5 = exports.encodeHTML4 = exports.escapeUTF8 = exports.escape = exports.encodeNonAsciiHTML = exports.encodeHTML = exports.encodeXML = exports.encode = exports.decodeStrict = exports.decode = void 0; -var decode_1 = __webpack_require__(53); -var encode_1 = __webpack_require__(54); +var decode_1 = __webpack_require__(54); +var encode_1 = __webpack_require__(55); /** * Decodes a string with entities. * @@ -43805,7 +43773,7 @@ function encode(data, level) { return (!level || level <= 0 ? encode_1.encodeXML : encode_1.encodeHTML)(data); } exports.encode = encode; -var encode_2 = __webpack_require__(54); +var encode_2 = __webpack_require__(55); Object.defineProperty(exports, "encodeXML", ({ enumerable: true, get: function () { return encode_2.encodeXML; } })); Object.defineProperty(exports, "encodeHTML", ({ enumerable: true, get: function () { return encode_2.encodeHTML; } })); Object.defineProperty(exports, "encodeNonAsciiHTML", ({ enumerable: true, get: function () { return encode_2.encodeNonAsciiHTML; } })); @@ -43814,7 +43782,7 @@ Object.defineProperty(exports, "escapeUTF8", ({ enumerable: true, get: function // Legacy aliases (deprecated) Object.defineProperty(exports, "encodeHTML4", ({ enumerable: true, get: function () { return encode_2.encodeHTML; } })); Object.defineProperty(exports, "encodeHTML5", ({ enumerable: true, get: function () { return encode_2.encodeHTML; } })); -var decode_2 = __webpack_require__(53); +var decode_2 = __webpack_require__(54); Object.defineProperty(exports, "decodeXML", ({ enumerable: true, get: function () { return decode_2.decodeXML; } })); Object.defineProperty(exports, "decodeHTML", ({ enumerable: true, get: function () { return decode_2.decodeHTML; } })); Object.defineProperty(exports, "decodeHTMLStrict", ({ enumerable: true, get: function () { return decode_2.decodeHTMLStrict; } })); @@ -43827,7 +43795,7 @@ Object.defineProperty(exports, "decodeXMLStrict", ({ enumerable: true, get: func /***/ }), -/* 53 */ +/* 54 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43837,10 +43805,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.decodeHTML = exports.decodeHTMLStrict = exports.decodeXML = void 0; -var entities_json_1 = __importDefault(__webpack_require__(42)); -var legacy_json_1 = __importDefault(__webpack_require__(43)); -var xml_json_1 = __importDefault(__webpack_require__(44)); -var decode_codepoint_1 = __importDefault(__webpack_require__(40)); +var entities_json_1 = __importDefault(__webpack_require__(43)); +var legacy_json_1 = __importDefault(__webpack_require__(44)); +var xml_json_1 = __importDefault(__webpack_require__(45)); +var decode_codepoint_1 = __importDefault(__webpack_require__(41)); var strictEntityRe = /&(?:[a-zA-Z0-9]+|#[xX][\da-fA-F]+|#\d+);/g; exports.decodeXML = getStrictDecoder(xml_json_1.default); exports.decodeHTMLStrict = getStrictDecoder(entities_json_1.default); @@ -43887,7 +43855,7 @@ function getReplacer(map) { /***/ }), -/* 54 */ +/* 55 */ /***/ (function(__unused_webpack_module, exports, __webpack_require__) { "use strict"; @@ -43897,7 +43865,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.escapeUTF8 = exports.escape = exports.encodeNonAsciiHTML = exports.encodeHTML = exports.encodeXML = void 0; -var xml_json_1 = __importDefault(__webpack_require__(44)); +var xml_json_1 = __importDefault(__webpack_require__(45)); var inverseXML = getInverseObj(xml_json_1.default); var xmlReplacer = getInverseReplacer(inverseXML); /** @@ -43908,7 +43876,7 @@ var xmlReplacer = getInverseReplacer(inverseXML); * numeric hexadecimal reference (eg. `ü`) will be used. */ exports.encodeXML = getASCIIEncoder(inverseXML); -var entities_json_1 = __importDefault(__webpack_require__(42)); +var entities_json_1 = __importDefault(__webpack_require__(43)); var inverseHTML = getInverseObj(entities_json_1.default); var htmlReplacer = getInverseReplacer(inverseHTML); /** @@ -44030,7 +43998,7 @@ function getASCIIEncoder(obj) { /***/ }), -/* 55 */ +/* 56 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -44140,14 +44108,14 @@ exports.attributeNames = new Map([ /***/ }), -/* 56 */ +/* 57 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.prevElementSibling = exports.nextElementSibling = exports.getName = exports.hasAttrib = exports.getAttributeValue = exports.getSiblings = exports.getParent = exports.getChildren = void 0; -var domhandler_1 = __webpack_require__(45); +var domhandler_1 = __webpack_require__(46); var emptyArray = []; /** * Get a node's children. @@ -44264,7 +44232,7 @@ exports.prevElementSibling = prevElementSibling; /***/ }), -/* 57 */ +/* 58 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -44400,14 +44368,14 @@ exports.prepend = prepend; /***/ }), -/* 58 */ +/* 59 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.findAll = exports.existsOne = exports.findOne = exports.findOneChild = exports.find = exports.filter = void 0; -var domhandler_1 = __webpack_require__(45); +var domhandler_1 = __webpack_require__(46); /** * Search a node and its children for nodes passing a test function. * @@ -44533,15 +44501,15 @@ exports.findAll = findAll; /***/ }), -/* 59 */ +/* 60 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getElementsByTagType = exports.getElementsByTagName = exports.getElementById = exports.getElements = exports.testElement = void 0; -var domhandler_1 = __webpack_require__(45); -var querying_1 = __webpack_require__(58); +var domhandler_1 = __webpack_require__(46); +var querying_1 = __webpack_require__(59); var Checks = { tag_name: function (name) { if (typeof name === "function") { @@ -44664,14 +44632,14 @@ exports.getElementsByTagType = getElementsByTagType; /***/ }), -/* 60 */ +/* 61 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.uniqueSort = exports.compareDocumentPosition = exports.removeSubsets = void 0; -var domhandler_1 = __webpack_require__(45); +var domhandler_1 = __webpack_require__(46); /** * Given an array of nodes, remove any member that is contained by another. * @@ -44796,15 +44764,15 @@ exports.uniqueSort = uniqueSort; /***/ }), -/* 61 */ +/* 62 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getFeed = void 0; -var stringify_1 = __webpack_require__(50); -var legacy_1 = __webpack_require__(59); +var stringify_1 = __webpack_require__(51); +var legacy_1 = __webpack_require__(60); /** * Get the feed object from the root of a DOM tree. * @@ -44993,7 +44961,7 @@ function isValidFeed(value) { /***/ }), -/* 62 */ +/* 63 */ /***/ ((module) => { "use strict"; @@ -45013,7 +44981,7 @@ module.exports = string => { /***/ }), -/* 63 */ +/* 64 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -45058,7 +45026,7 @@ exports.isPlainObject = isPlainObject; /***/ }), -/* 64 */ +/* 65 */ /***/ ((module) => { "use strict"; @@ -45198,7 +45166,7 @@ module.exports = deepmerge_1; /***/ }), -/* 65 */ +/* 66 */ /***/ (function(module, exports) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/** @@ -45529,30 +45497,30 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 66 */ +/* 67 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let CssSyntaxError = __webpack_require__(67) -let Declaration = __webpack_require__(70) -let LazyResult = __webpack_require__(75) -let Container = __webpack_require__(84) -let Processor = __webpack_require__(97) -let stringify = __webpack_require__(74) -let fromJSON = __webpack_require__(99) -let Document = __webpack_require__(86) -let Warning = __webpack_require__(89) -let Comment = __webpack_require__(85) -let AtRule = __webpack_require__(93) -let Result = __webpack_require__(88) -let Input = __webpack_require__(80) -let parse = __webpack_require__(90) -let list = __webpack_require__(96) -let Rule = __webpack_require__(95) -let Root = __webpack_require__(94) -let Node = __webpack_require__(71) +let CssSyntaxError = __webpack_require__(68) +let Declaration = __webpack_require__(71) +let LazyResult = __webpack_require__(76) +let Container = __webpack_require__(85) +let Processor = __webpack_require__(98) +let stringify = __webpack_require__(75) +let fromJSON = __webpack_require__(100) +let Document = __webpack_require__(87) +let Warning = __webpack_require__(90) +let Comment = __webpack_require__(86) +let AtRule = __webpack_require__(94) +let Result = __webpack_require__(89) +let Input = __webpack_require__(81) +let parse = __webpack_require__(91) +let list = __webpack_require__(97) +let Rule = __webpack_require__(96) +let Root = __webpack_require__(95) +let Node = __webpack_require__(72) function postcss(...plugins) { if (plugins.length === 1 && Array.isArray(plugins[0])) { @@ -45637,15 +45605,15 @@ postcss.default = postcss /***/ }), -/* 67 */ +/* 68 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let pico = __webpack_require__(68) +let pico = __webpack_require__(69) -let terminalHighlight = __webpack_require__(69) +let terminalHighlight = __webpack_require__(70) class CssSyntaxError extends Error { constructor(message, line, column, source, file, plugin) { @@ -45744,7 +45712,7 @@ CssSyntaxError.default = CssSyntaxError /***/ }), -/* 68 */ +/* 69 */ /***/ ((module) => { var x=String; @@ -45754,19 +45722,19 @@ module.exports.createColors = create; /***/ }), -/* 69 */ +/* 70 */ /***/ (() => { /* (ignored) */ /***/ }), -/* 70 */ +/* 71 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Node = __webpack_require__(71) +let Node = __webpack_require__(72) class Declaration extends Node { constructor(defaults) { @@ -45791,16 +45759,16 @@ Declaration.default = Declaration /***/ }), -/* 71 */ +/* 72 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { isClean, my } = __webpack_require__(72) -let CssSyntaxError = __webpack_require__(67) -let Stringifier = __webpack_require__(73) -let stringify = __webpack_require__(74) +let { isClean, my } = __webpack_require__(73) +let CssSyntaxError = __webpack_require__(68) +let Stringifier = __webpack_require__(74) +let stringify = __webpack_require__(75) function cloneNode(obj, parent) { let cloned = new obj.constructor() @@ -46177,7 +46145,7 @@ Node.default = Node /***/ }), -/* 72 */ +/* 73 */ /***/ ((module) => { "use strict"; @@ -46189,7 +46157,7 @@ module.exports.my = Symbol('my') /***/ }), -/* 73 */ +/* 74 */ /***/ ((module) => { "use strict"; @@ -46549,13 +46517,13 @@ Stringifier.default = Stringifier /***/ }), -/* 74 */ +/* 75 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Stringifier = __webpack_require__(73) +let Stringifier = __webpack_require__(74) function stringify(node, builder) { let str = new Stringifier(builder) @@ -46567,21 +46535,21 @@ stringify.default = stringify /***/ }), -/* 75 */ +/* 76 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { isClean, my } = __webpack_require__(72) -let MapGenerator = __webpack_require__(76) -let stringify = __webpack_require__(74) -let Container = __webpack_require__(84) -let Document = __webpack_require__(86) -let warnOnce = __webpack_require__(87) -let Result = __webpack_require__(88) -let parse = __webpack_require__(90) -let Root = __webpack_require__(94) +let { isClean, my } = __webpack_require__(73) +let MapGenerator = __webpack_require__(77) +let stringify = __webpack_require__(75) +let Container = __webpack_require__(85) +let Document = __webpack_require__(87) +let warnOnce = __webpack_require__(88) +let Result = __webpack_require__(89) +let parse = __webpack_require__(91) +let Root = __webpack_require__(95) const TYPE_TO_CLASS_NAME = { document: 'Document', @@ -47124,17 +47092,17 @@ Document.registerLazyResult(LazyResult) /***/ }), -/* 76 */ +/* 77 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(77) -let { dirname, resolve, relative, sep } = __webpack_require__(78) -let { pathToFileURL } = __webpack_require__(79) +let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(78) +let { dirname, resolve, relative, sep } = __webpack_require__(79) +let { pathToFileURL } = __webpack_require__(80) -let Input = __webpack_require__(80) +let Input = __webpack_require__(81) let sourceMapAvailable = Boolean(SourceMapConsumer && SourceMapGenerator) let pathAvailable = Boolean(dirname && resolve && relative && sep) @@ -47462,38 +47430,38 @@ module.exports = MapGenerator /***/ }), -/* 77 */ +/* 78 */ /***/ (() => { /* (ignored) */ /***/ }), -/* 78 */ +/* 79 */ /***/ (() => { /* (ignored) */ /***/ }), -/* 79 */ +/* 80 */ /***/ (() => { /* (ignored) */ /***/ }), -/* 80 */ +/* 81 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(77) -let { fileURLToPath, pathToFileURL } = __webpack_require__(79) -let { resolve, isAbsolute } = __webpack_require__(78) -let { nanoid } = __webpack_require__(81) +let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(78) +let { fileURLToPath, pathToFileURL } = __webpack_require__(80) +let { resolve, isAbsolute } = __webpack_require__(79) +let { nanoid } = __webpack_require__(82) -let terminalHighlight = __webpack_require__(69) -let CssSyntaxError = __webpack_require__(67) -let PreviousMap = __webpack_require__(82) +let terminalHighlight = __webpack_require__(70) +let CssSyntaxError = __webpack_require__(68) +let PreviousMap = __webpack_require__(83) let fromOffsetCache = Symbol('fromOffsetCache') @@ -47735,7 +47703,7 @@ if (terminalHighlight && terminalHighlight.registerInput) { /***/ }), -/* 81 */ +/* 82 */ /***/ ((module) => { let urlAlphabet = @@ -47762,15 +47730,15 @@ module.exports = { nanoid, customAlphabet } /***/ }), -/* 82 */ +/* 83 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(77) -let { existsSync, readFileSync } = __webpack_require__(83) -let { dirname, join } = __webpack_require__(78) +let { SourceMapConsumer, SourceMapGenerator } = __webpack_require__(78) +let { existsSync, readFileSync } = __webpack_require__(84) +let { dirname, join } = __webpack_require__(79) function fromBase64(str) { if (Buffer) { @@ -47911,22 +47879,22 @@ PreviousMap.default = PreviousMap /***/ }), -/* 83 */ +/* 84 */ /***/ (() => { /* (ignored) */ /***/ }), -/* 84 */ +/* 85 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let { isClean, my } = __webpack_require__(72) -let Declaration = __webpack_require__(70) -let Comment = __webpack_require__(85) -let Node = __webpack_require__(71) +let { isClean, my } = __webpack_require__(73) +let Declaration = __webpack_require__(71) +let Comment = __webpack_require__(86) +let Node = __webpack_require__(72) let parse, Rule, AtRule, Root @@ -48363,13 +48331,13 @@ Container.rebuild = node => { /***/ }), -/* 85 */ +/* 86 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Node = __webpack_require__(71) +let Node = __webpack_require__(72) class Comment extends Node { constructor(defaults) { @@ -48383,13 +48351,13 @@ Comment.default = Comment /***/ }), -/* 86 */ +/* 87 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Container = __webpack_require__(84) +let Container = __webpack_require__(85) let LazyResult, Processor @@ -48423,7 +48391,7 @@ Document.default = Document /***/ }), -/* 87 */ +/* 88 */ /***/ ((module) => { "use strict"; @@ -48443,13 +48411,13 @@ module.exports = function warnOnce(message) { /***/ }), -/* 88 */ +/* 89 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Warning = __webpack_require__(89) +let Warning = __webpack_require__(90) class Result { constructor(processor, root, opts) { @@ -48492,7 +48460,7 @@ Result.default = Result /***/ }), -/* 89 */ +/* 90 */ /***/ ((module) => { "use strict"; @@ -48536,15 +48504,15 @@ Warning.default = Warning /***/ }), -/* 90 */ +/* 91 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Container = __webpack_require__(84) -let Parser = __webpack_require__(91) -let Input = __webpack_require__(80) +let Container = __webpack_require__(85) +let Parser = __webpack_require__(92) +let Input = __webpack_require__(81) function parse(css, opts) { let input = new Input(css, opts) @@ -48585,18 +48553,18 @@ Container.registerParse(parse) /***/ }), -/* 91 */ +/* 92 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Declaration = __webpack_require__(70) -let tokenizer = __webpack_require__(92) -let Comment = __webpack_require__(85) -let AtRule = __webpack_require__(93) -let Root = __webpack_require__(94) -let Rule = __webpack_require__(95) +let Declaration = __webpack_require__(71) +let tokenizer = __webpack_require__(93) +let Comment = __webpack_require__(86) +let AtRule = __webpack_require__(94) +let Root = __webpack_require__(95) +let Rule = __webpack_require__(96) const SAFE_COMMENT_NEIGHBOR = { empty: true, @@ -49195,7 +49163,7 @@ module.exports = Parser /***/ }), -/* 92 */ +/* 93 */ /***/ ((module) => { "use strict"; @@ -49468,13 +49436,13 @@ module.exports = function tokenizer(input, options = {}) { /***/ }), -/* 93 */ +/* 94 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Container = __webpack_require__(84) +let Container = __webpack_require__(85) class AtRule extends Container { constructor(defaults) { @@ -49500,13 +49468,13 @@ Container.registerAtRule(AtRule) /***/ }), -/* 94 */ +/* 95 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Container = __webpack_require__(84) +let Container = __webpack_require__(85) let LazyResult, Processor @@ -49568,14 +49536,14 @@ Container.registerRoot(Root) /***/ }), -/* 95 */ +/* 96 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Container = __webpack_require__(84) -let list = __webpack_require__(96) +let Container = __webpack_require__(85) +let list = __webpack_require__(97) class Rule extends Container { constructor(defaults) { @@ -49602,7 +49570,7 @@ Container.registerRule(Rule) /***/ }), -/* 96 */ +/* 97 */ /***/ ((module) => { "use strict"; @@ -49667,16 +49635,16 @@ list.default = list /***/ }), -/* 97 */ +/* 98 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let NoWorkResult = __webpack_require__(98) -let LazyResult = __webpack_require__(75) -let Document = __webpack_require__(86) -let Root = __webpack_require__(94) +let NoWorkResult = __webpack_require__(99) +let LazyResult = __webpack_require__(76) +let Document = __webpack_require__(87) +let Root = __webpack_require__(95) class Processor { constructor(plugins = []) { @@ -49741,17 +49709,17 @@ Document.registerProcessor(Processor) /***/ }), -/* 98 */ +/* 99 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let MapGenerator = __webpack_require__(76) -let stringify = __webpack_require__(74) -let warnOnce = __webpack_require__(87) -let parse = __webpack_require__(90) -const Result = __webpack_require__(88) +let MapGenerator = __webpack_require__(77) +let stringify = __webpack_require__(75) +let warnOnce = __webpack_require__(88) +let parse = __webpack_require__(91) +const Result = __webpack_require__(89) class NoWorkResult { constructor(processor, css, opts) { @@ -49883,19 +49851,19 @@ NoWorkResult.default = NoWorkResult /***/ }), -/* 99 */ +/* 100 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; -let Declaration = __webpack_require__(70) -let PreviousMap = __webpack_require__(82) -let Comment = __webpack_require__(85) -let AtRule = __webpack_require__(93) -let Input = __webpack_require__(80) -let Root = __webpack_require__(94) -let Rule = __webpack_require__(95) +let Declaration = __webpack_require__(71) +let PreviousMap = __webpack_require__(83) +let Comment = __webpack_require__(86) +let AtRule = __webpack_require__(94) +let Input = __webpack_require__(81) +let Root = __webpack_require__(95) +let Rule = __webpack_require__(96) function fromJSON(json, inputs) { if (Array.isArray(json)) return json.map(n => fromJSON(n)) @@ -49944,7 +49912,7 @@ fromJSON.default = fromJSON /***/ }), -/* 100 */ +/* 101 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -49954,7 +49922,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PgpArmor = void 0; const buf_1 = __webpack_require__(4); const common_1 = __webpack_require__(23); -const openpgp_1 = __webpack_require__(101); +const openpgp_1 = __webpack_require__(102); class PgpArmor { } exports.PgpArmor = PgpArmor; @@ -50064,7 +50032,7 @@ PgpArmor.cryptoMsgPrepareForDecrypt = async (encrypted) => { /***/ }), -/* 101 */ +/* 102 */ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { "use strict"; @@ -93636,7 +93604,7 @@ var elliptic$1 = /*#__PURE__*/Object.freeze({ /***/ }), -/* 102 */ +/* 103 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -93645,14 +93613,14 @@ var _a; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PgpKey = void 0; const buf_1 = __webpack_require__(4); -const catch_1 = __webpack_require__(33); -const msg_block_parser_1 = __webpack_require__(34); -const pgp_armor_1 = __webpack_require__(100); -const store_1 = __webpack_require__(103); -const mnemonic_1 = __webpack_require__(104); +const catch_1 = __webpack_require__(34); +const msg_block_parser_1 = __webpack_require__(35); +const pgp_armor_1 = __webpack_require__(101); +const store_1 = __webpack_require__(104); +const mnemonic_1 = __webpack_require__(105); const util_1 = __webpack_require__(5); -const openpgp_1 = __webpack_require__(101); -const pgp_1 = __webpack_require__(105); +const openpgp_1 = __webpack_require__(102); +const pgp_1 = __webpack_require__(106); const require_1 = __webpack_require__(24); const common_1 = __webpack_require__(23); class PgpKey { @@ -93988,7 +93956,7 @@ PgpKey.revoke = async (key) => { /***/ }), -/* 103 */ +/* 104 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -94030,7 +93998,7 @@ Store.keyCacheRenewExpiry = () => { /***/ }), -/* 104 */ +/* 105 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -96110,16 +96078,16 @@ exports.mnemonic = mnemonic; /***/ }), -/* 105 */ +/* 106 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.isPacketDecrypted = exports.isFullyEncrypted = exports.isFullyDecrypted = void 0; -const pgp_key_1 = __webpack_require__(102); -const const_1 = __webpack_require__(106); -const openpgp_1 = __webpack_require__(101); +const pgp_key_1 = __webpack_require__(103); +const const_1 = __webpack_require__(107); +const openpgp_1 = __webpack_require__(102); openpgp_1.config.versionString = `FlowCrypt ${const_1.VERSION} Gmail Encryption`; openpgp_1.config.commentString = 'Seamlessly send and receive encrypted email'; openpgp_1.config.allowUnauthenticatedMessages = true; @@ -96163,7 +96131,7 @@ exports.isPacketDecrypted = isPacketDecrypted; /***/ }), -/* 106 */ +/* 107 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -96193,7 +96161,7 @@ exports.gmailBackupSearchQuery = gmailBackupSearchQuery; /***/ }), -/* 107 */ +/* 108 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; @@ -96201,16 +96169,16 @@ exports.gmailBackupSearchQuery = gmailBackupSearchQuery; var _a; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PgpMsg = exports.FormatError = exports.DecryptErrTypes = void 0; -const pgp_key_1 = __webpack_require__(102); +const pgp_key_1 = __webpack_require__(103); const msg_block_1 = __webpack_require__(3); const common_1 = __webpack_require__(23); const buf_1 = __webpack_require__(4); -const catch_1 = __webpack_require__(33); -const msg_block_parser_1 = __webpack_require__(34); -const pgp_armor_1 = __webpack_require__(100); -const store_1 = __webpack_require__(103); -const openpgp_1 = __webpack_require__(101); -const pgp_1 = __webpack_require__(105); +const catch_1 = __webpack_require__(34); +const msg_block_parser_1 = __webpack_require__(35); +const pgp_armor_1 = __webpack_require__(101); +const store_1 = __webpack_require__(104); +const openpgp_1 = __webpack_require__(102); +const pgp_1 = __webpack_require__(106); const require_1 = __webpack_require__(24); var DecryptErrTypes; (function (DecryptErrTypes) { @@ -96583,7 +96551,7 @@ PgpMsg.cryptoMsgDecryptCategorizeErr = (decryptErr, msgPwd) => { /***/ }), -/* 108 */ +/* 109 */ /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -96678,14 +96646,14 @@ PgpPwd.readableCrackTime = (totalSeconds) => { /***/ }), -/* 109 */ +/* 110 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readArmoredKeyOrThrow = exports.ValidateInput = void 0; -const openpgp_1 = __webpack_require__(101); +const openpgp_1 = __webpack_require__(102); class ValidateInput { } exports.ValidateInput = ValidateInput; @@ -96876,9 +96844,6 @@ exports.readArmoredKeyOrThrow = readArmoredKeyOrThrow; /******/ return module.exports; /******/ } /******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = __webpack_modules__; -/******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { @@ -96892,28 +96857,6 @@ exports.readArmoredKeyOrThrow = readArmoredKeyOrThrow; /******/ }; /******/ })(); /******/ -/******/ /* webpack/runtime/ensure chunk */ -/******/ (() => { -/******/ __webpack_require__.f = {}; -/******/ // This file contains only the entry chunk. -/******/ // The chunk loading function for additional chunks -/******/ __webpack_require__.e = (chunkId) => { -/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => { -/******/ __webpack_require__.f[key](chunkId, promises); -/******/ return promises; -/******/ }, [])); -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/get javascript chunk filename */ -/******/ (() => { -/******/ // This function allow to reference async chunks -/******/ __webpack_require__.u = (chunkId) => { -/******/ // return url for filenames based on template -/******/ return "" + chunkId + ".js"; -/******/ }; -/******/ })(); -/******/ /******/ /* webpack/runtime/global */ /******/ (() => { /******/ __webpack_require__.g = (function() { @@ -96931,52 +96874,6 @@ exports.readArmoredKeyOrThrow = readArmoredKeyOrThrow; /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ -/******/ /* webpack/runtime/load script */ -/******/ (() => { -/******/ var inProgress = {}; -/******/ var dataWebpackPrefix = "flowcrypt-mobile-core:"; -/******/ // loadScript function to load a script via script tag -/******/ __webpack_require__.l = (url, done, key, chunkId) => { -/******/ if(inProgress[url]) { inProgress[url].push(done); return; } -/******/ var script, needAttach; -/******/ if(key !== undefined) { -/******/ var scripts = document.getElementsByTagName("script"); -/******/ for(var i = 0; i < scripts.length; i++) { -/******/ var s = scripts[i]; -/******/ if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; } -/******/ } -/******/ } -/******/ if(!script) { -/******/ needAttach = true; -/******/ script = document.createElement('script'); -/******/ -/******/ script.charset = 'utf-8'; -/******/ script.timeout = 120; -/******/ if (__webpack_require__.nc) { -/******/ script.setAttribute("nonce", __webpack_require__.nc); -/******/ } -/******/ script.setAttribute("data-webpack", dataWebpackPrefix + key); -/******/ script.src = url; -/******/ } -/******/ inProgress[url] = [done]; -/******/ var onScriptComplete = (prev, event) => { -/******/ // avoid mem leaks in IE. -/******/ script.onerror = script.onload = null; -/******/ clearTimeout(timeout); -/******/ var doneFns = inProgress[url]; -/******/ delete inProgress[url]; -/******/ script.parentNode && script.parentNode.removeChild(script); -/******/ doneFns && doneFns.forEach((fn) => (fn(event))); -/******/ if(prev) return prev(event); -/******/ } -/******/ ; -/******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000); -/******/ script.onerror = onScriptComplete.bind(null, script.onerror); -/******/ script.onload = onScriptComplete.bind(null, script.onload); -/******/ needAttach && document.head.appendChild(script); -/******/ }; -/******/ })(); -/******/ /******/ /* webpack/runtime/make namespace object */ /******/ (() => { /******/ // define __esModule on exports @@ -96988,101 +96885,6 @@ exports.readArmoredKeyOrThrow = readArmoredKeyOrThrow; /******/ }; /******/ })(); /******/ -/******/ /* webpack/runtime/publicPath */ -/******/ (() => { -/******/ __webpack_require__.p = ""; -/******/ })(); -/******/ -/******/ /* webpack/runtime/jsonp chunk loading */ -/******/ (() => { -/******/ // no baseURI -/******/ -/******/ // object to store loaded and loading chunks -/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched -/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded -/******/ var installedChunks = { -/******/ 1: 0 -/******/ }; -/******/ -/******/ __webpack_require__.f.j = (chunkId, promises) => { -/******/ // JSONP chunk loading for javascript -/******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined; -/******/ if(installedChunkData !== 0) { // 0 means "already installed". -/******/ -/******/ // a Promise means "currently loading". -/******/ if(installedChunkData) { -/******/ promises.push(installedChunkData[2]); -/******/ } else { -/******/ if(true) { // all chunks have JS -/******/ // setup Promise in chunk cache -/******/ var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject])); -/******/ promises.push(installedChunkData[2] = promise); -/******/ -/******/ // start chunk loading -/******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId); -/******/ // create error before stack unwound to get useful stacktrace later -/******/ var error = new Error(); -/******/ var loadingEnded = (event) => { -/******/ if(__webpack_require__.o(installedChunks, chunkId)) { -/******/ installedChunkData = installedChunks[chunkId]; -/******/ if(installedChunkData !== 0) installedChunks[chunkId] = undefined; -/******/ if(installedChunkData) { -/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type); -/******/ var realSrc = event && event.target && event.target.src; -/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'; -/******/ error.name = 'ChunkLoadError'; -/******/ error.type = errorType; -/******/ error.request = realSrc; -/******/ installedChunkData[1](error); -/******/ } -/******/ } -/******/ }; -/******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId); -/******/ } else installedChunks[chunkId] = 0; -/******/ } -/******/ } -/******/ }; -/******/ -/******/ // no prefetching -/******/ -/******/ // no preloaded -/******/ -/******/ // no HMR -/******/ -/******/ // no HMR manifest -/******/ -/******/ // no on chunks loaded -/******/ -/******/ // install a JSONP callback for chunk loading -/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => { -/******/ var [chunkIds, moreModules, runtime] = data; -/******/ // add "moreModules" to the modules object, -/******/ // then flag all "chunkIds" as loaded and fire callback -/******/ var moduleId, chunkId, i = 0; -/******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) { -/******/ for(moduleId in moreModules) { -/******/ if(__webpack_require__.o(moreModules, moduleId)) { -/******/ __webpack_require__.m[moduleId] = moreModules[moduleId]; -/******/ } -/******/ } -/******/ if(runtime) var result = runtime(__webpack_require__); -/******/ } -/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data); -/******/ for(;i < chunkIds.length; i++) { -/******/ chunkId = chunkIds[i]; -/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { -/******/ installedChunks[chunkId][0](); -/******/ } -/******/ installedChunks[chunkId] = 0; -/******/ } -/******/ -/******/ } -/******/ -/******/ var chunkLoadingGlobal = this["webpackChunkflowcrypt_mobile_core"] = this["webpackChunkflowcrypt_mobile_core"] || []; -/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); -/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); -/******/ })(); -/******/ /************************************************************************/ var __webpack_exports__ = {}; // This entry need to be wrapped in an IIFE because it need to be in strict mode. diff --git a/appium/api-mocks/apis/google/google-endpoints.ts b/appium/api-mocks/apis/google/google-endpoints.ts index f9bd5747b..46f30e2f9 100644 --- a/appium/api-mocks/apis/google/google-endpoints.ts +++ b/appium/api-mocks/apis/google/google-endpoints.ts @@ -153,8 +153,7 @@ export const getMockGoogleEndpoints = ( const id = parseResourceId(req.url!); const msgs = (await GoogleData.withInitializedData(acct, googleConfig)).getMessagesAndDraftsByThread(id); if (!msgs.length) { - const statusCode = id === '16841ce0ce5cb74d' ? 404 : 400; // intentionally testing missing thread - throw new HttpErr(`MOCK thread not found for ${acct}: ${id}`, statusCode); + throw new HttpErr(`MOCK thread not found for ${acct}: ${id}`, 404); } return { id, historyId: msgs[0].historyId, messages: msgs.map(m => GoogleData.fmtMsg(m, parsedReq.query.format)) }; } else if (isPost(req)) { diff --git a/appium/tests/screenobjects/email.screen.ts b/appium/tests/screenobjects/email.screen.ts index 793c92d9d..dc9f613d8 100644 --- a/appium/tests/screenobjects/email.screen.ts +++ b/appium/tests/screenobjects/email.screen.ts @@ -298,6 +298,7 @@ class EmailScreen extends BaseScreen { } checkDraft = async (text: string, index: number) => { + await browser.pause(500); const draftBodyEl = await this.draftBody(index); expect(await draftBodyEl.getValue()).toEqual(text); } From 7e0b20a7cd9a71255079c14d7b5449bff8bd6722 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 4 Oct 2022 14:11:33 +0300 Subject: [PATCH 32/56] code cleanup --- .../Compose/ComposeViewController.swift | 4 -- .../ComposeViewController+Drafts.swift | 17 +++---- .../ComposeViewController+ErrorHandling.swift | 41 +++++++++------- .../ComposeViewController+Setup.swift | 3 +- FlowCrypt/Controllers/Inbox/InboxItem.swift | 1 + .../Search/SearchViewController.swift | 6 ++- .../Message Provider/MessageService.swift | 49 ++++++++++--------- .../Functionality/Services/EKMVcHelper.swift | 2 +- .../GoogleUserService.swift | 2 +- 9 files changed, 66 insertions(+), 59 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 418cc9375..bf734bdc1 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -257,7 +257,3 @@ final class ComposeViewController: TableNodeViewController { } extension ComposeViewController: FilesManagerPresenter {} - -/* - - split decryptAndProcess in MessageService for parsing drafts - */ diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 47b9c1651..3479609e4 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -32,17 +32,17 @@ extension ComposeViewController { contextToSend: contextToSend ) - if let existingDraft = composedLatestDraft { - return newDraft != existingDraft ? newDraft : nil - } else { // save initial draft + guard let existingDraft = composedLatestDraft else { composedLatestDraft = newDraft return nil } + + return newDraft != existingDraft ? newDraft : nil } - func saveDraftIfNeeded(withAlert: Bool = false, completion: ((Error?) -> Void)? = nil) { + func saveDraftIfNeeded(withAlert: Bool = false, handler: ((Error?) -> Void)? = nil) { guard let draft = createDraft() else { - completion?(nil) + handler?(nil) return } @@ -60,15 +60,14 @@ extension ComposeViewController { ) composedLatestDraft = draft - completion?(nil) + handler?(nil) } catch { if !(error is MessageValidationError) { // no need to save or notify user if validation error // for other errors show toast - // todo - should make sure that the toast doesn't hide the keyboard. Also should be toasted on top when keyboard open? - showToast("Error saving draft: \(error.errorMessage)") + showToast("Error saving draft: \(error.errorMessage)", position: .top) } - completion?(error) + handler?(error) } } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift index 4e3181d76..cbffb7f0a 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift @@ -13,37 +13,24 @@ extension ComposeViewController { func requestMissingPassPhraseWithModal(for signingKey: Keypair, isDraft: Bool = false, withDiscard: Bool = false) { let alert = alertsFactory.makePassPhraseAlert( onCancel: { [weak self] in - guard let self = self else { return } if !withDiscard { - self.handle(error: ComposeMessageError.passPhraseRequired) + self?.navigationController?.popViewController(animated: true) } else { - self.navigationController?.popViewController(animated: true) + self?.handle(error: ComposeMessageError.passPhraseRequired) } }, onCompletion: { [weak self] passPhrase in guard let self = self else { return } - Task { + Task { do { let matched = try await self.composeMessageService.handlePassPhraseEntry( passPhrase, for: signingKey ) - // TODO: make more readable + if matched { - if isDraft { - if self.didFinishSetup { - if withDiscard { - self.handleBackButtonTap() - } else { - self.saveDraftIfNeeded() - } - } else { - self.fillDataFromInput() - } - } else { - self.handleSendTap() - } + self.handleMatchedPassphrase(isDraft: isDraft, withDiscard: withDiscard) } else { self.handle(error: ComposeMessageError.passPhraseNoMatch) } @@ -56,6 +43,24 @@ extension ComposeViewController { present(alert, animated: true, completion: nil) } + private func handleMatchedPassphrase(isDraft: Bool, withDiscard: Bool) { + guard isDraft else { + handleSendTap() + return + } + + guard didFinishSetup else { + fillDataFromInput() + return + } + + if withDiscard { + handleBackButtonTap() + } else { + saveDraftIfNeeded() + } + } + func handle(error: Error) { reEnableSendButton() diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 87d30c720..798771a02 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -119,8 +119,7 @@ extension ComposeViewController { isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) contextToSend.message = processedMessage.text - setupTextNode() - reload(sections: [.compose]) + reload(sections: Section.recipientsSections) didFinishSetup = true } catch { if case .missingPassPhrase(let keyPair) = error as? MessageServiceError, let keyPair = keyPair { diff --git a/FlowCrypt/Controllers/Inbox/InboxItem.swift b/FlowCrypt/Controllers/Inbox/InboxItem.swift index 459c3312f..86621f557 100644 --- a/FlowCrypt/Controllers/Inbox/InboxItem.swift +++ b/FlowCrypt/Controllers/Inbox/InboxItem.swift @@ -92,6 +92,7 @@ extension InboxItem { return "To: \(recipients)".attributed(style, color: textColor) } else { let hasDrafts = messages.contains(where: { $0.isDraft }) + let senderNames = messages .compactMap(\.sender?.shortName) .unique() diff --git a/FlowCrypt/Controllers/Search/SearchViewController.swift b/FlowCrypt/Controllers/Search/SearchViewController.swift index f05cf4372..691bab5f9 100644 --- a/FlowCrypt/Controllers/Search/SearchViewController.swift +++ b/FlowCrypt/Controllers/Search/SearchViewController.swift @@ -16,12 +16,14 @@ class SearchViewController: InboxViewController { override func viewDidLoad() { super.viewDidLoad() - self.setupSearchUI() - self.setupSearch() + + setupSearchUI() + setupSearch() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + searchController.isActive = true } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 91c1ea65e..d0b7cd9aa 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -97,16 +97,17 @@ final class MessageService { id: identifier, folder: folder ) - if message.isPgp { - return try await decryptAndProcess( - message: message, - onlyLocalKeys: onlyLocalKeys, - userEmail: userEmail, - isUsingKeyManager: isUsingKeyManager - ) - } else { + + guard message.isPgp else { return ProcessedMessage(message: message) } + + return try await decryptAndProcess( + message: message, + onlyLocalKeys: onlyLocalKeys, + userEmail: userEmail, + isUsingKeyManager: isUsingKeyManager + ) } private func getKeypairs(email: String, isUsingKeyManager: Bool) async throws -> [Keypair] { @@ -122,13 +123,12 @@ final class MessageService { return keys } - func decrypt( + private func decrypt( text: String, - userEmail: String, - isUsingKeyManager: Bool - ) async throws -> String { - let keys = try await getKeypairs(email: userEmail, isUsingKeyManager: isUsingKeyManager) - + keys: [Keypair], + isMime: Bool = false, + verificationPubKeys: [String] = [] + ) async throws -> CoreRes.ParseDecryptMsg { let decrypted = try await core.parseDecryptMsg( encrypted: text.data(), keys: keys, @@ -142,6 +142,16 @@ final class MessageService { throw MessageServiceError.missingPassPhrase(keyPair) } + return decrypted + } + + func decrypt( + text: String, + userEmail: String, + isUsingKeyManager: Bool + ) async throws -> String { + let keys = try await getKeypairs(email: userEmail, isUsingKeyManager: isUsingKeyManager) + let decrypted = try await decrypt(text: text, keys: keys) return decrypted.text } @@ -165,19 +175,14 @@ final class MessageService { } let encrypted = message.raw ?? message.body.text - let decrypted = try await core.parseDecryptMsg( - encrypted: encrypted.data(), + + let decrypted = try await decrypt( + text: encrypted, keys: keys, - msgPwd: nil, isMime: message.raw != nil, verificationPubKeys: verificationPubKeys ) - guard !hasMsgBlockThatNeedsPassPhrase(decrypted) else { - let keyPair = keys.first(where: { $0.passphrase == nil }) - throw MessageServiceError.missingPassPhrase(keyPair) - } - return try await process( message: message, with: decrypted diff --git a/FlowCrypt/Functionality/Services/EKMVcHelper.swift b/FlowCrypt/Functionality/Services/EKMVcHelper.swift index 18eb4ce48..b8a986632 100644 --- a/FlowCrypt/Functionality/Services/EKMVcHelper.swift +++ b/FlowCrypt/Functionality/Services/EKMVcHelper.swift @@ -159,7 +159,7 @@ final class EKMVcHelper: EKMVcHelperType { viewController.presentedViewController?.dismiss(animated: true) - Task { + Task { do { let matched = try await self.handlePassPhraseEntry( appContext: self.appContext, diff --git a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift index 45a6c5d8d..d8a084900 100644 --- a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift @@ -154,7 +154,7 @@ extension GoogleUserService: UserServiceType { return continuation.resume(throwing: error) } } - Task { + Task { do { return continuation.resume( returning: try await self.handleGoogleAuthStateResult( From 6773d1e2e862c38e946430751b46cd596421fdbf Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 5 Oct 2022 13:50:07 +0300 Subject: [PATCH 33/56] code cleanup --- .../ComposeViewController+Nodes.swift | 10 ++++-- .../ComposeViewController+Setup.swift | 4 ++- FlowCrypt/Controllers/Inbox/InboxItem.swift | 31 ++++++++++++------- .../ComposeMessageService.swift | 1 + 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index 963f9e5f1..fa7f8dc9d 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -160,16 +160,20 @@ extension ComposeViewController { } DispatchQueue.main.async { - textNode.textView.attributedText = mutableString + if !mutableString.string.isEmpty { + textNode.textView.attributedText = mutableString + } + // Set cursor position to start of text view textNode.textView.textView.selectedTextRange = textNode.textView.textView.textRange( from: textNode.textView.textView.beginningOfDocument, to: textNode.textView.textView.beginningOfDocument ) + self.node.reloadData() if self.input.shouldFocusTextNode { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { textNode.becomeFirstResponder() - }) + } } } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 798771a02..4d09c2af9 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -81,6 +81,8 @@ extension ComposeViewController { } private func addRecipients(from info: ComposeMessageInput.MessageQuoteInfo) { + guard contextToSend.recipients.isEmpty else { return } + for recipient in info.recipients { add(recipient: recipient, type: .to) } @@ -139,7 +141,7 @@ extension ComposeViewController { search .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() - .map { [weak self] query -> String in + .map { [weak self] query in if query.isEmpty { self?.updateView(newState: .main) } diff --git a/FlowCrypt/Controllers/Inbox/InboxItem.swift b/FlowCrypt/Controllers/Inbox/InboxItem.swift index 86621f557..9fa619799 100644 --- a/FlowCrypt/Controllers/Inbox/InboxItem.swift +++ b/FlowCrypt/Controllers/Inbox/InboxItem.swift @@ -27,6 +27,7 @@ extension InboxItem { return nil } } + var subject: String? { messages .compactMap(\.subject) @@ -74,6 +75,17 @@ extension InboxItem { DateFormatter().formatDate(date) } + var recipients: [String] { + messages + .flatMap(\.allRecipients) + .map(\.shortName) + .unique() + } + + var senderNames: [String] { + messages.compactMap(\.sender?.shortName).unique() + } + var title: NSAttributedString { let style: NSAttributedString.Style = isRead ? .regular(17) @@ -84,29 +96,24 @@ extension InboxItem { : .mainTextUnreadColor if folderPath == MessageLabel.sent.value || folderPath == MessageLabel.draft.value { - let recipients = messages - .flatMap(\.allRecipients) - .map(\.shortName) - .unique() - .joined(separator: ", ") - return "To: \(recipients)".attributed(style, color: textColor) + let recipientsList = recipients.joined(separator: ",") + return "To: \(recipientsList)".attributed(style, color: textColor) } else { let hasDrafts = messages.contains(where: { $0.isDraft }) - let senderNames = messages - .compactMap(\.sender?.shortName) - .unique() + let sendersList = senderNames .joined(separator: ",") .attributed(style, color: textColor) if hasDrafts { - let draftLabel = "compose_draft".localized.attributed(style, color: .red.withAlphaComponent(0.65)) - let title = senderNames.mutable() + let draftLabel = "compose_draft".localized + .attributed(style, color: .red.withAlphaComponent(0.65)) + let title = sendersList.mutable() title.append(",".attributed(style, color: textColor)) title.append(draftLabel) return title } else { - return senderNames + return sendersList } } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 56c568147..2e051fe80 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -248,6 +248,7 @@ final class ComposeMessageService { func deleteDraft() async throws { guard let draftId = messageIdentifier?.draftId else { return } try await draftGateway?.deleteDraft(with: draftId) + messageIdentifier = nil } // MARK: - Encrypt and Send From 34ce1d6b39102f0e563d4ad013d3a0115cce3f61 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 5 Oct 2022 14:17:46 +0300 Subject: [PATCH 34/56] update ui test --- .swift-version | 2 +- appium/tests/data/index.ts | 6 +++++ .../CheckDraftFunctionality.spec.ts | 23 ++++++++++++++----- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.swift-version b/.swift-version index 2df33d769..760606e1f 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -5.6 +5.7 diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index 9140882a6..7e5f3eb76 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -46,6 +46,12 @@ export const CommonData = { secondDate: 'Feb 07', thirdDate: 'Feb 08', }, + draft: { + subject: 'Draft subject', + text1: 'Draft text', + updatedText1: 'Some new text', + text2: 'Another draft' + }, archivedThread: { subject: 'Archived thread' }, diff --git a/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts b/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts index 04caf61be..ac9e31946 100644 --- a/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts +++ b/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts @@ -1,6 +1,7 @@ import { MockApi } from 'api-mocks/mock'; import { MockApiConfig } from 'api-mocks/mock-config'; import { MockUserList } from 'api-mocks/mock-data'; +import { CommonData } from 'tests/data'; import { EmailScreen, MailFolderScreen, MenuBarScreen, NewMessageScreen, @@ -14,16 +15,16 @@ describe('COMPOSE EMAIL: ', () => { const mockApi = new MockApi(); const recipient = MockUserList.robot; - const subject = 'Test 1'; - const draftSubject = "Draft subject"; - const draftText1 = 'Draft text'; - const updatedDraftText = 'Some new text'; - const draftText2 = 'Another draft'; + const subject = CommonData.simpleEmail.subject; + const draftSubject = CommonData.draft.subject; + const draftText1 = CommonData.draft.text1; + const updatedDraftText = CommonData.draft.updatedText1; + const draftText2 = CommonData.draft.text2; mockApi.fesConfig = MockApiConfig.defaultEnterpriseFesConfiguration; mockApi.ekmConfig = MockApiConfig.defaultEnterpriseEkmConfiguration; mockApi.addGoogleAccount('e2e.enterprise.test@flowcrypt.com', { - messages: [subject], + messages: ['Test 1'], }); mockApi.attesterConfig = { servedPubkeys: { @@ -37,18 +38,23 @@ describe('COMPOSE EMAIL: ', () => { await MailFolderScreen.checkInboxScreen(); await MailFolderScreen.clickOnEmailBySubject(subject); + // compose draft as reply to existing thread await EmailScreen.clickReplyButton(); await NewMessageScreen.checkMessageFieldFocus(); await NewMessageScreen.addMessageText(draftText1); await NewMessageScreen.clickBackButton(); + // compose another draft await EmailScreen.clickReplyButton(); await NewMessageScreen.checkMessageFieldFocus(); await NewMessageScreen.addMessageText(draftText2); await NewMessageScreen.clickBackButton(); + // check if drafts are added to thread messages await EmailScreen.checkDraft(draftText1, 1); await EmailScreen.checkDraft(draftText2, 2); + + // update draft and check if changes are applied await EmailScreen.openDraft(1); await NewMessageScreen.setComposeSecurityMessage(updatedDraftText); @@ -56,12 +62,15 @@ describe('COMPOSE EMAIL: ', () => { await EmailScreen.checkDraft(updatedDraftText, 1); await EmailScreen.checkDraft(draftText2, 2); + + // delete draft from thread screen await EmailScreen.deleteDraft(1); await EmailScreen.clickBackButton(); await MenuBarScreen.clickMenuBtn(); await MenuBarScreen.clickDraftsButton(); + // delete draft from compose screen await MailFolderScreen.clickOnEmailBySubject(subject); await EmailScreen.checkDraft(draftText2, 1); await EmailScreen.openDraft(1); @@ -73,11 +82,13 @@ describe('COMPOSE EMAIL: ', () => { await MenuBarScreen.clickMenuBtn(); await MenuBarScreen.clickInboxButton(); + // compose new draft await MailFolderScreen.checkInboxScreen(); await MailFolderScreen.clickCreateEmail(); await NewMessageScreen.composeEmail(recipient.email, draftSubject, draftText1); await NewMessageScreen.clickBackButton(); + // send draft and check if sent message added to 'sent' folder await MenuBarScreen.clickMenuBtn(); await MenuBarScreen.clickDraftsButton(); await MailFolderScreen.clickOnEmailBySubject(draftSubject); From 2fa34bc381c7a482519c12da899ff2a09d945430 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 5 Oct 2022 15:32:26 +0300 Subject: [PATCH 35/56] add toasts for draft actions --- .../Compose/ComposeViewControllerInput.swift | 2 ++ .../ComposeViewController+Drafts.swift | 5 +-- FlowCrypt/Controllers/Inbox/InboxItem.swift | 2 +- .../Threads/ThreadDetailsViewController.swift | 6 +++- FlowCrypt/Core/Core.swift | 2 +- .../Message Provider/MessageAttachment.swift | 4 +-- .../ComposeMessageService.swift | 33 +++++++++++-------- .../Resources/en.lproj/Localizable.strings | 9 ++--- 8 files changed, 39 insertions(+), 24 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index 29bcf8f39..11a057246 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -24,6 +24,7 @@ struct ComposeMessageInput: Equatable { let replyToMsgId: String? let inReplyTo: String? let rfc822MsgId: String? + let shouldEncrypt: Bool let attachments: [MessageAttachment] } @@ -124,6 +125,7 @@ extension ComposeMessageInput.MessageQuoteInfo { self.rfc822MsgId = message.rfc822MsgId self.replyToMsgId = message.replyToMsgId self.inReplyTo = message.inReplyTo + self.shouldEncrypt = message.isPgp self.attachments = processed?.attachments ?? message.attachments } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 3479609e4..9e282d96f 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -54,9 +54,10 @@ extension ComposeViewController { isDraft: true ) - try await composeMessageService.encryptAndSaveDraft( + try await composeMessageService.saveDraft( message: sendableMsg, - threadId: draft.input.threadId + threadId: draft.input.threadId, + shouldEncrypt: draft.input.isPgp ) composedLatestDraft = draft diff --git a/FlowCrypt/Controllers/Inbox/InboxItem.swift b/FlowCrypt/Controllers/Inbox/InboxItem.swift index 9fa619799..6928933fd 100644 --- a/FlowCrypt/Controllers/Inbox/InboxItem.swift +++ b/FlowCrypt/Controllers/Inbox/InboxItem.swift @@ -106,7 +106,7 @@ extension InboxItem { .attributed(style, color: textColor) if hasDrafts { - let draftLabel = "compose_draft".localized + let draftLabel = "draft".localized .attributed(style, color: .red.withAlphaComponent(0.65)) let title = sendersList.mutable() title.append(",".attributed(style, color: textColor)) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index eedf75921..6ea7c0ced 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -242,6 +242,7 @@ extension ThreadDetailsViewController { } handle(processedMessage: processedMessage, at: IndexPath(row: 0, section: section)) + showToast("draft_saved".localized) } } @@ -273,6 +274,8 @@ extension ThreadDetailsViewController { input.remove(at: index) node.deleteSections([index + 1], with: .automatic) + + showToast("draft_deleted".localized) } private func getAndProcessMessage( @@ -409,6 +412,7 @@ extension ThreadDetailsViewController { replyToMsgId: replyToMsgId, inReplyTo: input.rawMessage.inReplyTo, rfc822MsgId: input.rawMessage.rfc822MsgId, + shouldEncrypt: input.rawMessage.isPgp, attachments: attachments ) @@ -819,7 +823,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { return LabelCellNode( input: .init( - title: "compose_draft".localized.attributed(color: .red), + title: "draft".localized.attributed(color: .red), text: body.removingMailThreadQuote().attributed(color: .secondaryLabel), accessibilityIdentifier: "aid-draft-body-\(messageIndex)", labelAccessibilityIdentifier: "aid-draft-label-\(messageIndex)", diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index fb5bb8a76..71a54319e 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -211,7 +211,7 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { "inReplyTo": msg.inReplyTo, "atts": msg.atts.map { att in ["name": att.name, "type": att.type, "base64": att.base64] }, "format": fmt.rawValue, - "pubKeys": msg.pubKeys, + "pubKeys": fmt == .plain ? nil : msg.pubKeys, "signingPrv": msg.signingPrv.ifNotNil(\.prvKeyInfoJsonDictForCore) ], data: nil) return CoreRes.ComposeEmail(mimeEncoded: r.data) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageAttachment.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageAttachment.swift index 20bcf94e6..83ddb62f2 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageAttachment.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageAttachment.swift @@ -53,7 +53,7 @@ extension MessageAttachment { } extension MessageAttachment { - func toSendableMsgAttachment() -> SendableMsg.Attachment { - return SendableMsg.Attachment(name: name, type: type, base64: data?.base64EncodedString() ?? "") + var sendableMsgAttachment: SendableMsg.Attachment { + SendableMsg.Attachment(name: name, type: type, base64: data?.base64EncodedString() ?? "") } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 2e051fe80..46b7cc02b 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -144,16 +144,23 @@ final class ComposeMessageService { } } - let sendableAttachments: [SendableMsg.Attachment] = !isDraft - ? contextToSend.attachments.map { $0.toSendableMsgAttachment() } - : [] - - let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) - let validPubKeys = try validate( - recipients: recipientsWithPubKeys, - hasMessagePassword: contextToSend.hasMessagePassword, - ignoreErrors: isDraft - ) + let sendableAttachments: [SendableMsg.Attachment] = isDraft + ? [] + : contextToSend.attachments.map(\.sendableMsgAttachment) + + let pubKeys: [String] + + if isDraft { + pubKeys = [] + } else { + let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) + let validPubKeys = try validate( + recipients: recipientsWithPubKeys, + hasMessagePassword: contextToSend.hasMessagePassword, + ignoreErrors: isDraft + ) + pubKeys = senderKeys.map(\.public) + validPubKeys + } let signingPrv = isDraft ? nil : try await prepareSigningKey(senderEmail: contextToSend.sender) @@ -168,7 +175,7 @@ final class ComposeMessageService { replyToMsgId: input.replyToMsgId, inReplyTo: input.inReplyTo, atts: sendableAttachments, - pubKeys: senderKeys.map(\.public) + validPubKeys, + pubKeys: pubKeys, signingPrv: signingPrv, password: contextToSend.messagePassword ) @@ -226,11 +233,11 @@ final class ComposeMessageService { self.messageIdentifier = try await draftGateway?.fetchDraftIdentifier(for: identifier) } - func encryptAndSaveDraft(message: SendableMsg, threadId: String?) async throws { + func saveDraft(message: SendableMsg, threadId: String?, shouldEncrypt: Bool) async throws { do { let mime = try await core.composeEmail( msg: message, - fmt: .encryptInline + fmt: shouldEncrypt ? .encryptInline : .plain ).mimeEncoded self.messageIdentifier = try await draftGateway?.saveDraft( diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 9b3c1433d..2be160f8b 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -123,9 +123,6 @@ "compose_sign_passphrase_required" = "Passphrase is required for message signing."; "compose_sign_passphrase_no_match" = "This pass phrase did not match your signing private key."; "compose_sign_no_keys" = "Cannot sign message: none of your %@ account keys can be used for sending address %@"; -"compose_draft_passphrase_alert" = "Enter passphrase to save draft"; -"compose_draft_discard" = "Discard draft"; -"compose_draft_passphrase_placeholder" = "Draft not saved - tap to add pass phrase"; "compose_password_placeholder" = "Tap to add password for recipients who don't have encryption set up."; "compose_password_set_message" = "Web portal password added"; "compose_password_modal_title" = "Set web portal password"; @@ -154,7 +151,11 @@ "compose_recipient_copy" = "Copy"; "compose_recipient_edit" = "Edit"; "compose_recipient_remove" = "Remove"; -"compose_draft" = "Draft"; + +// Drafts +"draft" = "Draft"; +"draft_saved" = "Draft saved"; +"draft_deleted" = "Draft deleted"; // Folders "folder_all_mail" = "All Mail"; From 48f068dc875b8eb29a92058332d921f873fff417 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 5 Oct 2022 16:41:58 +0300 Subject: [PATCH 36/56] fix draft encrypting --- .../ComposeViewController+Drafts.swift | 2 +- .../ComposeViewController+TapActions.swift | 1 + .../Threads/ThreadDetailsViewController.swift | 3 +- .../ComposeMessageService.swift | 47 +++++++------------ 4 files changed, 21 insertions(+), 32 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 9e282d96f..0a43748f3 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -57,7 +57,7 @@ extension ComposeViewController { try await composeMessageService.saveDraft( message: sendableMsg, threadId: draft.input.threadId, - shouldEncrypt: draft.input.isPgp + shouldEncrypt: draft.input.type.info?.shouldEncrypt ?? true ) composedLatestDraft = draft diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index a0294d40e..2d7bc9b97 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -48,6 +48,7 @@ extension ComposeViewController { try await composeMessageService.deleteDraft() if let messageIdentifier = composeMessageService.messageIdentifier { + composeMessageService.messageIdentifier = nil handleAction?(.delete(messageIdentifier)) } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 6ea7c0ced..5b574afcc 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -843,7 +843,8 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { actionButtonTitle: "delete".localized, actionStyle: .destructive, onAction: { [weak self] _ in - guard let self = self else { return } + guard let self else { return } + Task { try await self.messageOperationsProvider.deleteMessage( id: id, diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 46b7cc02b..d5db60992 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -147,23 +147,14 @@ final class ComposeMessageService { let sendableAttachments: [SendableMsg.Attachment] = isDraft ? [] : contextToSend.attachments.map(\.sendableMsgAttachment) - - let pubKeys: [String] - - if isDraft { - pubKeys = [] - } else { - let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) - let validPubKeys = try validate( - recipients: recipientsWithPubKeys, - hasMessagePassword: contextToSend.hasMessagePassword, - ignoreErrors: isDraft - ) - pubKeys = senderKeys.map(\.public) + validPubKeys - } - let signingPrv = isDraft ? nil : try await prepareSigningKey(senderEmail: contextToSend.sender) + let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) + let validPubKeys = try validate( + recipients: recipientsWithPubKeys, + hasMessagePassword: contextToSend.hasMessagePassword + ) + return SendableMsg( text: contextToSend.message ?? "", html: nil, @@ -175,7 +166,7 @@ final class ComposeMessageService { replyToMsgId: input.replyToMsgId, inReplyTo: input.inReplyTo, atts: sendableAttachments, - pubKeys: pubKeys, + pubKeys: senderKeys.map(\.public) + validPubKeys, signingPrv: signingPrv, password: contextToSend.messagePassword ) @@ -199,8 +190,7 @@ final class ComposeMessageService { private func validate( recipients: [RecipientWithSortedPubKeys], - hasMessagePassword: Bool, - ignoreErrors: Bool = false + hasMessagePassword: Bool ) throws -> [String] { func contains(keyState: PubKeyState) -> Bool { recipients.contains(where: { $0.keyState == keyState }) @@ -209,24 +199,22 @@ final class ComposeMessageService { logger.logDebug("validate recipients: \(recipients)") logger.logDebug("validate recipient keyStates: \(recipients.map(\.keyState))") - if !ignoreErrors { - guard hasMessagePassword || !contains(keyState: .empty) else { - throw MessageValidationError.noPubRecipients - } + guard hasMessagePassword || !contains(keyState: .empty) else { + throw MessageValidationError.noPubRecipients + } - guard !contains(keyState: .expired) else { - throw MessageValidationError.expiredKeyRecipients - } - guard !contains(keyState: .revoked) else { - throw MessageValidationError.revokedKeyRecipients - } + guard !contains(keyState: .expired) else { + throw MessageValidationError.expiredKeyRecipients + } + guard !contains(keyState: .revoked) else { + throw MessageValidationError.revokedKeyRecipients } return recipients.flatMap(\.activePubKeys).map(\.armored) } // MARK: - Drafts - private(set) var messageIdentifier: MessageIdentifier? + var messageIdentifier: MessageIdentifier? func fetchDraftIdentifier(for messageId: String) async throws { let identifier = Identifier(stringId: messageId) @@ -255,7 +243,6 @@ final class ComposeMessageService { func deleteDraft() async throws { guard let draftId = messageIdentifier?.draftId else { return } try await draftGateway?.deleteDraft(with: draftId) - messageIdentifier = nil } // MARK: - Encrypt and Send From d69a79b969f6651b642576fd162c364498af2d25 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 5 Oct 2022 16:51:51 +0300 Subject: [PATCH 37/56] update draft toasts --- .../Compose/Extensions/ComposeViewController+Setup.swift | 1 + .../Compose/Extensions/ComposeViewController+TapActions.swift | 1 + .../Controllers/Threads/ThreadDetailsViewController.swift | 3 --- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 4d09c2af9..c1635bff4 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -170,6 +170,7 @@ extension ComposeViewController: NavigationChildController { messageIdentifier.draftMessageId = self.input.type.info?.id self.handleAction?(.update(messageIdentifier)) } + self.showToast("draft_saved".localized) self.navigationController?.popViewController(animated: true) } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index 2d7bc9b97..876408b37 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -52,6 +52,7 @@ extension ComposeViewController { handleAction?(.delete(messageIdentifier)) } + showToast("draft_deleted".localized) navigationController?.popViewController(animated: true) } catch { handle(error: error) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 5b574afcc..d18358d1a 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -242,7 +242,6 @@ extension ThreadDetailsViewController { } handle(processedMessage: processedMessage, at: IndexPath(row: 0, section: section)) - showToast("draft_saved".localized) } } @@ -274,8 +273,6 @@ extension ThreadDetailsViewController { input.remove(at: index) node.deleteSections([index + 1], with: .automatic) - - showToast("draft_deleted".localized) } private func getAndProcessMessage( From 0cf01ad269c39d45aad0379430fec59dd652a1d9 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 5 Oct 2022 21:42:32 +0300 Subject: [PATCH 38/56] code cleanup --- .../ComposeViewController+Setup.swift | 18 +++--------------- ...reatePassphraseAbstractViewController.swift | 2 +- .../Message Gateway/GmailService+draft.swift | 4 ++-- .../Message Gateway/GmailService+send.swift | 2 +- .../Message Gateway/Imap+send.swift | 2 +- .../Message Provider/Gmail+Message.swift | 4 ++-- .../Gmail+MessagesList.swift | 2 +- .../Threads/MessagesThreadProvider.swift | 4 ++-- .../Backup Services/BackupService.swift | 2 +- .../Functionality/Services/EKMVcHelper.swift | 2 +- .../GoogleUserService.swift | 2 +- FlowCrypt/Models/Common/RecipientBase.swift | 4 ++-- .../Models/Inbox Models/InboxViewModel.swift | 4 ---- 13 files changed, 18 insertions(+), 34 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index c1635bff4..c5d12c52f 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -101,26 +101,14 @@ extension ComposeViewController { } private func decodeDraft(from info: ComposeMessageInput.MessageQuoteInfo) { - let message = Message( - identifier: .random, - date: info.sentDate, - sender: info.sender, - subject: info.subject, - size: nil, - labels: [], - attachmentIds: [], - body: .init(text: info.text, html: nil) - ) - Task { do { - let processedMessage = try await messageService.decryptAndProcess( - message: message, - onlyLocalKeys: false, + let decrypted = try await messageService.decrypt( + text: info.text, userEmail: appContext.user.email, isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) - contextToSend.message = processedMessage.text + contextToSend.message = decrypted reload(sections: Section.recipientsSections) didFinishSetup = true } catch { diff --git a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift index 916a60191..d9498dd09 100644 --- a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift @@ -146,7 +146,7 @@ extension SetupCreatePassphraseAbstractViewController { } private func awaitUserPassPhraseEntry() async throws -> String? { - return await withCheckedContinuation { (continuation: CheckedContinuation) in + return await withCheckedContinuation { continuation in DispatchQueue.main.async { let alert = UIAlertController( title: "setup_pass_phrase_title".localized, diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index a2060feb9..815ba524f 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -16,7 +16,7 @@ extension GmailService: DraftGateway { query.q = "rfc822msgid:\(id)" query.maxResults = 1 - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in gmailService.executeQuery(query) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) @@ -36,7 +36,7 @@ extension GmailService: DraftGateway { } func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageIdentifier { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + try await withCheckedThrowingContinuation { continuation in guard let raw = GTLREncodeBase64(input.mime) else { return continuation.resume(throwing: GmailServiceError.messageEncode) } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift index f6216a431..411057c62 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+send.swift @@ -10,7 +10,7 @@ import GoogleAPIClientForREST_Gmail extension GmailService: MessageGateway { func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws -> Identifier { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + try await withCheckedThrowingContinuation { continuation in guard let raw = GTLREncodeBase64(input.mime) else { return continuation.resume(throwing: GmailServiceError.messageEncode) } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift index b36ffb534..87ab852ff 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/Imap+send.swift @@ -6,7 +6,7 @@ import Foundation extension Imap: MessageGateway { func sendMail(input: MessageGatewayInput, progressHandler: ((Float) -> Void)?) async throws -> Identifier { - try await withCheckedThrowingContinuation { [weak self] (continuation: CheckedContinuation) in + try await withCheckedThrowingContinuation { [weak self] continuation in do { let session = try self?.smtpSess session?.sendOperation(with: input.mime) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift index 5d3cadb6a..4f94ac95e 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift @@ -20,7 +20,7 @@ extension GmailService: MessageProvider { } let query = createMessageQuery(identifier: identifier, format: kGTLRGmailFormatFull) - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in self.gmailService.executeQuery(query) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) @@ -46,7 +46,7 @@ extension GmailService: MessageProvider { } let query = createMessageQuery(identifier: identifier, format: kGTLRGmailFormatRaw) - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in self.gmailService.executeQuery(query) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift index d0763950d..7b5332ddb 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Gmail+MessagesList.swift @@ -60,7 +60,7 @@ extension GmailService { query.q = searchQuery } - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in gmailService.executeQuery(query) { _, data, error in if let error = error { return continuation.resume(throwing: GmailServiceError.providerError(error)) diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift index cca956aff..de0d29de3 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift @@ -42,7 +42,7 @@ extension GmailService: MessagesThreadProvider { private func getThreadsList(using context: FetchMessageContext) async throws -> GTLRGmail_ListThreadsResponse { let query = try makeQuery(using: context) return try await Task.retrying { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + try await withCheckedThrowingContinuation { continuation in self.gmailService.executeQuery(query) { _, data, error in if let error = error { let gmailError = GmailServiceError.convert(from: error as NSError) @@ -60,7 +60,7 @@ extension GmailService: MessagesThreadProvider { func fetchThread(identifier: String, path: String) async throws -> MessageThread { return try await Task.retrying { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + try await withCheckedThrowingContinuation { continuation in self.gmailService.executeQuery( GTLRGmailQuery_UsersThreadsGet.query(withUserId: .me, identifier: identifier) ) { _, data, error in diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift index 89f9a7c5b..e2cbf0d83 100644 --- a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift @@ -70,7 +70,7 @@ extension BackupService: BackupServiceType { ) let t = try await core.composeEmail(msg: message, fmt: .plain) - try await messageGateway.sendMail( + _ = try await messageGateway.sendMail( input: MessageGatewayInput(mime: t.mimeEncoded, threadId: nil), progressHandler: nil ) diff --git a/FlowCrypt/Functionality/Services/EKMVcHelper.swift b/FlowCrypt/Functionality/Services/EKMVcHelper.swift index b8a986632..7dcb6d209 100644 --- a/FlowCrypt/Functionality/Services/EKMVcHelper.swift +++ b/FlowCrypt/Functionality/Services/EKMVcHelper.swift @@ -146,7 +146,7 @@ final class EKMVcHelper: EKMVcHelperType { @MainActor private func requestPassPhraseWithModal(in viewController: UIViewController) async throws -> String { - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in let alert = alertsFactory.makePassPhraseAlert( title: "refresh_key_alert_title".localized, onCancel: { diff --git a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift index d8a084900..cacada275 100644 --- a/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/Google User Service/GoogleUserService.swift @@ -274,7 +274,7 @@ extension GoogleUserService { private func fetchGoogleUser( with authorization: GTMAppAuthFetcherAuthorization ) async throws -> GTLROauth2_Userinfo { - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { continuation in let query = GTLROauth2Query_UserinfoGet.query() let authService = GTLROauth2Service() if Bundle.shouldUseMockGmailApi { diff --git a/FlowCrypt/Models/Common/RecipientBase.swift b/FlowCrypt/Models/Common/RecipientBase.swift index 81558a52c..a4d2429fc 100644 --- a/FlowCrypt/Models/Common/RecipientBase.swift +++ b/FlowCrypt/Models/Common/RecipientBase.swift @@ -15,7 +15,7 @@ protocol RecipientBase { extension RecipientBase { var formatted: String { - guard let name = name else { return email } + guard let name else { return email } return "\(name) <\(email)>" } @@ -26,7 +26,7 @@ extension RecipientBase { } var displayName: String { - if let name = name, !name.isEmpty { + if let name, !name.isEmpty { return name } return email diff --git a/FlowCrypt/Models/Inbox Models/InboxViewModel.swift b/FlowCrypt/Models/Inbox Models/InboxViewModel.swift index e331330a5..d09c8042e 100644 --- a/FlowCrypt/Models/Inbox Models/InboxViewModel.swift +++ b/FlowCrypt/Models/Inbox Models/InboxViewModel.swift @@ -16,10 +16,6 @@ struct InboxViewModel { self.folderName = folderName self.path = folderName.isEmpty ? "Inbox" : path } - - var isDrafts: Bool { - return folderName == "Drafts" - } } extension InboxViewModel { From 4f3405c4490836c2c125793eb6205c6677abc90f Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 6 Oct 2022 22:22:25 +0300 Subject: [PATCH 39/56] pr fixes --- FlowCrypt/Controllers/Compose/ComposeViewController.swift | 2 +- .../Controllers/Compose/ComposeViewControllerInput.swift | 3 +-- .../Controllers/Threads/ThreadDetailsViewController.swift | 2 +- .../Mail Provider/MessagesList Provider/Model/Message.swift | 2 +- .../Compose Message Service/ComposeMessageService.swift | 4 +--- FlowCrypt/Resources/en.lproj/Localizable.strings | 1 + FlowCryptCommon/Extensions/StringExtensions.swift | 4 ++++ 7 files changed, 10 insertions(+), 8 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index bf734bdc1..7d136c1b8 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -126,7 +126,7 @@ final class ComposeViewController: TableNodeViewController { if let composeMessageService = composeMessageService { self.composeMessageService = composeMessageService } else { - self.composeMessageService = try await ComposeMessageService( + self.composeMessageService = ComposeMessageService( appContext: appContext, keyMethods: keyMethods, draftGateway: draftGateway diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index 11a057246..fba4a5952 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -46,8 +46,7 @@ struct ComposeMessageInput: Equatable { } var isPgp: Bool { - guard let text = text else { return false } - return text.contains("-----BEGIN PGP ") && text.contains("-----END PGP ") + text?.isPgp ?? false } var replyToMsgId: String? { diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index d18358d1a..aa6ebe863 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -516,7 +516,7 @@ extension ThreadDetailsViewController { if let someError = error as NSError?, someError.code == Imap.Err.fetch.rawValue { // todo - the missing msg should be removed from the list in inbox view // reproduce: 1) load inbox 2) move msg to trash on another email client 3) open trashed message in inbox - showToast("Message not found in folder: \(inboxItem.folderPath)") + showToast("message_not_found_in_folder".localized + inboxItem.folderPath) } else { showRetryAlert(message: error.errorMessage, onRetry: { [weak self] _ in self?.fetchDecryptAndRenderMsg(at: indexPath) diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index a416a505f..22b34025c 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -44,7 +44,7 @@ struct Message: Hashable { var isDraft: Bool { labels.contains(.draft) } var isPgp: Bool { - (body.text.contains("-----BEGIN PGP ") && body.text.contains("-----END PGP ")) || hasSignatureAttachment + body.text.isPgp || hasSignatureAttachment } var hasSignatureAttachment: Bool { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index d5db60992..dd5dfa529 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -24,7 +24,6 @@ final class ComposeMessageService { private let localContactsProvider: LocalContactsProviderType private let core: CoreComposeMessageType & KeyParser private let draftGateway: DraftGateway? - private let messageOperationsProvider: MessageOperationsProvider private lazy var logger = Logger.nested(Self.self) private struct ReplyInfo: Encodable { @@ -42,13 +41,12 @@ final class ComposeMessageService { draftGateway: DraftGateway? = nil, core: CoreComposeMessageType & KeyParser = Core.shared, localContactsProvider: LocalContactsProviderType? = nil - ) async throws { + ) { self.appContext = appContext self.keyMethods = keyMethods self.draftGateway = draftGateway self.core = core self.localContactsProvider = localContactsProvider ?? LocalContactsProvider(encryptedStorage: appContext.encryptedStorage) - self.messageOperationsProvider = try await appContext.getRequiredMailProvider().messageOperationsProvider } private var onStateChanged: ((State) -> Void)? diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 2be160f8b..acfe6e8b0 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -69,6 +69,7 @@ "message_decrypt_error" = "decrypt error"; "message_mark_read_error" = "Could not mark message as read: %@"; "message_reply_all" = "Reply all"; +"message_not_found_in_folder" = "Message not found in folder: "; // ERROR "error_fetch_folders" = "Could not fetch folders"; diff --git a/FlowCryptCommon/Extensions/StringExtensions.swift b/FlowCryptCommon/Extensions/StringExtensions.swift index fc5bab734..14bbf509f 100644 --- a/FlowCryptCommon/Extensions/StringExtensions.swift +++ b/FlowCryptCommon/Extensions/StringExtensions.swift @@ -9,6 +9,10 @@ public extension String { !trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + var isPgp: Bool { + contains("-----BEGIN PGP ") && contains("-----END PGP ") + } + var trimLeadingSlash: String { if isNotEmpty, self[startIndex] == "/" { return String(dropFirst()) From aeede8c18b89a01122087db75224aecfc3f52a49 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 7 Oct 2022 14:38:45 +0300 Subject: [PATCH 40/56] pr fixes --- .../xcshareddata/swiftpm/Package.resolved | 8 +-- .../ComposeViewController+Drafts.swift | 2 +- .../ComposeViewController+Setup.swift | 4 +- .../ComposeViewController+TapActions.swift | 2 +- FlowCrypt/Controllers/Inbox/InboxItem.swift | 13 ++++ .../Controllers/Inbox/InboxProviders.swift | 11 +-- .../Inbox/InboxViewController.swift | 68 +++++++------------ .../ComposeMessageService.swift | 21 +++--- 8 files changed, 63 insertions(+), 66 deletions(-) diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5d34ef24d..36e18bdc1 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-cocoa", "state" : { - "revision" : "437fd367c4fcdaf7c532f1f40cb4ed5fab804e7c", - "version" : "10.30.0" + "revision" : "ae4278abe1fcdcd616b410e098a744c1cc73e0bd", + "version" : "10.31.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-core", "state" : { - "revision" : "18abbb4e9dc268620fa499923a92921bf26db8c6", - "version" : "12.7.0" + "revision" : "b86d2107fec4246b55af24a3b11e00275575f808", + "version" : "12.9.0" } }, { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 0a43748f3..b855d32ba 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -40,7 +40,7 @@ extension ComposeViewController { return newDraft != existingDraft ? newDraft : nil } - func saveDraftIfNeeded(withAlert: Bool = false, handler: ((Error?) -> Void)? = nil) { + func saveDraftIfNeeded(handler: ((Error?) -> Void)? = nil) { guard let draft = createDraft() else { handler?(nil) return diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index c5d12c52f..3f4eb1865 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -148,7 +148,7 @@ extension ComposeViewController: NavigationChildController { func handleBackButtonTap() { stopDraftTimer(withSave: false) - saveDraftIfNeeded(withAlert: true) { [weak self] error in + saveDraftIfNeeded { [weak self] error in guard let self = self else { return } if let error = error { @@ -158,7 +158,7 @@ extension ComposeViewController: NavigationChildController { messageIdentifier.draftMessageId = self.input.type.info?.id self.handleAction?(.update(messageIdentifier)) } - self.showToast("draft_saved".localized) + self.showToast("draft_saved".localized, duration: 1.0) self.navigationController?.popViewController(animated: true) } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index 876408b37..5c49c5878 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -52,7 +52,7 @@ extension ComposeViewController { handleAction?(.delete(messageIdentifier)) } - showToast("draft_deleted".localized) + showToast("draft_deleted".localized, duration: 1.0) navigationController?.popViewController(animated: true) } catch { handle(error: error) diff --git a/FlowCrypt/Controllers/Inbox/InboxItem.swift b/FlowCrypt/Controllers/Inbox/InboxItem.swift index 6928933fd..7c9549af4 100644 --- a/FlowCrypt/Controllers/Inbox/InboxItem.swift +++ b/FlowCrypt/Controllers/Inbox/InboxItem.swift @@ -166,3 +166,16 @@ extension InboxItem { } } } + +extension [InboxItem] { + func firstIndex(with messageIdentifier: MessageIdentifier) -> Int? { + firstIndex(where: { + switch $0.type { + case .thread(let threadId): + return threadId == messageIdentifier.threadId + case .message(let messageId): + return messageId == messageIdentifier.messageId + } + }) + } +} diff --git a/FlowCrypt/Controllers/Inbox/InboxProviders.swift b/FlowCrypt/Controllers/Inbox/InboxProviders.swift index c325f5672..30b69daef 100644 --- a/FlowCrypt/Controllers/Inbox/InboxProviders.swift +++ b/FlowCrypt/Controllers/Inbox/InboxProviders.swift @@ -14,7 +14,7 @@ struct InboxContext { } protocol InboxDataProvider { - func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxItem? + func fetchInboxItem(identifier: MessageIdentifier, path: String) async throws -> InboxItem? func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext } @@ -26,8 +26,8 @@ class InboxMessageThreadsProvider: InboxDataProvider { self.provider = provider } - func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxItem? { - guard let id = identifier.stringId else { return nil } + func fetchInboxItem(identifier: MessageIdentifier, path: String) async throws -> InboxItem? { + guard let id = identifier.threadId?.stringId else { return nil } let thread = try await provider.fetchThread(identifier: id, path: path) return InboxItem(thread: thread, folderPath: path) } @@ -58,8 +58,9 @@ class InboxMessageListProvider: InboxDataProvider { self.provider = provider } - func fetchInboxItem(identifier: Identifier, path: String) async throws -> InboxItem? { - let message = try await provider.fetchMessage(id: identifier, folder: path) + func fetchInboxItem(identifier: MessageIdentifier, path: String) async throws -> InboxItem? { + guard let id = identifier.messageId else { return nil } + let message = try await provider.fetchMessage(id: id, folder: path) return InboxItem(message: message) } diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 7922ac73c..05f922c28 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -645,68 +645,46 @@ extension InboxViewController { input: .init(type: .draft(draftInfo)), handleAction: { [weak self] action in switch action { - case .update(let identifier), .sent(let identifier), .delete(let identifier): + case .update(let identifier): self?.fetchUpdatedInboxItem(identifier: identifier) + case .sent(let identifier), .delete(let identifier): + self?.deleteInboxItem(identifier: identifier) } } ) navigationController?.pushViewController(controller, animated: true) } catch { - showAlert(message: error.localizedDescription) + showAlert(message: error.errorMessage) } } } private func fetchUpdatedInboxItem(identifier: MessageIdentifier) { - guard let index = findInboxItemIndex(identifier: identifier) else { - addInboxItem(identifier: identifier) - return - } - - updateInboxItem(at: index) - } - - private func findInboxItemIndex(identifier: MessageIdentifier) -> Int? { - return inboxInput.firstIndex(where: { - switch $0.type { - case .thread(let threadId): - return threadId == identifier.threadId - case .message(let messageId): - return messageId == identifier.messageId + Task { + guard let inboxItem = try await inboxDataProvider.fetchInboxItem( + identifier: identifier, + path: path + ), !inboxItem.messages(with: path).isEmpty else { + deleteInboxItem(identifier: identifier) + return } - }) - } - private func addInboxItem(identifier: MessageIdentifier) { - Task { - guard let threadId = identifier.threadId, - let inboxItem = try await inboxDataProvider.fetchInboxItem(identifier: threadId, path: path), - !inboxItem.messages(with: path).isEmpty - else { return } + guard let index = inboxInput.firstIndex(with: identifier) else { + inboxInput.insert(inboxItem, at: 0) + tableNode.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic) + return + } - inboxInput.insert(inboxItem, at: 0) - tableNode.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic) + inboxInput[index] = inboxItem + tableNode.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) } } - private func updateInboxItem(at index: Int) { - Task { - switch inboxInput[index].type { - case .thread(let threadId): - guard let inboxItem = try? await inboxDataProvider.fetchInboxItem(identifier: threadId, path: path), - !inboxItem.messages(with: path).isEmpty - else { - inboxInput.remove(at: index) - tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) - return - } - inboxInput[index] = inboxItem - tableNode.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) - case .message: - // used only with imap, can be implemented later - break - } - } + private func deleteInboxItem(identifier: MessageIdentifier) { + guard let index = inboxInput.firstIndex(with: identifier) else { return } + + inboxInput.remove(at: index) + tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) } // MARK: Operation diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index dd5dfa529..c7508854a 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -93,12 +93,13 @@ final class ComposeMessageService { ) async throws -> SendableMsg { let recipients = contextToSend.recipients let subject = contextToSend.subject ?? "" + let pubKeys: [String] let senderKeys = try await keyMethods.chooseSenderKeys( for: .encryption, keys: try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: sender), senderEmail: contextToSend.sender - ) + ).map(\.public) if !isDraft { onStateChanged?(.validatingMessage) @@ -140,6 +141,16 @@ final class ComposeMessageService { guard senderKeys.isNotEmpty else { throw MessageValidationError.noUsableAccountKeys } + + let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) + let validPubKeys = try validate( + recipients: recipientsWithPubKeys, + hasMessagePassword: contextToSend.hasMessagePassword + ) + + pubKeys = senderKeys + validPubKeys + } else { + pubKeys = senderKeys } let sendableAttachments: [SendableMsg.Attachment] = isDraft @@ -147,12 +158,6 @@ final class ComposeMessageService { : contextToSend.attachments.map(\.sendableMsgAttachment) let signingPrv = isDraft ? nil : try await prepareSigningKey(senderEmail: contextToSend.sender) - let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) - let validPubKeys = try validate( - recipients: recipientsWithPubKeys, - hasMessagePassword: contextToSend.hasMessagePassword - ) - return SendableMsg( text: contextToSend.message ?? "", html: nil, @@ -164,7 +169,7 @@ final class ComposeMessageService { replyToMsgId: input.replyToMsgId, inReplyTo: input.inReplyTo, atts: sendableAttachments, - pubKeys: senderKeys.map(\.public) + validPubKeys, + pubKeys: pubKeys, signingPrv: signingPrv, password: contextToSend.messagePassword ) From 2bc394ef664398f8d9a3347928b21776b2fa15a7 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 7 Oct 2022 15:32:45 +0300 Subject: [PATCH 41/56] fix compose screen leak --- .../Controllers/Compose/ComposeViewController.swift | 2 +- .../ComposeViewController+ActionHandling.swift | 6 ++++-- .../Extensions/ComposeViewController+Drafts.swift | 9 +++++++-- .../Extensions/ComposeViewController+MessageSend.swift | 2 +- .../Extensions/ComposeViewController+TapActions.swift | 4 ++++ 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 7d136c1b8..8f79e9172 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -191,7 +191,7 @@ final class ComposeViewController: TableNodeViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - startDraftTimer() + startDraftTimer(withFire: true) guard shouldEvaluateRecipientInput else { shouldEvaluateRecipientInput = true diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift index b7a0d4343..1e0fe0b99 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ActionHandling.swift @@ -184,8 +184,10 @@ extension ComposeViewController { // MARK: - Message password func setMessagePassword() { Task { + stopDraftTimer(withSave: false) contextToSend.messagePassword = await enterMessagePassword() reload(sections: [.password]) + startDraftTimer() } } @@ -211,8 +213,8 @@ extension ComposeViewController { $0.addTarget(self, action: #selector(self.messagePasswordTextFieldDidChange), for: .editingChanged) } - let cancelAction = UIAlertAction(title: "cancel".localized, style: .cancel) { _ in - return continuation.resume(returning: self.contextToSend.messagePassword) + let cancelAction = UIAlertAction(title: "cancel".localized, style: .cancel) { [weak self] _ in + return continuation.resume(returning: self?.contextToSend.messagePassword) } alert.addAction(cancelAction) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index b855d32ba..4401719b5 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -8,11 +8,16 @@ // MARK: - Drafts extension ComposeViewController { - @objc func startDraftTimer() { + @objc func startDraftTimer(withFire: Bool = false) { + guard saveDraftTimer == nil else { return } + saveDraftTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in self?.saveDraftIfNeeded() } - saveDraftTimer?.fire() + + if withFire { + saveDraftTimer?.fire() + } } @objc func stopDraftTimer(withSave: Bool = true) { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift index fde1de808..5eb92e9ae 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+MessageSend.swift @@ -13,7 +13,7 @@ import FlowCryptUI extension ComposeViewController { func sendMessage() async throws { view.endEditing(true) - stopDraftTimer(withSave: false) + navigationItem.rightBarButtonItem?.isEnabled = false let spinnerTitle = contextToSend.attachments.isEmpty ? "sending_title" : "encrypting_title" diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift index 5c49c5878..d0ac340f6 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TapActions.swift @@ -17,6 +17,8 @@ extension ComposeViewController { } func handleSendTap() { + stopDraftTimer(withSave: false) + Task { do { guard contextToSend.hasMessagePasswordIfNeeded else { @@ -43,6 +45,8 @@ extension ComposeViewController { } private func deleteDraft() { + stopDraftTimer(withSave: false) + Task { do { try await composeMessageService.deleteDraft() From 2115e069ccdf140842edc1d27fbced2e46f5922c Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 7 Oct 2022 16:36:21 +0300 Subject: [PATCH 42/56] save drafts in background --- .../ComposeViewController+Drafts.swift | 18 +++--- .../ComposeViewController+Setup.swift | 12 ++-- .../ComposeMessageContext.swift | 4 ++ .../ComposeMessageService.swift | 61 +++++++++++-------- .../Resources/en.lproj/Localizable.strings | 1 + 5 files changed, 60 insertions(+), 36 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 4401719b5..96743afea 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -45,35 +45,39 @@ extension ComposeViewController { return newDraft != existingDraft ? newDraft : nil } - func saveDraftIfNeeded(handler: ((Error?) -> Void)? = nil) { + func saveDraftIfNeeded(handler: ((Bool, Error?) -> Void)? = nil) { guard let draft = createDraft() else { - handler?(nil) + handler?(false, nil) return } Task { do { + let shouldEncrypt = draft.input.type.info?.shouldEncrypt == true || + contextToSend.hasRecipientsWithActivePubKey + let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg( input: draft.input, contextToSend: draft.contextToSend, - isDraft: true + isDraft: true, + withPubKeys: shouldEncrypt ) try await composeMessageService.saveDraft( message: sendableMsg, threadId: draft.input.threadId, - shouldEncrypt: draft.input.type.info?.shouldEncrypt ?? true + shouldEncrypt: shouldEncrypt ) composedLatestDraft = draft - handler?(nil) + handler?(true, nil) } catch { if !(error is MessageValidationError) { // no need to save or notify user if validation error // for other errors show toast - showToast("Error saving draft: \(error.errorMessage)", position: .top) + showToast("draft_error".localizeWithArguments(error.errorMessage), position: .top) } - handler?(error) + handler?(false, error) } } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 3f4eb1865..974cea1aa 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -148,19 +148,23 @@ extension ComposeViewController: NavigationChildController { func handleBackButtonTap() { stopDraftTimer(withSave: false) - saveDraftIfNeeded { [weak self] error in + saveDraftIfNeeded { [weak self] didSave, error in guard let self = self else { return } if let error = error { - self.handle(error: error) + self.showToast("draft_error".localizeWithArguments(error.errorMessage)) } else { if var messageIdentifier = self.composeMessageService.messageIdentifier { messageIdentifier.draftMessageId = self.input.type.info?.id self.handleAction?(.update(messageIdentifier)) } - self.showToast("draft_saved".localized, duration: 1.0) - self.navigationController?.popViewController(animated: true) + + if didSave { + self.showToast("draft_saved".localized, duration: 1.0) + } } } + + navigationController?.popViewController(animated: true) } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index d233ee2f9..cb82913c4 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -51,6 +51,10 @@ extension ComposeMessageContext { recipients.contains(where: { $0.type == .cc || $0.type == .bcc }) } + var hasRecipientsWithActivePubKey: Bool { + recipients.contains(where: { $0.keyState == .active }) + } + var hasRecipientsWithoutPubKey: Bool { recipients.contains(where: { $0.keyState == .empty }) } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index c7508854a..6f3d14543 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -89,26 +89,19 @@ final class ComposeMessageService { func validateAndProduceSendableMsg( input: ComposeMessageInput, contextToSend: ComposeMessageContext, - isDraft: Bool = false + isDraft: Bool = false, + withPubKeys: Bool = true ) async throws -> SendableMsg { - let recipients = contextToSend.recipients let subject = contextToSend.subject ?? "" - let pubKeys: [String] - - let senderKeys = try await keyMethods.chooseSenderKeys( - for: .encryption, - keys: try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: sender), - senderEmail: contextToSend.sender - ).map(\.public) if !isDraft { onStateChanged?(.validatingMessage) - guard recipients.isNotEmpty else { + guard contextToSend.recipients.isNotEmpty else { throw MessageValidationError.emptyRecipient } - let emails = recipients.map(\.email) + let emails = contextToSend.recipients.map(\.email) let emptyEmails = emails.filter { !$0.hasContent } guard emails.isNotEmpty, emptyEmails.isEmpty else { @@ -137,22 +130,15 @@ final class ComposeMessageService { throw MessageValidationError.notUniquePassword } } - - guard senderKeys.isNotEmpty else { - throw MessageValidationError.noUsableAccountKeys - } - - let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) - let validPubKeys = try validate( - recipients: recipientsWithPubKeys, - hasMessagePassword: contextToSend.hasMessagePassword - ) - - pubKeys = senderKeys + validPubKeys - } else { - pubKeys = senderKeys } + let pubKeys = withPubKeys ? try await getPubKeys( + senderEmail: contextToSend.sender, + recipients: contextToSend.recipients, + hasMessagePassword: contextToSend.hasMessagePassword, + withValidation: !isDraft + ) : [] + let sendableAttachments: [SendableMsg.Attachment] = isDraft ? [] : contextToSend.attachments.map(\.sendableMsgAttachment) @@ -175,6 +161,31 @@ final class ComposeMessageService { ) } + private func getPubKeys( + senderEmail: String, + recipients: [ComposeMessageRecipient], + hasMessagePassword: Bool, + withValidation: Bool + ) async throws -> [String] { + let senderKeys = try await keyMethods.chooseSenderKeys( + for: .encryption, + keys: try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: sender), + senderEmail: senderEmail + ).map(\.public) + + if withValidation, senderKeys.isEmpty { + throw MessageValidationError.noUsableAccountKeys + } + + let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) + let validPubKeys = try validate( + recipients: recipientsWithPubKeys, + hasMessagePassword: hasMessagePassword + ) + + return senderKeys + validPubKeys + } + private func getRecipientKeys(for composeRecipients: [ComposeMessageRecipient]) async throws -> [RecipientWithSortedPubKeys] { let recipients = composeRecipients.map(Recipient.init) var recipientsWithKeys: [RecipientWithSortedPubKeys] = [] diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index acfe6e8b0..e3698495d 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -157,6 +157,7 @@ "draft" = "Draft"; "draft_saved" = "Draft saved"; "draft_deleted" = "Draft deleted"; +"draft_error" = "Failed to save draft: %@"; // Folders "folder_all_mail" = "All Mail"; From eaf6b1dabe7dd8896a8af7a6ae549ec3c71b67bf Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 10 Oct 2022 16:29:24 +0300 Subject: [PATCH 43/56] save drafts in background --- Core/package-lock.json | 163 +++++++++--------- .../Compose/ComposeViewController.swift | 5 - .../ComposeViewController+Drafts.swift | 10 +- ...ComposeViewController+RecipientPopup.swift | 15 +- .../ComposeViewController+Setup.swift | 17 +- FlowCrypt/Core/CoreTypes.swift | 3 + .../Backup Services/BackupService.swift | 1 + .../ComposeMessageService.swift | 10 ++ .../Resources/en.lproj/Localizable.strings | 1 + FlowCrypt/Resources/flowcrypt-ios-prod.js.txt | 22 +-- 10 files changed, 132 insertions(+), 115 deletions(-) diff --git a/Core/package-lock.json b/Core/package-lock.json index c453ff5a7..b5c609d09 100644 --- a/Core/package-lock.json +++ b/Core/package-lock.json @@ -91,13 +91,13 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "node_modules/@nodelib/fs.scandir": { @@ -147,9 +147,9 @@ "dev": true }, "node_modules/@types/encoding-japanese": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/encoding-japanese/-/encoding-japanese-2.0.0.tgz", - "integrity": "sha512-0qi0L/DjrPex/ZmZ/w/iUhg5hPm7eDCPVRhPLuBW1QdZEl/XrEM6NfrWm9TbwBIwAh6U2Lrn4HhdMUxzF7/QUg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/encoding-japanese/-/encoding-japanese-2.0.1.tgz", + "integrity": "sha512-JaCXs2HLniKY8xXeWlg8MAtd4iKhNh8LwutW3yDMWY4usEdTZ2va1x9kd8V3179OAIUTgGQVA63XJrHettpVFQ==", "dev": true }, "node_modules/@types/eslint": { @@ -829,9 +829,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001412", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz", - "integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA==", + "version": "1.0.30001418", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", + "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", "dev": true, "funding": [ { @@ -881,9 +881,9 @@ } }, "node_modules/chalk": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", - "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.0.tgz", + "integrity": "sha512-56zD4khRTBoIyzUYAFgDDaPhUMN/fC/rySe6aZGqbj/VWiU2eI3l6ZLOtYGFZAV5v02mwPjtpzlrOveJiz5eZQ==", "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -944,9 +944,9 @@ "dev": true }, "node_modules/ci-info": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.4.0.tgz", - "integrity": "sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, "node_modules/ci-parallel-vars": { @@ -996,14 +996,17 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/cliui/node_modules/ansi-regex": { @@ -1452,9 +1455,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.265", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.265.tgz", - "integrity": "sha512-38KaYBNs0oCzWCpr6j7fY/W9vF0vSp4tKFIshQTgdZMhUpkxgotkQgjJP6iGMdmlsgMs3i0/Hkko4UXLTrkYVQ==", + "version": "1.4.276", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz", + "integrity": "sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ==", "dev": true }, "node_modules/emittery": { @@ -2867,9 +2870,9 @@ } }, "node_modules/postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.17.tgz", + "integrity": "sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==", "funding": [ { "type": "opencollective", @@ -3176,9 +3179,9 @@ } }, "node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3450,9 +3453,9 @@ } }, "node_modules/terser": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", - "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", + "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.2", @@ -3588,9 +3591,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", - "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", "dev": true, "funding": [ { @@ -3940,12 +3943,12 @@ "dev": true }, "node_modules/yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", + "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", "dev": true, "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", @@ -4081,13 +4084,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "@nodelib/fs.scandir": { @@ -4128,9 +4131,9 @@ "dev": true }, "@types/encoding-japanese": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/encoding-japanese/-/encoding-japanese-2.0.0.tgz", - "integrity": "sha512-0qi0L/DjrPex/ZmZ/w/iUhg5hPm7eDCPVRhPLuBW1QdZEl/XrEM6NfrWm9TbwBIwAh6U2Lrn4HhdMUxzF7/QUg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/encoding-japanese/-/encoding-japanese-2.0.1.tgz", + "integrity": "sha512-JaCXs2HLniKY8xXeWlg8MAtd4iKhNh8LwutW3yDMWY4usEdTZ2va1x9kd8V3179OAIUTgGQVA63XJrHettpVFQ==", "dev": true }, "@types/eslint": { @@ -4665,9 +4668,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001412", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz", - "integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA==", + "version": "1.0.30001418", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", + "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", "dev": true }, "caseless": { @@ -4701,9 +4704,9 @@ } }, "chalk": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", - "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.1.0.tgz", + "integrity": "sha512-56zD4khRTBoIyzUYAFgDDaPhUMN/fC/rySe6aZGqbj/VWiU2eI3l6ZLOtYGFZAV5v02mwPjtpzlrOveJiz5eZQ==", "dev": true }, "check-error": { @@ -4741,9 +4744,9 @@ "dev": true }, "ci-info": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.4.0.tgz", - "integrity": "sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, "ci-parallel-vars": { @@ -4778,13 +4781,13 @@ } }, "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" }, "dependencies": { @@ -5132,9 +5135,9 @@ } }, "electron-to-chromium": { - "version": "1.4.265", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.265.tgz", - "integrity": "sha512-38KaYBNs0oCzWCpr6j7fY/W9vF0vSp4tKFIshQTgdZMhUpkxgotkQgjJP6iGMdmlsgMs3i0/Hkko4UXLTrkYVQ==", + "version": "1.4.276", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz", + "integrity": "sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ==", "dev": true }, "emittery": { @@ -6144,9 +6147,9 @@ } }, "postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.17.tgz", + "integrity": "sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==", "requires": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", @@ -6340,9 +6343,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -6533,9 +6536,9 @@ "dev": true }, "terser": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", - "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", + "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", "dev": true, "requires": { "@jridgewell/source-map": "^0.3.2", @@ -6616,9 +6619,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", - "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", "dev": true, "requires": { "escalade": "^3.1.1", @@ -6853,12 +6856,12 @@ "dev": true }, "yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", + "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 8f79e9172..8428927a9 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -19,11 +19,6 @@ final class ComposeViewController: TableNodeViewController { static let minRecipientsPartHeight: CGFloat = 32 } - struct ComposedDraft: Equatable { - let input: ComposeMessageInput - let contextToSend: ComposeMessageContext - } - enum State { case main, searchEmails([Recipient]) } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift index 96743afea..087468e25 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Drafts.swift @@ -45,12 +45,14 @@ extension ComposeViewController { return newDraft != existingDraft ? newDraft : nil } - func saveDraftIfNeeded(handler: ((Bool, Error?) -> Void)? = nil) { + func saveDraftIfNeeded(handler: ((DraftSaveState) -> Void)? = nil) { guard let draft = createDraft() else { - handler?(false, nil) + handler?(.cancelled) return } + handler?(.saving(draft)) + Task { do { let shouldEncrypt = draft.input.type.info?.shouldEncrypt == true || @@ -70,14 +72,14 @@ extension ComposeViewController { ) composedLatestDraft = draft - handler?(true, nil) + handler?(.success(sendableMsg)) } catch { if !(error is MessageValidationError) { // no need to save or notify user if validation error // for other errors show toast showToast("draft_error".localizeWithArguments(error.errorMessage), position: .top) } - handler?(false, error) + handler?(.error(error)) } } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift index 29008fb60..3958fa027 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+RecipientPopup.swift @@ -22,7 +22,7 @@ extension ComposeViewController { popoverVC.popoverPresentationController?.permittedArrowDirections = .up popoverVC.popoverPresentationController?.delegate = self popoverVC.delegate = self - self.present(popoverVC, animated: true, completion: nil) + present(popoverVC, animated: true, completion: nil) } func hideRecipientPopOver() { @@ -41,9 +41,8 @@ extension ComposeViewController: UIPopoverPresentationControllerDelegate { } public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - guard let popoverVC = presentationController.presentedViewController as? ComposeRecipientPopupViewController else { - return - } + guard let popoverVC = presentationController.presentedViewController as? ComposeRecipientPopupViewController + else { return } let recipients = contextToSend.recipients(type: popoverVC.type) let selectedRecipients = recipients.filter { $0.state.isSelected } // Deselect previous selected receipients @@ -56,21 +55,21 @@ extension ComposeViewController: UIPopoverPresentationControllerDelegate { extension ComposeViewController: ComposeRecipientPopupViewControllerProtocol { func removeRecipient(email: String, type: RecipientType) { - let tempRecipients = self.contextToSend.recipients(type: type) - self.contextToSend.remove(recipient: email, type: type) + let tempRecipients = contextToSend.recipients(type: type) + contextToSend.remove(recipient: email, type: type) reload(sections: [.password]) refreshRecipient(for: email, type: type, refreshType: .delete, tempRecipients: tempRecipients) } func editRecipient(email: String, type: RecipientType) { removeRecipient(email: email, type: type) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if let textField = self.recipientsTextField(type: type) { textField.text = email if !textField.isFirstResponder() { textField.becomeFirstResponder() } } - }) + } } } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 974cea1aa..ba82d581c 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -148,20 +148,23 @@ extension ComposeViewController: NavigationChildController { func handleBackButtonTap() { stopDraftTimer(withSave: false) - saveDraftIfNeeded { [weak self] didSave, error in - guard let self = self else { return } + saveDraftIfNeeded { [weak self] state in + guard let self else { return } - if let error = error { + switch state { + case .cancelled: + break + case .error(let error): self.showToast("draft_error".localizeWithArguments(error.errorMessage)) - } else { + case .success: if var messageIdentifier = self.composeMessageService.messageIdentifier { messageIdentifier.draftMessageId = self.input.type.info?.id self.handleAction?(.update(messageIdentifier)) } - if didSave { - self.showToast("draft_saved".localized, duration: 1.0) - } + self.showToast("draft_saved".localized, duration: 1.0) + case .saving: + self.showToast("draft_saving".localized, duration: 10.0) } } diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index 466df14c6..c6efbe57e 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -128,6 +128,8 @@ struct SendableMsg: Equatable { let from: String let subject: String let replyToMsgId: String? + let threadId: String? + var draftId: Identifier? let inReplyTo: String? let atts: [Attachment] let pubKeys: [String]? @@ -146,6 +148,7 @@ extension SendableMsg { from: self.from, subject: self.subject, replyToMsgId: self.replyToMsgId, + threadId: self.threadId, inReplyTo: self.inReplyTo, atts: atts, pubKeys: pubKeys, diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift index e2cbf0d83..677ffe613 100644 --- a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift @@ -62,6 +62,7 @@ extension BackupService: BackupServiceType { from: userId.toMime, subject: "backup_subject".localized, replyToMsgId: nil, + threadId: nil, inReplyTo: nil, atts: attachments, pubKeys: nil, diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 6f3d14543..2bfa5139a 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -17,6 +17,15 @@ protocol CoreComposeMessageType { func encrypt(file: Data, name: String, pubKeys: [String]?) async throws -> Data } +enum DraftSaveState { + case cancelled, saving(ComposedDraft), success(SendableMsg), error(Error) +} + +struct ComposedDraft: Equatable { + let input: ComposeMessageInput + let contextToSend: ComposeMessageContext +} + final class ComposeMessageService { private let appContext: AppContextWithUser @@ -153,6 +162,7 @@ final class ComposeMessageService { from: contextToSend.sender, subject: subject, replyToMsgId: input.replyToMsgId, + threadId: input.threadId, inReplyTo: input.inReplyTo, atts: sendableAttachments, pubKeys: pubKeys, diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index e3698495d..57d699dea 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -155,6 +155,7 @@ // Drafts "draft" = "Draft"; +"draft_saving" = "Saving draft..."; "draft_saved" = "Draft saved"; "draft_deleted" = "Draft deleted"; "draft_error" = "Failed to save draft: %@"; diff --git a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt index 45b63755f..42fe0f2d5 100644 --- a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt +++ b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt @@ -48067,16 +48067,16 @@ class Container extends Node { } insertBefore(exist, add) { - exist = this.index(exist) - + let existIndex = this.index(exist) let type = exist === 0 ? 'prepend' : false - let nodes = this.normalize(add, this.proxyOf.nodes[exist], type).reverse() - for (let node of nodes) this.proxyOf.nodes.splice(exist, 0, node) + let nodes = this.normalize(add, this.proxyOf.nodes[existIndex], type).reverse() + existIndex = this.index(exist) + for (let node of nodes) this.proxyOf.nodes.splice(existIndex, 0, node) let index for (let id in this.indexes) { index = this.indexes[id] - if (exist <= index) { + if (existIndex <= index) { this.indexes[id] = index + nodes.length } } @@ -48087,15 +48087,15 @@ class Container extends Node { } insertAfter(exist, add) { - exist = this.index(exist) - - let nodes = this.normalize(add, this.proxyOf.nodes[exist]).reverse() - for (let node of nodes) this.proxyOf.nodes.splice(exist + 1, 0, node) + let existIndex = this.index(exist) + let nodes = this.normalize(add, this.proxyOf.nodes[existIndex]).reverse() + existIndex = this.index(exist) + for (let node of nodes) this.proxyOf.nodes.splice(existIndex + 1, 0, node) let index for (let id in this.indexes) { index = this.indexes[id] - if (exist < index) { + if (existIndex < index) { this.indexes[id] = index + nodes.length } } @@ -49648,7 +49648,7 @@ let Root = __webpack_require__(95) class Processor { constructor(plugins = []) { - this.version = '8.4.16' + this.version = '8.4.17' this.plugins = this.normalize(plugins) } From 6f44ca402a4e4754ecd9c7e1470ff9ca679ed709 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 10 Oct 2022 16:35:00 +0300 Subject: [PATCH 44/56] fix tests --- FlowCrypt/Core/CoreTypes.swift | 3 --- .../Functionality/Services/Backup Services/BackupService.swift | 1 - .../Compose Message Service/ComposeMessageService.swift | 1 - 3 files changed, 5 deletions(-) diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index c6efbe57e..466df14c6 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -128,8 +128,6 @@ struct SendableMsg: Equatable { let from: String let subject: String let replyToMsgId: String? - let threadId: String? - var draftId: Identifier? let inReplyTo: String? let atts: [Attachment] let pubKeys: [String]? @@ -148,7 +146,6 @@ extension SendableMsg { from: self.from, subject: self.subject, replyToMsgId: self.replyToMsgId, - threadId: self.threadId, inReplyTo: self.inReplyTo, atts: atts, pubKeys: pubKeys, diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift index 677ffe613..e2cbf0d83 100644 --- a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift @@ -62,7 +62,6 @@ extension BackupService: BackupServiceType { from: userId.toMime, subject: "backup_subject".localized, replyToMsgId: nil, - threadId: nil, inReplyTo: nil, atts: attachments, pubKeys: nil, diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 2bfa5139a..d7ca5fd8c 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -162,7 +162,6 @@ final class ComposeMessageService { from: contextToSend.sender, subject: subject, replyToMsgId: input.replyToMsgId, - threadId: input.threadId, inReplyTo: input.inReplyTo, atts: sendableAttachments, pubKeys: pubKeys, From b7068159bbabb4b65a0d9d8167990e93eb983ba8 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 11 Oct 2022 15:47:23 +0300 Subject: [PATCH 45/56] fix ui tests --- Core/source/mobile-interface/endpoints.ts | 9 +++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- FlowCrypt/Controllers/Inbox/InboxItem.swift | 2 +- .../Message Provider/MessageService.swift | 2 +- .../ComposeMessageService.swift | 40 ++++++++++++------- FlowCrypt/Resources/flowcrypt-ios-prod.js.txt | 6 ++- ...ec.ts => CheckDraftsFunctionality.spec.ts} | 0 ...ithDisallowAttesterSearchForDomain.spec.ts | 1 + 8 files changed, 40 insertions(+), 24 deletions(-) rename appium/tests/specs/mock/composeEmail/{CheckDraftFunctionality.spec.ts => CheckDraftsFunctionality.spec.ts} (100%) diff --git a/Core/source/mobile-interface/endpoints.ts b/Core/source/mobile-interface/endpoints.ts index bae090dbd..048ec54b3 100644 --- a/Core/source/mobile-interface/endpoints.ts +++ b/Core/source/mobile-interface/endpoints.ts @@ -5,7 +5,7 @@ import { Buffers, EndpointRes, fmtContentBlock, fmtRes, isContentBlock } from './format-output'; import { DecryptErrTypes, PgpMsg } from '../core/pgp-msg'; import { KeyDetails, PgpKey } from '../core/pgp-key'; -import { Mime, RichHeaders } from '../core/mime'; +import { Mime, RichHeaders, SendableMsgBody } from '../core/mime'; import { Att } from '../core/att'; import { Buf } from '../core/buf'; @@ -55,9 +55,10 @@ export class Endpoints { if (req.format === 'plain') { const atts = (req.atts || []).map(({ name, type, base64 }) => new Att({ name, type, data: Buf.fromBase64Str(base64) })); - return fmtRes({}, Buf.fromUtfStr(await Mime.encode( - // eslint-disable-next-line @typescript-eslint/naming-convention - { 'text/plain': req.text, 'text/html': req.html }, mimeHeaders, atts))); + // eslint-disable-next-line @typescript-eslint/naming-convention + const body: SendableMsgBody = { 'text/plain': req.text }; + if (req.html) { body['text/html'] = req.html; } + return fmtRes({}, Buf.fromUtfStr(await Mime.encode(body, mimeHeaders, atts))); } else if (req.format === 'encrypt-inline') { const encryptedAtts: Att[] = []; for (const att of req.atts || []) { diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 36e18bdc1..8b6b9e5da 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-cocoa", "state" : { - "revision" : "ae4278abe1fcdcd616b410e098a744c1cc73e0bd", - "version" : "10.31.0" + "revision" : "7055d820a133da4395742758bd48bab0841cc4bf", + "version" : "10.32.0" } }, { diff --git a/FlowCrypt/Controllers/Inbox/InboxItem.swift b/FlowCrypt/Controllers/Inbox/InboxItem.swift index 7c9549af4..c9ffa1150 100644 --- a/FlowCrypt/Controllers/Inbox/InboxItem.swift +++ b/FlowCrypt/Controllers/Inbox/InboxItem.swift @@ -128,7 +128,7 @@ extension InboxItem { } func messages(with label: String?) -> [Message] { - guard let label = label else { return messages } + guard let label = label, !label.isEmpty else { return messages } let messageLabel = MessageLabel(gmailLabel: label) return messages.filter { $0.labels.contains(messageLabel) } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index d0b7cd9aa..c28121d5a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -134,7 +134,7 @@ final class MessageService { keys: keys, msgPwd: nil, isMime: false, - verificationPubKeys: [] + verificationPubKeys: verificationPubKeys ) guard !hasMsgBlockThatNeedsPassPhrase(decrypted) else { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index d7ca5fd8c..2204633ab 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -35,6 +35,8 @@ final class ComposeMessageService { private let draftGateway: DraftGateway? private lazy var logger = Logger.nested(Self.self) + private var saveDraftTask: Task? + private struct ReplyInfo: Encodable { let sender: String let recipient: [String] @@ -245,22 +247,30 @@ final class ComposeMessageService { } func saveDraft(message: SendableMsg, threadId: String?, shouldEncrypt: Bool) async throws { - do { - let mime = try await core.composeEmail( - msg: message, - fmt: shouldEncrypt ? .encryptInline : .plain - ).mimeEncoded - - self.messageIdentifier = try await draftGateway?.saveDraft( - input: MessageGatewayInput( - mime: mime, - threadId: threadId - ), - draftId: self.messageIdentifier?.draftId - ) - } catch { - throw ComposeMessageError.gatewayError(error) + saveDraftTask?.cancel() + + saveDraftTask = Task { + do { + let mime = try await self.core.composeEmail( + msg: message, + fmt: shouldEncrypt ? .encryptInline : .plain + ).mimeEncoded + + if Task.isCancelled { return self.messageIdentifier } + + return try await self.draftGateway?.saveDraft( + input: MessageGatewayInput( + mime: mime, + threadId: threadId + ), + draftId: self.messageIdentifier?.draftId + ) + } catch { + throw ComposeMessageError.gatewayError(error) + } } + + messageIdentifier = try await saveDraftTask?.value } func deleteDraft() async throws { diff --git a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt index 42fe0f2d5..48f615e90 100644 --- a/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt +++ b/FlowCrypt/Resources/flowcrypt-ios-prod.js.txt @@ -28595,7 +28595,11 @@ class Endpoints { } if (req.format === 'plain') { const atts = (req.atts || []).map(({ name, type, base64 }) => new att_1.Att({ name, type, data: buf_1.Buf.fromBase64Str(base64) })); - return (0, format_output_1.fmtRes)({}, buf_1.Buf.fromUtfStr(await mime_1.Mime.encode({ 'text/plain': req.text, 'text/html': req.html }, mimeHeaders, atts))); + const body = { 'text/plain': req.text }; + if (req.html) { + body['text/html'] = req.html; + } + return (0, format_output_1.fmtRes)({}, buf_1.Buf.fromUtfStr(await mime_1.Mime.encode(body, mimeHeaders, atts))); } else if (req.format === 'encrypt-inline') { const encryptedAtts = []; diff --git a/appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts b/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts similarity index 100% rename from appium/tests/specs/mock/composeEmail/CheckDraftFunctionality.spec.ts rename to appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts diff --git a/appium/tests/specs/mock/setup/CannotFindEmailOnAttesterWithDisallowAttesterSearchForDomain.spec.ts b/appium/tests/specs/mock/setup/CannotFindEmailOnAttesterWithDisallowAttesterSearchForDomain.spec.ts index 1d2c44505..d75cbb2d9 100644 --- a/appium/tests/specs/mock/setup/CannotFindEmailOnAttesterWithDisallowAttesterSearchForDomain.spec.ts +++ b/appium/tests/specs/mock/setup/CannotFindEmailOnAttesterWithDisallowAttesterSearchForDomain.spec.ts @@ -22,6 +22,7 @@ describe('SETUP: ', () => { } }; mockApi.ekmConfig = MockApiConfig.defaultEnterpriseEkmConfiguration; + mockApi.addGoogleAccount('e2e.enterprise.test@flowcrypt.com') await mockApi.withMockedApis(async () => { await SplashScreen.mockLogin(); From a74b172a424fe32ac3436259c916c8592fc64708 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 11 Oct 2022 20:34:15 +0300 Subject: [PATCH 46/56] fix duplicated drafts --- .../Compose/ComposeViewControllerInput.swift | 2 ++ .../ComposeViewController+Setup.swift | 6 ++---- FlowCrypt/Controllers/Inbox/InboxItem.swift | 7 ++++++- .../Controllers/Inbox/InboxProviders.swift | 2 +- .../Threads/ThreadDetailsViewController.swift | 1 + .../Message Gateway/GmailService+draft.swift | 20 +++++++++++++++++++ .../Message Gateway/MessageGateway.swift | 1 + .../MessagesList Provider/Model/Message.swift | 3 +++ .../ComposeMessageService.swift | 12 ++++++++--- .../Mocks/DraftGatewayMock.swift | 4 ++++ 10 files changed, 49 insertions(+), 9 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index fba4a5952..3c2647a7b 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -24,6 +24,7 @@ struct ComposeMessageInput: Equatable { let replyToMsgId: String? let inReplyTo: String? let rfc822MsgId: String? + let draftId: Identifier? let shouldEncrypt: Bool let attachments: [MessageAttachment] } @@ -122,6 +123,7 @@ extension ComposeMessageInput.MessageQuoteInfo { self.text = processed?.text ?? message.body.text self.threadId = message.threadId self.rfc822MsgId = message.rfc822MsgId + self.draftId = message.draftId self.replyToMsgId = message.replyToMsgId self.inReplyTo = message.inReplyTo self.shouldEncrypt = message.isPgp diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index ba82d581c..ff651e7b1 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -60,10 +60,8 @@ extension ComposeViewController { return } - if case .draft = input.type, let messageId = input.type.info?.rfc822MsgId { - Task { - try await composeMessageService.fetchDraftIdentifier(for: messageId) - } + if case .draft = input.type { + composeMessageService.fetchMessageIdentifier(info: info) } contextToSend.subject = info.subject diff --git a/FlowCrypt/Controllers/Inbox/InboxItem.swift b/FlowCrypt/Controllers/Inbox/InboxItem.swift index c9ffa1150..53907a11d 100644 --- a/FlowCrypt/Controllers/Inbox/InboxItem.swift +++ b/FlowCrypt/Controllers/Inbox/InboxItem.swift @@ -146,10 +146,15 @@ extension InboxItem { self.type = .message(message.identifier) } - init(thread: MessageThread, folderPath: String?) { + init(thread: MessageThread, folderPath: String?, identifier: MessageIdentifier? = nil) { self.messages = thread.messages self.folderPath = folderPath ?? "" self.type = .thread(Identifier(stringId: thread.identifier)) + + if let draftId = identifier?.draftId, let messageId = identifier?.messageId { + guard let index = self.messages.firstIndex(where: { $0.identifier == messageId }) else { return } + self.messages[index].draftId = draftId + } } mutating func update(labelsToAdd: [MessageLabel] = [], labelsToRemove: [MessageLabel] = []) { diff --git a/FlowCrypt/Controllers/Inbox/InboxProviders.swift b/FlowCrypt/Controllers/Inbox/InboxProviders.swift index 30b69daef..5335c3247 100644 --- a/FlowCrypt/Controllers/Inbox/InboxProviders.swift +++ b/FlowCrypt/Controllers/Inbox/InboxProviders.swift @@ -29,7 +29,7 @@ class InboxMessageThreadsProvider: InboxDataProvider { func fetchInboxItem(identifier: MessageIdentifier, path: String) async throws -> InboxItem? { guard let id = identifier.threadId?.stringId else { return nil } let thread = try await provider.fetchThread(identifier: id, path: path) - return InboxItem(thread: thread, folderPath: path) + return InboxItem(thread: thread, folderPath: path, identifier: identifier) } func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index aa6ebe863..90a09a70f 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -409,6 +409,7 @@ extension ThreadDetailsViewController { replyToMsgId: replyToMsgId, inReplyTo: input.rawMessage.inReplyTo, rfc822MsgId: input.rawMessage.rfc822MsgId, + draftId: nil, shouldEncrypt: input.rawMessage.isPgp, attachments: attachments ) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 815ba524f..cc19ec967 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -9,6 +9,25 @@ import GoogleAPIClientForREST_Gmail extension GmailService: DraftGateway { + func fetchDraft(id: Identifier) async throws -> MessageIdentifier? { + guard let identifier = id.stringId else { return nil } + let query = GTLRGmailQuery_UsersDraftsGet.query(withUserId: .me, identifier: identifier) + return try await withCheckedThrowingContinuation { continuation in + gmailService.executeQuery(query) { _, data, error in + if let error = error { + return continuation.resume(throwing: GmailServiceError.providerError(error)) + } + + guard let gmailDraft = data as? GTLRGmail_Draft else { + return continuation.resume(throwing: AppErr.cast("GTLRGmail_Draft")) + } + + let draft = MessageIdentifier(gmailDraft: gmailDraft) + return continuation.resume(returning: draft) + } + } + } + func fetchDraftIdentifier(for messageId: Identifier) async throws -> MessageIdentifier? { guard let id = messageId.stringId else { return nil } @@ -29,6 +48,7 @@ extension GmailService: DraftGateway { guard let gmailDraft = list.drafts?.first else { return continuation.resume(returning: nil) } + let draft = MessageIdentifier(gmailDraft: gmailDraft) return continuation.resume(returning: draft) } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift index 243275d5b..197af3516 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/MessageGateway.swift @@ -18,6 +18,7 @@ protocol MessageGateway { } protocol DraftGateway { + func fetchDraft(id: Identifier) async throws -> MessageIdentifier? func fetchDraftIdentifier(for messageId: Identifier) async throws -> MessageIdentifier? func saveDraft(input: MessageGatewayInput, draftId: Identifier?) async throws -> MessageIdentifier func deleteDraft(with identifier: Identifier) async throws diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index 22b34025c..d39d0769a 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -23,6 +23,7 @@ struct Message: Hashable { var attachments: [MessageAttachment] let threadId: String? let rfc822MsgId: String? + var draftId: Identifier? var raw: String? let body: MessageBody let inReplyTo: String? @@ -63,6 +64,7 @@ struct Message: Hashable { attachments: [MessageAttachment] = [], threadId: String? = nil, rfc822MsgId: String? = nil, + draftId: Identifier? = nil, raw: String? = nil, to: String? = nil, cc: String? = nil, @@ -82,6 +84,7 @@ struct Message: Hashable { self.body = body self.threadId = threadId self.rfc822MsgId = rfc822MsgId + self.draftId = draftId self.raw = raw self.to = Self.parseRecipients(to) self.cc = Self.parseRecipients(cc) diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 2204633ab..a605a95f0 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -241,9 +241,15 @@ final class ComposeMessageService { // MARK: - Drafts var messageIdentifier: MessageIdentifier? - func fetchDraftIdentifier(for messageId: String) async throws { - let identifier = Identifier(stringId: messageId) - self.messageIdentifier = try await draftGateway?.fetchDraftIdentifier(for: identifier) + func fetchMessageIdentifier(info: ComposeMessageInput.MessageQuoteInfo) { + Task { + if let draftId = info.draftId { + messageIdentifier = try await draftGateway?.fetchDraft(id: draftId) + } else if let messageId = info.rfc822MsgId { + let identifier = Identifier(stringId: messageId) + messageIdentifier = try await draftGateway?.fetchDraftIdentifier(for: identifier) + } + } } func saveDraft(message: SendableMsg, threadId: String?, shouldEncrypt: Bool) async throws { diff --git a/FlowCryptAppTests/Mocks/DraftGatewayMock.swift b/FlowCryptAppTests/Mocks/DraftGatewayMock.swift index e8ccc271d..4d6d466c8 100644 --- a/FlowCryptAppTests/Mocks/DraftGatewayMock.swift +++ b/FlowCryptAppTests/Mocks/DraftGatewayMock.swift @@ -10,6 +10,10 @@ import GoogleAPIClientForREST_Gmail class DraftGatewayMock: DraftGateway { + func fetchDraft(id: Identifier) async throws -> MessageIdentifier? { + return nil + } + func fetchDraftIdentifier(for messageId: Identifier) async throws -> MessageIdentifier? { return nil } From 17e95b233dbfcc1d13013d2193fcc5ec071b958d Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 11 Oct 2022 21:42:27 +0300 Subject: [PATCH 47/56] fix message decrypt --- .../Mail Provider/Message Provider/MessageService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index c28121d5a..3483b4391 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -133,7 +133,7 @@ final class MessageService { encrypted: text.data(), keys: keys, msgPwd: nil, - isMime: false, + isMime: isMime, verificationPubKeys: verificationPubKeys ) From b8723f8f28132fb85cb8c44d383abbe1cafd51e0 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 12 Oct 2022 17:34:14 +0300 Subject: [PATCH 48/56] fix compose crash --- FlowCrypt.xcodeproj/project.pbxproj | 2 +- .../Compose/ComposeViewDecorator.swift | 8 ++-- .../ComposeViewController+Nodes.swift | 42 ++++++++++--------- .../ComposeViewController+TableView.swift | 2 +- .../Api/Remote Pub Key Apis/WkdApi.swift | 2 +- .../Cell Nodes/RecipientEmailsCellNode.swift | 4 +- 6 files changed, 32 insertions(+), 28 deletions(-) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 02557681d..c8f708107 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -3624,7 +3624,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 6DZ6CC3YMY; - ENABLE_BITCODE = NO; + ENABLE_BITCODE = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 949149d26..c54f0dd8a 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -163,7 +163,7 @@ struct ComposeViewDecorator { type: RecipientType, completion: (() -> Void)? = nil ) { - let currentHeight = self.recipientsNodeHeight(type: type) + let currentHeight = recipientsNodeHeight(type: type) guard currentHeight != layoutHeight, layoutHeight > 0 else { return @@ -171,11 +171,11 @@ struct ComposeViewDecorator { switch type { case .to: - self.calculatedRecipientsToPartHeight = layoutHeight + calculatedRecipientsToPartHeight = layoutHeight case .cc: - self.calculatedRecipientsCcPartHeight = layoutHeight + calculatedRecipientsCcPartHeight = layoutHeight case .bcc: - self.calculatedRecipientsBccPartHeight = layoutHeight + calculatedRecipientsBccPartHeight = layoutHeight default: break } diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift index fa7f8dc9d..8bcd7f5e6 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Nodes.swift @@ -26,17 +26,19 @@ extension ComposeViewController { } func showRecipientLabelIfNecessary() { - let isRecipientLoading = self.contextToSend.recipients.filter { $0.state == decorator.recipientIdleState }.isNotEmpty + let isRecipientLoading = contextToSend.recipients.contains(where: { + $0.state == decorator.recipientIdleState + }) + guard !isRecipientLoading, contextToSend.recipients.isNotEmpty, - userTappedOutSideRecipientsArea else { - return - } - if !shouldShowEmailRecipientsLabel { - shouldShowEmailRecipientsLabel = true - userTappedOutSideRecipientsArea = false - reload(sections: Section.recipientsSections + [.recipientsLabel]) - } + userTappedOutSideRecipientsArea, + !shouldShowEmailRecipientsLabel + else { return } + + shouldShowEmailRecipientsLabel = true + userTappedOutSideRecipientsArea = false + reload(sections: Section.recipientsSections + [.recipientsLabel]) } func hideRecipientLabel() { @@ -62,7 +64,7 @@ extension ComposeViewController { } } .onShouldReturn { [weak self] _ in - guard let self = self else { return true } + guard let self else { return true } if !self.input.isQuote, let node = self.node.visibleNodes.compactMap({ $0 as? TextViewCellNode }).first { node.becomeFirstResponder() } else { @@ -138,7 +140,7 @@ extension ComposeViewController { accessibilityIdentifier: "aid-message-text-view" ) ) { [weak self] event in - guard let self = self else { return } + guard let self else { return } switch event { case .didBeginEditing: self.userTappedOutSideRecipientsArea = true @@ -169,7 +171,7 @@ extension ComposeViewController { from: textNode.textView.textView.beginningOfDocument, to: textNode.textView.textView.beginningOfDocument ) - self.node.reloadData() + if self.input.shouldFocusTextNode { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { textNode.becomeFirstResponder() @@ -216,26 +218,28 @@ extension ComposeViewController { type: type, completion: { if let indexPath = self?.recipientsIndexPath(type: type), - let emailNode = self?.node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode { - emailNode.style.preferredSize.height = layoutHeight - emailNode.setNeedsLayout() + let emailsNode = self?.node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode { + emailsNode.style.preferredSize.height = layoutHeight + emailsNode.setNeedsLayout() } } ) } .onItemSelect { [weak self] action in + guard let self else { return } + switch action { case let .imageTap(indexPath): - self?.handleRecipientAction(with: indexPath, type: type) + self.handleRecipientAction(with: indexPath, type: type) case let .select(indexPath, sender): - self?.handleRecipientSelection(with: indexPath, type: type) - self?.displayRecipientPopOver(with: indexPath, type: type, sender: sender) + self.handleRecipientSelection(with: indexPath, type: type) + self.displayRecipientPopOver(with: indexPath, type: type, sender: sender) } } } func recipientInput(type: RecipientType) -> RecipientEmailTextFieldNode { - return RecipientEmailTextFieldNode( + RecipientEmailTextFieldNode( input: decorator.styledTextFieldInput( with: "", keyboardType: .emailAddress, diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift index 875bf6d4f..e0e17366c 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift @@ -48,7 +48,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { // swiftlint:disable cyclomatic_complexity func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { return { [weak self] in - guard let self = self, + guard let self, let section = self.sectionsList[safe: indexPath.section] else { return ASCellNode() } diff --git a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift index 9a4f67b60..87dd9000f 100644 --- a/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift +++ b/FlowCrypt/Functionality/Api/Remote Pub Key Apis/WkdApi.swift @@ -89,7 +89,7 @@ class WkdApi: WkdApiType { ) _ = try await ApiCall.call(request) } catch { - Logger.nested("WkdApi").logInfo("Failed to load \(urls.policy) with error \(error)") + Logger.nested("WkdApi").logInfo("Failed to load \(urls.policy) with error \(error.errorMessage)") return InternalResult(hasPolicy: false, keys: nil, method: urls.method) } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index f5810c140..7435bda5b 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -138,7 +138,7 @@ extension RecipientEmailsCellNode: ASCollectionDelegate, ASCollectionDataSource let width = collectionNode.style.preferredSize.width return { [weak self] in - guard let self = self else { + guard let self else { return ASCellNode() } if indexPath.row == self.recipients.count { @@ -180,7 +180,7 @@ extension RecipientEmailsCellNode { toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: angle) } - let angle = self.isToggleButtonRotated ? .pi : 0 + let angle = isToggleButtonRotated ? .pi : 0 if animated { UIView.animate(withDuration: 0.3) { rotateButton(angle: angle) From f6e9fc9870bd751c2817526346e44aded8fb6afc Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 12 Oct 2022 22:09:05 +0300 Subject: [PATCH 49/56] update drafts ui test --- .../InvalidStorageViewController.swift | 2 +- FlowCrypt/Controllers/Inbox/InboxItem.swift | 2 +- .../Threads/ThreadDetailsViewController.swift | 2 +- .../CheckDraftsFunctionality.spec.ts | 28 +++++++++++++++++-- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift b/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift index df8f526ff..fb52c4753 100644 --- a/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift +++ b/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift @@ -102,7 +102,7 @@ extension InvalidStorageViewController: ASTableDelegate, ASTableDataSource { func tableNode(_ node: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { return { [weak self] in - guard let self = self, let part = Parts(rawValue: indexPath.row) else { + guard let self, let part = Parts(rawValue: indexPath.row) else { return ASCellNode() } diff --git a/FlowCrypt/Controllers/Inbox/InboxItem.swift b/FlowCrypt/Controllers/Inbox/InboxItem.swift index 53907a11d..6acc7028b 100644 --- a/FlowCrypt/Controllers/Inbox/InboxItem.swift +++ b/FlowCrypt/Controllers/Inbox/InboxItem.swift @@ -107,7 +107,7 @@ extension InboxItem { if hasDrafts { let draftLabel = "draft".localized - .attributed(style, color: .red.withAlphaComponent(0.65)) + .attributed(style, color: .systemRed.withAlphaComponent(0.75)) let title = sendersList.mutable() title.append(",".attributed(style, color: textColor)) title.append(draftLabel) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 90a09a70f..eef89256e 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -821,7 +821,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { return LabelCellNode( input: .init( - title: "draft".localized.attributed(color: .red), + title: "draft".localized.attributed(color: .systemRed), text: body.removingMailThreadQuote().attributed(color: .secondaryLabel), accessibilityIdentifier: "aid-draft-body-\(messageIndex)", labelAccessibilityIdentifier: "aid-draft-label-\(messageIndex)", diff --git a/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts b/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts index ac9e31946..beac23001 100644 --- a/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts +++ b/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts @@ -15,6 +15,7 @@ describe('COMPOSE EMAIL: ', () => { const mockApi = new MockApi(); const recipient = MockUserList.robot; + const recipientWithoutPubKeys = MockUserList.demo; const subject = CommonData.simpleEmail.subject; const draftSubject = CommonData.draft.subject; const draftText1 = CommonData.draft.text1; @@ -82,13 +83,34 @@ describe('COMPOSE EMAIL: ', () => { await MenuBarScreen.clickMenuBtn(); await MenuBarScreen.clickInboxButton(); - // compose new draft + // compose 2 new drafts and then delete them both await MailFolderScreen.checkInboxScreen(); await MailFolderScreen.clickCreateEmail(); - await NewMessageScreen.composeEmail(recipient.email, draftSubject, draftText1); + await NewMessageScreen.composeEmail(recipientWithoutPubKeys.email, draftSubject, draftText1); + await NewMessageScreen.clickBackButton(); + + await MailFolderScreen.checkInboxScreen(); + await MailFolderScreen.clickCreateEmail(); + await NewMessageScreen.composeEmail(recipient.email, subject, draftText2); await NewMessageScreen.clickBackButton(); - // send draft and check if sent message added to 'sent' folder + await MailFolderScreen.clickOnEmailBySubject(subject); + await NewMessageScreen.clickDeleteButton(); + await NewMessageScreen.confirmDelete(); + + await MailFolderScreen.checkInboxScreen(); + await MailFolderScreen.clickOnEmailBySubject(draftSubject); + await NewMessageScreen.clickDeleteButton(); + await NewMessageScreen.confirmDelete(); + + await MailFolderScreen.checkInboxScreen(); + await MailFolderScreen.checkIfFolderIsEmpty(); + + // compose draft, send it and check if sent message added to 'sent' folder + await MailFolderScreen.checkInboxScreen(); + await MailFolderScreen.clickCreateEmail(); + await NewMessageScreen.composeEmail(recipient.email, draftSubject, draftText1); + await NewMessageScreen.clickBackButton(); await MenuBarScreen.clickMenuBtn(); await MenuBarScreen.clickDraftsButton(); await MailFolderScreen.clickOnEmailBySubject(draftSubject); From adfc15b39801549a6361ffcd4c8bad782c0e89b9 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 13 Oct 2022 20:58:14 +0300 Subject: [PATCH 50/56] fix inReplyTo value for compose --- FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index eef89256e..fdfed15e5 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -394,7 +394,7 @@ extension ThreadDetailsViewController { let subject = input.rawMessage.subject ?? "(no subject)" let threadId = quoteType == .forward ? nil : input.rawMessage.threadId - let replyToMsgId = input.rawMessage.identifier.stringId + let replyToMsgId = quoteType == .forward ? nil : input.rawMessage.rfc822MsgId let replyInfo = ComposeMessageInput.MessageQuoteInfo( id: nil, From acedebc9086f97b2c92c58079d8cab928e5a0bdb Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 13 Oct 2022 23:51:55 +0300 Subject: [PATCH 51/56] pr fix --- Core/source/mobile-interface/endpoints.ts | 4 +++- appium/config/wdio.live.conf.js | 2 +- appium/config/wdio.mock.conf.js | 2 +- appium/package.json | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Core/source/mobile-interface/endpoints.ts b/Core/source/mobile-interface/endpoints.ts index 048ec54b3..c65220dea 100644 --- a/Core/source/mobile-interface/endpoints.ts +++ b/Core/source/mobile-interface/endpoints.ts @@ -57,7 +57,9 @@ export class Endpoints { new Att({ name, type, data: Buf.fromBase64Str(base64) })); // eslint-disable-next-line @typescript-eslint/naming-convention const body: SendableMsgBody = { 'text/plain': req.text }; - if (req.html) { body['text/html'] = req.html; } + if (req.html) { + body['text/html'] = req.html; + } return fmtRes({}, Buf.fromUtfStr(await Mime.encode(body, mimeHeaders, atts))); } else if (req.format === 'encrypt-inline') { const encryptedAtts: Att[] = []; diff --git a/appium/config/wdio.live.conf.js b/appium/config/wdio.live.conf.js index 544d37559..1456fed35 100644 --- a/appium/config/wdio.live.conf.js +++ b/appium/config/wdio.live.conf.js @@ -15,7 +15,7 @@ config.capabilities = [ 'appium:automationName': 'XCUITest', 'appium:options': { deviceName: 'iPhone 14', - platformVersion: '16.0', + platformVersion: '16.1', app: join(process.cwd(), './FlowCrypt.app'), }, }, diff --git a/appium/config/wdio.mock.conf.js b/appium/config/wdio.mock.conf.js index b667d309d..7ad8fb61e 100644 --- a/appium/config/wdio.mock.conf.js +++ b/appium/config/wdio.mock.conf.js @@ -30,7 +30,7 @@ config.capabilities = [ 'appium:automationName': 'XCUITest', 'appium:options': { deviceName: 'iPhone 14', - platformVersion: '16.0', + platformVersion: '16.1', app: join(process.cwd(), './FlowCrypt.app'), processArguments: { 'args': ['--mock-fes-api', '--mock-attester-api', '--mock-gmail-api'] }, }, diff --git a/appium/package.json b/appium/package.json index 362c478fe..04ea446f6 100644 --- a/appium/package.json +++ b/appium/package.json @@ -48,4 +48,4 @@ "wdio-video-reporter": "^3.2.0", "webdriverio": "^7.16.15" } -} \ No newline at end of file +} From 339e228b7dd7e9cf9d4e6365f2c650825d98a463 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 14 Oct 2022 14:32:17 +0300 Subject: [PATCH 52/56] pr fixes --- .../ComposeViewController+Setup.swift | 5 +++-- .../Inbox/InboxViewController.swift | 17 ++++++++++++++--- .../ComposeMessageService.swift | 18 ++++++++---------- FlowCryptUI/Cell Nodes/EmptyCellNode.swift | 2 +- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index ff651e7b1..1cb0ec489 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -107,8 +107,8 @@ extension ComposeViewController { isUsingKeyManager: appContext.clientConfigurationService.configuration.isUsingKeyManager ) contextToSend.message = decrypted - reload(sections: Section.recipientsSections) didFinishSetup = true + reload(sections: Section.recipientsSections + [.compose]) } catch { if case .missingPassPhrase(let keyPair) = error as? MessageServiceError, let keyPair = keyPair { requestMissingPassPhraseWithModal(for: keyPair, isDraft: true) @@ -151,7 +151,8 @@ extension ComposeViewController: NavigationChildController { switch state { case .cancelled: - break + guard let identifier = self.composeMessageService.messageIdentifier else { break } + self.handleAction?(.update(identifier)) case .error(let error): self.showToast("draft_error".localizeWithArguments(error.errorMessage)) case .success: diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 05f922c28..014d2ba69 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -450,7 +450,7 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { private func cellNode(for indexPath: IndexPath, and size: CGSize) -> ASCellNodeBlock { return { [weak self] in - guard let self = self else { return ASCellNode() } + guard let self else { return ASCellNode() } switch self.state { case .empty: @@ -613,7 +613,7 @@ extension InboxViewController { appContext: appContext, inboxItem: inboxItem, onComposeMessageAction: { [weak self] action in - guard let self = self else { return } + guard let self else { return } switch action { case .update(let identifier), .sent(let identifier), .delete(let identifier): @@ -660,6 +660,11 @@ extension InboxViewController { } private func fetchUpdatedInboxItem(identifier: MessageIdentifier) { + guard !inboxInput.isEmpty else { + fetchAndRenderEmails(nil) + return + } + Task { guard let inboxItem = try await inboxDataProvider.fetchInboxItem( identifier: identifier, @@ -684,7 +689,13 @@ extension InboxViewController { guard let index = inboxInput.firstIndex(with: identifier) else { return } inboxInput.remove(at: index) - tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + + if inboxInput.isEmpty { + state = .empty + tableNode.reloadData() + } else { + tableNode.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + } } // MARK: Operation diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index a605a95f0..bc820d2fd 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -147,7 +147,7 @@ final class ComposeMessageService { senderEmail: contextToSend.sender, recipients: contextToSend.recipients, hasMessagePassword: contextToSend.hasMessagePassword, - withValidation: !isDraft + shouldValidate: !isDraft ) : [] let sendableAttachments: [SendableMsg.Attachment] = isDraft @@ -176,7 +176,7 @@ final class ComposeMessageService { senderEmail: String, recipients: [ComposeMessageRecipient], hasMessagePassword: Bool, - withValidation: Bool + shouldValidate: Bool ) async throws -> [String] { let senderKeys = try await keyMethods.chooseSenderKeys( for: .encryption, @@ -184,15 +184,15 @@ final class ComposeMessageService { senderEmail: senderEmail ).map(\.public) - if withValidation, senderKeys.isEmpty { + if shouldValidate, senderKeys.isEmpty { throw MessageValidationError.noUsableAccountKeys } let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) - let validPubKeys = try validate( - recipients: recipientsWithPubKeys, - hasMessagePassword: hasMessagePassword - ) + if shouldValidate { + try validate(recipients: recipientsWithPubKeys, hasMessagePassword: hasMessagePassword) + } + let validPubKeys = recipientsWithPubKeys.flatMap(\.activePubKeys).map(\.armored) return senderKeys + validPubKeys } @@ -216,7 +216,7 @@ final class ComposeMessageService { private func validate( recipients: [RecipientWithSortedPubKeys], hasMessagePassword: Bool - ) throws -> [String] { + ) throws { func contains(keyState: PubKeyState) -> Bool { recipients.contains(where: { $0.keyState == keyState }) } @@ -234,8 +234,6 @@ final class ComposeMessageService { guard !contains(keyState: .revoked) else { throw MessageValidationError.revokedKeyRecipients } - - return recipients.flatMap(\.activePubKeys).map(\.armored) } // MARK: - Drafts diff --git a/FlowCryptUI/Cell Nodes/EmptyCellNode.swift b/FlowCryptUI/Cell Nodes/EmptyCellNode.swift index 3e5c1dbb7..e27309fa8 100644 --- a/FlowCryptUI/Cell Nodes/EmptyCellNode.swift +++ b/FlowCryptUI/Cell Nodes/EmptyCellNode.swift @@ -70,7 +70,7 @@ public final class EmptyCellNode: CellNode { ) spec.style.preferredSize = size return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16), + insets: UIEdgeInsets.side(16), child: ASCenterLayoutSpec(child: spec) ) } From 12944c1d58fddc54548c5b234b39cf3ea8a8dbd3 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 14 Oct 2022 16:51:13 +0300 Subject: [PATCH 53/56] fix thread table separators --- .../Threads/ThreadDetailsViewController.swift | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index fdfed15e5..5fa77babe 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -35,7 +35,7 @@ final class ThreadDetailsViewController: TableNodeViewController { } private enum Parts: Int, CaseIterable { - case thread, message + case thread, message, divider } private let appContext: AppContextWithUser @@ -137,7 +137,7 @@ extension ThreadDetailsViewController { } }, completion: { [weak self] _ in - guard let self = self else { return } + guard let self else { return } if let processedMessage = self.input[indexPath.section - 1].processedMessage { self.handle(processedMessage: processedMessage, at: indexPath) @@ -734,7 +734,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { guard section > 0, input[section - 1].isExpanded, !input[section - 1].rawMessage.isDraft - else { return 1 } + else { return 2 } let attachmentsCount = input[section - 1].processedMessage?.attachments.count ?? 0 return Parts.allCases.count + attachmentsCount @@ -742,11 +742,15 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { return { [weak self] in - guard let self = self else { return ASCellNode() } + guard let self else { return ASCellNode() } guard indexPath.section > 0 else { - let subject = self.inboxItem.subject ?? "no subject" - return MessageSubjectNode(subject.attributed(.medium(18))) + if indexPath.row == 0 { + let subject = self.inboxItem.subject ?? "no subject" + return MessageSubjectNode(subject.attributed(.medium(18))) + } else { + return self.dividerNode(indexPath: indexPath) + } } let messageIndex = indexPath.section - 1 @@ -762,25 +766,31 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } if message.rawMessage.isDraft { - return self.draftNode(messageIndex: messageIndex, isExpanded: message.isExpanded) + if indexPath.row == 0 { + return self.draftNode(messageIndex: messageIndex, isExpanded: message.isExpanded) + } else { + return self.dividerNode(indexPath: indexPath) + } } - guard let processedMessage = message.processedMessage else { - return ASCellNode() - } + guard message.isExpanded, let processedMessage = message.processedMessage + else { return self.dividerNode(indexPath: indexPath) } guard indexPath.row > 1 else { return MessageTextSubjectNode(processedMessage.attributedMessage, index: messageIndex) } let attachmentIndex = indexPath.row - 2 - let attachment = processedMessage.attachments[attachmentIndex] - return AttachmentNode( - input: .init( - msgAttachment: attachment, - index: attachmentIndex + if let attachment = processedMessage.attachments[safe: attachmentIndex] { + return AttachmentNode( + input: .init( + msgAttachment: attachment, + index: attachmentIndex + ) ) - ) + } else { + return self.dividerNode(indexPath: indexPath) + } } } @@ -799,14 +809,6 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } } - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - dividerView() - } - - func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - section > 0 && section < input.count ? 1 / UIScreen.main.nativeScale : 0 - } - private func draftNode(messageIndex: Int, isExpanded: Bool) -> ASCellNode { let data = input[messageIndex] @@ -863,14 +865,13 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { ) } - private func dividerView() -> UIView { - UIView().then { - let frame = CGRect(x: 8, y: 0, width: view.frame.width - 16, height: 1 / UIScreen.main.nativeScale) - let divider = UIView(frame: frame) - $0.addSubview(divider) - $0.backgroundColor = .clear - divider.backgroundColor = .borderColor - } + private func dividerNode(indexPath: IndexPath) -> ASCellNode { + let height = indexPath.section < input.count ? 1 / UIScreen.main.nativeScale : 0 + return DividerCellNode( + inset: .init(top: 0, left: 8, bottom: 0, right: 8), + color: .borderColor, + height: height + ) } } From 2e31f06db0dfe4d1d7002a0427e27de6c4cc3e96 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Sat, 15 Oct 2022 20:11:44 +0300 Subject: [PATCH 54/56] pr fixes --- .semaphore/semaphore.yml | 4 +++- .../ComposeViewController+ErrorHandling.swift | 20 ++++++------------- .../ComposeViewController+Setup.swift | 15 +++++++------- .../Threads/ThreadDetailsViewController.swift | 8 ++++---- appium/config/wdio.live.conf.js | 2 +- appium/config/wdio.mock.conf.js | 2 +- 6 files changed, 23 insertions(+), 28 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index ad5b95ec6..f084a8f53 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -28,6 +28,7 @@ blocks: jobs: - name: Build Xcode Project + Swift Unit Test commands: + - rm -rf /var/tmp/derived_data/SourcePackages/artifacts/* - xcversion select 14.0 - bundle exec fastlane build - bundle exec fastlane test @@ -73,6 +74,7 @@ blocks: value: /Users/semaphore/git/flowcrypt-ios prologue: commands: + - sudo xcode-select -s /Applications/Xcode-14.app - checkout && cd ~/git/flowcrypt-ios/ && cache restore && make dependencies - mv ~/appium-env ~/git/flowcrypt-ios/appium/.env - sem-version node 16 && cache restore appium-npm && cd ./appium && npm i && cd .. && cache store appium-npm appium/node_modules @@ -111,4 +113,4 @@ after_pipeline: jobs: - name: Publish Results commands: - - test-results gen-pipeline-report + - test-results gen-pipeline-report \ No newline at end of file diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift index cbffb7f0a..321de06dc 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift @@ -10,14 +10,10 @@ import UIKit // MARK: - Error handling extension ComposeViewController { - func requestMissingPassPhraseWithModal(for signingKey: Keypair, isDraft: Bool = false, withDiscard: Bool = false) { + func requestMissingPassPhraseWithModal(for signingKey: Keypair, isDraft: Bool = false) { let alert = alertsFactory.makePassPhraseAlert( onCancel: { [weak self] in - if !withDiscard { - self?.navigationController?.popViewController(animated: true) - } else { - self?.handle(error: ComposeMessageError.passPhraseRequired) - } + self?.navigationController?.popViewController(animated: true) }, onCompletion: { [weak self] passPhrase in guard let self = self else { return } @@ -30,7 +26,7 @@ extension ComposeViewController { ) if matched { - self.handleMatchedPassphrase(isDraft: isDraft, withDiscard: withDiscard) + self.handleMatchedPassphrase(isDraft: isDraft) } else { self.handle(error: ComposeMessageError.passPhraseNoMatch) } @@ -43,7 +39,7 @@ extension ComposeViewController { present(alert, animated: true, completion: nil) } - private func handleMatchedPassphrase(isDraft: Bool, withDiscard: Bool) { + private func handleMatchedPassphrase(isDraft: Bool) { guard isDraft else { handleSendTap() return @@ -54,11 +50,7 @@ extension ComposeViewController { return } - if withDiscard { - handleBackButtonTap() - } else { - saveDraftIfNeeded() - } + saveDraftIfNeeded() } func handle(error: Error) { @@ -71,7 +63,7 @@ extension ComposeViewController { let hideSpinnerAnimationDuration: TimeInterval = 1 DispatchQueue.main.asyncAfter(deadline: .now() + hideSpinnerAnimationDuration) { [weak self] in - guard let self = self else { return } + guard let self else { return } if self.isMessagePasswordSupported { switch error { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift index 1cb0ec489..202612dda 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Setup.swift @@ -151,16 +151,11 @@ extension ComposeViewController: NavigationChildController { switch state { case .cancelled: - guard let identifier = self.composeMessageService.messageIdentifier else { break } - self.handleAction?(.update(identifier)) + self.handleUpdateAction() case .error(let error): self.showToast("draft_error".localizeWithArguments(error.errorMessage)) case .success: - if var messageIdentifier = self.composeMessageService.messageIdentifier { - messageIdentifier.draftMessageId = self.input.type.info?.id - self.handleAction?(.update(messageIdentifier)) - } - + self.handleUpdateAction() self.showToast("draft_saved".localized, duration: 1.0) case .saving: self.showToast("draft_saving".localized, duration: 10.0) @@ -169,4 +164,10 @@ extension ComposeViewController: NavigationChildController { navigationController?.popViewController(animated: true) } + + private func handleUpdateAction() { + guard var messageIdentifier = composeMessageService.messageIdentifier else { return } + messageIdentifier.draftMessageId = input.type.info?.id + handleAction?(.update(messageIdentifier)) + } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 5fa77babe..da8844607 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -801,11 +801,11 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { case is AttachmentNode: handleAttachmentTap(at: indexPath) default: - let message = input[indexPath.section - 1] + guard let message = input[safe: indexPath.section - 1], + message.rawMessage.isDraft + else { return } - if message.rawMessage.isDraft { - handleDraftTap(at: indexPath) - } + handleDraftTap(at: indexPath) } } diff --git a/appium/config/wdio.live.conf.js b/appium/config/wdio.live.conf.js index 1456fed35..544d37559 100644 --- a/appium/config/wdio.live.conf.js +++ b/appium/config/wdio.live.conf.js @@ -15,7 +15,7 @@ config.capabilities = [ 'appium:automationName': 'XCUITest', 'appium:options': { deviceName: 'iPhone 14', - platformVersion: '16.1', + platformVersion: '16.0', app: join(process.cwd(), './FlowCrypt.app'), }, }, diff --git a/appium/config/wdio.mock.conf.js b/appium/config/wdio.mock.conf.js index 7ad8fb61e..b667d309d 100644 --- a/appium/config/wdio.mock.conf.js +++ b/appium/config/wdio.mock.conf.js @@ -30,7 +30,7 @@ config.capabilities = [ 'appium:automationName': 'XCUITest', 'appium:options': { deviceName: 'iPhone 14', - platformVersion: '16.1', + platformVersion: '16.0', app: join(process.cwd(), './FlowCrypt.app'), processArguments: { 'args': ['--mock-fes-api', '--mock-attester-api', '--mock-gmail-api'] }, }, From b956a6354bdc91b7c8e5a31fb335ef5f0c53ecdb Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 17 Oct 2022 21:35:13 +0300 Subject: [PATCH 55/56] fix ui test --- .../Inbox/InboxViewDecorator.swift | 3 +- .../Threads/ThreadDetailsViewController.swift | 3 ++ .../ComposeMessageService.swift | 2 + appium/api-mocks/apis/google/google-data.ts | 6 ++- .../api-mocks/apis/google/google-endpoints.ts | 16 ++++--- appium/package.json | 4 +- appium/tests/data/index.ts | 3 +- .../tests/screenobjects/mail-folder.screen.ts | 27 +++++++++--- .../CheckDraftsFunctionality.spec.ts | 42 ++++++++++--------- 9 files changed, 67 insertions(+), 39 deletions(-) diff --git a/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift b/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift index 8e1b31a6f..bddbdfeb3 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift @@ -49,7 +49,8 @@ struct InboxViewDecorator { backgroundColor: .backgroundColor, title: title + " " + "empty".localized, size: size, - imageName: imageName + imageName: imageName, + accessibilityIdentifier: "aid-empty-cell-node" ) } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index da8844607..7fd536d43 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -492,6 +492,9 @@ extension ThreadDetailsViewController { self.node.reloadSections([indexPath.section], with: .automatic) } else { self.node.insertSections([indexPath.section], with: .automatic) + if indexPath.section > 0 { + self.node.reloadSections([indexPath.section - 1], with: .automatic) + } } }, completion: { [weak self] _ in diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index bc820d2fd..df07fde8b 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -262,6 +262,8 @@ final class ComposeMessageService { if Task.isCancelled { return self.messageIdentifier } + let threadId = self.messageIdentifier?.threadId?.stringId ?? threadId + return try await self.draftGateway?.saveDraft( input: MessageGatewayInput( mime: mime, diff --git a/appium/api-mocks/apis/google/google-data.ts b/appium/api-mocks/apis/google/google-data.ts index ac3335d72..8f0a35a46 100644 --- a/appium/api-mocks/apis/google/google-data.ts +++ b/appium/api-mocks/apis/google/google-data.ts @@ -354,6 +354,10 @@ export class GoogleData { DATA[this.acct].messages = DATA[this.acct].messages.filter(m => !ids.includes(m.id)); } + public deleteDraft = (id: string) => { + DATA[this.acct].messages = DATA[this.acct].messages.filter(m => id !== m.draftId); + } + public addDraft = (raw: string, mimeMsg: ParsedMail, id?: string, threadId?: string) => { const draftId = id ?? `draft_id_${lousyRandom()}`; const msgId = `msg_id_${lousyRandom()}`; @@ -370,7 +374,7 @@ export class GoogleData { }; public getDraft = (id: string): GmailMsg | undefined => { - return DATA[this.acct].drafts.find(d => d.id === id); + return DATA[this.acct].messages.find(d => d.draftId === id); }; public getAttachment = (attachmentId: string) => { diff --git a/appium/api-mocks/apis/google/google-endpoints.ts b/appium/api-mocks/apis/google/google-endpoints.ts index 46f30e2f9..441208757 100644 --- a/appium/api-mocks/apis/google/google-endpoints.ts +++ b/appium/api-mocks/apis/google/google-endpoints.ts @@ -184,10 +184,6 @@ export const getMockGoogleEndpoints = ( throw new HttpErr('The thread you are replying to not found', 404); } const decoded = await Parse.convertBase64ToMimeMsg(body.message.raw); - // if (!decoded.text?.startsWith('[flowcrypt:') && !decoded.text?.startsWith('(saving of this draft was interrupted - to decrypt it, send it to yourself)')) { - // throw new Error(`The "flowcrypt" draft prefix was not found in the draft. Instead starts with: ${decoded.text?.substring(0, 100)}`); - // } - const draft = data.addDraft(body.message.raw, decoded, undefined, body.message.threadId); return { @@ -196,7 +192,7 @@ export const getMockGoogleEndpoints = ( labelIds: ['DRAFT'], threadId: draft.threadId } - }; + } } } else if (isGet(req)) { const acct = oauth.checkAuthorizationHeaderWithAccessToken(req.headers.authorization); @@ -220,7 +216,7 @@ export const getMockGoogleEndpoints = ( const data = (await GoogleData.withInitializedData(acct, googleConfig)); const draft = data.getDraft(id); if (draft) { - return { id: draft.id, message: draft }; + return { id: draft.draftId, message: draft }; } throw new HttpErr(`MOCK draft not found for ${acct} (draftId: ${id})`, Status.NOT_FOUND); } else if (isPut(req)) { @@ -232,17 +228,19 @@ export const getMockGoogleEndpoints = ( const decoded = await Parse.convertBase64ToMimeMsg(raw); const data = (await GoogleData.withInitializedData(acct, googleConfig)); - const draft = data.addDraft(raw, decoded, body.id, body.message?.threadId) + const draft = data.addDraft(raw, decoded, body.id, body.message?.threadId); - // const mimeMsg = await Parse.convertBase64ToMimeMsg(raw); return { id: draft.draftId, message: { id: draft.id, labelIds: ['DRAFT'], threadId: draft.threadId } - }; + } } else if (isDelete(req)) { + const id = parseResourceId(req.url!); + const data = (await GoogleData.withInitializedData(acct, googleConfig)); + data.deleteDraft(id); return {}; } throw new HttpErr(`Method not implemented for ${req.url}: ${req.method}`); diff --git a/appium/package.json b/appium/package.json index 04ea446f6..1339b4c67 100644 --- a/appium/package.json +++ b/appium/package.json @@ -30,7 +30,7 @@ "@wdio/junit-reporter": "^7.16.15", "@wdio/local-runner": "^7.16.15", "@wdio/spec-reporter": "^7.16.14", - "appium": "^2.0.0-beta.44", + "appium": "^2.0.0-beta.46", "appium-xcuitest-driver": "^4.12.1", "dotenv": "^16.0.0", "eslint-plugin-node": "^11.1.0", @@ -48,4 +48,4 @@ "wdio-video-reporter": "^3.2.0", "webdriverio": "^7.16.15" } -} +} \ No newline at end of file diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index 7e5f3eb76..a177e01bb 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -47,7 +47,8 @@ export const CommonData = { thirdDate: 'Feb 08', }, draft: { - subject: 'Draft subject', + subject1: 'Draft subject', + subject2: 'Subject for another draft', text1: 'Draft text', updatedText1: 'Some new text', text2: 'Another draft' diff --git a/appium/tests/screenobjects/mail-folder.screen.ts b/appium/tests/screenobjects/mail-folder.screen.ts index 429a5224a..6b39302a5 100644 --- a/appium/tests/screenobjects/mail-folder.screen.ts +++ b/appium/tests/screenobjects/mail-folder.screen.ts @@ -7,6 +7,7 @@ const SELECTORS = { SENT_HEADER: '~aid-navigation-item-sent', CREATE_EMAIL_BUTTON: '~aid-compose-message-button', INBOX_HEADER: '~aid-navigation-item-inbox', + DRAFTS_HEADER: '~aid-navigation-item-drafts', SEARCH_BTN: '~aid-search-btn', HELP_BTN: '~aid-help-btn', SEARCH_FIELD: '~aid-search-all-emails', @@ -14,7 +15,8 @@ const SELECTORS = { // INBOX_ITEM: '~aid-inbox-item', // TODO: Couldn't use accessibility identifier because $$ selector returns only visible cells INBOX_ITEM: '-ios class chain:**/XCUIElementTypeOther/XCUIElementTypeTable[2]/XCUIElementTypeCell', - IDLE_NODE: '~aid-inbox-idle-node' + IDLE_NODE: '~aid-inbox-idle-node', + EMPTY_CELL_NODE: '~aid-empty-cell-node' }; class MailFolderScreen extends BaseScreen { @@ -42,6 +44,10 @@ class MailFolderScreen extends BaseScreen { return $(SELECTORS.INBOX_HEADER) } + get draftsHeader() { + return $(SELECTORS.DRAFTS_HEADER) + } + get createEmailButton() { return $(SELECTORS.CREATE_EMAIL_BUTTON); } @@ -62,6 +68,10 @@ class MailFolderScreen extends BaseScreen { return $(SELECTORS.IDLE_NODE); } + get emptyCellNode() { + return $(SELECTORS.EMPTY_CELL_NODE); + } + checkTrashScreen = async () => { await ElementHelper.waitElementVisible(await this.trashHeader); await ElementHelper.waitElementVisible(await this.searchBtn); @@ -112,10 +122,10 @@ class MailFolderScreen extends BaseScreen { await TouchHelper.scrollDownToElement(await elem); }; - getEmailCount = async () => { + checkEmailCount = async (expectedCount: number) => { await ElementHelper.waitElementInvisible(await this.idleNode); await browser.pause(1000); - return await this.inboxList.length; + expect(await this.inboxList.length).toEqual(expectedCount); }; scrollUpToFirstEmail = async () => { @@ -131,16 +141,21 @@ class MailFolderScreen extends BaseScreen { await ElementHelper.waitElementVisible(await this.helpBtn); } + checkDraftsScreen = async () => { + await ElementHelper.waitElementVisible(await this.draftsHeader); + await ElementHelper.waitElementVisible(await this.searchBtn); + await ElementHelper.waitElementVisible(await this.helpBtn); + } + checkIfFolderIsEmpty = async () => { - const emailCount = await this.getEmailCount(); - return emailCount === 0; + await ElementHelper.waitElementVisible(await this.emptyCellNode); } emptyFolder = async () => { await ElementHelper.waitAndClick(await this.emptyFolderBtn); await BaseScreen.clickConfirmButton(); // Give some time to delete messages - await browser.pause(3000); + await browser.pause(500); } clickSearchButton = async () => { diff --git a/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts b/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts index beac23001..1ec8366dd 100644 --- a/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts +++ b/appium/tests/specs/mock/composeEmail/CheckDraftsFunctionality.spec.ts @@ -14,10 +14,11 @@ describe('COMPOSE EMAIL: ', () => { it('check drafts functionality', async () => { const mockApi = new MockApi(); - const recipient = MockUserList.robot; + const recipient = MockUserList.dmitry; const recipientWithoutPubKeys = MockUserList.demo; const subject = CommonData.simpleEmail.subject; - const draftSubject = CommonData.draft.subject; + const draftSubject1 = CommonData.draft.subject1; + const draftSubject2 = CommonData.draft.subject2; const draftText1 = CommonData.draft.text1; const updatedDraftText = CommonData.draft.updatedText1; const draftText2 = CommonData.draft.text2; @@ -29,7 +30,7 @@ describe('COMPOSE EMAIL: ', () => { }); mockApi.attesterConfig = { servedPubkeys: { - [MockUserList.robot.email]: MockUserList.robot.pub! + [MockUserList.dmitry.email]: MockUserList.dmitry.pub!, } }; @@ -70,6 +71,7 @@ describe('COMPOSE EMAIL: ', () => { await MenuBarScreen.clickMenuBtn(); await MenuBarScreen.clickDraftsButton(); + await MailFolderScreen.checkDraftsScreen(); // delete draft from compose screen await MailFolderScreen.clickOnEmailBySubject(subject); @@ -80,45 +82,47 @@ describe('COMPOSE EMAIL: ', () => { await NewMessageScreen.confirmDelete(); await EmailScreen.clickBackButton(); - await MenuBarScreen.clickMenuBtn(); - await MenuBarScreen.clickInboxButton(); + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.checkIfFolderIsEmpty(); // compose 2 new drafts and then delete them both - await MailFolderScreen.checkInboxScreen(); await MailFolderScreen.clickCreateEmail(); - await NewMessageScreen.composeEmail(recipientWithoutPubKeys.email, draftSubject, draftText1); + await NewMessageScreen.composeEmail(recipientWithoutPubKeys.email, draftSubject1, draftText1); await NewMessageScreen.clickBackButton(); - await MailFolderScreen.checkInboxScreen(); + await MailFolderScreen.checkDraftsScreen(); await MailFolderScreen.clickCreateEmail(); - await NewMessageScreen.composeEmail(recipient.email, subject, draftText2); + await NewMessageScreen.composeEmail(recipient.email, draftSubject2, draftText2); await NewMessageScreen.clickBackButton(); - await MailFolderScreen.clickOnEmailBySubject(subject); + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.checkEmailCount(2); + + await MailFolderScreen.clickOnEmailBySubject(draftSubject1); await NewMessageScreen.clickDeleteButton(); await NewMessageScreen.confirmDelete(); - await MailFolderScreen.checkInboxScreen(); - await MailFolderScreen.clickOnEmailBySubject(draftSubject); + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.clickOnEmailBySubject(draftSubject2); await NewMessageScreen.clickDeleteButton(); await NewMessageScreen.confirmDelete(); - await MailFolderScreen.checkInboxScreen(); + await MailFolderScreen.checkDraftsScreen(); await MailFolderScreen.checkIfFolderIsEmpty(); // compose draft, send it and check if sent message added to 'sent' folder - await MailFolderScreen.checkInboxScreen(); await MailFolderScreen.clickCreateEmail(); - await NewMessageScreen.composeEmail(recipient.email, draftSubject, draftText1); + await NewMessageScreen.composeEmail(recipient.email, draftSubject1, draftText1); await NewMessageScreen.clickBackButton(); - await MenuBarScreen.clickMenuBtn(); - await MenuBarScreen.clickDraftsButton(); - await MailFolderScreen.clickOnEmailBySubject(draftSubject); + await MailFolderScreen.checkDraftsScreen(); + await MailFolderScreen.clickOnEmailBySubject(draftSubject1); await NewMessageScreen.clickSendButton(); + await MailFolderScreen.checkDraftsScreen(); await MailFolderScreen.checkIfFolderIsEmpty(); await MenuBarScreen.clickMenuBtn(); await MenuBarScreen.clickSentButton(); - await MailFolderScreen.clickOnEmailBySubject(draftSubject); + await MailFolderScreen.checkSentScreen(); + await MailFolderScreen.clickOnEmailBySubject(draftSubject1); }); }); }); \ No newline at end of file From ed691543ad1a4c7e23210cc06950726b7b80bd32 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 18 Oct 2022 13:49:26 +0300 Subject: [PATCH 56/56] fix duplicated drafts --- .../Compose Message Service/ComposeMessageService.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index df07fde8b..18947c544 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -251,7 +251,9 @@ final class ComposeMessageService { } func saveDraft(message: SendableMsg, threadId: String?, shouldEncrypt: Bool) async throws { - saveDraftTask?.cancel() + if let saveDraftTask { + _ = try await saveDraftTask.value + } saveDraftTask = Task { do { @@ -260,8 +262,6 @@ final class ComposeMessageService { fmt: shouldEncrypt ? .encryptInline : .plain ).mimeEncoded - if Task.isCancelled { return self.messageIdentifier } - let threadId = self.messageIdentifier?.threadId?.stringId ?? threadId return try await self.draftGateway?.saveDraft( @@ -277,6 +277,7 @@ final class ComposeMessageService { } messageIdentifier = try await saveDraftTask?.value + saveDraftTask = nil } func deleteDraft() async throws {