diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 8361b6332..839d836d0 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -68,7 +68,7 @@ 5180CB9127356D48001FC7EF /* MessageSubjectNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */; }; 5180CB9327357B67001FC7EF /* RawClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9227357B67001FC7EF /* RawClientConfiguration.swift */; }; 5180CB9527357BB0001FC7EF /* WkdApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9427357BB0001FC7EF /* WkdApi.swift */; }; - 5180CB97273724E9001FC7EF /* ThreadMessageSenderCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB96273724E9001FC7EF /* ThreadMessageSenderCellNode.swift */; }; + 5180CB97273724E9001FC7EF /* ThreadMessageInfoCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB96273724E9001FC7EF /* ThreadMessageInfoCellNode.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 */; }; @@ -76,6 +76,7 @@ 51B0C774272AB61000124663 /* StringTestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B0C773272AB61000124663 /* StringTestExtension.swift */; }; 51B4AE51271444580001F33B /* PubKeyRealmObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B4AE50271444580001F33B /* PubKeyRealmObject.swift */; }; 51B4AE5327144E590001F33B /* PubKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B4AE5227144E590001F33B /* PubKey.swift */; }; + 51B9EE6F27567B520080B2D5 /* MessageRecipientsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */; }; 51C0C1EF271982A1000C9738 /* MailCore in Frameworks */ = {isa = PBXBuildFile; productRef = 51C0C1EE271982A1000C9738 /* MailCore */; }; 51C0C1F2271987DB000C9738 /* Toast in Frameworks */ = {isa = PBXBuildFile; productRef = 51C0C1F1271987DB000C9738 /* Toast */; }; 51DA5BD62721AB07001C4359 /* PubKeyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DA5BD52721AB07001C4359 /* PubKeyState.swift */; }; @@ -493,7 +494,7 @@ 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSubjectNode.swift; sourceTree = ""; }; 5180CB9227357B67001FC7EF /* RawClientConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawClientConfiguration.swift; sourceTree = ""; }; 5180CB9427357BB0001FC7EF /* WkdApi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WkdApi.swift; sourceTree = ""; }; - 5180CB96273724E9001FC7EF /* ThreadMessageSenderCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMessageSenderCellNode.swift; sourceTree = ""; }; + 5180CB96273724E9001FC7EF /* ThreadMessageInfoCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMessageInfoCellNode.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 = ""; }; @@ -501,6 +502,7 @@ 51B0C773272AB61000124663 /* StringTestExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringTestExtension.swift; sourceTree = ""; }; 51B4AE50271444580001F33B /* PubKeyRealmObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKeyRealmObject.swift; sourceTree = ""; }; 51B4AE5227144E590001F33B /* PubKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKey.swift; sourceTree = ""; }; + 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRecipientsNode.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 = ""; }; @@ -1984,7 +1986,7 @@ 5A39F433239EC61C001F4607 /* TitleCellNode.swift */, 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */, 9F82779D23737E3800E19C07 /* MessageTextSubjectNode.swift */, - 5180CB96273724E9001FC7EF /* ThreadMessageSenderCellNode.swift */, + 5180CB96273724E9001FC7EF /* ThreadMessageInfoCellNode.swift */, 9F56BD3123438B5B00A7371A /* InboxCellNode.swift */, 9F56BD3523438B9D00A7371A /* TextCellNode.swift */, D24ABA6223FDB4FF002EE9DD /* RecipientEmailsCellNode.swift */, @@ -2025,6 +2027,7 @@ D24FAFAA2520BFAE00BF46C5 /* CheckBoxNode.swift */, 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */, 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */, + 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */, ); path = Nodes; sourceTree = ""; @@ -2502,8 +2505,6 @@ 9FC413182683C492004C0A69 /* InMemoryPassPhraseStorageTest.swift in Sources */, 9F9764C5267E14AB0058419D /* GeneralConstantsTest.swift in Sources */, 9F976507267E165D0058419D /* ZBase32EncodingTests.swift in Sources */, - 9F9764C5267E14AB0058419D /* GeneralConstantsTest.swift in Sources */, - 9F976507267E165D0058419D /* ZBase32EncodingTests.swift in Sources */, 2C2A3B4D2719EF7300B7F27B /* PassPhraseServiceMock.swift in Sources */, 9F5F504A26FA6C8F00294FA2 /* ClientConfigurationProviderMock.swift in Sources */, 9FC4117D268118AE004C0A69 /* PassPhraseStorageMock.swift in Sources */, @@ -2530,13 +2531,13 @@ 51775C32270B01C200D7C944 /* PrvKeyInfoTests.swift in Sources */, 9F9500AF26F4BAE300E8C78B /* ClientConfigurationTests.swift in Sources */, 9FC413442683C912004C0A69 /* GmailServiceTest.swift in Sources */, - 9FC413442683C912004C0A69 /* GmailServiceTest.swift in Sources */, 9F976556267E186D0058419D /* RawClientConfigurationTests.swift in Sources */, A36108E9273C7A2E00A90E34 /* MockError.swift in Sources */, 9F7E8EC6269877E70021C07F /* KeyInfoTests.swift in Sources */, 9F5F504326FA6C7500294FA2 /* EnterpriseServerApiMock.swift in Sources */, 51DA5BDA2722C82E001C4359 /* RecipientTests.swift in Sources */, 9FC41171268118A7004C0A69 /* PassPhraseStorageTests.swift in Sources */, + 9F6F3C7626ADFC37005BD9C6 /* KeyStorageMock.swift in Sources */, 51DA5BDA2722C82E001C4359 /* RecipientTests.swift in Sources */, 9FC41171268118A7004C0A69 /* PassPhraseStorageTests.swift in Sources */, ); @@ -2779,7 +2780,7 @@ D2A9CA432426210200E1D898 /* SetupTitleNode.swift in Sources */, D2F6D12F24324ACC00DB4065 /* SwitchCellNode.swift in Sources */, 51EBC5702746A06600178DE8 /* TextWithIconNode.swift in Sources */, - 5180CB97273724E9001FC7EF /* ThreadMessageSenderCellNode.swift in Sources */, + 5180CB97273724E9001FC7EF /* ThreadMessageInfoCellNode.swift in Sources */, D2E26F6824F169E300612AF1 /* ContactCellNode.swift in Sources */, D2A9CA38242618DF00E1D898 /* LinkButtonNode.swift in Sources */, D24FAFA42520BF9100BF46C5 /* CheckBoxCircleView.swift in Sources */, @@ -2804,6 +2805,7 @@ 5109A77C272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift in Sources */, D27177462424D59800BDA9A9 /* InboxCellNode.swift in Sources */, D27177472424D59800BDA9A9 /* TextCellNode.swift in Sources */, + 51B9EE6F27567B520080B2D5 /* MessageRecipientsNode.swift in Sources */, D211CE6E23FC354200D1CE38 /* CellNode.swift in Sources */, D24ABA6023FDB26C002EE9DD /* Helpers.swift in Sources */, D211CE7323FC35AC00D1CE38 /* TextViewCellNode.swift in Sources */, diff --git a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift index 6eeb8f3aa..dd7191bdd 100644 --- a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift +++ b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift @@ -70,17 +70,11 @@ extension InboxRenderable { // for now its not exactly clear how titles on other folders should looks like // so in scope of this PR we are applying this title presentation only for "sent" folder if folderPath == MessageLabelType.sent.value { - var emails = thread.messages.compactMap(\.sender).unique() - // if we have only one email, it means that it could be "me" and we are not - // clearing our own email from that - if emails.count > 1 { - if let i = emails.firstIndex(of: activeUserEmail) { - emails.remove(at: i) - } - } - let recipients = emails - .compactMap { $0.components(separatedBy: "@").first } - .joined(separator: ",") + let recipients = thread.messages + .flatMap(\.allRecipients) + .map(\.displayName) + .unique() + .joined(separator: ", ") return "To: \(recipients)" } else { diff --git a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift index 2fadaff24..f09086b06 100644 --- a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift @@ -16,7 +16,7 @@ import FlowCryptUI */ class SetupCreatePassphraseAbstractViewController: TableNodeViewController, PassPhraseSaveable, NavigationChildController { - + enum Parts: Int, CaseIterable { case title, description, passPhrase, divider, saveLocally, saveInMemory, action, subtitle, fetchedKeys } diff --git a/FlowCrypt/Controllers/Threads/MessageAction.swift b/FlowCrypt/Controllers/Threads/MessageAction.swift index 0506bacd3..aa744370e 100644 --- a/FlowCrypt/Controllers/Threads/MessageAction.swift +++ b/FlowCrypt/Controllers/Threads/MessageAction.swift @@ -5,7 +5,7 @@ // Created by Anton Kharchevskyi on 21.10.2021 // 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 e6b012622..7a16a2335 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift @@ -9,30 +9,37 @@ import FlowCryptUI import UIKit -extension ThreadMessageSenderCellNode.Input { +extension ThreadMessageInfoCellNode.Input { init(threadMessage: ThreadDetailsViewController.Input) { let sender = threadMessage.rawMessage.sender ?? "message_unknown_sender".localized + let recipientPrefix = "to".localized + let recipientsList = threadMessage.rawMessage + .allRecipients + .map(\.displayName) + .joined(separator: ", ") + let recipientLabel = [recipientPrefix, recipientsList].joined(separator: " ") let date = DateFormatter().formatDate(threadMessage.rawMessage.date) let isMessageRead = threadMessage.rawMessage.isMessageRead let style: NSAttributedString.Style = isMessageRead - ? .regular(17) - : .bold(17) + ? .regular(16) + : .bold(16) let dateColor: UIColor = isMessageRead ? .lightGray : .main - let textColor: UIColor = isMessageRead - ? .lightGray - : .mainTextUnreadColor - self.init( encryptionBadge: makeEncryptionBadge(threadMessage), signatureBadge: makeSignatureBadge(threadMessage), - sender: NSAttributedString.text(from: sender, style: style, color: textColor), + sender: NSAttributedString.text(from: sender, style: style, color: .label), + recipientLabel: NSAttributedString.text(from: recipientLabel, style: style, color: .secondaryLabel), + recipients: threadMessage.rawMessage.recipients.map(\.rawString), + ccRecipients: threadMessage.rawMessage.cc.map(\.rawString), + bccRecipients: threadMessage.rawMessage.bcc.map(\.rawString), date: NSAttributedString.text(from: date, style: style, color: dateColor), isExpanded: threadMessage.isExpanded, + shouldShowRecipientsList: threadMessage.shouldShowRecipientsList, 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 279e86cf2..c72b59f90 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -18,11 +18,13 @@ final class ThreadDetailsViewController: TableNodeViewController { class Input { var rawMessage: Message var isExpanded: Bool + var shouldShowRecipientsList: Bool var processedMessage: ProcessedMessage? - init(message: Message, isExpanded: Bool) { + init(message: Message, isExpanded: Bool = false, shouldShowRecipientsList: Bool = false) { self.rawMessage = message self.isExpanded = isExpanded + self.shouldShowRecipientsList = shouldShowRecipientsList } } @@ -91,7 +93,7 @@ final class ThreadDetailsViewController: TableNodeViewController { self.onComplete = completion self.input = thread.messages .sorted(by: >) - .map { Input(message: $0, isExpanded: false) } + .map { Input(message: $0) } super.init(node: TableNode()) } @@ -119,7 +121,7 @@ extension ThreadDetailsViewController { } private func handleExpandTap(at indexPath: IndexPath) { - guard let threadNode = node.nodeForRow(at: indexPath) as? ThreadMessageSenderCellNode else { + guard let threadNode = node.nodeForRow(at: indexPath) as? ThreadMessageInfoCellNode else { logger.logError("Fail to handle tap at \(indexPath)") return } @@ -149,6 +151,14 @@ extension ThreadDetailsViewController { } } + private func handleRecipientsTap(at indexPath: IndexPath) { + input[indexPath.section - 1].shouldShowRecipientsList.toggle() + + UIView.animate(withDuration: 0.3) { + self.node.reloadSections(IndexSet(integer: indexPath.section), with: .automatic) + } + } + private func handleReplyTap(at indexPath: IndexPath) { composeNewMessage(at: indexPath, quoteType: .reply) } @@ -185,7 +195,7 @@ extension ThreadDetailsViewController { let replyInfo = ComposeMessageInput.MessageQuoteInfo( recipients: recipients, sender: input.rawMessage.sender, - subject: "\(quoteType.subjectPrefix)\(subject)", + subject: [quoteType.subjectPrefix, subject].joined(), mime: processedMessage.rawMimeData, sentDate: input.rawMessage.date, message: processedMessage.text, @@ -374,7 +384,7 @@ extension ThreadDetailsViewController { } extension ThreadDetailsViewController: MessageActionsHandler { - + private func handleSuccessfulMessage(action: MessageAction) { hideSpinner() onComplete(action, .init(thread: thread, folderPath: currentFolderPath, activeUserEmail: user.email)) @@ -454,37 +464,37 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { return MessageSubjectNode(subject.attributed(.medium(18))) } - let section = self.input[indexPath.section-1] + let message = self.input[indexPath.section-1] if indexPath.row == 0 { - return ThreadMessageSenderCellNode( - input: .init(threadMessage: section), + return ThreadMessageInfoCellNode( + input: .init(threadMessage: message), onReplyTap: { [weak self] _ in self?.handleReplyTap(at: indexPath) }, - onMenuTap: { [weak self] _ in self?.handleMenuTap(at: indexPath) } + onMenuTap: { [weak self] _ in self?.handleMenuTap(at: indexPath) }, + onRecipientsTap: { [weak self] _ in self?.handleRecipientsTap(at: indexPath) } ) } - if indexPath.row == 1, let message = section.processedMessage { - return MessageTextSubjectNode(message.attributedMessage) - } + guard let processedMessage = message.processedMessage + else { return ASCellNode() } - if indexPath.row > 1, let message = section.processedMessage { - let attachment = message.attachments[indexPath.row - 2] - return AttachmentNode( - input: .init( - msgAttachment: attachment, - index: indexPath.row - 2 - ), - onDownloadTap: { [weak self] in self?.attachmentManager.open(attachment) } - ) - } + guard indexPath.row > 1 + else { return MessageTextSubjectNode(processedMessage.attributedMessage) } - return ASCellNode() + let attachmentIndex = indexPath.row - 2 + let attachment = processedMessage.attachments[attachmentIndex] + return AttachmentNode( + input: .init( + msgAttachment: attachment, + index: attachmentIndex + ), + onDownloadTap: { [weak self] in self?.attachmentManager.open(attachment) } + ) } } func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { - guard tableNode.nodeForRow(at: indexPath) is ThreadMessageSenderCellNode else { + guard tableNode.nodeForRow(at: indexPath) is ThreadMessageInfoCellNode else { return } handleExpandTap(at: indexPath) diff --git a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift index bc8f3e146..9382109b2 100644 --- a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift @@ -11,7 +11,7 @@ import Foundation import GoogleAPIClientForREST_Gmail class GmailService: MailServiceProvider { - + let mailServiceProviderType = MailServiceProviderType.gmail let userService: GoogleUserServiceType let backupSearchQueryProvider: GmailBackupSearchQueryProviderType @@ -55,5 +55,7 @@ extension String { static let subject = "subject" static let date = "date" static let to = "to" + static let cc = "cc" + static let bcc = "bcc" static let identifier = "Message-ID" } diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift index a0e18a044..64addcac1 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift @@ -6,7 +6,7 @@ import Foundation import MailCore extension Imap { - + func fetchMsg(message: MCOIMAPMessage, folder: String) async throws -> Data { return try await execute("fetchMsg", { sess, respond in sess.fetchMessageOperation( diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/ImapSessionProvider.swift b/FlowCrypt/Functionality/Mail Provider/Imap/ImapSessionProvider.swift index c448a6659..45b517726 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/ImapSessionProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/ImapSessionProvider.swift @@ -14,9 +14,9 @@ protocol ImapSessionProviderType { } class ImapSessionProvider: ImapSessionProviderType { - + private let user: User - + init(user: User) { self.user = user } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift index e3a353bd6..55409e0fe 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( message: Message, folder: String, diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index 06c7dfb65..ceef163e2 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -14,7 +14,9 @@ struct Message: Hashable { let identifier: Identifier let date: Date let sender: String? - let recipient: String? + let recipients: [MessageRecipient] + let cc: [MessageRecipient] + let bcc: [MessageRecipient] let subject: String? let size: Int? let attachmentIds: [String] @@ -47,7 +49,9 @@ struct Message: Hashable { threadId: String? = nil, draftIdentifier: String? = nil, raw: String? = nil, - recipient: String? = nil + recipient: String? = nil, + cc: String? = nil, + bcc: String? = nil ) { self.identifier = identifier self.date = date @@ -59,7 +63,9 @@ struct Message: Hashable { self.threadId = threadId self.draftIdentifier = draftIdentifier self.raw = raw - self.recipient = recipient + self.recipients = Message.parseRecipients(recipient) + self.cc = Message.parseRecipients(cc) + self.bcc = Message.parseRecipients(bcc) } } @@ -73,6 +79,12 @@ extension Message: Equatable, Comparable { } } +extension Message { + static func parseRecipients(_ string: String?) -> [MessageRecipient] { + string?.components(separatedBy: ", ").map(MessageRecipient.init) ?? [] + } +} + extension Message { func markAsRead(_ isRead: Bool) -> Message { var copy = self @@ -84,6 +96,10 @@ extension Message { } return copy } + + var allRecipients: [MessageRecipient] { + [recipients, cc, bcc].flatMap { $0 } + } } struct Identifier: Equatable, Hashable { @@ -95,3 +111,35 @@ struct Identifier: Equatable, Hashable { self.intId = intId } } + +struct MessageRecipient: Hashable { + let name: String? + let email: String + + init(_ string: String) { + let parts = string.components(separatedBy: " ") + + guard parts.count > 1, let email = parts.last else { + self.name = nil + self.email = string + return + } + + self.email = email.filter { !["<", ">"].contains($0) } + let name = string + .replacingOccurrences(of: email, with: "") + .replacingOccurrences(of: "\"", with: "") + .trimmingCharacters(in: .whitespaces) + self.name = name == self.email ? nil : name + } +} + +extension MessageRecipient { + var displayName: String { + name?.components(separatedBy: " ").first ?? + email.components(separatedBy: "@").first ?? + "unknown" + } + + var rawString: (String?, String) { (name, email) } +} diff --git a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift index 526d489bb..7ea744222 100644 --- a/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Threads/MessagesThreadProvider.swift @@ -148,6 +148,8 @@ extension Message { var sender: String? var subject: String? var recipient: String? + var cc: String? + var bcc: String? messageHeaders.compactMap { $0 }.forEach { guard let name = $0.name?.lowercased() else { return } @@ -156,6 +158,8 @@ extension Message { case .from: sender = value case .subject: subject = value case .to: recipient = value + case .cc: cc = value + case .bcc: bcc = value default: break } } @@ -178,7 +182,9 @@ extension Message { threadId: message.threadId, draftIdentifier: draftIdentifier, raw: message.raw, - recipient: recipient + recipient: recipient, + cc: cc, + bcc: bcc ) } } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 2de765c50..84e893f69 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -15,6 +15,9 @@ "allow" = "Allow"; "later" = "Later"; "forward" = "Forward"; +"to" = "to"; +"cc" = "cc"; +"bcc" = "bcc"; // EMAIL "email_removed" = "Email moved to Trash"; @@ -26,6 +29,7 @@ "message_failed_load" = "Failed to load messages"; "message_compose_secure" = "Compose Secure Message"; "message_unknown_sender" = "(unknown sender)"; +"message_no_recipients" = "(no recipients)"; "message_missed_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?"; diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift new file mode 100644 index 000000000..77bb48f25 --- /dev/null +++ b/FlowCryptUI/Cell Nodes/ThreadMessageInfoCellNode.swift @@ -0,0 +1,288 @@ +// +// ThreadMessageInfoCellNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 06/11/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit +import UIKit + +public final class ThreadMessageInfoCellNode: CellNode { + // MARK: - Input + public struct Input { + public let encryptionBadge: BadgeNode.Input + public let signatureBadge: BadgeNode.Input? + public let sender: NSAttributedString + public let recipientLabel: NSAttributedString + public let recipients: [MessageRecipient] + public let ccRecipients: [MessageRecipient] + public let bccRecipients: [MessageRecipient] + public let date: NSAttributedString? + public let isExpanded: Bool + public let shouldShowRecipientsList: Bool + public let buttonColor: UIColor + public let nodeInsets: UIEdgeInsets + + public init(encryptionBadge: BadgeNode.Input, + signatureBadge: BadgeNode.Input?, + sender: NSAttributedString, + recipientLabel: NSAttributedString, + recipients: [(String?, String)], + ccRecipients: [(String?, String)], + bccRecipients: [(String?, String)], + date: NSAttributedString, + isExpanded: Bool, + shouldShowRecipientsList: Bool, + buttonColor: UIColor, + nodeInsets: UIEdgeInsets) { + self.encryptionBadge = encryptionBadge + self.signatureBadge = signatureBadge + self.sender = sender + self.recipientLabel = recipientLabel + self.recipients = recipients + self.ccRecipients = ccRecipients + self.bccRecipients = bccRecipients + self.date = date + self.isExpanded = isExpanded + self.shouldShowRecipientsList = shouldShowRecipientsList + self.buttonColor = buttonColor + self.nodeInsets = nodeInsets + } + + 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? { + let configuration = UIImage.SymbolConfiguration(pointSize: pointSize) + return UIImage(systemName: systemName, withConfiguration: configuration) + } + } + + // MARK: - Node State + private enum InfoNodeState { + case collapsed, expanded, expandedWithRecipients + } + + private var nodeState: InfoNodeState { + guard input.isExpanded else { return .collapsed } + guard input.shouldShowRecipientsList else { return .expanded } + return .expandedWithRecipients + } + + // MARK: - Specs + private lazy var headerSpec: ASStackLayoutSpec = { + ASStackLayoutSpec( + direction: .horizontal, + spacing: 2, + justifyContent: .spaceBetween, + alignItems: .start, + children: [infoSpec, replyNode, menuNode] + ) + }() + + private lazy var infoSpec: ASStackLayoutSpec = { + let node = ASStackLayoutSpec( + direction: .vertical, + spacing: 6, + justifyContent: .spaceBetween, + alignItems: .start, + children: infoSpecChildren + ) + node.style.flexGrow = 1 + node.style.flexShrink = 1 + return node + }() + + private var infoSpecChildren: [ASLayoutElement] { + switch nodeState { + case .collapsed: + return [senderNode, dateNode] + case .expanded: + return [senderNode, recipientButtonNode, dateNode] + case .expandedWithRecipients: + return [senderNode, recipientButtonNode] + } + } + + private lazy var recipientsSpec: ASStackLayoutSpec = { + ASStackLayoutSpec( + direction: .vertical, + spacing: 4, + justifyContent: .spaceBetween, + alignItems: .start, + children: [recipientsListNode, dateNode] + ) + }() + + private lazy var encryptionInfoSpec: ASStackLayoutSpec = { + let spacer = ASLayoutSpec() + spacer.style.flexGrow = 1.0 + + return ASStackLayoutSpec( + direction: .horizontal, + spacing: 4, + justifyContent: .spaceBetween, + alignItems: .start, + children: [encryptionNode, signatureNode, spacer].compactMap { $0 } + ) + }() + + // MARK: - Nodes + private let senderNode = ASTextNode2() + private let recipientButtonNode = ASButtonNode() + private let dateNode = ASTextNode2() + + private let replyNode = ASButtonNode() + private let menuNode = ASButtonNode() + public private(set) var expandNode = ASImageNode() + + private lazy var recipientsListNode: ASDisplayNode = { + MessageRecipientsNode( + input: .init( + recipients: input.recipients, + ccRecipients: input.ccRecipients, + bccRecipients: input.bccRecipients + ) + ) + }() + + private lazy var encryptionNode: BadgeNode = { + BadgeNode(input: input.encryptionBadge) + }() + + private lazy var signatureNode: BadgeNode? = { + input.signatureBadge.map(BadgeNode.init) + }() + + // MARK: - Properties + private let input: ThreadMessageInfoCellNode.Input + + private let onReplyTap: ((ThreadMessageInfoCellNode) -> Void)? + private let onMenuTap: ((ThreadMessageInfoCellNode) -> Void)? + private let onRecipientsTap: ((ThreadMessageInfoCellNode) -> Void)? + + private var recipientButtonImage: UIImage? { + let configuration = UIImage.SymbolConfiguration(font: .systemFont(ofSize: 12, weight: .medium)) + let imageName = input.shouldShowRecipientsList ? "chevron.up" : "chevron.down" + return UIImage(systemName: imageName, withConfiguration: configuration) + } + + // MARK: - Init + public init(input: ThreadMessageInfoCellNode.Input, + onReplyTap: ((ThreadMessageInfoCellNode) -> Void)?, + onMenuTap: ((ThreadMessageInfoCellNode) -> Void)?, + onRecipientsTap: ((ThreadMessageInfoCellNode) -> Void)?) { + self.input = input + self.onReplyTap = onReplyTap + self.onMenuTap = onMenuTap + self.onRecipientsTap = onRecipientsTap + + super.init() + automaticallyManagesSubnodes = true + + senderNode.attributedText = input.sender + senderNode.accessibilityIdentifier = "messageSenderLabel" + + dateNode.attributedText = input.date + + setupRecipientButton() + setupReplyNode() + setupMenuNode() + setupExpandNode() + } + + // MARK: - Setup + private func setupRecipientButton() { + recipientButtonNode.setImage(recipientButtonImage, for: .normal) + recipientButtonNode.imageAlignment = .end + recipientButtonNode.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) + + recipientButtonNode.setAttributedTitle(input.recipientLabel, for: .normal) + recipientButtonNode.titleNode.maximumNumberOfLines = 1 + recipientButtonNode.titleNode.truncationMode = .byTruncatingTail + recipientButtonNode.contentSpacing = 4 + + recipientButtonNode.addTarget(self, action: #selector(onRecipientsNodeTap), forControlEvents: .touchUpInside) + recipientButtonNode.accessibilityIdentifier = "messageRecipientButton" + } + + private func setupReplyNode() { + setup(buttonNode: replyNode, + with: input.replyImage, + action: #selector(onReplyNodeTap), + accessibilityIdentifier: "replyButton") + } + + private func setupMenuNode() { + setup(buttonNode: menuNode, + with: input.menuImage, + action: #selector(onMenuNodeTap), + accessibilityIdentifier: "messageMenuButton") + } + + 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.addTarget(self, action: action, forControlEvents: .touchUpInside) + node.accessibilityIdentifier = accessibilityIdentifier + } + + private func setupExpandNode() { + expandNode.image = input.expandImage + expandNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) + expandNode.contentMode = .center + } + + // MARK: - Callbacks + @objc private func onReplyNodeTap() { + onReplyTap?(self) + } + + @objc private func onMenuNodeTap() { + onMenuTap?(self) + } + + @objc private func onRecipientsNodeTap() { + onRecipientsTap?(self) + } + + // MARK: - Layout + private var contentSpec: ASStackLayoutSpec { + switch nodeState { + case .collapsed: + return ASStackLayoutSpec( + direction: .horizontal, + spacing: 4, + justifyContent: .spaceBetween, + alignItems: .start, + children: [infoSpec, expandNode] + ) + case .expanded, .expandedWithRecipients: + let children = nodeState == .expanded ? [headerSpec, encryptionInfoSpec] : [headerSpec, recipientsSpec, encryptionInfoSpec] + return ASStackLayoutSpec( + direction: .vertical, + spacing: 8, + justifyContent: .spaceBetween, + alignItems: .stretch, + children: children + ) + } + } + + public override 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) + + return ASInsetLayoutSpec( + insets: input.nodeInsets, + child: contentSpec + ) + } +} diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift deleted file mode 100644 index 9ebf5512a..000000000 --- a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift +++ /dev/null @@ -1,183 +0,0 @@ -// -// ThreadMessageSenderCellNode.swift -// FlowCryptUI -// -// Created by Roma Sosnovsky on 06/11/21 -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import AsyncDisplayKit -import UIKit - -public final class ThreadMessageSenderCellNode: CellNode { - public struct Input { - public let encryptionBadge: BadgeNode.Input - public let signatureBadge: BadgeNode.Input? - public let sender: NSAttributedString - public let date: NSAttributedString? - public let isExpanded: Bool - public let buttonColor: UIColor - public let nodeInsets: UIEdgeInsets - - public init(encryptionBadge: BadgeNode.Input, - signatureBadge: BadgeNode.Input?, - sender: NSAttributedString, - date: NSAttributedString, - isExpanded: Bool, - buttonColor: UIColor, - nodeInsets: UIEdgeInsets) { - self.encryptionBadge = encryptionBadge - self.signatureBadge = signatureBadge - self.sender = sender - self.date = date - self.isExpanded = isExpanded - self.buttonColor = buttonColor - self.nodeInsets = nodeInsets - } - - 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? { - let configuration = UIImage.SymbolConfiguration(pointSize: pointSize) - return UIImage(systemName: systemName, withConfiguration: configuration) - } - } - - private lazy var encryptionNode: BadgeNode = { - return BadgeNode(input: input.encryptionBadge) - }() - - private lazy var signatureNode: BadgeNode? = { - return input.signatureBadge.map(BadgeNode.init) - }() - - private let senderNode = ASTextNode2() - private let dateNode = ASTextNode2() - - public private(set) var replyNode = 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 onMenuTap: ((ThreadMessageSenderCellNode) -> Void)? - - public init(input: ThreadMessageSenderCellNode.Input, - onReplyTap: ((ThreadMessageSenderCellNode) -> Void)?, - onMenuTap: ((ThreadMessageSenderCellNode) -> Void)?) { - self.input = input - self.onReplyTap = onReplyTap - self.onMenuTap = onMenuTap - super.init() - automaticallyManagesSubnodes = true - - senderNode.attributedText = input.sender - senderNode.accessibilityIdentifier = "senderEmail" - dateNode.attributedText = input.date - - - setupReplyNode() - setupMenuNode() - setupExpandNode() - } - - private func setupReplyNode() { - setup(buttonNode: replyNode, - with: input.replyImage, - action: #selector(onReplyNodeTap), - accessibilityIdentifier: "replyButton") - } - - private func setupMenuNode() { - setup(buttonNode: menuNode, - with: input.menuImage, - action: #selector(onMenuNodeTap), - accessibilityIdentifier: "messageMenuButton") - } - - 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.addTarget(self, action: action, forControlEvents: .touchUpInside) - node.accessibilityIdentifier = accessibilityIdentifier - } - - private func setupExpandNode() { - expandNode.image = input.expandImage - expandNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) - expandNode.contentMode = .center - } - - @objc private func onReplyNodeTap() { - onReplyTap?(self) - } - - @objc private func onMenuNodeTap() { - onMenuTap?(self) - } - - public override 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) - - let infoNode = ASStackLayoutSpec( - direction: .vertical, - spacing: 4, - justifyContent: .spaceBetween, - alignItems: .start, - children: [senderNode, dateNode] - ) - infoNode.style.flexGrow = 1 - infoNode.style.flexShrink = 1 - - 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 - - let signatureSpec = ASStackLayoutSpec( - direction: .horizontal, - spacing: 4, - justifyContent: .spaceBetween, - alignItems: .start, - children: [encryptionNode, signatureNode, spacer].compactMap { $0 } - ) - - contentSpec = ASStackLayoutSpec( - direction: .vertical, - spacing: 4, - justifyContent: .spaceBetween, - alignItems: .stretch, - children: [senderSpec, signatureSpec] - ) - } else { - contentSpec = ASStackLayoutSpec( - direction: .horizontal, - spacing: 4, - justifyContent: .spaceBetween, - alignItems: .start, - children: [infoNode, expandNode] - ) - } - - return ASInsetLayoutSpec( - insets: input.nodeInsets, - child: contentSpec - ) - } -} diff --git a/FlowCryptUI/Nodes/BadgeNode.swift b/FlowCryptUI/Nodes/BadgeNode.swift index 63a62f2e6..70996428b 100644 --- a/FlowCryptUI/Nodes/BadgeNode.swift +++ b/FlowCryptUI/Nodes/BadgeNode.swift @@ -5,7 +5,6 @@ // Created by Roma Sosnovsky on 12/11/21 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - import AsyncDisplayKit diff --git a/FlowCryptUI/Nodes/MessageRecipientsNode.swift b/FlowCryptUI/Nodes/MessageRecipientsNode.swift new file mode 100644 index 000000000..ea90f77c4 --- /dev/null +++ b/FlowCryptUI/Nodes/MessageRecipientsNode.swift @@ -0,0 +1,122 @@ +// +// MessageRecipientsNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 30/11/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +public typealias MessageRecipient = (name: String?, email: String) + +public final class MessageRecipientsNode: ASDisplayNode { + public struct Input { + let recipients: [MessageRecipient] + let ccRecipients: [MessageRecipient] + let bccRecipients: [MessageRecipient] + + public init(recipients: [MessageRecipient], + ccRecipients: [MessageRecipient], + bccRecipients: [MessageRecipient]) { + self.recipients = recipients + self.ccRecipients = ccRecipients + self.bccRecipients = bccRecipients + } + } + + private let input: MessageRecipientsNode.Input + + private enum RecipientType: String, CaseIterable { + case to, cc, bcc + } + + public init(input: MessageRecipientsNode.Input) { + self.input = input + + super.init() + + automaticallyManagesSubnodes = true + + setupBorder() + } + + private func setupBorder() { + borderColor = UIColor.tertiaryLabel.cgColor + borderWidth = 1 + cornerRadius = 6 + } + + private func recipientList(label: String, recipients: [MessageRecipient]) -> ASStackLayoutSpec? { + guard recipients.isNotEmpty else { return nil } + + let labelNode = ASTextNode2() + labelNode.attributedText = label.localizedCapitalized.attributed() + labelNode.style.preferredSize = CGSize(width: 30, height: 20) + + let children = recipients.enumerated().map { index, recipient in + return recipientNode(for: recipient, identifier: "\(label)Label\(index)") + } + let recipientsList = ASStackLayoutSpec( + direction: .vertical, + spacing: 4, + justifyContent: .start, + alignItems: .start, + children: children + ) + recipientsList.style.flexShrink = 1 + + return ASStackLayoutSpec( + direction: .horizontal, + spacing: 4, + justifyContent: .start, + alignItems: .start, + children: [labelNode, recipientsList] + ) + } + + private func recipientNode(for recipient: MessageRecipient, identifier: String) -> ASTextNode2 { + let style: NSAttributedString.Style = .regular(15) + let nameString = recipient.name?.attributed(style, color: .label) + let emailString = recipient.email.attributed(style, color: .secondaryLabel) + let separator = " ".attributed(style) + + let node = ASTextNode2() + node.accessibilityIdentifier = identifier + node.attributedText = [nameString, emailString] + .compactMap { $0 } + .reduce(NSMutableAttributedString(), { + if !$0.string.isEmpty { $0.append(separator) } + $0.append($1); + return $0 + }) + + return node + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + let recipientsNodes: [ASStackLayoutSpec] = RecipientType.allCases.compactMap { type in + let recipients: [MessageRecipient] + switch type { + case .to: + recipients = input.recipients + case .cc: + recipients = input.ccRecipients + case .bcc: + recipients = input.bccRecipients + } + return recipientList(label: type.rawValue, recipients: recipients) + } + + return ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6), + child: ASStackLayoutSpec( + direction: .vertical, + spacing: 6, + justifyContent: .spaceBetween, + alignItems: .start, + children: recipientsNodes + ) + ) + } +} diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index 0db478fc0..886677e8f 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -38,6 +38,15 @@ export const CommonData = { subject: 'Encrypted message with key mismatch', message: 'Could not decrypt:', }, + recipientsListEmail: { + sender: 'flowcrypt.compatibility@gmail.com', + subject: 'CC and BCC test', + message: 'Test message for CC and BCC recipients', + recipients: 'to Robot, robot+cc, e2e.enterprise.test', + to: 'Robot FlowCrypt robot@flowcrypt.com', + cc: 'robot+cc@flowcrypt.com', + bcc: 'e2e.enterprise.test@flowcrypt.com' + }, encryptedMDCHashMismatchEmail: { senderEmail: 'flowcrypt.compatibility@gmail.com', subject: 'encrypted - MDC hash mismatch - modification detected - should fail', diff --git a/appium/tests/helpers/ElementHelper.ts b/appium/tests/helpers/ElementHelper.ts index 4d546c95b..e4ee38984 100644 --- a/appium/tests/helpers/ElementHelper.ts +++ b/appium/tests/helpers/ElementHelper.ts @@ -39,6 +39,11 @@ class ElementHelper { await ElementHelper.waitAndClick(await this.staticText(label)); } + static checkStaticText = async (element: WebdriverIO.Element, label: string) => { + await this.waitElementVisible(element); + await expect(element).toHaveText(label); + } + static doubleClick = async (element: WebdriverIO.Element) => { await this.waitElementVisible(element); await element.doubleClick(); diff --git a/appium/tests/screenobjects/email.screen.ts b/appium/tests/screenobjects/email.screen.ts index e6ea1597e..16f4b74a1 100644 --- a/appium/tests/screenobjects/email.screen.ts +++ b/appium/tests/screenobjects/email.screen.ts @@ -9,11 +9,15 @@ const SELECTORS = { WRONG_PASS_PHRASE_MESSAGE: '-ios class chain:**/XCUIElementTypeStaticText[`label == "Wrong pass phrase, please try again"`]', DOWNLOAD_ATTACHMENT_BUTTON: '~attachmentDownloadButton0', REPLY_BUTTON: '~replyButton', + RECIPIENTS_BUTTON: '~messageRecipientButton', + RECIPIENTS_TO_LABEL: '~toLabel0', + RECIPIENTS_CC_LABEL: '~ccLabel0', + RECIPIENTS_BCC_LABEL: '~bccLabel0', MENU_BUTTON: '~messageMenuButton', FORWARD_BUTTON: '~Forward', DELETE_BUTTON: '~Delete', CONFIRM_DELETING: '~OK', - SENDER_EMAIL: '~senderEmail', + SENDER_EMAIL: '~messageSenderLabel', ENCRYPTION_BADGE: '~encryptionBadge', SIGNATURE_BADGE: '~signatureBadge' }; @@ -48,6 +52,22 @@ class EmailScreen extends BaseScreen { return $(SELECTORS.REPLY_BUTTON); } + get recipientsButton() { + return $(SELECTORS.RECIPIENTS_BUTTON); + } + + get recipientsToLabel() { + return $(SELECTORS.RECIPIENTS_TO_LABEL); + } + + get recipientsCcLabel() { + return $(SELECTORS.RECIPIENTS_CC_LABEL); + } + + get recipientsBccLabel() { + return $(SELECTORS.RECIPIENTS_BCC_LABEL); + } + get menuButton() { return $(SELECTORS.MENU_BUTTON); } @@ -77,8 +97,7 @@ class EmailScreen extends BaseScreen { } checkEmailAddress = async (email: string) => { - await (await this.senderEmail).waitForDisplayed(); - await expect(await this.senderEmail).toHaveText(email); + await ElementHelper.checkStaticText(await this.senderEmail, email); } checkEmailSubject = async (subject: string) => { @@ -133,6 +152,10 @@ class EmailScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.replyButton); } + clickRecipientsButton =async () => { + await ElementHelper.waitAndClick(await this.recipientsButton); + } + clickMenuButton = async () => { await ElementHelper.waitAndClick(await this.menuButton); } @@ -149,14 +172,22 @@ class EmailScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.confirmDeletingButton); } + checkRecipientsButton = async (value: string) => { + await ElementHelper.checkStaticText(await this.recipientsButton, value); + } + + checkRecipientsList = async (to: string, cc: string, bcc: string) => { + await ElementHelper.checkStaticText(await this.recipientsToLabel, to); + await ElementHelper.checkStaticText(await this.recipientsCcLabel, cc); + await ElementHelper.checkStaticText(await this.recipientsBccLabel, bcc); + } + checkEncryptionBadge = async (value: string) => { - await (await this.encryptionBadge).waitForDisplayed(); - await expect(await this.encryptionBadge).toHaveText(value); + await ElementHelper.checkStaticText(await this.encryptionBadge, value); } checkSignatureBadge = async (value: string) => { - await (await this.signatureBadge).waitForDisplayed(); - await expect(await this.signatureBadge).toHaveText(value); + await ElementHelper.checkStaticText(await this.signatureBadge, value); } } diff --git a/appium/tests/specs/live/inbox/ReadTextEmail.spec.ts b/appium/tests/specs/live/inbox/ReadTextEmail.spec.ts index 0b447419a..c9945852b 100644 --- a/appium/tests/specs/live/inbox/ReadTextEmail.spec.ts +++ b/appium/tests/specs/live/inbox/ReadTextEmail.spec.ts @@ -9,11 +9,15 @@ import { CommonData } from '../../../data'; describe('INBOX: ', () => { - it('user is able to view text email', async () => { + it('user is able to view text email and recipients list', async () => { - const senderEmail = CommonData.sender.email; - const emailSubject = CommonData.simpleEmail.subject; - const emailText = CommonData.simpleEmail.message; + const senderEmail = CommonData.recipientsListEmail.sender; + const emailSubject = CommonData.recipientsListEmail.subject; + const emailText = CommonData.recipientsListEmail.message; + const recipientsButton = CommonData.recipientsListEmail.recipients; + const toLabel = CommonData.recipientsListEmail.to; + const ccLabel = CommonData.recipientsListEmail.cc; + const bccLabel = CommonData.recipientsListEmail.bcc; await SplashScreen.login(); await SetupKeyScreen.setPassPhrase(); @@ -22,5 +26,8 @@ describe('INBOX: ', () => { await MailFolderScreen.searchEmailBySubject(emailSubject); await MailFolderScreen.clickOnEmailBySubject(emailSubject); await EmailScreen.checkOpenedEmail(senderEmail, emailSubject, emailText); + await EmailScreen.checkRecipientsButton(recipientsButton); + await EmailScreen.clickRecipientsButton(); + await EmailScreen.checkRecipientsList(toLabel, ccLabel, bccLabel); }); });