diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 0b16909bc..a721e0631 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -110,6 +110,7 @@ 9F9AAFFD2383E216000A00F1 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9AAFFC2383E216000A00F1 /* Document.swift */; }; 9F9ABC8723AC1EAA00D560E3 /* MessageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9ABC8623AC1EAA00D560E3 /* MessageContext.swift */; }; 9FA19890253C841F008C9CF2 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA1988F253C841F008C9CF2 /* TableViewController.swift */; }; + 9FA9C83C264C2D75005A9670 /* MessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA9C83B264C2D75005A9670 /* MessageService.swift */; }; 9FB22CD625715CA10026EE64 /* BackupServiceErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB22CD525715CA10026EE64 /* BackupServiceErrorHandler.swift */; }; 9FB22CDD25715CF50026EE64 /* GmailServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB22CDC25715CF50026EE64 /* GmailServiceError.swift */; }; 9FB22CE425715D3E0026EE64 /* GmailServiceErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB22CE325715D3E0026EE64 /* GmailServiceErrorHandler.swift */; }; @@ -474,6 +475,7 @@ 9F9AAFFC2383E216000A00F1 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; 9F9ABC8623AC1EAA00D560E3 /* MessageContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContext.swift; sourceTree = ""; }; 9FA1988F253C841F008C9CF2 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; + 9FA9C83B264C2D75005A9670 /* MessageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageService.swift; sourceTree = ""; }; 9FB22CD525715CA10026EE64 /* BackupServiceErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupServiceErrorHandler.swift; sourceTree = ""; }; 9FB22CDC25715CF50026EE64 /* GmailServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GmailServiceError.swift; sourceTree = ""; }; 9FB22CE325715D3E0026EE64 /* GmailServiceErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GmailServiceErrorHandler.swift; sourceTree = ""; }; @@ -1034,6 +1036,7 @@ 9F9361A42573CE260009912F /* MessageProvider.swift */, 9F93623E2573D16F0009912F /* Gmail+Message.swift */, 9F9362182573D10E0009912F /* Imap+Message.swift */, + 9FA9C83B264C2D75005A9670 /* MessageService.swift */, ); path = "Message Provider"; sourceTree = ""; @@ -2333,6 +2336,7 @@ 9F0C3C102316DD5B00299985 /* GoogleUserService.swift in Sources */, D24F4C2223E2359B00C5EEE4 /* BootstrapViewController.swift in Sources */, D211CE7623FC36BC00D1CE38 /* UIColorExtension.swift in Sources */, + 9FA9C83C264C2D75005A9670 /* MessageService.swift in Sources */, D2FF6966243115EC007182F0 /* EmailProviderViewController.swift in Sources */, D2CDC3D22402D4DA002B045F /* UIViewControllerExtensions.swift in Sources */, D297990D2444A76D004A3E31 /* UserObject+Empty.swift in Sources */, diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift index da0a913e4..e23f09675 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewController.swift @@ -13,8 +13,12 @@ final class MessageViewController: TableNodeViewController { var path = "" } + enum Sections: Int, CaseIterable { + case main, attributes + } + enum Parts: Int, CaseIterable { - case sender, subject, text, attachment + case sender, subject, text var indexPath: IndexPath { IndexPath(row: rawValue, section: 0) @@ -47,38 +51,27 @@ final class MessageViewController: TableNodeViewController { private let onCompletion: MsgViewControllerCompletion? private var input: MessageViewController.Input? - private let decorator: MessageViewDecoratorType - private var dataService: DataServiceType & KeyDataServiceType - private let core: Core - private let messageProvider: MessageProvider + private let decorator: MessageViewDecorator + private let messageService: MessageService private let messageOperationsProvider: MessageOperationsProvider - private var message: NSAttributedString - private var attachments: [Attachment] private let trashFolderProvider: TrashFolderProviderType + private var fetchedMessage: FetchedMessage = .empty init( - messageProvider: MessageProvider = MailProvider.shared.messageProvider, + messageService: MessageService = MessageService(), messageOperationsProvider: MessageOperationsProvider = MailProvider.shared.messageOperationsProvider, - decorator: MessageViewDecoratorType = MessageViewDecorator(dateFormatter: DateFormatter()), + decorator: MessageViewDecorator = MessageViewDecorator(dateFormatter: DateFormatter()), storage: DataServiceType & KeyDataServiceType = DataService.shared, - core: Core = Core.shared, trashFolderProvider: TrashFolderProviderType = TrashFolderProvider(), input: MessageViewController.Input, completion: MsgViewControllerCompletion? ) { - self.messageProvider = messageProvider + self.messageService = messageService self.messageOperationsProvider = messageOperationsProvider self.input = input self.decorator = decorator - self.dataService = storage - self.core = core self.trashFolderProvider = trashFolderProvider self.onCompletion = completion - self.attachments = [] - self.message = decorator.attributed( - text: "loading_title".localized + "...", - color: .lightGray - ) super.init(node: TableNode()) } @@ -142,60 +135,24 @@ extension MessageViewController { private func fetchDecryptAndRenderMsg() { guard let input = input else { return } showSpinner("loading_title".localized, isUserInteractionEnabled: true) + Promise { [weak self] in - self?.message = try awaitPromise(self!.fetchMessage()) - }.then(on: .main) { [weak self] in + guard let self = self else { return } + let promise = self.messageService.getMessage(with: input.objMessage, folder: input.path) + let message = try awaitPromise(promise) + self.fetchedMessage = message + } + .then(on: .main) { [weak self] in self?.hideSpinner() - self?.node.reloadRows(at: [Parts.text.indexPath, Parts.attachment.indexPath], with: .fade) + self?.node.reloadData() self?.asyncMarkAsReadIfNotAlreadyMarked() - }.catch(on: .main) { [weak self] error in + } + .catch(on: .main) { [weak self] error in self?.hideSpinner() self?.handleError(error, path: input.path) } } - private func fetchMessage() -> Promise { - Promise { [weak self] resolve, reject in - guard let self = self, let input = self.input else { return } - - let rawMimeData: Data = try awaitPromise(self.messageProvider.fetchMsg(message: input.objMessage, folder: input.path)) - self.input?.bodyMessage = rawMimeData - - guard let keys = self.dataService.keys else { - reject(CoreError.notReady("Could not fetch keys")) - return - } - - let decrypted = try self.core.parseDecryptMsg( - encrypted: rawMimeData, - keys: keys, - msgPwd: nil, - isEmail: true - ) - let decryptErrBlocks = decrypted.blocks.filter { $0.decryptErr != nil } - let decryptAttBlocks = decrypted.blocks.filter { $0.type == .plainAtt || $0.type == .encryptedAtt || $0.type == .decryptedAtt } - - let attachments = decryptAttBlocks.map { Attachment(name: $0.attMeta?.name ?? "Attachment", size: $0.attMeta?.length ?? 0) } - self.attachments = attachments - - let message: NSAttributedString - if let decryptErrBlock = decryptErrBlocks.first { - let rawMsg = decryptErrBlock.content - let err = decryptErrBlock.decryptErr?.error - message = self.decorator.attributed( - text: "Could not decrypt:\n\(err?.type.rawValue ?? "UNKNOWN"): \(err?.message ?? "??")\n\n\n\(rawMsg)", - color: .red - ) - } else { - message = self.decorator.attributed( - text: decrypted.text, - color: decrypted.replyType == CoreRes.ReplyType.encrypted ? .main : UIColor.mainTextColor - ) - } - resolve(message) - } - } - private func handleError(_ error: Error, path: String) { 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 @@ -333,9 +290,9 @@ extension MessageViewController { let replyInfo = ComposeViewController.Input.ReplyInfo( recipient: input.objMessage.sender, subject: input.objMessage.subject, - mime: input.bodyMessage, + mime: fetchedMessage.rawMimeData, sentDate: input.objMessage.date, - message: message.string + message: fetchedMessage.text ) navigationController?.pushViewController( @@ -362,11 +319,38 @@ extension MessageViewController: NavigationChildController { // MARK: - ASTableDelegate, ASTableDataSource extension MessageViewController: ASTableDelegate, ASTableDataSource { - func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { - Parts.allCases.count + func numberOfSections(in tableNode: ASTableNode) -> Int { + Sections.allCases.count + } + + func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int { + guard let section = Sections(rawValue: section) else { + return 0 + } + switch section { + case .main: + return Parts.allCases.count + case .attributes: + return fetchedMessage.attachments.count + } } func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { + { [weak self] in + guard let self = self, let section = Sections(rawValue: indexPath.section) else { return ASCellNode() } + + switch section { + case .main: + return self.mainSectionNode(for: indexPath.row) + case .attributes: + return self.attachmentNode(for: indexPath.row) + } + } + } + + private func mainSectionNode(for index: Int) -> ASCellNode { + guard let part = Parts(rawValue: index) else { return ASCellNode() } + let senderTitle = decorator.attributed( title: input?.objMessage.sender ?? "(unknown sender)" ) @@ -377,22 +361,24 @@ extension MessageViewController: ASTableDelegate, ASTableDataSource { date: input?.objMessage.date ) - return { [weak self] in - guard let self = self, let part = Parts(rawValue: indexPath.row) else { return ASCellNode() } - switch part { - case .sender: - return MessageSenderNode(senderTitle) { [weak self] in - self?.handleReplyTap() - } - case .subject: - return MessageSubjectNode(subject, time: time) - case .text: - return MessageTextSubjectNode(self.message) - case .attachment: - return AttachmentsNode(attachments: self.attachments) { [weak self] in - self?.handleAttachmentTap() - } + switch part { + case .sender: + return MessageSenderNode(senderTitle) { [weak self] in + self?.handleReplyTap() } + case .subject: + return MessageSubjectNode(subject, time: time) + case .text: + let messageInput = self.decorator.attributedMessage(from: self.fetchedMessage) + return MessageTextSubjectNode(messageInput) } } + + private func attachmentNode(for index: Int) -> ASCellNode { + AttachmentNode( + input: .init( + msgAttachment: fetchedMessage.attachments[index] + ) + ) + } } diff --git a/FlowCrypt/Controllers/Msg/MessageViewDecorator.swift b/FlowCrypt/Controllers/Msg/MessageViewDecorator.swift index 961aaa4fd..e30b49aef 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewDecorator.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewDecorator.swift @@ -6,16 +6,10 @@ // Copyright © 2019 FlowCrypt Limited. All rights reserved. // +import FlowCryptUI import UIKit -protocol MessageViewDecoratorType { - func attributed(title: String) -> NSAttributedString - func attributed(subject: String) -> NSAttributedString - func attributed(date: Date?) -> NSAttributedString - func attributed(text: String?, color: UIColor) -> NSAttributedString -} - -struct MessageViewDecorator: MessageViewDecoratorType { +struct MessageViewDecorator { let dateFormatter: DateFormatter func attributed(title: String) -> NSAttributedString { @@ -36,4 +30,19 @@ struct MessageViewDecorator: MessageViewDecoratorType { func attributed(text: String?, color: UIColor) -> NSAttributedString { (text ?? "").attributed(.regular(17), color: color) } + + func attributedMessage(from fetchedMessage: FetchedMessage) -> NSAttributedString { + fetchedMessage.text.attributed() + } +} + +extension AttachmentNode.Input { + init(msgAttachment: MessageAttachment) { + self.init( + name: msgAttachment.name + .attributed(.regular(18), color: .textColor, alignment: .left), + size: "\(msgAttachment.size)" + .attributed(.medium(12), color: .textColor, alignment: .left) + ) + } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift new file mode 100644 index 000000000..4c86e8983 --- /dev/null +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -0,0 +1,119 @@ +// +// MessageService.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 12.05.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import Promises + +// MARK: - MessageAttachment +struct MessageAttachment { + let name: String + let size: Int +} + +// MARK: - FetchedMessage +struct FetchedMessage { + enum MessageType { + case error, encrypted, plain + } + + let rawMimeData: Data + let text: String + let attachments: [MessageAttachment] + let messageType: MessageType +} + +extension FetchedMessage { + // TODO: - ANTON - fix with empty state for MessageViewController + static let empty = FetchedMessage( + rawMimeData: Data(), + text: "loading_title".localized + "...", + attachments: [], + messageType: .plain + ) +} + +// MARK: - MessageService +final class MessageService { + private let messageProvider: MessageProvider + private let dataService: DataServiceType & KeyDataServiceType + private let core: Core + + init( + messageProvider: MessageProvider = MailProvider.shared.messageProvider, + dataService: DataServiceType & KeyDataServiceType = DataService.shared, + core: Core = Core.shared + ) { + self.messageProvider = messageProvider + self.dataService = dataService + self.core = core + } + + func getMessage(with input: Message, folder: String) -> Promise { + Promise { [weak self] resolve, reject in + guard let self = self else { return } + + let rawMimeData = try awaitPromise( + self.messageProvider.fetchMsg(message: input, folder: folder) + ) + + guard let keys = self.dataService.keys else { + reject(CoreError.notReady("Could not fetch keys")) + return + } + + let decrypted = try self.core.parseDecryptMsg( + encrypted: rawMimeData, + keys: keys, + msgPwd: nil, + isEmail: true + ) + + let decryptErrBlocks = decrypted.blocks + .filter { $0.decryptErr != nil } + + let attachments = decrypted.blocks + .filter(\.isAttachmentBlock) + .map(MessageAttachment.init) + + let messageType: FetchedMessage.MessageType + let text: String + + if let decryptErrBlock = decryptErrBlocks.first { + let rawMsg = decryptErrBlock.content + let err = decryptErrBlock.decryptErr?.error + text = "Could not decrypt:\n\(err?.type.rawValue ?? "UNKNOWN"): \(err?.message ?? "??")\n\n\n\(rawMsg)" + messageType = .error + } else { + text = decrypted.text + messageType = decrypted.replyType == CoreRes.ReplyType.encrypted ? .encrypted : .plain + } + + let fetchedMessage = FetchedMessage( + rawMimeData: rawMimeData, + text: text, + attachments: attachments, + messageType: messageType + ) + + resolve(fetchedMessage) + } + } +} + +private extension MessageAttachment { + init(block: MsgBlock) { + self.name = block.attMeta?.name ?? "Attachment" + self.size = block.attMeta?.length ?? 0 + } +} + +private extension MsgBlock { + var isAttachmentBlock: Bool { + type == .plainAtt || type == .encryptedAtt || type == .decryptedAtt + } +} diff --git a/FlowCryptUI/Nodes/AttachmentNode.swift b/FlowCryptUI/Nodes/AttachmentNode.swift index 163354add..d80e529e0 100644 --- a/FlowCryptUI/Nodes/AttachmentNode.swift +++ b/FlowCryptUI/Nodes/AttachmentNode.swift @@ -2,93 +2,42 @@ // AttachmentNode.swift // FlowCryptUI // -// Created by QSD BiH on 16. 4. 2021.. -// Copyright © 2021 FlowCrypt Limited. All rights reserved. -// import AsyncDisplayKit -public struct Attachment { - var name, size: String - - public init( - name: String, - size: Int - ) { - self.name = name - self.size = ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file) - } -} - -public final class AttachmentsNode: CellNode { +public final class AttachmentNode: CellNode { public struct Input { - let name: String - let size: String + let name: NSAttributedString + let size: NSAttributedString - public init( - name: String, - size: String - ) { + public init(name: NSAttributedString, size: NSAttributedString) { self.name = name self.size = size } } - - private var attachmentNodes: [AttachmentNode] = [] - private var onTap: (() -> Void)? - - public init(attachments: [Attachment], onTap: (() -> Void)?) { - super.init() - self.onTap = onTap - attachmentNodes = attachments.map { AttachmentNode(input: AttachmentNode.Input(name: $0.name, size: $0.size), - onTap: { - self.onTap?() - }) - } - } - - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { - return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), - child: ASStackLayoutSpec( - direction: .vertical, - spacing: 8, - justifyContent: .start, - alignItems: .stretch, - children: attachmentNodes)) - } -} - -public final class AttachmentNode: CellNode { - public struct Input { - var name, size: String - } - + private let titleNode = ASTextNode() private let subtitleNode = ASTextNode2() private let imageNode = ASImageNode() private let buttonNode = ASButtonNode() - - private var onTap: (() -> Void)? - - public init(input: Input, onTap: (() -> Void)?) { + private let borderNode = ASDisplayNode() + + public init(input: Input) { super.init() - self.onTap = onTap - - self.borderWidth = 1.0 - self.cornerRadius = 8.0 - self.borderColor = UIColor.lightGray.cgColor + + borderNode.borderWidth = 1.0 + borderNode.cornerRadius = 8.0 + borderNode.borderColor = UIColor.lightGray.cgColor imageNode.tintColor = .gray buttonNode.tintColor = .gray - + imageNode.image = UIImage(named: "paperclip")?.tinted(.gray) buttonNode.setImage(UIImage(named: "download")?.tinted(.gray), for: .normal) - buttonNode.addTarget(self, action: #selector(tapHandle), forControlEvents: .touchUpInside) - titleNode.attributedText = NSAttributedString.text(from: input.name, style: .regular(18), color: .gray, alignment: .left) - subtitleNode.attributedText = NSAttributedString.text(from: input.size, style: .medium(12), color: .gray, alignment: .left) + titleNode.attributedText = input.name + subtitleNode.attributedText = input.size } - + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let verticalStack = ASStackLayoutSpec.vertical() verticalStack.spacing = 3 @@ -96,7 +45,7 @@ public final class AttachmentNode: CellNode { verticalStack.style.flexGrow = 1.0 verticalStack.children = [titleNode, subtitleNode] - + let finalSpec = ASStackLayoutSpec( direction: .horizontal, spacing: 10, @@ -105,13 +54,24 @@ public final class AttachmentNode: CellNode { children: [imageNode, verticalStack, buttonNode] ) - return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20), + let borderInset = UIEdgeInsets.side(8) + + let resultSpec = ASInsetLayoutSpec( + insets: UIEdgeInsets( + top: 8 + borderInset.top, + left: 16 + borderInset.left, + bottom: 8 + borderInset.bottom, + right: 17 + borderInset.right + ), child: finalSpec ) - } - - @objc private func tapHandle() { - onTap?() + + return ASOverlayLayoutSpec( + child: resultSpec, + overlay: ASInsetLayoutSpec( + insets: borderInset, + child: borderNode + ) + ) } } diff --git a/Podfile b/Podfile index 9592abd01..dd80a24e6 100644 --- a/Podfile +++ b/Podfile @@ -24,7 +24,7 @@ def shared_pods pod 'SwiftyRSA' pod 'IDZSwiftCommonCrypto' pod 'mailcore2-ios' - pod 'BigInt', '~> 5.0' + pod 'BigInt', '~> 5.2' end def ui_pods diff --git a/Podfile.lock b/Podfile.lock index f2bdbfdea..7134a26bf 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -77,7 +77,7 @@ PODS: - Toast (4.0.0) DEPENDENCIES: - - BigInt (~> 5.0) + - BigInt (~> 5.2) - ENSwiftSideMenu (~> 0.1.4) - GoogleAPIClientForREST/Gmail - GoogleSignIn @@ -142,6 +142,6 @@ SPEC CHECKSUMS: Texture: 2f109e937850d94d1d07232041c9c7313ccddb81 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 -PODFILE CHECKSUM: fea310024146a7697fe1f2e91fe1d27f10198833 +PODFILE CHECKSUM: 7b986642d7194ebd77a029368e3a1935dc0675bc COCOAPODS: 1.10.1