diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 71d5bb3cb..280a86701 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 32DCAF683D87EA6221F71335 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA058652FD4616FB04FB6 /* SequenceExtensions.swift */; }; 32DCAF95A6A329C3136B1C8E /* Imap+msg.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA55C094E9745AA1FD210 /* Imap+msg.swift */; }; 32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA9701B2D5052225A0414 /* SignInViewController.swift */; }; + 50531BE42629B9A80039BAE9 /* AttachmentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */; }; 5A39F42D239EC321001F4607 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A39F42C239EC321001F4607 /* SettingsViewController.swift */; }; 5A39F430239EC396001F4607 /* SettingsViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A39F42F239EC396001F4607 /* SettingsViewDecorator.swift */; }; 5A39F437239ECC23001F4607 /* KeySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A39F436239ECC23001F4607 /* KeySettingsViewController.swift */; }; @@ -134,7 +135,6 @@ 9FE1B3802563F85400D6D086 /* MessagesListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE1B37F2563F85400D6D086 /* MessagesListProvider.swift */; }; 9FE1B3942563F98600D6D086 /* Imap+MessagesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE1B3932563F98600D6D086 /* Imap+MessagesList.swift */; }; 9FE1B3A02565B0CE00D6D086 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE1B39F2565B0CD00D6D086 /* Message.swift */; }; - 9FE233D825712E51003D7C7E /* test-ci-secrets.json in Resources */ = {isa = PBXBuildFile; fileRef = 9FE233D725712E51003D7C7E /* test-ci-secrets.json */; }; 9FE743072347AA54005E2DBB /* MainNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE743062347AA54005E2DBB /* MainNavigationController.swift */; }; 9FEED1D2230DAD1E00700F8E /* InboxViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEED1D1230DAD1E00700F8E /* InboxViewModel.swift */; }; 9FF0670825520CF800FCC9E6 /* GmailService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF0670725520CF800FCC9E6 /* GmailService.swift */; }; @@ -370,6 +370,7 @@ 411CEC75050F852F172CD687 /* Pods-FlowCryptTests.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptTests.testflight.xcconfig"; path = "Target Support Files/Pods-FlowCryptTests/Pods-FlowCryptTests.testflight.xcconfig"; sourceTree = ""; }; 44D0BF0D60EF854CEC17561C /* Pods-FlowCryptUIApplication.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUIApplication.testflight.xcconfig"; path = "Target Support Files/Pods-FlowCryptUIApplication/Pods-FlowCryptUIApplication.testflight.xcconfig"; sourceTree = ""; }; 4A76C3D4559C9F415D392A62 /* Pods-FlowCryptTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptTests.debug.xcconfig"; path = "Target Support Files/Pods-FlowCryptTests/Pods-FlowCryptTests.debug.xcconfig"; sourceTree = ""; }; + 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentNode.swift; sourceTree = ""; }; 567BA6739257FE0D2924D82C /* Pods_FlowCryptUIApplication.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FlowCryptUIApplication.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5A39F42C239EC321001F4607 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 5A39F42F239EC396001F4607 /* SettingsViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewDecorator.swift; sourceTree = ""; }; @@ -1590,6 +1591,7 @@ 9F4453BF236B894D005D7D05 /* TableNode.swift */, 9F4453C1236B9273005D7D05 /* TextFieldNode.swift */, D24FAFAA2520BFAE00BF46C5 /* CheckBoxNode.swift */, + 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */, ); path = Nodes; sourceTree = ""; @@ -1866,7 +1868,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9FE233D825712E51003D7C7E /* test-ci-secrets.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2419,6 +2420,7 @@ D28655912423B4580066F52E /* HeaderNode.swift in Sources */, D271774C242558DA00BDA9A9 /* MessageTextSubjectNode.swift in Sources */, D27177502425659F00BDA9A9 /* SettingsCellNode.swift in Sources */, + 50531BE42629B9A80039BAE9 /* AttachmentNode.swift in Sources */, D2CDC3D824047066002B045F /* RecipientEmailNode.swift in Sources */, D2717753242568A600BDA9A9 /* NavigationBarItemsView.swift in Sources */, D211CE7023FC35AC00D1CE38 /* TableNode.swift in Sources */, diff --git a/FlowCrypt/Assets.xcassets/download.imageset/Contents.json b/FlowCrypt/Assets.xcassets/download.imageset/Contents.json new file mode 100644 index 000000000..f612e0ad5 --- /dev/null +++ b/FlowCrypt/Assets.xcassets/download.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Light-M.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FlowCrypt/Assets.xcassets/download.imageset/Light-M.png b/FlowCrypt/Assets.xcassets/download.imageset/Light-M.png new file mode 100644 index 000000000..6a3c3a5ea Binary files /dev/null and b/FlowCrypt/Assets.xcassets/download.imageset/Light-M.png differ diff --git a/FlowCrypt/Assets.xcassets/paperclip.imageset/Contents.json b/FlowCrypt/Assets.xcassets/paperclip.imageset/Contents.json index 5f5e212a9..f977fb7df 100644 --- a/FlowCrypt/Assets.xcassets/paperclip.imageset/Contents.json +++ b/FlowCrypt/Assets.xcassets/paperclip.imageset/Contents.json @@ -1,23 +1,23 @@ { "images" : [ { - "idiom" : "universal", "filename" : "paperclip.png", + "idiom" : "universal", "scale" : "1x" }, { - "idiom" : "universal", "filename" : "paperclip@2x.png", + "idiom" : "universal", "scale" : "2x" }, { - "idiom" : "universal", "filename" : "paperclip@3x.png", + "idiom" : "universal", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift index 5b775609c..202997b7b 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewController.swift @@ -14,7 +14,7 @@ final class MessageViewController: TableNodeViewController { } enum Parts: Int, CaseIterable { - case sender, subject, text + case sender, subject, text, attachment var indexPath: IndexPath { IndexPath(row: rawValue, section: 0) @@ -53,6 +53,7 @@ final class MessageViewController: TableNodeViewController { private let messageProvider: MessageProvider private let messageOperationsProvider: MessageOperationsProvider private var message: NSAttributedString + private var attachments: [Attachment] private let trashFolderProvider: TrashFolderProviderType init( @@ -73,6 +74,7 @@ final class MessageViewController: TableNodeViewController { self.core = core self.trashFolderProvider = trashFolderProvider self.onCompletion = completion + self.attachments = [] self.message = decorator.attributed( text: "loading_title".localized + "...", color: .lightGray @@ -143,7 +145,7 @@ extension MessageViewController { self?.message = try awaitPromise(self!.fetchMessage()) }.then(on: .main) { [weak self] in self?.hideSpinner() - self?.node.reloadRows(at: [Parts.text.indexPath], with: .fade) + self?.node.reloadRows(at: [Parts.text.indexPath, Parts.attachment.indexPath], with: .fade) self?.asyncMarkAsReadIfNotAlreadyMarked() }.catch(on: .main) { [weak self] error in self?.hideSpinner() @@ -170,6 +172,10 @@ extension MessageViewController { 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 { @@ -242,6 +248,10 @@ extension MessageViewController { showToast("Marking as unread will be implemented soon") } + @objc private func handleAttachmentTap() { + showToast("Downloading attachments is not implemented yet") + } + @objc private func handleTrashTap() { showSpinner() @@ -377,6 +387,10 @@ extension MessageViewController: ASTableDelegate, ASTableDataSource { return MessageSubjectNode(subject, time: time) case .text: return MessageTextSubjectNode(self.message) + case .attachment: + return AttachmentsNode(attachments: self.attachments) { [weak self] in + self?.handleAttachmentTap() + } } } } diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index 5b3e7c7db..49a0ccd22 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -124,15 +124,15 @@ struct SendableMsg { struct MsgBlock: Decodable { static func blockParseErr(with content: String) -> MsgBlock { - MsgBlock(type: .blockParseErr, content: content, decryptErr: nil, keyDetails: nil) + MsgBlock(type: .blockParseErr, content: content, decryptErr: nil, keyDetails: nil, attMeta: nil) } let type: BlockType let content: String let decryptErr: DecryptErr? // always present in decryptErr BlockType let keyDetails: KeyDetails? // always present in publicKey BlockType + let attMeta: AttMeta? // always present in plainAtt, encryptedAtt, decryptedAtt, encryptedAttLink // let verifyRes: VerifyRes?, - // let attMeta: AttMeta?; // always present in plainAtt, encryptedAtt, decryptedAtt, encryptedAttLink // let signature: String? // possibly not neded in Swift @@ -166,6 +166,12 @@ struct MsgBlock: Decodable { } } + struct AttMeta: Decodable { + let name: String + let data: Data + let length: Int + } + enum BlockType: String, Decodable { case plainHtml // all content blocks, regardless if encrypted or not, formatted as a plainHtml (todo - rename this one day to formattedHtml) case publicKey diff --git a/FlowCryptUI/Nodes/AttachmentNode.swift b/FlowCryptUI/Nodes/AttachmentNode.swift new file mode 100644 index 000000000..163354add --- /dev/null +++ b/FlowCryptUI/Nodes/AttachmentNode.swift @@ -0,0 +1,117 @@ +// +// 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 struct Input { + let name: String + let size: String + + public init( + name: String, + size: String + ) { + 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)?) { + super.init() + self.onTap = onTap + + self.borderWidth = 1.0 + self.cornerRadius = 8.0 + self.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) + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + let verticalStack = ASStackLayoutSpec.vertical() + verticalStack.spacing = 3 + verticalStack.style.flexShrink = 1.0 + verticalStack.style.flexGrow = 1.0 + + verticalStack.children = [titleNode, subtitleNode] + + let finalSpec = ASStackLayoutSpec( + direction: .horizontal, + spacing: 10, + justifyContent: .start, + alignItems: .center, + children: [imageNode, verticalStack, buttonNode] + ) + + return ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20), + child: finalSpec + ) + } + + @objc private func tapHandle() { + onTap?() + } +}