From 3de4b65ae94a734ea99817f788b503a0f5e9d5bc Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 22 Nov 2021 22:26:02 +0200 Subject: [PATCH 1/9] issue #900 add forward button --- FlowCrypt.xcodeproj/project.pbxproj | 3 +- .../xcschemes/Debug FlowCrypt.xcscheme | 2 +- .../xcschemes/Enterprise FlowCrypt.xcscheme | 2 +- .../xcshareddata/xcschemes/FlowCrypt.xcscheme | 2 +- .../xcschemes/FlowCryptAppTests.xcscheme | 2 +- .../xcschemes/FlowCryptCommon.xcscheme | 2 +- .../xcschemes/FlowCryptUI.xcscheme | 2 +- .../xcschemes/FlowCryptUIApplication.xcscheme | 2 +- .../Compose/ComposeViewController.swift | 10 ++-- .../Compose/ComposeViewControllerInput.swift | 23 ++++++---- .../Compose/ComposeViewDecorator.swift | 2 +- .../Threads/ThreadDetailsViewController.swift | 31 +++++++++++-- .../ComposeMessageService.swift | 2 +- .../Resources/en.lproj/Localizable.strings | 3 +- .../ThreadMessageSenderCellNode.swift | 46 +++++++++++++++---- 15 files changed, 96 insertions(+), 38 deletions(-) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 3f48df481..7d9f449ad 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -2220,7 +2220,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1240; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1310; ORGANIZATIONNAME = "FlowCrypt Limited"; TargetAttributes = { 9F2AC5C5267BE99E00F6149B = { @@ -2574,7 +2574,6 @@ 21489B80267CC39E00BDE4AC /* ClientConfigurationService.swift in Sources */, D28655932423B4EE0066F52E /* MyMenuViewDecorator.swift in Sources */, 04B4728D1ECE29D200B8266F /* KeyInfoRealmObject.swift in Sources */, - 04B4728D1ECE29D200B8266F /* KeyInfoRealmObject.swift in Sources */, 9F3EF32F23B172D300FA0CEF /* SearchViewController.swift in Sources */, F191F621272511790053833E /* BlurViewController.swift in Sources */, D2F6D147243506DA00DB4065 /* MailSettingsCredentials.swift in Sources */, diff --git a/FlowCrypt.xcodeproj/xcshareddata/xcschemes/Debug FlowCrypt.xcscheme b/FlowCrypt.xcodeproj/xcshareddata/xcschemes/Debug FlowCrypt.xcscheme index 8ceb7630c..688d5b1eb 100644 --- a/FlowCrypt.xcodeproj/xcshareddata/xcschemes/Debug FlowCrypt.xcscheme +++ b/FlowCrypt.xcodeproj/xcshareddata/xcschemes/Debug FlowCrypt.xcscheme @@ -1,6 +1,6 @@ NSAttributedString { - guard case let .reply(info) = input.type else { return NSAttributedString(string: "") } + guard case let .quote(info) = input.type else { return NSAttributedString(string: "") } let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index b1cd5fc7d..68ea1f712 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -123,8 +123,9 @@ extension ThreadDetailsViewController { let processedMessage = input.processedMessage else { return } - let replyInfo = ComposeMessageInput.ReplyInfo( + let replyInfo = ComposeMessageInput.MessageQuoteInfo( recipient: input.rawMessage.sender, + sender: input.rawMessage.sender, subject: input.rawMessage.subject, mime: processedMessage.rawMimeData, sentDate: input.rawMessage.date, @@ -132,7 +133,30 @@ extension ThreadDetailsViewController { threadId: input.rawMessage.threadId ) - let composeInput = ComposeMessageInput(type: .reply(replyInfo)) + let composeInput = ComposeMessageInput(type: .quote(replyInfo)) + navigationController?.pushViewController( + ComposeViewController(email: email, input: composeInput), + animated: true + ) + } + + private func handleForwardTap(at indexPath: IndexPath) { + guard let email = DataService.shared.email, + let input = input[safe: indexPath.section-1], + let processedMessage = input.processedMessage + else { return } + + let replyInfo = ComposeMessageInput.MessageQuoteInfo( + recipient: nil, + sender: input.rawMessage.sender, + subject: input.rawMessage.subject, + mime: processedMessage.rawMimeData, + sentDate: input.rawMessage.date, + message: processedMessage.text, + threadId: input.rawMessage.threadId + ) + + let composeInput = ComposeMessageInput(type: .quote(replyInfo)) navigationController?.pushViewController( ComposeViewController(email: email, input: composeInput), animated: true @@ -397,7 +421,8 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { if indexPath.row == 0 { return ThreadMessageSenderCellNode( input: .init(threadMessage: section), - onReplyTap: { [weak self] _ in self?.handleReplyTap(at: indexPath) } + onReplyTap: { [weak self] _ in self?.handleReplyTap(at: indexPath) }, + onForwardTap: { [weak self] _ in self?.handleForwardTap(at: indexPath) } ) } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index f32ca67f3..4d5bb5926 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -78,7 +78,7 @@ final class ComposeMessageService { throw MessageValidationError.invalidEmailRecipient } - guard input.isReply || contextToSend.subject?.hasContent ?? false else { + guard input.isQuote || contextToSend.subject?.hasContent ?? false else { throw MessageValidationError.emptySubject } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 80a8ed328..fbcc13444 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -76,8 +76,9 @@ "compose_recipient_expired" = "One or more of your recipients have expired public keys (marked in orange).\n\nPlease ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin."; "compose_recipient_invalid_email" = "One or more of your recipients have invalid email address (marked in red)"; "compose_error" = "Could not compose message"; -"compose_reply_successfull" = "Reply successfully sent"; +"compose_reply_successful" = "Reply successfully sent"; "compose_reply_from" = "On %@ at %@ %@ wrote:"; // Date, time, sender +"compose_forward_successful" = "Message forwarded successfully"; "compose_encrypted_sent" = "Encrypted message sent"; "compose_enter_subject" = "Enter subject"; "compose_enter_secure" = "Enter secure message"; diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift index 6b8214975..8f2f288b4 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -38,6 +38,9 @@ public final class ThreadMessageSenderCellNode: CellNode { var replyImage: UIImage? { return createButtonImage(systemName: "arrowshape.turn.up.left") } + var forwardImage: UIImage? { + return createButtonImage(systemName: "arrowshape.turn.up.right") + } var expandImage: UIImage? { let systemName = isExpanded ? "chevron.up" : "chevron.down" return createButtonImage(systemName: systemName) @@ -61,15 +64,19 @@ public final class ThreadMessageSenderCellNode: CellNode { private let dateNode = ASTextNode2() public private(set) var replyNode = ASButtonNode() + public private(set) var forwardNode = ASButtonNode() public private(set) var expandNode = ASImageNode() private let input: ThreadMessageSenderCellNode.Input private var onReplyTap: ((ThreadMessageSenderCellNode) -> Void)? + private var onForwardTap: ((ThreadMessageSenderCellNode) -> Void)? public init(input: ThreadMessageSenderCellNode.Input, - onReplyTap: ((ThreadMessageSenderCellNode) -> Void)?) { + onReplyTap: ((ThreadMessageSenderCellNode) -> Void)?, + onForwardTap: ((ThreadMessageSenderCellNode) -> Void)?) { self.input = input self.onReplyTap = onReplyTap + self.onForwardTap = onForwardTap super.init() automaticallyManagesSubnodes = true @@ -77,16 +84,34 @@ public final class ThreadMessageSenderCellNode: CellNode { dateNode.attributedText = input.date setupReplyNode() + setupForwardNode() setupExpandNode() } private func setupReplyNode() { - replyNode.setImage(input.replyImage, for: .normal) - replyNode.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) - replyNode.contentMode = .center - replyNode.alpha = input.isExpanded ? 1 : 0 - replyNode.addTarget(self, action: #selector(onReplyNodeTap), forControlEvents: .touchUpInside) - replyNode.accessibilityIdentifier = "replyButton" + setup(buttonNode: replyNode, + with: input.replyImage, + action: #selector(onReplyNodeTap), + accessibilityIdentifier: "replyButton") + } + + private func setupForwardNode() { + setup(buttonNode: forwardNode, + with: input.forwardImage, + action: #selector(onForwardNodeTap), + accessibilityIdentifier: "forwardButton") + } + + private func setup(buttonNode node: ASButtonNode, + with image: UIImage?, + action: Selector, + accessibilityIdentifier: String) { + node.setImage(image, for: .normal) + node.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) + node.contentMode = .center + node.alpha = input.isExpanded ? 1 : 0 + node.addTarget(self, action: action, forControlEvents: .touchUpInside) + node.accessibilityIdentifier = accessibilityIdentifier } private func setupExpandNode() { @@ -99,8 +124,13 @@ public final class ThreadMessageSenderCellNode: CellNode { onReplyTap?(self) } + @objc private func onForwardNodeTap() { + onForwardTap?(self) + } + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { replyNode.style.preferredSize = CGSize(width: 44, height: 44) + forwardNode.style.preferredSize = CGSize(width: 44, height: 44) expandNode.style.preferredSize = CGSize(width: 18, height: 44) let infoNode = ASStackLayoutSpec( @@ -118,7 +148,7 @@ public final class ThreadMessageSenderCellNode: CellNode { spacing: 4, justifyContent: .spaceBetween, alignItems: .start, - children: [infoNode, replyNode, expandNode] + children: [infoNode, replyNode, forwardNode, expandNode] ) let contentSpec: ASStackLayoutSpec From 9d490be88f960ab6a2575b9c7a2a0c10f80fa73e Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 23 Nov 2021 12:18:43 +0200 Subject: [PATCH 2/9] issue #900 add message forwarding functionality --- FlowCrypt.xcodeproj/project.pbxproj | 4 ++ .../xcshareddata/swiftpm/Package.resolved | 8 ++-- .../Compose/ComposeViewController.swift | 4 +- .../Compose/ComposeViewControllerInput.swift | 15 ++++--- .../Compose/ComposeViewDecorator.swift | 2 +- .../Threads/ThreadDetailsViewController.swift | 43 +++++++++---------- .../Model/MessageQuoteType.swift | 24 +++++++++++ .../ComposeMessageService.swift | 2 +- .../ThreadMessageSenderCellNode.swift | 12 +++--- 9 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageQuoteType.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 7d9f449ad..1a629999f 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ 5180CB97273724E9001FC7EF /* ThreadMessageSenderCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB96273724E9001FC7EF /* ThreadMessageSenderCellNode.swift */; }; 518389C82726D7DD00131B2C /* UIViewController+Spinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518389C72726D7DD00131B2C /* UIViewController+Spinner.swift */; }; 518389CA2726D8F700131B2C /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518389C92726D8F700131B2C /* UIApplicationExtension.swift */; }; + 51938DC1274CC291007AD57B /* MessageQuoteType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DC0274CC291007AD57B /* MessageQuoteType.swift */; }; 51B0C7712729861C00124663 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B0C7702729861C00124663 /* String+Extension.swift */; }; 51B0C774272AB61000124663 /* StringTestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B0C773272AB61000124663 /* StringTestExtension.swift */; }; 51B4AE51271444580001F33B /* PubKeyRealmObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B4AE50271444580001F33B /* PubKeyRealmObject.swift */; }; @@ -493,6 +494,7 @@ 5180CB96273724E9001FC7EF /* ThreadMessageSenderCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMessageSenderCellNode.swift; sourceTree = ""; }; 518389C72726D7DD00131B2C /* UIViewController+Spinner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Spinner.swift"; sourceTree = ""; }; 518389C92726D8F700131B2C /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; + 51938DC0274CC291007AD57B /* MessageQuoteType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageQuoteType.swift; sourceTree = ""; }; 51B0C7702729861C00124663 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 51B0C773272AB61000124663 /* StringTestExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringTestExtension.swift; sourceTree = ""; }; 51B4AE50271444580001F33B /* PubKeyRealmObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKeyRealmObject.swift; sourceTree = ""; }; @@ -1560,6 +1562,7 @@ 9F9ABC8623AC1EAA00D560E3 /* MessageContext.swift */, 9FE1B39F2565B0CD00D6D086 /* Message.swift */, 9F5C2A76257D705100DE9B4B /* MessageLabel.swift */, + 51938DC0274CC291007AD57B /* MessageQuoteType.swift */, ); path = Model; sourceTree = ""; @@ -2557,6 +2560,7 @@ 9FDF3654235A218E00614596 /* main.swift in Sources */, D2E26F6C24F25B1F00612AF1 /* KeyAlgo.swift in Sources */, D2891AC424C62446008918E3 /* ErrorHandler.swift in Sources */, + 51938DC1274CC291007AD57B /* MessageQuoteType.swift in Sources */, 9F41FA2F253B7624003B970D /* BackupSelectKeyDecorator.swift in Sources */, D227C0E8250538A90070F805 /* FoldersService.swift in Sources */, 5ADEDCB623A426E300EC495E /* KeyDetailViewController.swift in Sources */, diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0fb96c3e3..0819df7bf 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -87,8 +87,8 @@ "repositoryURL": "https://github.com/realm/realm-cocoa", "state": { "branch": null, - "revision": "328425bfc372ce77ec1f4f2701f61ececbb97d84", - "version": "10.19.0" + "revision": "bdbbd57f411a0f4e72b359113dbc6d23fdf96680", + "version": "10.20.0" } }, { @@ -96,8 +96,8 @@ "repositoryURL": "https://github.com/realm/realm-core", "state": { "branch": null, - "revision": "b170db6a47789ff5f2fbc3eeed0220b4b0a3f6b7", - "version": "11.6.0" + "revision": "c3c11a841642ac93c27bd1edd61f989fc0bfb809", + "version": "11.6.1" } }, { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 0b29e2e45..4fe601641 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -240,7 +240,7 @@ extension ComposeViewController { } private func setupReply() { - guard input.isQuote, let email = input.recipientReplyTitle else { return } + guard input.isQuote, let email = input.recipientQuoteTitle else { return } let recipient = ComposeMessageRecipient(email: email, state: decorator.recipientIdleState) contextToSend.recipients.append(recipient) @@ -536,7 +536,7 @@ extension ComposeViewController { return true } .then { - let subject = input.isQuote ? input.subjectReplyTitle : contextToSend.subject + let subject = input.isQuote ? input.subjectQuoteTitle : contextToSend.subject $0.attributedText = decorator.styledTitle(with: subject) } } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index f5332d540..9d913de82 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -35,22 +35,25 @@ struct ComposeMessageInput: Equatable { } } - var recipientReplyTitle: String? { + var recipientQuoteTitle: String? { guard case let .quote(info) = type else { return nil } return info.recipient } - var subjectReplyTitle: String? { + var subjectQuoteTitle: String? { guard case let .quote(info) = type else { return nil } - return "Re: \(info.subject ?? "(no subject)")" + return info.subject } var successfullySentToast: String { switch type { case .idle: return "compose_encrypted_sent".localized - case .quote: return "compose_reply_successful".localized - // TODO - // case .forward: return "compose_forward_successful".localized + case .quote(let info): + if info.recipient == nil { + return "compose_forward_successful".localized + } else { + return "compose_reply_successful".localized + } } } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 5469dcaff..a2b8c794f 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -85,7 +85,7 @@ struct ComposeViewDecorator { dateFormatter.timeStyle = .short let time = dateFormatter.string(from: info.sentDate) - let from = info.recipient ?? "unknown sender" + let from = info.sender ?? "unknown sender" let text: String = "\n\n" + "compose_reply_from".localizeWithArguments(date, time, from) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 68ea1f712..d23268b8a 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -102,7 +102,9 @@ extension ThreadDetailsViewController { UIView.animate( withDuration: 0.3, animations: { - threadNode.replyNode.view.alpha = self.input[indexPath.section-1].isExpanded ? 0 : 1 + let isExpanded = self.input[indexPath.section-1].isExpanded + threadNode.replyNode.view.alpha = isExpanded ? 0 : 1 + threadNode.forwardNode.view.alpha = isExpanded ? 0 : 1 threadNode.expandNode.view.transform = CGAffineTransform(rotationAngle: .pi) }, completion: { [weak self] _ in @@ -118,38 +120,33 @@ extension ThreadDetailsViewController { } private func handleReplyTap(at indexPath: IndexPath) { - guard let email = DataService.shared.email, - let input = input[safe: indexPath.section-1], - let processedMessage = input.processedMessage - else { return } - - let replyInfo = ComposeMessageInput.MessageQuoteInfo( - recipient: input.rawMessage.sender, - sender: input.rawMessage.sender, - subject: input.rawMessage.subject, - mime: processedMessage.rawMimeData, - sentDate: input.rawMessage.date, - message: processedMessage.text, - threadId: input.rawMessage.threadId - ) - - let composeInput = ComposeMessageInput(type: .quote(replyInfo)) - navigationController?.pushViewController( - ComposeViewController(email: email, input: composeInput), - animated: true - ) + composeNewMessage(at: indexPath, quoteType: .reply) } private func handleForwardTap(at indexPath: IndexPath) { + composeNewMessage(at: indexPath, quoteType: .forward) + } + + private func composeNewMessage(at indexPath: IndexPath, quoteType: MessageQuoteType) { guard let email = DataService.shared.email, let input = input[safe: indexPath.section-1], let processedMessage = input.processedMessage else { return } + let recipient: String? + switch quoteType { + case .reply: + recipient = input.rawMessage.sender + case .forward: + recipient = nil + } + + let subject = input.rawMessage.subject ?? "(no subject)" + let replyInfo = ComposeMessageInput.MessageQuoteInfo( - recipient: nil, + recipient: recipient, sender: input.rawMessage.sender, - subject: input.rawMessage.subject, + subject: "\(quoteType.subjectPrefix)\(subject)", mime: processedMessage.rawMimeData, sentDate: input.rawMessage.date, message: processedMessage.text, diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageQuoteType.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageQuoteType.swift new file mode 100644 index 000000000..7a91fc4b8 --- /dev/null +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/MessageQuoteType.swift @@ -0,0 +1,24 @@ +// +// MessageQuoteType.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 23/11/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +enum MessageQuoteType { + case reply, forward +} + +extension MessageQuoteType { + var subjectPrefix: String { + switch self { + case .reply: + return "Re: " + case .forward: + return "Fwd: " + } + } +} diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 4d5bb5926..12caf68e4 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -86,7 +86,7 @@ final class ComposeMessageService { throw MessageValidationError.emptyMessage } - let subject = input.subjectReplyTitle + let subject = input.subjectQuoteTitle ?? contextToSend.subject ?? "(no subject)" diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift index 8f2f288b4..e1908560f 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -36,10 +36,10 @@ public final class ThreadMessageSenderCellNode: CellNode { } var replyImage: UIImage? { - return createButtonImage(systemName: "arrowshape.turn.up.left") + return createButtonImage(systemName: "arrow.uturn.backward") } var forwardImage: UIImage? { - return createButtonImage(systemName: "arrowshape.turn.up.right") + return createButtonImage(systemName: "arrow.uturn.forward") } var expandImage: UIImage? { let systemName = isExpanded ? "chevron.up" : "chevron.down" @@ -129,9 +129,9 @@ public final class ThreadMessageSenderCellNode: CellNode { } public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { - replyNode.style.preferredSize = CGSize(width: 44, height: 44) - forwardNode.style.preferredSize = CGSize(width: 44, height: 44) - expandNode.style.preferredSize = CGSize(width: 18, height: 44) + replyNode.style.preferredSize = CGSize(width: 36, height: 44) + forwardNode.style.preferredSize = CGSize(width: 36, height: 44) + expandNode.style.preferredSize = CGSize(width: 20, height: 44) let infoNode = ASStackLayoutSpec( direction: .vertical, @@ -145,7 +145,7 @@ public final class ThreadMessageSenderCellNode: CellNode { let senderSpec = ASStackLayoutSpec( direction: .horizontal, - spacing: 4, + spacing: 2, justifyContent: .spaceBetween, alignItems: .start, children: [infoNode, replyNode, forwardNode, expandNode] From 630e28c5ef1ca99d6a80b9d9616627876925e9d0 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 23 Nov 2021 12:55:13 +0200 Subject: [PATCH 3/9] issue #900 update naming --- .../Compose/ComposeViewController.swift | 14 +++++++------- .../Controllers/Compose/ComposeViewDecorator.swift | 4 ++-- FlowCrypt/Resources/en.lproj/Localizable.strings | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 4fe601641..90f8196f3 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -19,7 +19,7 @@ private struct ComposedDraft: Equatable { /** * View controller to compose the message and send it * - User can be redirected here from *InboxViewController* by tapping on *+* - * - Or from *ThreadDetailsViewController* controller by tapping on *reply* + * - Or from *ThreadDetailsViewController* controller by tapping on *reply* or *forward* **/ final class ComposeViewController: TableNodeViewController { private lazy var logger = Logger.nested(Self.self) @@ -114,7 +114,7 @@ final class ComposeViewController: TableNodeViewController { setupNavigationBar() observeKeyboardNotifications() observerAppStates() - setupReply() + setupQuote() } override func viewWillDisappear(_ animated: Bool) { @@ -239,7 +239,7 @@ extension ComposeViewController { } } - private func setupReply() { + private func setupQuote() { guard input.isQuote, let email = input.recipientQuoteTitle else { return } let recipient = ComposeMessageRecipient(email: email, state: decorator.recipientIdleState) @@ -542,8 +542,8 @@ extension ComposeViewController { } private func textNode() -> ASCellNode { - let replyQuote = decorator.styledReplyQuote(with: input) - let height = max(decorator.frame(for: replyQuote).height, 40) + let styledQuote = decorator.styledQuote(with: input) + let height = max(decorator.frame(for: styledQuote).height, 40) return TextViewCellNode( decorator.styledTextViewInput(with: height) @@ -558,9 +558,9 @@ extension ComposeViewController { .then { let messageText = decorator.styledMessage(with: contextToSend.message ?? "") - if input.isQuote && !messageText.string.contains(replyQuote.string) { + if input.isQuote && !messageText.string.contains(styledQuote.string) { let mutableString = NSMutableAttributedString(attributedString: messageText) - mutableString.append(replyQuote) + mutableString.append(styledQuote) $0.textView.attributedText = mutableString $0.becomeFirstResponder() } else { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index a2b8c794f..f8f09cd65 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -72,7 +72,7 @@ struct ComposeViewDecorator { text.attributed(.regular(17)) } - func styledReplyQuote(with input: ComposeMessageInput) -> NSAttributedString { + func styledQuote(with input: ComposeMessageInput) -> NSAttributedString { guard case let .quote(info) = input.type else { return NSAttributedString(string: "") } let dateFormatter = DateFormatter() @@ -88,7 +88,7 @@ struct ComposeViewDecorator { let from = info.sender ?? "unknown sender" let text: String = "\n\n" - + "compose_reply_from".localizeWithArguments(date, time, from) + + "compose_quote_from".localizeWithArguments(date, time, from) + "\n" let message = " > " + info.message.replacingOccurrences(of: "\n", with: "\n > ") diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index fbcc13444..1f85d5618 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -77,7 +77,7 @@ "compose_recipient_invalid_email" = "One or more of your recipients have invalid email address (marked in red)"; "compose_error" = "Could not compose message"; "compose_reply_successful" = "Reply successfully sent"; -"compose_reply_from" = "On %@ at %@ %@ wrote:"; // Date, time, sender +"compose_quote_from" = "On %@ at %@ %@ wrote:"; // Date, time, sender "compose_forward_successful" = "Message forwarded successfully"; "compose_encrypted_sent" = "Encrypted message sent"; "compose_enter_subject" = "Enter subject"; From 935340d7c6946f28a1d52d5b674617ee02a1d1f0 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 23 Nov 2021 15:01:48 +0200 Subject: [PATCH 4/9] issue #900 support multiple recipients --- .../Controllers/Compose/ComposeViewController.swift | 10 ++++++---- .../Compose/ComposeViewControllerInput.swift | 10 +++++----- .../Threads/ThreadDetailsViewController.swift | 8 ++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 90f8196f3..aa1a50db4 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -240,11 +240,13 @@ extension ComposeViewController { } private func setupQuote() { - guard input.isQuote, let email = input.recipientQuoteTitle else { return } + guard input.isQuote else { return } - let recipient = ComposeMessageRecipient(email: email, state: decorator.recipientIdleState) - contextToSend.recipients.append(recipient) - evaluate(recipient: recipient) + input.quoteRecipients.forEach { email in + let recipient = ComposeMessageRecipient(email: email, state: decorator.recipientIdleState) + contextToSend.recipients.append(recipient) + evaluate(recipient: recipient) + } } } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index 9d913de82..c430191e2 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 recipient: String? + let recipients: [String] let sender: String? let subject: String? let mime: Data? @@ -35,9 +35,9 @@ struct ComposeMessageInput: Equatable { } } - var recipientQuoteTitle: String? { - guard case let .quote(info) = type else { return nil } - return info.recipient + var quoteRecipients: [String] { + guard case let .quote(info) = type else { return [] } + return info.recipients } var subjectQuoteTitle: String? { @@ -49,7 +49,7 @@ struct ComposeMessageInput: Equatable { switch type { case .idle: return "compose_encrypted_sent".localized case .quote(let info): - if info.recipient == nil { + if info.recipients.isEmpty { return "compose_forward_successful".localized } else { return "compose_reply_successful".localized diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index d23268b8a..b8a00f09b 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -133,18 +133,18 @@ extension ThreadDetailsViewController { let processedMessage = input.processedMessage else { return } - let recipient: String? + let recipients: [String] switch quoteType { case .reply: - recipient = input.rawMessage.sender + recipients = [input.rawMessage.sender].compactMap { $0 } case .forward: - recipient = nil + recipients = [] } let subject = input.rawMessage.subject ?? "(no subject)" let replyInfo = ComposeMessageInput.MessageQuoteInfo( - recipient: recipient, + recipients: recipients, sender: input.rawMessage.sender, subject: "\(quoteType.subjectPrefix)\(subject)", mime: processedMessage.rawMimeData, From cbb976a58553e188b3cb693339b75c3e3ea0600b Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 23 Nov 2021 22:56:01 +0200 Subject: [PATCH 5/9] issue #900 added ui test for forward functionality --- .../Cell Nodes/RecipientEmailsCellNode.swift | 1 + appium/tests/screenobjects/email.screen.ts | 9 +++++++++ .../tests/screenobjects/new-message.screen.ts | 18 +++++++++++++++++- ...ckReplyAndForwardForEncryptedEmail.spec.ts} | 11 ++++++++--- 4 files changed, 35 insertions(+), 4 deletions(-) rename appium/tests/specs/inbox/{CheckReplyForEncryptedEmail.spec.ts => CheckReplyAndForwardForEncryptedEmail.spec.ts} (65%) diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index 12408f06f..6addc6928 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -31,6 +31,7 @@ final public class RecipientEmailsCellNode: CellNode { layout.minimumLineSpacing = Constants.minimumLineSpacing layout.sectionInset = Constants.sectionInset let collectionNode = ASCollectionNode(collectionViewLayout: layout) + collectionNode.accessibilityIdentifier = "recipientsList" collectionNode.backgroundColor = .clear return collectionNode }() diff --git a/appium/tests/screenobjects/email.screen.ts b/appium/tests/screenobjects/email.screen.ts index 73c6b9b7b..b94a3feeb 100644 --- a/appium/tests/screenobjects/email.screen.ts +++ b/appium/tests/screenobjects/email.screen.ts @@ -9,6 +9,7 @@ const SELECTORS = { WRONG_PASS_PHRASE_MESSAGE: '-ios class chain:**/XCUIElementTypeStaticText[`label == "Wrong pass phrase, please try again"`]', DOWNLOAD_ATTACHMENT_BUTTON: '~downloadButton', REPLY_BUTTON: '~replyButton', + FORWARD_BUTTON: '~forwardButton', DELETE_BUTTON: '~Delete', CONFIRM_DELETING: '~OK' }; @@ -43,6 +44,10 @@ class EmailScreen extends BaseScreen { return $(SELECTORS.REPLY_BUTTON); } + get forwardButton() { + return $(SELECTORS.FORWARD_BUTTON); + } + get deleteButton() { return $(SELECTORS.DELETE_BUTTON) } @@ -108,6 +113,10 @@ class EmailScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.replyButton); } + clickForwardButton = async () => { + await ElementHelper.waitAndClick(await this.forwardButton); + } + clickDeleteButton = async () => { await ElementHelper.waitAndClick(await this.deleteButton); } diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index e50874e41..496c72c1f 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -5,6 +5,7 @@ const SELECTORS = { ADD_RECIPIENT_FIELD: '-ios class chain:**/XCUIElementTypeTextField[`value == "Add Recipient"`]', SUBJECT_FIELD: '-ios class chain:**/XCUIElementTypeTextField[`value == "Subject"`]', COMPOSE_SECURITY_MESSAGE: '-ios predicate string:type == "XCUIElementTypeTextView"', + RECIPIENTS_LIST: '~recipientsList', ADDED_RECIPIENT: '-ios class chain:**/XCUIElementTypeWindow[1]/XCUIElementTypeOther/XCUIElementTypeOther' + '/XCUIElementTypeOther/XCUIElementTypeOther[1]/XCUIElementTypeOther/XCUIElementTypeTable' + '/XCUIElementTypeCell[1]/XCUIElementTypeOther/XCUIElementTypeCollectionView/XCUIElementTypeCell' + @@ -33,6 +34,10 @@ class NewMessageScreen extends BaseScreen { return $(SELECTORS.COMPOSE_SECURITY_MESSAGE); } + get recipientsList() { + return $(SELECTORS.RECIPIENTS_LIST); + } + get addedRecipientEmail() { return $(SELECTORS.ADDED_RECIPIENT); } @@ -87,9 +92,20 @@ class NewMessageScreen extends BaseScreen { expect(this.composeSecurityMessage).toHaveText(message); const element = await this.filledSubject(subject); await element.waitForDisplayed(); - await this.checkAddedRecipient(recipient); + + if (recipient.length === 0) { + await this.checkEmptyRecipientsList(); + } else { + await this.checkAddedRecipient(recipient); + } }; + checkEmptyRecipientsList = async () => { + const list = await this.recipientsList; + const listText = await list.getText(); + expect(listText.length).toEqual(0); + } + checkAddedRecipient = async (recipient: string) => { const addedRecipientEl = await this.addedRecipientEmail; const value = await addedRecipientEl.getValue(); diff --git a/appium/tests/specs/inbox/CheckReplyForEncryptedEmail.spec.ts b/appium/tests/specs/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts similarity index 65% rename from appium/tests/specs/inbox/CheckReplyForEncryptedEmail.spec.ts rename to appium/tests/specs/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts index 4950c81e8..0754d62eb 100644 --- a/appium/tests/specs/inbox/CheckReplyForEncryptedEmail.spec.ts +++ b/appium/tests/specs/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts @@ -10,14 +10,15 @@ import { CommonData } from '../../data'; describe('INBOX: ', () => { - it('user is able to reply email and check info from reply email', async () => { + it('user is able to reply or forward email and check info from composed email', async () => { const senderEmail = CommonData.sender.email; const emailSubject = CommonData.encryptedEmail.subject; const emailText = CommonData.encryptedEmail.message; const replySubject = `Re: ${emailSubject}`; - const replyText = `On 10/26/21 at 2:43 PM ${senderEmail} wrote:\n > ${emailText}`; + const forwardSubject = `Fwd: ${emailSubject}`; + const quoteText = `On 10/26/21 at 2:43 PM ${senderEmail} wrote:\n > ${emailText}`; await SplashScreen.login(); await SetupKeyScreen.setPassPhrase(); @@ -27,6 +28,10 @@ describe('INBOX: ', () => { await EmailScreen.checkOpenedEmail(senderEmail, emailSubject, emailText); await EmailScreen.clickReplyButton(); - await NewMessageScreen.checkFilledComposeEmailInfo(senderEmail, replySubject, replyText); + await NewMessageScreen.checkFilledComposeEmailInfo(senderEmail, replySubject, quoteText); + + await NewMessageScreen.clickBackButton(); + await EmailScreen.clickForwardButton(); + await NewMessageScreen.checkFilledComposeEmailInfo("", forwardSubject, quoteText); }); }); From 3d586d602bf6a0066d43caf6e5f4da1b50c2e5c3 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 24 Nov 2021 13:21:15 +0200 Subject: [PATCH 6/9] issue #900 add menu button to messages --- .../Controllers/Inbox/InboxProviders.swift | 2 +- .../ContactKeyDetailDecorator.swift | 2 +- .../ContactKeyDetailViewController.swift | 2 +- .../Controllers/Threads/MessageAction.swift | 1 - .../Threads/ThreadDetailsDecorator.swift | 3 +- .../Threads/ThreadDetailsViewController.swift | 19 +++-- .../Mail Provider/Imap/Imap+retry.swift | 3 +- .../PassPhraseService.swift | 4 +- .../Resources/en.lproj/Localizable.strings | 1 + .../ThreadMessageSenderCellNode.swift | 85 +++++++++---------- 10 files changed, 62 insertions(+), 60 deletions(-) diff --git a/FlowCrypt/Controllers/Inbox/InboxProviders.swift b/FlowCrypt/Controllers/Inbox/InboxProviders.swift index a3c256c16..15cfd372b 100644 --- a/FlowCrypt/Controllers/Inbox/InboxProviders.swift +++ b/FlowCrypt/Controllers/Inbox/InboxProviders.swift @@ -5,7 +5,7 @@ // Created by Anton Kharchevskyi on 11.10.2021 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - + import Foundation struct InboxContext { diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift index a80651ea4..1fababdb2 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift @@ -5,7 +5,7 @@ // Created by Roma Sosnovsky on 13/10/21 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - + import FlowCryptUI import Foundation diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailViewController.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailViewController.swift index 7f258598a..cb7c88caf 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailViewController.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailViewController.swift @@ -5,7 +5,7 @@ // Created by Roma Sosnovsky on 13/10/21 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - + import AsyncDisplayKit import FlowCryptCommon import FlowCryptUI diff --git a/FlowCrypt/Controllers/Threads/MessageAction.swift b/FlowCrypt/Controllers/Threads/MessageAction.swift index ef5fb2074..0506bacd3 100644 --- a/FlowCrypt/Controllers/Threads/MessageAction.swift +++ b/FlowCrypt/Controllers/Threads/MessageAction.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - import Foundation typealias MessageActionCompletion = (MessageAction, InboxRenderable) -> Void diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift index bf914053c..3a8e6221b 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift @@ -33,7 +33,8 @@ extension ThreadMessageSenderCellNode.Input { sender: NSAttributedString.text(from: sender, style: style, color: textColor), date: NSAttributedString.text(from: date, style: style, color: dateColor), isExpanded: threadMessage.isExpanded, - buttonColor: .messageButtonColor + buttonColor: .messageButtonColor, + nodeInsets: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 8) ) } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index b8a00f09b..db23965ff 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -103,9 +103,7 @@ extension ThreadDetailsViewController { withDuration: 0.3, animations: { let isExpanded = self.input[indexPath.section-1].isExpanded - threadNode.replyNode.view.alpha = isExpanded ? 0 : 1 - threadNode.forwardNode.view.alpha = isExpanded ? 0 : 1 - threadNode.expandNode.view.transform = CGAffineTransform(rotationAngle: .pi) + threadNode.expandNode.view.alpha = isExpanded ? 1 : 0 }, completion: { [weak self] _ in guard let self = self else { return } @@ -123,8 +121,17 @@ extension ThreadDetailsViewController { composeNewMessage(at: indexPath, quoteType: .reply) } - private func handleForwardTap(at indexPath: IndexPath) { - composeNewMessage(at: indexPath, quoteType: .forward) + private func handleMenuTap(at indexPath: IndexPath) { + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alert.addAction( + UIAlertAction( + title: "forward".localized, + style: .default) { [weak self] _ in + self?.composeNewMessage(at: indexPath, quoteType: .forward) + } + ) + alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel)) + present(alert, animated: true, completion: nil) } private func composeNewMessage(at indexPath: IndexPath, quoteType: MessageQuoteType) { @@ -419,7 +426,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { return ThreadMessageSenderCellNode( input: .init(threadMessage: section), onReplyTap: { [weak self] _ in self?.handleReplyTap(at: indexPath) }, - onForwardTap: { [weak self] _ in self?.handleForwardTap(at: indexPath) } + onMenuTap: { [weak self] _ in self?.handleMenuTap(at: indexPath) } ) } diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift index d450a2fc6..75f5c8274 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+retry.swift @@ -109,7 +109,8 @@ extension Imap { try await renewSession() // todo - log time return true case .connection: - // the connection has dropped, so it's probably ok to not officially "close" it. but maybe there could be a cleaner way to dispose of the connection? + // the connection has dropped, so it's probably ok to not officially "close" it. + // but maybe there could be a cleaner way to dispose of the connection? imapSess = nil smtpSess = nil // this is a mess, neads a real refactor. use DI diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift index eb29f1380..7fecbb17a 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift @@ -30,7 +30,9 @@ struct PassPhrase: Codable, Hashable, Equatable { } // (tom) todo - this is a confusing thing to do - // when comparing pass phrases to one another, you would expect that it's compared by the pass phrase string itself, and not by primary fingerprint of the associated key. I understand this is being used somewhere, but I suggest to refactor it to avoid defining this == overload. + // when comparing pass phrases to one another, you would expect that it's compared by the pass phrase string + // itself, and not by primary fingerprint of the associated key. I understand this is being used somewhere, + // but I suggest to refactor it to avoid defining this == overload. static func == (lhs: PassPhrase, rhs: PassPhrase) -> Bool { lhs.primaryFingerprintOfAssociatedKey == rhs.primaryFingerprintOfAssociatedKey } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 29502b152..b04e574a5 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -14,6 +14,7 @@ "error" = "Error"; "allow" = "Allow"; "later" = "Later"; +"forward" = "Forward"; // EMAIL "email_removed" = "Email moved to Trash"; diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift index e1908560f..41237f1d5 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -25,7 +25,7 @@ public final class ThreadMessageSenderCellNode: CellNode { date: NSAttributedString, isExpanded: Bool, buttonColor: UIColor, - nodeInsets: UIEdgeInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 12)) { + nodeInsets: UIEdgeInsets) { self.encryptionBadge = encryptionBadge self.signatureBadge = signatureBadge self.sender = sender @@ -35,18 +35,11 @@ public final class ThreadMessageSenderCellNode: CellNode { self.nodeInsets = nodeInsets } - var replyImage: UIImage? { - return createButtonImage(systemName: "arrow.uturn.backward") - } - var forwardImage: UIImage? { - return createButtonImage(systemName: "arrow.uturn.forward") - } - var expandImage: UIImage? { - let systemName = isExpanded ? "chevron.up" : "chevron.down" - return createButtonImage(systemName: systemName) - } + var replyImage: UIImage? { createButtonImage("arrow.turn.up.left") } + var menuImage: UIImage? { createButtonImage("ellipsis") } + var expandImage: UIImage? { createButtonImage("chevron.down") } - private func createButtonImage(systemName: String, pointSize: CGFloat = 18) -> UIImage? { + private func createButtonImage(_ systemName: String, pointSize: CGFloat = 18) -> UIImage? { let configuration = UIImage.SymbolConfiguration(pointSize: pointSize) return UIImage(systemName: systemName, withConfiguration: configuration) } @@ -64,19 +57,19 @@ public final class ThreadMessageSenderCellNode: CellNode { private let dateNode = ASTextNode2() public private(set) var replyNode = ASButtonNode() - public private(set) var forwardNode = ASButtonNode() + public private(set) var menuNode = ASButtonNode() public private(set) var expandNode = ASImageNode() private let input: ThreadMessageSenderCellNode.Input private var onReplyTap: ((ThreadMessageSenderCellNode) -> Void)? - private var onForwardTap: ((ThreadMessageSenderCellNode) -> Void)? + private var onMenuTap: ((ThreadMessageSenderCellNode) -> Void)? public init(input: ThreadMessageSenderCellNode.Input, onReplyTap: ((ThreadMessageSenderCellNode) -> Void)?, - onForwardTap: ((ThreadMessageSenderCellNode) -> Void)?) { + onMenuTap: ((ThreadMessageSenderCellNode) -> Void)?) { self.input = input self.onReplyTap = onReplyTap - self.onForwardTap = onForwardTap + self.onMenuTap = onMenuTap super.init() automaticallyManagesSubnodes = true @@ -84,7 +77,7 @@ public final class ThreadMessageSenderCellNode: CellNode { dateNode.attributedText = input.date setupReplyNode() - setupForwardNode() + setupMenuNode() setupExpandNode() } @@ -95,11 +88,11 @@ public final class ThreadMessageSenderCellNode: CellNode { accessibilityIdentifier: "replyButton") } - private func setupForwardNode() { - setup(buttonNode: forwardNode, - with: input.forwardImage, - action: #selector(onForwardNodeTap), - accessibilityIdentifier: "forwardButton") + private func setupMenuNode() { + setup(buttonNode: menuNode, + with: input.menuImage, + action: #selector(onMenuNodeTap), + accessibilityIdentifier: "messageMenuButton") } private func setup(buttonNode node: ASButtonNode, @@ -108,8 +101,6 @@ public final class ThreadMessageSenderCellNode: CellNode { accessibilityIdentifier: String) { node.setImage(image, for: .normal) node.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) - node.contentMode = .center - node.alpha = input.isExpanded ? 1 : 0 node.addTarget(self, action: action, forControlEvents: .touchUpInside) node.accessibilityIdentifier = accessibilityIdentifier } @@ -117,21 +108,21 @@ public final class ThreadMessageSenderCellNode: CellNode { private func setupExpandNode() { expandNode.image = input.expandImage expandNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) - expandNode.contentMode = .right + expandNode.contentMode = .center } @objc private func onReplyNodeTap() { onReplyTap?(self) } - @objc private func onForwardNodeTap() { - onForwardTap?(self) + @objc private func onMenuNodeTap() { + onMenuTap?(self) } public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { - replyNode.style.preferredSize = CGSize(width: 36, height: 44) - forwardNode.style.preferredSize = CGSize(width: 36, height: 44) - expandNode.style.preferredSize = CGSize(width: 20, height: 44) + replyNode.style.preferredSize = CGSize(width: 44, height: 44) + menuNode.style.preferredSize = CGSize(width: 36, height: 44) + expandNode.style.preferredSize = CGSize(width: 36, height: 44) let infoNode = ASStackLayoutSpec( direction: .vertical, @@ -143,32 +134,26 @@ public final class ThreadMessageSenderCellNode: CellNode { infoNode.style.flexGrow = 1 infoNode.style.flexShrink = 1 - let senderSpec = ASStackLayoutSpec( - direction: .horizontal, - spacing: 2, - justifyContent: .spaceBetween, - alignItems: .start, - children: [infoNode, replyNode, forwardNode, expandNode] - ) - let contentSpec: ASStackLayoutSpec + if input.isExpanded { + let senderSpec = ASStackLayoutSpec( + direction: .horizontal, + spacing: 2, + justifyContent: .spaceBetween, + alignItems: .start, + children: [infoNode, replyNode, menuNode] + ) + let spacer = ASLayoutSpec() spacer.style.flexGrow = 1.0 - var children: [ASLayoutElement] = [] - children.append(encryptionNode) - if let signatureNode = signatureNode { - children.append(signatureNode) - } - children.append(spacer) - let signatureSpec = ASStackLayoutSpec( direction: .horizontal, spacing: 4, justifyContent: .spaceBetween, alignItems: .start, - children: children + children: [encryptionNode, signatureNode, spacer].compactMap { $0 } ) contentSpec = ASStackLayoutSpec( @@ -179,7 +164,13 @@ public final class ThreadMessageSenderCellNode: CellNode { children: [senderSpec, signatureSpec] ) } else { - contentSpec = senderSpec + contentSpec = ASStackLayoutSpec( + direction: .horizontal, + spacing: 4, + justifyContent: .spaceBetween, + alignItems: .start, + children: [infoNode, expandNode] + ) } return ASInsetLayoutSpec( From e86d88b62f2cd933fc619692f160e93c0a4b610d Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 24 Nov 2021 14:01:56 +0200 Subject: [PATCH 7/9] issue #900 improve messages expand/collapse --- .../Threads/ThreadDetailsViewController.swift | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index db23965ff..f7e3b6663 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -99,22 +99,29 @@ extension ThreadDetailsViewController { return } - UIView.animate( - withDuration: 0.3, - animations: { - let isExpanded = self.input[indexPath.section-1].isExpanded - threadNode.expandNode.view.alpha = isExpanded ? 1 : 0 - }, - completion: { [weak self] _ in - guard let self = self else { return } + input[indexPath.section - 1].isExpanded.toggle() - if let processedMessage = self.input[indexPath.section-1].processedMessage { - self.handleReceived(message: processedMessage, at: indexPath) - } else { - self.fetchDecryptAndRenderMsg(at: indexPath) + if input[indexPath.section-1].isExpanded { + UIView.animate( + withDuration: 0.3, + animations: { + threadNode.expandNode.view.alpha = 0 + }, + completion: { [weak self] _ in + guard let self = self else { return } + + if let processedMessage = self.input[indexPath.section-1].processedMessage { + self.handleReceived(message: processedMessage, at: indexPath) + } else { + self.fetchDecryptAndRenderMsg(at: indexPath) + } } + ) + } else { + UIView.animate(withDuration: 0.3) { + self.node.reloadSections(IndexSet(integer: indexPath.section), with: .automatic) } - ) + } } private func handleReplyTap(at indexPath: IndexPath) { From f9e4391c215f72c28ae59ffe079fc954f81bafaa Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 24 Nov 2021 15:31:13 +0200 Subject: [PATCH 8/9] issue #900 add thread messages separator --- .../Threads/ThreadDetailsViewController.swift | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index f7e3b6663..540cb7c95 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -414,8 +414,8 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { guard section > 0, input[section-1].isExpanded else { return 1 } - let count = input[section-1].processedMessage?.attachments.count ?? 0 - return Parts.allCases.count + count + let attachmentsCount = input[section-1].processedMessage?.attachments.count ?? 0 + return Parts.allCases.count + attachmentsCount } func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { @@ -461,6 +461,24 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } handleExpandTap(at: indexPath) } + + 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 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 + } + } } extension ThreadDetailsViewController: NavigationChildController { From 48d9db146148a95f167d3e5a09bb92ea77b4696d Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 24 Nov 2021 15:54:42 +0200 Subject: [PATCH 9/9] issue #900 update ui test --- appium/tests/screenobjects/email.screen.ts | 11 ++++++++++- .../CheckReplyAndForwardForEncryptedEmail.spec.ts | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/appium/tests/screenobjects/email.screen.ts b/appium/tests/screenobjects/email.screen.ts index b94a3feeb..e29b4a803 100644 --- a/appium/tests/screenobjects/email.screen.ts +++ b/appium/tests/screenobjects/email.screen.ts @@ -9,7 +9,8 @@ const SELECTORS = { WRONG_PASS_PHRASE_MESSAGE: '-ios class chain:**/XCUIElementTypeStaticText[`label == "Wrong pass phrase, please try again"`]', DOWNLOAD_ATTACHMENT_BUTTON: '~downloadButton', REPLY_BUTTON: '~replyButton', - FORWARD_BUTTON: '~forwardButton', + MENU_BUTTON: '~messageMenuButton', + FORWARD_BUTTON: '~Forward', DELETE_BUTTON: '~Delete', CONFIRM_DELETING: '~OK' }; @@ -44,6 +45,10 @@ class EmailScreen extends BaseScreen { return $(SELECTORS.REPLY_BUTTON); } + get menuButton() { + return $(SELECTORS.MENU_BUTTON); + } + get forwardButton() { return $(SELECTORS.FORWARD_BUTTON); } @@ -113,6 +118,10 @@ class EmailScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.replyButton); } + clickMenuButton = async () => { + await ElementHelper.waitAndClick(await this.menuButton); + } + clickForwardButton = async () => { await ElementHelper.waitAndClick(await this.forwardButton); } diff --git a/appium/tests/specs/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts b/appium/tests/specs/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts index 0754d62eb..786b51218 100644 --- a/appium/tests/specs/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts +++ b/appium/tests/specs/inbox/CheckReplyAndForwardForEncryptedEmail.spec.ts @@ -31,6 +31,7 @@ describe('INBOX: ', () => { await NewMessageScreen.checkFilledComposeEmailInfo(senderEmail, replySubject, quoteText); await NewMessageScreen.clickBackButton(); + await EmailScreen.clickMenuButton(); await EmailScreen.clickForwardButton(); await NewMessageScreen.checkFilledComposeEmailInfo("", forwardSubject, quoteText); });