From 73438a69111bbcea0d6a06f4622055fd81a740ca Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 9 Feb 2022 12:15:14 +0200 Subject: [PATCH 01/19] #954 add cc and bcc recipients --- FlowCrypt/AppDelegate.swift | 2 +- .../Attachment/AttachmentViewController.swift | 1 - .../Compose/ComposeViewController.swift | 380 ++++++++++-------- .../Compose/ComposeViewDecorator.swift | 4 +- .../Threads/ThreadDetailsViewController.swift | 4 +- .../KeyServiceErrorHandler.swift | 7 +- .../ComposeMessageContext.swift | 96 ++++- .../ComposeMessageRecipient.swift | 13 +- .../ComposeMessageService.swift | 15 +- .../Resources/en.lproj/Localizable.strings | 4 +- .../Cell Nodes/RecipientEmailNode.swift | 2 +- .../Cell Nodes/RecipientEmailsCellNode.swift | 38 +- .../RecipientEmailsCellNodeInput.swift | 10 +- Podfile.lock | 4 +- 14 files changed, 390 insertions(+), 190 deletions(-) diff --git a/FlowCrypt/AppDelegate.swift b/FlowCrypt/AppDelegate.swift index 9aef7d467..e8792bfaf 100644 --- a/FlowCrypt/AppDelegate.swift +++ b/FlowCrypt/AppDelegate.swift @@ -12,7 +12,7 @@ import FlowCryptCommon class AppDelegate: UIResponder, UIApplicationDelegate, AppDelegateGoogleSesssionContainer { var blurViewController: BlurViewController? var googleAuthSession: OIDExternalUserAgentSession? - let window: UIWindow = UIWindow(frame: UIScreen.main.bounds) + let window = UIWindow(frame: UIScreen.main.bounds) func application(_ application: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { if application.isRunningTests { diff --git a/FlowCrypt/Controllers/Attachment/AttachmentViewController.swift b/FlowCrypt/Controllers/Attachment/AttachmentViewController.swift index d0389abdd..bea5fd7f7 100644 --- a/FlowCrypt/Controllers/Attachment/AttachmentViewController.swift +++ b/FlowCrypt/Controllers/Attachment/AttachmentViewController.swift @@ -9,7 +9,6 @@ import UIKit import WebKit import FlowCryptUI -import Combine import FlowCryptCommon @MainActor diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index e1ab108c6..c9b97b50b 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -10,21 +10,17 @@ import Foundation import PhotosUI // swiftlint:disable file_length -private struct ComposedDraft: Equatable { - let email: String - let input: ComposeMessageInput - let contextToSend: ComposeMessageContext -} - /** * 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 *forward* **/ final class ComposeViewController: TableNodeViewController { - var calculatedRecipientsPartHeight: CGFloat? { + // TODO: + private var calculatedRecipientsPartHeight: CGFloat? { didSet { - node.reloadRows(at: [recipientsIndexPath], with: .fade) + let sections: [Section] = [.to, .cc, .bcc] + node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) } } @@ -33,16 +29,29 @@ final class ComposeViewController: TableNodeViewController { static let minRecipientsPartHeight: CGFloat = 44 } - enum State { - case main, searchEmails([String]) + private struct ComposedDraft: Equatable { + let email: String + let input: ComposeMessageInput + let contextToSend: ComposeMessageContext } - private enum Section: Int, CaseIterable { - case recipient, password, compose, attachments + private enum State { + case main, searchEmails([String]) } - private enum RecipientPart: Int, CaseIterable { - case list, input + private enum Section: Int, CaseIterable { + case to, cc, bcc, password, compose, attachments + + static func recipientsSection(for type: RecipientType) -> Section { + switch type { + case .to: + return .to + case .cc: + return .cc + case .bcc: + return .bcc + } + } } private enum ComposePart: Int, CaseIterable { @@ -85,6 +94,8 @@ final class ComposeViewController: TableNodeViewController { navigationController?.navigationBar.frame.maxY ?? 0 } + private var selectedRecipientType: RecipientType? + init( appContext: AppContextWithUser, notificationCenter: NotificationCenter = .default, @@ -173,7 +184,7 @@ final class ComposeViewController: TableNodeViewController { cancellable.forEach { $0.cancel() } setupSearch() - evaluateIfNeeded() + evaluateAllRecipients() } override func viewDidLayoutSubviews() { @@ -189,20 +200,22 @@ final class ComposeViewController: TableNodeViewController { NotificationCenter.default.removeObserver(self) } - private func evaluateIfNeeded() { - guard contextToSend.recipients.isNotEmpty else { - return - } - - for recipient in contextToSend.recipients { - evaluate(recipient: recipient) + private func evaluateAllRecipients() { + contextToSend.recipients.forEach { + evaluate(recipient: $0) } } func update(with message: Message) { self.contextToSend.subject = message.subject self.contextToSend.message = message.raw - self.contextToSend.recipients = [ComposeMessageRecipient(email: "tom@flowcrypt.com", state: decorator.recipientIdleState)] + self.contextToSend.recipients = [ + ComposeMessageRecipient( + email: "tom@flowcrypt.com", + type: .to, + state: decorator.recipientIdleState + ) + ] } private func observeComposeUpdates() { @@ -285,7 +298,6 @@ extension ComposeViewController { } // MARK: - Setup UI - extension ComposeViewController { private func setupNavigationBar() { navigationItem.rightBarButtonItem = NavigationBarItemsView( @@ -323,8 +335,8 @@ extension ComposeViewController { guard input.isQuote else { return } input.quoteRecipients.forEach { email in - let recipient = ComposeMessageRecipient(email: email, state: decorator.recipientIdleState) - contextToSend.recipients.append(recipient) + let recipient = ComposeMessageRecipient(email: email, type: .to, state: decorator.recipientIdleState) + contextToSend.add(recipient: recipient) evaluate(recipient: recipient) } } @@ -557,17 +569,23 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int { switch (state, section) { - case (_, Section.recipient.rawValue): - return RecipientPart.allCases.count + case (.main, Section.to.rawValue), (.main, Section.cc.rawValue), (.main, Section.bcc.rawValue): + return 1 case (.main, Section.password.rawValue): return isMessagePasswordSupported && contextToSend.hasRecipientsWithoutPubKey ? 1 : 0 case (.main, Section.compose.rawValue): return ComposePart.allCases.count case (.main, Section.attachments.rawValue): return contextToSend.attachments.count - case let (.searchEmails(emails), 1): + case (.searchEmails, Section.to.rawValue): + return selectedRecipientType == .to ? 1 : 0 + case (.searchEmails, Section.cc.rawValue): + return selectedRecipientType == .cc ? 1 : 0 + case (.searchEmails, Section.bcc.rawValue): + return selectedRecipientType == .bcc ? 1 : 0 + case let (.searchEmails(emails), RecipientType.allCases.count): return emails.isNotEmpty ? emails.count + 1 : 2 - case (.searchEmails, 2): + case (.searchEmails, RecipientType.allCases.count + 1): return cloudContactProvider.isContactsScopeEnabled ? 0 : 2 default: return 0 @@ -580,12 +598,8 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { guard let self = self else { return ASCellNode() } switch (self.state, indexPath.section) { - case (_, Section.recipient.rawValue): - guard let part = RecipientPart(rawValue: indexPath.row) else { return ASCellNode() } - switch part { - case .input: return self.recipientInput() - case .list: return self.recipientsNode() - } + case (_, Section.to.rawValue), (_, Section.cc.rawValue), (_, Section.bcc.rawValue): + return self.recipientsNode(at: indexPath) case (.main, Section.password.rawValue): return self.messagePasswordNode() case (.main, Section.compose.rawValue): @@ -600,11 +614,11 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { return ASCellNode() } return self.attachmentNode(for: indexPath.row) - case let (.searchEmails(emails), 1): + case let (.searchEmails(emails), RecipientType.allCases.count): guard indexPath.row > 0 else { return DividerCellNode() } guard emails.isNotEmpty else { return self.noSearchResultsNode() } return InfoCellNode(input: self.decorator.styledRecipientInfo(with: emails[indexPath.row-1])) - case (.searchEmails, 2): + case (.searchEmails, RecipientType.allCases.count + 1): return indexPath.row == 0 ? DividerCellNode() : self.enableGoogleContactsNode() default: return ASCellNode() @@ -613,12 +627,13 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { } func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { - if case let .searchEmails(emails) = state { + if case let .searchEmails(emails) = state, let recipientType = selectedRecipientType { switch indexPath.section { - case 1: + case RecipientType.allCases.count: let selectedEmail = emails[safe: indexPath.row-1] - handleEndEditingAction(with: selectedEmail) - case 2: + print(selectedEmail) + handleEndEditingAction(with: selectedEmail, for: recipientType) + case RecipientType.allCases.count + 1: askForContactsPermission() default: break @@ -634,7 +649,6 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { } // MARK: - Nodes - extension ComposeViewController { private func subjectNode() -> ASCellNode { TextFieldCellNode( @@ -724,11 +738,21 @@ extension ComposeViewController { } } - private func recipientsNode() -> RecipientEmailsCellNode { + private func recipientsNode(at indexPath: IndexPath) -> ASCellNode { + let recipientType = RecipientType.allCases[indexPath.section] + let recipients = contextToSend.recipients(of: recipientType) + return RecipientEmailsCellNode( - recipients: recipients.map(RecipientEmailsCellNode.Input.init), - height: calculatedRecipientsPartHeight ?? Constants.minRecipientsPartHeight - ) + recipients: recipients.map(RecipientEmailsCellNode.RecipientInput.init), + height: calculatedRecipientsPartHeight ?? Constants.minRecipientsPartHeight, + textFieldInput: RecipientEmailsCellNode.TextFieldInput( + placeholder: recipientType.inputPlaceholder.attributed( + .regular(17), + color: .lightGray + ) + )) { [weak self] action in + self?.handle(textFieldAction: action, at: indexPath, for: recipientType) + } .onLayoutHeightChanged { [weak self] layoutHeight in guard self?.calculatedRecipientsPartHeight != layoutHeight, layoutHeight > 0 else { return @@ -745,21 +769,24 @@ extension ComposeViewController { } } - private func recipientInput() -> TextFieldCellNode { - TextFieldCellNode( + private func recipientInput(at indexPath: IndexPath) -> ASCellNode { + let recipientType = RecipientType.allCases[indexPath.section] + + return TextFieldCellNode( input: decorator.styledTextFieldInput( - with: "compose_recipient".localized, + with: recipientType.inputPlaceholder, keyboardType: .emailAddress, - accessibilityIdentifier: "aid-recipient-text-field") + accessibilityIdentifier: "aid-recipient-text-field-\(recipientType.rawValue)" + ) ) { [weak self] action in - self?.handleTextFieldAction(with: action) + self?.handle(textFieldAction: action, at: indexPath, for: recipientType) } .onShouldReturn { textField -> Bool in textField.resignFirstResponder() return true } .onShouldChangeCharacters { [weak self] textField, character -> (Bool) in - self?.shouldChange(with: textField, and: character) ?? true + self?.shouldChange(with: textField, and: character, for: recipientType) ?? true } .then { $0.isLowercased = true @@ -805,23 +832,7 @@ extension ComposeViewController { // MARK: - Recipients Input extension ComposeViewController { - private var textField: TextFieldNode? { - let indexPath = IndexPath( - row: RecipientPart.input.rawValue, - section: Section.recipient.rawValue - ) - return (node.nodeForRow(at: indexPath) as? TextFieldCellNode)?.textField - } - - private var recipientsIndexPath: IndexPath { - IndexPath(row: RecipientPart.list.rawValue, section: Section.recipient.rawValue) - } - - private var recipients: [ComposeMessageRecipient] { - contextToSend.recipients - } - - private func shouldChange(with textField: UITextField, and character: String) -> Bool { + private func shouldChange(with textField: UITextField, and character: String, for recipientType: RecipientType) -> Bool { func nextResponder() { guard let node = node.visibleNodes[safe: ComposePart.subject.rawValue] as? TextFieldCellNode else { return } node.becomeFirstResponder() @@ -835,11 +846,11 @@ extension ComposeViewController { let recipients = character.components(separatedBy: characterSet) guard recipients.count > 1 else { return true } recipients.forEach { - handleEndEditingAction(with: $0) + handleEndEditingAction(with: $0, for: recipientType) } return false } else if Constants.endTypingCharacters.contains(character) { - handleEndEditingAction(with: textField.text) + handleEndEditingAction(with: textField.text, for: recipientType) nextResponder() return false } else { @@ -847,22 +858,28 @@ extension ComposeViewController { } } - private func handleTextFieldAction(with action: TextFieldActionType) { - switch action { - case let .deleteBackward(textField): handleBackspaceAction(with: textField) - case let .didEndEditing(text): handleEndEditingAction(with: text) - case let .editingChanged(text): handleEditingChanged(with: text) + private func handle(textFieldAction: TextFieldActionType, at indexPath: IndexPath, for recipientType: RecipientType) { + print("HANDLE TEXTFIELD \(textFieldAction)") + switch textFieldAction { + case let .deleteBackward(textField): handleBackspaceAction(with: textField, for: recipientType) + case let .didEndEditing(text): handleEndEditingAction(with: text, for: recipientType) + case let .editingChanged(text): handleEditingChanged(with: text, for: recipientType) case .didBeginEditing: handleDidBeginEditing() } } - private func handleEndEditingAction(with text: String?) { + private func handleEndEditingAction(with text: String?, for recipientType: RecipientType) { guard shouldEvaluateRecipientInput, let text = text, text.isNotEmpty else { return } + print("HANDLE EDITING END \(text)") + let recipients = contextToSend.recipients(of: recipientType) + let recipientsIndexPath = recipientsIndexPath(for: recipientType) + + recipientsTextField(at: recipientsIndexPath)?.reset() // Set all recipients to idle state - contextToSend.recipients = recipients.map { recipient in + let idleRecipients: [ComposeMessageRecipient] = recipients.map { recipient in var recipient = recipient if recipient.state.isSelected { recipient.state = self.decorator.recipientIdleState @@ -870,17 +887,19 @@ extension ComposeViewController { return recipient } - let newRecipient = ComposeMessageRecipient(email: text, state: decorator.recipientIdleState) + contextToSend.set(recipients: idleRecipients, for: recipientType) + + let newRecipient = ComposeMessageRecipient(email: text, type: recipientType, state: decorator.recipientIdleState) let indexOfRecipient: Int - if let index = contextToSend.recipients.firstIndex(where: { $0.email == newRecipient.email }) { + if let index = idleRecipients.firstIndex(where: { $0.email == newRecipient.email }) { // recipient already in list evaluate(recipient: newRecipient) indexOfRecipient = index } else { // add new recipient - contextToSend.recipients.append(newRecipient) - node.reloadRows(at: [recipientsIndexPath], with: .fade) + contextToSend.add(recipient: newRecipient) + node.reloadRows(at: [recipientsIndexPath], with: .automatic) evaluate(recipient: newRecipient) // scroll to the latest recipient @@ -896,33 +915,52 @@ extension ComposeViewController { ) } - // reset textfield - textField?.reset() node.view.keyboardDismissMode = .interactive search.send("") updateState(with: .main) } - private func handleBackspaceAction(with textField: UITextField) { + private func recipientsIndexPath(for recipientType: RecipientType) -> IndexPath { + switch recipientType { + case .to: + return [Section.to.rawValue, 0] + case .cc: + return [Section.cc.rawValue, 0] + case .bcc: + return [Section.bcc.rawValue, 0] + } + } + + private func recipientsTextField(at indexPath: IndexPath) -> TextFieldNode? { + (node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode)?.textField + } + + private func handleBackspaceAction(with textField: UITextField, for recipientType: RecipientType) { guard textField.text == "" else { return } - let selectedRecipients = recipients - .filter { $0.state.isSelected } + var recipients = contextToSend.recipients(of: recipientType) + + let recipientsIndexPath = recipientsIndexPath(for: recipientType) + let selectedRecipients = recipients.filter { $0.state.isSelected } guard selectedRecipients.isEmpty else { - // remove selected recipients - contextToSend.recipients = recipients.filter { !$0.state.isSelected } - node.reloadSections([Section.recipient.rawValue, Section.password.rawValue], - with: .automatic) + let notSelectedRecipients = recipients.filter { !$0.state.isSelected } + contextToSend.set(recipients: notSelectedRecipients, for: recipientType) + // TODO: + node.reloadSections( + [Section.to.rawValue, Section.password.rawValue], + with: .automatic + ) + return } - if let lastRecipient = contextToSend.recipients.popLast() { + if var lastRecipient = recipients.last { // select last recipient in a list - var last = lastRecipient - last.state = self.decorator.recipientSelectedState - contextToSend.recipients.append(last) + lastRecipient.state = self.decorator.recipientSelectedState + recipients.append(lastRecipient) + contextToSend.set(recipients: recipients, for: recipientType) node.reloadRows(at: [recipientsIndexPath], with: .fade) node.reloadSections([Section.password.rawValue], with: .automatic) } else { @@ -931,7 +969,8 @@ extension ComposeViewController { } } - private func handleEditingChanged(with text: String?) { + private func handleEditingChanged(with text: String?, for recipientType: RecipientType) { + selectedRecipientType = recipientType search.send(text ?? "") } @@ -953,7 +992,10 @@ extension ComposeViewController { private func evaluate(recipient: ComposeMessageRecipient) { guard recipient.email.isValidEmail else { - handleEvaluation(for: recipient, with: decorator.recipientInvalidEmailState) + updateRecipient( + email: recipient.email, + state: decorator.recipientInvalidEmailState + ) return } @@ -966,7 +1008,7 @@ extension ComposeViewController { let contactWithFetchedKeys = try await service.fetchContact(with: recipient.email) handleEvaluation(for: contactWithFetchedKeys) } catch { - handleEvaluation(error: error, with: recipient) + handleEvaluation(error: error, with: recipient.email) } } } @@ -974,13 +1016,11 @@ extension ComposeViewController { private func handleEvaluation(for recipient: RecipientWithSortedPubKeys) { let state = getRecipientState(from: recipient) - let composeRecipient = ComposeMessageRecipient( + updateRecipient( email: recipient.email, state: state, keyState: recipient.keyState ) - - handleEvaluation(for: composeRecipient) } private func getRecipientState(from recipient: RecipientWithSortedPubKeys) -> RecipientState { @@ -996,15 +1036,7 @@ extension ComposeViewController { } } - private func handleEvaluation(for composeRecipient: ComposeMessageRecipient, with state: RecipientState? = nil) { - updateRecipientWithNew( - state: state ?? composeRecipient.state, - keyState: composeRecipient.keyState, - for: .left(composeRecipient) - ) - } - - private func handleEvaluation(error: Error, with recipient: ComposeMessageRecipient) { + private func handleEvaluation(error: Error, with email: String) { let recipientState: RecipientState = { switch error { case ContactsError.keyMissing: @@ -1014,62 +1046,84 @@ extension ComposeViewController { } }() - updateRecipientWithNew( + updateRecipient( + email: email, state: recipientState, - keyState: nil, - for: .left(recipient) + keyState: nil ) } - private func updateRecipientWithNew( + private func updateRecipient( + email: String, state: RecipientState, - keyState: PubKeyState?, - for context: Either + keyState: PubKeyState? = nil ) { - let index: Int? = { - switch context { - case let .left(recipient): - return recipients.firstIndex(of: recipient) - case let .right(index): - return index.row - } - }() - - guard let recipientIndex = index else { return } - - let recipient = contextToSend.recipients[recipientIndex] - let needsReload = recipient.state != state || recipient.keyState != keyState - - guard needsReload else { return } + contextToSend.recipients.indices.forEach { + guard contextToSend.recipients[$0].email == email else { return } - contextToSend.recipients[recipientIndex].state = state - contextToSend.recipients[recipientIndex].keyState = keyState + let recipient = contextToSend.recipients[$0] + let needsReload = recipient.state != state || recipient.keyState != keyState - node.reloadSections([Section.password.rawValue], with: .automatic) - node.reloadRows(at: [recipientsIndexPath], with: .automatic) + contextToSend.recipients[$0].state = state + contextToSend.recipients[$0].keyState = keyState +// +// if needsReload, selectedRecipientType == nil || selectedRecipientType == recipient.type { +// let section = Section.recipientsSection(for: recipient.type).rawValue +// print(section) +// node.reloadSections([section], with: .automatic) +// // node.reloadRows(at: [recipientsIndexPath], with: .automatic) +// } + } + // contextToSend.updateRecipient(email: email, state: state, keyState: keyState) + // TODO: +// let index: Int? = { +// switch context { +// case let .left(recipient): +// return recipients.firstIndex(of: recipient) +// case let .right(index): +// return index.row +// } +// }() +// +// guard let recipientIndex = index else { return } +// +// let recipient = contextToSend.recipients[recipientIndex] +// let needsReload = recipient.state != state || recipient.keyState != keyState +// +// guard needsReload else { return } +// +// contextToSend.recipients[recipientIndex].state = state +// contextToSend.recipients[recipientIndex].keyState = keyState +// + // TODO + // node.reloadSections([Section.password.rawValue], with: .automatic) +// node.reloadRows(at: [recipientsIndexPath], with: .automatic) } private func handleRecipientSelection(with indexPath: IndexPath) { - var recipient = contextToSend.recipients[indexPath.row] - - if recipient.state.isSelected { - recipient.state = decorator.recipientIdleState - contextToSend.recipients[indexPath.row].state = decorator.recipientIdleState - evaluate(recipient: recipient) - } else { - contextToSend.recipients[indexPath.row].state = decorator.recipientSelectedState - } + guard var recipient = contextToSend.recipient(at: indexPath) else { return } + + // TODO +// if recipient.state.isSelected { +// recipient.state = decorator.recipientIdleState +// contextToSend.recipients[indexPath.row].state = decorator.recipientIdleState +// evaluate(recipient: recipient) +// } else { +// contextToSend.recipients[indexPath.row].state = decorator.recipientSelectedState +// } - node.reloadRows(at: [recipientsIndexPath], with: .automatic) +// node.reloadRows(at: [recipientsIndexPath], with: .automatic) - if !(textField?.isFirstResponder() ?? true) { - textField?.becomeFirstResponder() - } - textField?.reset() + // TODO +// if !(textField?.isFirstResponder() ?? true) { +// textField?.becomeFirstResponder() +// } +// textField?.reset() } private func handleRecipientAction(with indexPath: IndexPath) { - let recipient = contextToSend.recipients[indexPath.row] + guard let recipient = contextToSend.recipient(at: indexPath) else { return } + switch recipient.state { case .idle: handleRecipientSelection(with: indexPath) @@ -1077,13 +1131,16 @@ extension ComposeViewController { break case let .error(_, isRetryError): if isRetryError { - updateRecipientWithNew(state: decorator.recipientIdleState, - keyState: nil, - for: .right(indexPath)) + updateRecipient( + email: recipient.email, + state: decorator.recipientIdleState, + keyState: nil + ) evaluate(recipient: recipient) } else { - contextToSend.recipients.remove(at: indexPath.row) - node.reloadRows(at: [recipientsIndexPath], with: .fade) + contextToSend.removeRecipient(at: indexPath) + // TODO + // node.reloadRows(at: [recipientsIndexPath], with: .fade) } } } @@ -1144,11 +1201,19 @@ extension ComposeViewController { private func updateState(with newState: State) { state = newState + print("UPDATE STATE \(newState)") switch state { case .main: node.reloadData() case .searchEmails: - let sections: [Section] = [.password, .compose, .attachments] + // TODO: filter sections + let sections: [Section] + if let type = selectedRecipientType { + let selectedRecipientSection = Section.recipientsSection(for: type) + sections = Section.allCases.filter { $0 != selectedRecipientSection } + } else { + sections = Section.allCases + } node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) } } @@ -1240,6 +1305,7 @@ extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationCo composeMessageAttachment = MessageAttachment(cameraSourceMediaInfo: info) default: fatalError("No other image picker's sources should be used") } + guard let attachment = composeMessageAttachment else { showAlert(message: "files_picking_photos_error_message".localized) return diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index aeb261c4c..9c865c869 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -9,7 +9,7 @@ import FlowCryptUI import UIKit -typealias RecipientStateContext = RecipientEmailsCellNode.Input.StateContext +typealias RecipientStateContext = RecipientEmailsCellNode.RecipientInput.StateContext struct ComposeViewDecorator { let recipientIdleState: RecipientState = .idle(idleStateContext) @@ -269,7 +269,7 @@ extension ComposeViewDecorator { } // MARK: - RecipientEmailsCellNode.Input -extension RecipientEmailsCellNode.Input { +extension RecipientEmailsCellNode.RecipientInput { init(_ recipient: ComposeMessageRecipient) { self.init( email: recipient.email.lowercased().attributed( diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 014b08266..9ad21b699 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -610,7 +610,9 @@ extension ThreadDetailsViewController: NavigationChildController { func handleBackButtonTap() { let isRead = input.contains(where: { $0.rawMessage.isMessageRead }) logger.logInfo("Back button. Are all messages read \(isRead)") - onComplete(MessageAction.markAsRead(isRead), .init(thread: thread, folderPath: currentFolderPath, activeUserEmail: appContext.user.email)) + onComplete(MessageAction.markAsRead(isRead), + .init(thread: thread, folderPath: currentFolderPath, activeUserEmail: appContext.user.email) + ) navigationController?.popViewController(animated: true) } } diff --git a/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift b/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift index 3f3a8fef6..3c797eb82 100644 --- a/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift +++ b/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift @@ -26,7 +26,12 @@ extension CreateKeyError: CustomStringConvertible { var description: String { switch self { case .weakPassPhrase(let strength): - return "Pass phrase strength: \(strength.word.word)\ncrack time: \(strength.time)\n\nWe recommend to use 5-6 unrelated words as your Pass Phrase." + return """ + Pass phrase strength: \(strength.word.word) + crack time: \(strength.time) + + We recommend to use 5-6 unrelated words as your Pass Phrase. + """ case .missingUserEmail: return "backupServiceError_email".localized case .missingUserName: diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index ca8ce8d97..4caba0e47 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -10,9 +10,9 @@ import Foundation struct ComposeMessageContext: Equatable { var message: String? - var recipients: [ComposeMessageRecipient] = [] + var recipients: [ComposeMessageRecipient] var subject: String? - var attachments: [MessageAttachment] = [] + var attachments: [MessageAttachment] var messagePassword: String? { get { (_messagePassword ?? "").isNotEmpty ? _messagePassword : nil @@ -48,7 +48,95 @@ extension ComposeMessageContext { } var hasMessagePasswordIfNeeded: Bool { - guard hasRecipientsWithoutPubKey else { return true } - return hasMessagePassword + !hasRecipientsWithoutPubKey || hasMessagePassword + } + + func recipients(of type: RecipientType) -> [ComposeMessageRecipient] { + recipients.filter { $0.type == type } + } + + func recipientEmails(of type: RecipientType) -> [String] { + recipients(of: type).map(\.email) + } + + func recipient(at indexPath: IndexPath) -> ComposeMessageRecipient? { + // TODO + return nil +// guard let recipientType = RecipientType(rawValue: indexPath.section) else { return nil } +// +// switch recipientType { +// case .to: +// return to[indexPath.row] +// case .cc: +// return cc[indexPath.row] +// case .bcc: +// return bcc[indexPath.row] +// } + } + + mutating func add(recipient: ComposeMessageRecipient) { + recipients.append(recipient) + } + + mutating func set(recipients: [ComposeMessageRecipient], for recipientType: RecipientType) { + // TODO: +// switch recipientType { +// case .to: +// self.recipients[.to] = recipients +// case .cc: +// self.recipients[.cc] = recipients +// case .bcc: +// self.recipients[.bcc] = recipients +// } + } + + mutating func updateRecipient(email: String, state: RecipientState, keyState: PubKeyState?) { + recipients.indices.forEach { + guard recipients[$0].email == email else { return } + recipients[$0].state = state + recipients[$0].keyState = keyState + } + } + + mutating func updateRecipient(at indexPath: IndexPath, state: RecipientState, keyState: PubKeyState?) { + // TODO +// guard let recipientType = RecipientType(rawValue: indexPath.section) else { return } +// +// switch recipientType { +// case .to: +// to[indexPath.row].state = state +// to[indexPath.row].keyState = keyState +// case .cc: +// cc[indexPath.row].state = state +// cc[indexPath.row].keyState = keyState +// case .bcc: +// bcc[indexPath.row].state = state +// bcc[indexPath.row].keyState = keyState +// } + } + + mutating func removeRecipient(at indexPath: IndexPath) { + // TODO +// guard let recipientType = RecipientType(rawValue: indexPath.section) else { return } +// +// switch recipientType { +// case .to: +// to.remove(at: indexPath.row) +// case .cc: +// cc.remove(at: indexPath.row) +// case .bcc: +// bcc.remove(at: indexPath.row) +// } + } + + mutating func updateRecipient(email: String, state: RecipientState, keyState: PubKeyState) { + // TODO +// RecipientType.allCases.forEach { type in +// guard let index = recipients[type]?.firstIndex(where: { $0.email == email }) +// else { return } +// +// recipients[type]?[index].state = state +// recipients[type]?[index].keyState = keyState +// } } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift index 18094f5f9..4813bd857 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift @@ -10,12 +10,23 @@ import Foundation struct ComposeMessageRecipient { let email: String + let type: RecipientType var state: RecipientState var keyState: PubKeyState? } extension ComposeMessageRecipient: Equatable { static func == (lhs: ComposeMessageRecipient, rhs: ComposeMessageRecipient) -> Bool { - return lhs.email == rhs.email + return lhs.email == rhs.email && lhs.type == rhs.type + } +} + +enum RecipientType: String, CaseIterable { + case to, cc, bcc +} + +extension RecipientType { + var inputPlaceholder: String { + "compose_recipient_\(rawValue)".localized } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index a8c272d6f..ff38e6e0a 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -11,7 +11,7 @@ import Foundation import GoogleAPIClientForREST_Gmail import FlowCryptCommon -typealias RecipientState = RecipientEmailsCellNode.Input.State +typealias RecipientState = RecipientEmailsCellNode.RecipientInput.State protocol CoreComposeMessageType { func composeEmail(msg: SendableMsg, fmt: MsgFmt) async throws -> CoreRes.ComposeEmail @@ -78,12 +78,11 @@ final class ComposeMessageService { ) async throws -> SendableMsg { onStateChanged?(.validatingMessage) - let recipients = contextToSend.recipients - guard recipients.isNotEmpty else { + guard contextToSend.recipients.isNotEmpty else { throw MessageValidationError.emptyRecipient } - let emails = recipients.map(\.email) + let emails = contextToSend.recipients.map(\.email) let emptyEmails = emails.filter { !$0.hasContent } guard emails.isNotEmpty, emptyEmails.isEmpty else { @@ -112,7 +111,7 @@ final class ComposeMessageService { ? contextToSend.attachments.map { $0.toSendableMsgAttachment() } : [] - let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) + let recipientsWithPubKeys = try await getRecipientKeys(for: contextToSend.recipients) let validPubKeys = try validate( recipients: recipientsWithPubKeys, hasMessagePassword: contextToSend.hasMessagePassword @@ -134,9 +133,9 @@ final class ComposeMessageService { return SendableMsg( text: text, html: nil, - to: recipients.map(\.email), - cc: [], - bcc: [], + to: contextToSend.recipientEmails(of: .to), + cc: contextToSend.recipientEmails(of: .cc), + bcc: contextToSend.recipientEmails(of: .bcc), from: sender, subject: subject, replyToMimeMsg: replyToMimeMsg, diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 20cd613d8..d68853256 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -115,7 +115,9 @@ "compose_message_sent" = "Message sent"; "compose_enter_subject" = "Enter subject"; "compose_enter_secure" = "Enter secure message"; -"compose_recipient" = "Add Recipient"; +"compose_recipient_to" = "Add Recipient"; +"compose_recipient_cc" = "Add CC Recipient"; +"compose_recipient_bcc" = "Add BCC Recipient"; "compose_subject" = "Subject"; "compose_enable_google_contacts_search" = "Enable Google Contact Search"; "compose_no_contacts_found" = "No contacts found"; diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift index 13f4b35f8..092275ed3 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift @@ -21,7 +21,7 @@ final class RecipientEmailNode: CellNode { } struct Input { - let recipient: RecipientEmailsCellNode.Input + let recipient: RecipientEmailsCellNode.RecipientInput let width: CGFloat } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index ad52f08bb..8a38bd9c8 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -24,6 +24,8 @@ final public class RecipientEmailsCellNode: CellNode { private var onAction: RecipientTap? + private var textFieldAction: TextFieldAction? + private lazy var layout: LeftAlignedCollectionViewFlowLayout = { let layout = LeftAlignedCollectionViewFlowLayout() layout.scrollDirection = .vertical @@ -33,19 +35,29 @@ final public class RecipientEmailsCellNode: CellNode { return layout }() + public let textField: TextFieldNode + public lazy var collectionNode: ASCollectionNode = { let node = ASCollectionNode(collectionViewLayout: layout) node.accessibilityIdentifier = "aid-recipients-list" node.backgroundColor = .clear return node }() - private var collectionLayoutHeight: CGFloat - private var recipients: [Input] = [] + private var recipients: [RecipientInput] = [] - public init(recipients: [Input], height: CGFloat) { + public init(recipients: [RecipientInput], height: CGFloat, textFieldInput: TextFieldInput, textFieldAction: TextFieldAction?) { self.recipients = recipients self.collectionLayoutHeight = height + self.textFieldAction = textFieldAction + self.textField = TextFieldNode( + preferredHeight: 40, // input.height, + action: textFieldAction, + accessibilityIdentifier: "" // input.accessibilityIdentifier, + ) + self.textField.isLowercased = true + self.textField.keyboardType = .emailAddress + self.textField.attributedPlaceholderText = textFieldInput.placeholder super.init() collectionNode.dataSource = self collectionNode.delegate = self @@ -58,16 +70,24 @@ final public class RecipientEmailsCellNode: CellNode { } public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { - guard recipients.isNotEmpty else { - return ASInsetLayoutSpec(insets: .zero, child: collectionNode) - } + textField.style.preferredSize = CGSize( + width: 200, // input.width ?? (constrainedSize.max.width - input.insets.width), + height: 44 // input.height + ) - collectionNode.style.preferredSize.height = collectionLayoutHeight +// guard recipients.isNotEmpty else { +// return ASInsetLayoutSpec(insets: .zero, child: collectionNode) +// } + + collectionNode.style.preferredSize.height = recipients.isEmpty ? 0 : collectionLayoutHeight collectionNode.style.preferredSize.width = constrainedSize.max.width + let stack = ASStackLayoutSpec.vertical() + stack.children = recipients.isEmpty ? [textField] : [collectionNode, textField] + return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8), - child: collectionNode + insets: UIEdgeInsets.deviceSpecificTextInsets(top: 0, bottom: 0), + child: stack ) } } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift index 660e9b084..a502ad557 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift @@ -10,7 +10,7 @@ import UIKit // MARK: Input extension RecipientEmailsCellNode { - public struct Input { + public struct RecipientInput { public struct StateContext: Equatable { let backgroundColor, borderColor, textColor: UIColor let image: UIImage? @@ -107,4 +107,12 @@ extension RecipientEmailsCellNode { self.state = state } } + + public struct TextFieldInput { + let placeholder: NSAttributedString + + public init(placeholder: NSAttributedString) { + self.placeholder = placeholder + } + } } diff --git a/Podfile.lock b/Podfile.lock index c2c55c34e..5cde9aa36 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -15,7 +15,7 @@ PODS: - PINRemoteImage/PINCache (3.0.3): - PINCache (~> 3.0.3) - PINRemoteImage/Core - - SwiftFormat/CLI (0.49.3) + - SwiftFormat/CLI (0.49.4) - SwiftLint (0.46.2) - SwiftyRSA (1.7.0): - SwiftyRSA/ObjC (= 1.7.0) @@ -64,7 +64,7 @@ SPEC CHECKSUMS: PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086 PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20 PINRemoteImage: f1295b29f8c5e640e25335a1b2bd9d805171bd01 - SwiftFormat: a3b79e8b5f8ecdec7a716b998aee230d08512894 + SwiftFormat: 8acc16efcecb563206cbe90b4cb047cfdf9aafdb SwiftLint: 6bc52a21f0fd44cab9aa2dc8e534fb9f5e3ec507 SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6 Texture: 2e8ab2519452515f7f5a520f5a8f7e0a413abfa3 From ba731dbf52bcd96ff0f03c64639d01f43c26fa96 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 11 Feb 2022 11:59:44 +0200 Subject: [PATCH 02/19] #954 cc and bcc recipients --- FlowCrypt.xcodeproj/project.pbxproj | 4 + .../Compose/ComposeViewController.swift | 227 +++++++++++------- .../ComposeMessageContext.swift | 77 ++---- .../RecipientEmailTextFieldNode.swift | 61 +++++ .../Cell Nodes/RecipientEmailsCellNode.swift | 36 +-- .../Cell Nodes/TextFieldCellNode.swift | 4 +- 6 files changed, 240 insertions(+), 169 deletions(-) create mode 100644 FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 463ecd506..b60645028 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -71,6 +71,7 @@ 514C34DD276CE1C000FCAB79 /* ComposeMessageRecipient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */; }; 514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */; }; 5152F196277E5AED00BE8A5B /* MessageUploadDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */; }; + 5165ABCC27B526D100CCC379 /* RecipientEmailTextFieldNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165ABCB27B526D100CCC379 /* RecipientEmailTextFieldNode.swift */; }; 51689F3F2795C1D90050A9B8 /* ProcessedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */; }; 5168FB0B274F94D300131072 /* MessageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5168FB0A274F94D300131072 /* MessageAttachment.swift */; }; 51775C32270B01C200D7C944 /* PrvKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */; }; @@ -510,6 +511,7 @@ 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageRecipient.swift; sourceTree = ""; }; 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeMessageService+State.swift"; sourceTree = ""; }; 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageUploadDetails.swift; sourceTree = ""; }; + 5165ABCB27B526D100CCC379 /* RecipientEmailTextFieldNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientEmailTextFieldNode.swift; sourceTree = ""; }; 51689F3E2795C1D90050A9B8 /* ProcessedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedMessage.swift; sourceTree = ""; }; 5168FB0A274F94D300131072 /* MessageAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageAttachment.swift; sourceTree = ""; }; 51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrvKeyInfoTests.swift; sourceTree = ""; }; @@ -2047,6 +2049,7 @@ D24ABA6223FDB4FF002EE9DD /* RecipientEmailsCellNode.swift */, D26F132624509EB6009175BA /* RecipientEmailsCellNodeInput.swift */, D2531F3523FFEDA2007E5198 /* RecipientEmailNode.swift */, + 5165ABCB27B526D100CCC379 /* RecipientEmailTextFieldNode.swift */, 9F1797692368EE90002BF770 /* ButtonCellNode.swift */, 9F4453C3236B96F9005D7D05 /* DividerCellNode.swift */, 9F23EA4D237216FA0017DFED /* TextViewCellNode.swift */, @@ -2825,6 +2828,7 @@ D2A9CA38242618DF00E1D898 /* LinkButtonNode.swift in Sources */, D24FAFA42520BF9100BF46C5 /* CheckBoxCircleView.swift in Sources */, D2CDC3D72404704D002B045F /* RecipientEmailsCellNode.swift in Sources */, + 5165ABCC27B526D100CCC379 /* RecipientEmailTextFieldNode.swift in Sources */, D2717752242567EB00BDA9A9 /* KeyTextCellNode.swift in Sources */, 511D07E12769FBBA0050417B /* MessagePasswordCellNode.swift in Sources */, D211CE7B23FC59ED00D1CE38 /* InfoCellNode.swift in Sources */, diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index c9b97b50b..5dc4218c6 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -16,10 +16,21 @@ import PhotosUI * - Or from *ThreadDetailsViewController* controller by tapping on *reply* or *forward* **/ final class ComposeViewController: TableNodeViewController { - // TODO: - private var calculatedRecipientsPartHeight: CGFloat? { + private var calculatedRecipientsToPartHeight: CGFloat? { didSet { - let sections: [Section] = [.to, .cc, .bcc] + let sections: [Section] = [.to, .password] + node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) + } + } + private var calculatedRecipientsCcPartHeight: CGFloat? { + didSet { + let sections: [Section] = [.to, .cc, .password] + node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) + } + } + private var calculatedRecipientsBccPartHeight: CGFloat? { + didSet { + let sections: [Section] = [.to, .bcc, .password] node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) } } @@ -42,7 +53,7 @@ final class ComposeViewController: TableNodeViewController { private enum Section: Int, CaseIterable { case to, cc, bcc, password, compose, attachments - static func recipientsSection(for type: RecipientType) -> Section { + static func recipientsSection(type: RecipientType) -> Section { switch type { case .to: return .to @@ -54,6 +65,10 @@ final class ComposeViewController: TableNodeViewController { } } + private enum RecipientPart: Int, CaseIterable { + case list, input + } + private enum ComposePart: Int, CaseIterable { case topDivider, subject, subjectDivider, text } @@ -94,7 +109,7 @@ final class ComposeViewController: TableNodeViewController { navigationController?.navigationBar.frame.maxY ?? 0 } - private var selectedRecipientType: RecipientType? + private var selectedRecipientType: RecipientType? = .to init( appContext: AppContextWithUser, @@ -569,8 +584,10 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int { switch (state, section) { - case (.main, Section.to.rawValue), (.main, Section.cc.rawValue), (.main, Section.bcc.rawValue): - return 1 + case (.main, Section.to.rawValue): + return RecipientPart.allCases.count + case (.main, Section.cc.rawValue), (.main, Section.bcc.rawValue): + return selectedRecipientType == .to ? 0 : RecipientPart.allCases.count case (.main, Section.password.rawValue): return isMessagePasswordSupported && contextToSend.hasRecipientsWithoutPubKey ? 1 : 0 case (.main, Section.compose.rawValue): @@ -578,11 +595,11 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case (.main, Section.attachments.rawValue): return contextToSend.attachments.count case (.searchEmails, Section.to.rawValue): - return selectedRecipientType == .to ? 1 : 0 + return selectedRecipientType == .to ? RecipientPart.allCases.count : 0 case (.searchEmails, Section.cc.rawValue): - return selectedRecipientType == .cc ? 1 : 0 + return selectedRecipientType == .cc ? RecipientPart.allCases.count : 0 case (.searchEmails, Section.bcc.rawValue): - return selectedRecipientType == .bcc ? 1 : 0 + return selectedRecipientType == .bcc ? RecipientPart.allCases.count : 0 case let (.searchEmails(emails), RecipientType.allCases.count): return emails.isNotEmpty ? emails.count + 1 : 2 case (.searchEmails, RecipientType.allCases.count + 1): @@ -599,7 +616,12 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { switch (self.state, indexPath.section) { case (_, Section.to.rawValue), (_, Section.cc.rawValue), (_, Section.bcc.rawValue): - return self.recipientsNode(at: indexPath) + let recipientType = RecipientType.allCases[indexPath.section] + if indexPath.row == 0 { + return self.recipientsNode(type: recipientType) + } else { + return self.recipientInput(type: recipientType) + } case (.main, Section.password.rawValue): return self.messagePasswordNode() case (.main, Section.compose.rawValue): @@ -631,7 +653,6 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { switch indexPath.section { case RecipientType.allCases.count: let selectedEmail = emails[safe: indexPath.row-1] - print(selectedEmail) handleEndEditingAction(with: selectedEmail, for: recipientType) case RecipientType.allCases.count + 1: askForContactsPermission() @@ -738,61 +759,98 @@ extension ComposeViewController { } } - private func recipientsNode(at indexPath: IndexPath) -> ASCellNode { - let recipientType = RecipientType.allCases[indexPath.section] - let recipients = contextToSend.recipients(of: recipientType) + private func recipientsNode(type: RecipientType) -> ASCellNode { + let recipients = contextToSend.recipients(of: type) + + let calculatedHeight: CGFloat? + + switch type { + case .to: + calculatedHeight = calculatedRecipientsToPartHeight + case .cc: + calculatedHeight = calculatedRecipientsCcPartHeight + case .bcc: + calculatedHeight = calculatedRecipientsBccPartHeight + } return RecipientEmailsCellNode( recipients: recipients.map(RecipientEmailsCellNode.RecipientInput.init), - height: calculatedRecipientsPartHeight ?? Constants.minRecipientsPartHeight, + height: calculatedHeight ?? Constants.minRecipientsPartHeight, textFieldInput: RecipientEmailsCellNode.TextFieldInput( - placeholder: recipientType.inputPlaceholder.attributed( + placeholder: type.inputPlaceholder.attributed( .regular(17), color: .lightGray ) )) { [weak self] action in - self?.handle(textFieldAction: action, at: indexPath, for: recipientType) + self?.handle(textFieldAction: action, for: type) } .onLayoutHeightChanged { [weak self] layoutHeight in - guard self?.calculatedRecipientsPartHeight != layoutHeight, layoutHeight > 0 else { + // TODO: Improve + guard let self = self else { return } + let previousHeight: CGFloat? + + switch type { + case .to: + previousHeight = self.calculatedRecipientsToPartHeight + case .cc: + previousHeight = self.calculatedRecipientsCcPartHeight + case .bcc: + previousHeight = self.calculatedRecipientsBccPartHeight + } + + guard previousHeight != layoutHeight, layoutHeight > 0 else { return } - self?.calculatedRecipientsPartHeight = layoutHeight + + switch type { + case .to: + self.calculatedRecipientsToPartHeight = layoutHeight + case .cc: + self.calculatedRecipientsCcPartHeight = layoutHeight + case .bcc: + self.calculatedRecipientsBccPartHeight = layoutHeight + } } .onItemSelect { [weak self] (action: RecipientEmailsCellNode.RecipientEmailTapAction) in switch action { case let .imageTap(indexPath): - self?.handleRecipientAction(with: indexPath) + self?.handleRecipientAction(with: indexPath, type: type) case let .select(indexPath): - self?.handleRecipientSelection(with: indexPath) + self?.handleRecipientSelection(with: indexPath, type: type) } } } - private func recipientInput(at indexPath: IndexPath) -> ASCellNode { - let recipientType = RecipientType.allCases[indexPath.section] - - return TextFieldCellNode( + private func recipientInput(type: RecipientType) -> ASCellNode { + return RecipientEmailTextFieldNode( input: decorator.styledTextFieldInput( - with: recipientType.inputPlaceholder, + with: type.inputPlaceholder, keyboardType: .emailAddress, - accessibilityIdentifier: "aid-recipient-text-field-\(recipientType.rawValue)" - ) - ) { [weak self] action in - self?.handle(textFieldAction: action, at: indexPath, for: recipientType) - } - .onShouldReturn { textField -> Bool in - textField.resignFirstResponder() + accessibilityIdentifier: "aid-recipient-text-field-\(type.rawValue)" + ), + action: { [weak self] action in + self?.handle(textFieldAction: action, for: type) + }, + buttonAction: type == .to && !contextToSend.hasCcOrBccRecipients ? { [weak self] in + guard type == .to else { return } + self?.toggleRecipientsList() + } : nil + ) + .onShouldReturn { + $0.resignFirstResponder() return true } .onShouldChangeCharacters { [weak self] textField, character -> (Bool) in - self?.shouldChange(with: textField, and: character, for: recipientType) ?? true + self?.shouldChange(with: textField, and: character, for: type) ?? true } .then { $0.isLowercased = true - if self.input.isForward || self.input.isIdle { - $0.becomeFirstResponder() - } + + guard type == .to, + self.input.isForward || self.input.isIdle + else { return } + + $0.becomeFirstResponder() } } @@ -858,8 +916,7 @@ extension ComposeViewController { } } - private func handle(textFieldAction: TextFieldActionType, at indexPath: IndexPath, for recipientType: RecipientType) { - print("HANDLE TEXTFIELD \(textFieldAction)") + private func handle(textFieldAction: TextFieldActionType, for recipientType: RecipientType) { switch textFieldAction { case let .deleteBackward(textField): handleBackspaceAction(with: textField, for: recipientType) case let .didEndEditing(text): handleEndEditingAction(with: text, for: recipientType) @@ -872,11 +929,12 @@ extension ComposeViewController { guard shouldEvaluateRecipientInput, let text = text, text.isNotEmpty else { return } - print("HANDLE EDITING END \(text)") + let recipients = contextToSend.recipients(of: recipientType) - let recipientsIndexPath = recipientsIndexPath(for: recipientType) + let indexPath = recipientsIndexPath(type: recipientType, part: .list) - recipientsTextField(at: recipientsIndexPath)?.reset() + let textField = recipientsTextField(type: recipientType) + textField?.reset() // Set all recipients to idle state let idleRecipients: [ComposeMessageRecipient] = recipients.map { recipient in @@ -899,14 +957,14 @@ extension ComposeViewController { } else { // add new recipient contextToSend.add(recipient: newRecipient) - node.reloadRows(at: [recipientsIndexPath], with: .automatic) + node.reloadRows(at: [indexPath], with: .automatic) evaluate(recipient: newRecipient) // scroll to the latest recipient indexOfRecipient = recipients.endIndex - 1 } - let collectionNode = (node.nodeForRow(at: recipientsIndexPath) as? RecipientEmailsCellNode)?.collectionNode + let collectionNode = (node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode)?.collectionNode DispatchQueue.main.asyncAfter(deadline: .now() + 1) { collectionNode?.scrollToItem( at: IndexPath(row: indexOfRecipient, section: 0), @@ -921,19 +979,14 @@ extension ComposeViewController { updateState(with: .main) } - private func recipientsIndexPath(for recipientType: RecipientType) -> IndexPath { - switch recipientType { - case .to: - return [Section.to.rawValue, 0] - case .cc: - return [Section.cc.rawValue, 0] - case .bcc: - return [Section.bcc.rawValue, 0] - } + private func recipientsIndexPath(type: RecipientType, part: RecipientPart) -> IndexPath { + let section = Section.recipientsSection(type: type) + return IndexPath(row: part.rawValue, section: section.rawValue) } - private func recipientsTextField(at indexPath: IndexPath) -> TextFieldNode? { - (node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode)?.textField + private func recipientsTextField(type: RecipientType) -> TextFieldNode? { + let indexPath = recipientsIndexPath(type: type, part: .input) + return (node.nodeForRow(at: indexPath) as? RecipientEmailTextFieldNode)?.textField } private func handleBackspaceAction(with textField: UITextField, for recipientType: RecipientType) { @@ -941,15 +994,16 @@ extension ComposeViewController { var recipients = contextToSend.recipients(of: recipientType) - let recipientsIndexPath = recipientsIndexPath(for: recipientType) + let indexPath = recipientsIndexPath(type: recipientType, part: .input) let selectedRecipients = recipients.filter { $0.state.isSelected } + let recipientsSection = Section.recipientsSection(type: recipientType) guard selectedRecipients.isEmpty else { let notSelectedRecipients = recipients.filter { !$0.state.isSelected } contextToSend.set(recipients: notSelectedRecipients, for: recipientType) - // TODO: + node.reloadSections( - [Section.to.rawValue, Section.password.rawValue], + [recipientsSection.rawValue, Section.password.rawValue], with: .automatic ) @@ -961,7 +1015,7 @@ extension ComposeViewController { lastRecipient.state = self.decorator.recipientSelectedState recipients.append(lastRecipient) contextToSend.set(recipients: recipients, for: recipientType) - node.reloadRows(at: [recipientsIndexPath], with: .fade) + node.reloadRows(at: [indexPath], with: .fade) node.reloadSections([Section.password.rawValue], with: .automatic) } else { // dismiss keyboard if no recipients left @@ -977,6 +1031,12 @@ extension ComposeViewController { private func handleDidBeginEditing() { node.view.keyboardDismissMode = .none } + + private func toggleRecipientsList() { + let sections: [Section] = [.cc, .bcc] + selectedRecipientType = selectedRecipientType == .to ? nil : .to + node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) + } } // MARK: - Action Handling @@ -1100,33 +1160,33 @@ extension ComposeViewController { // node.reloadRows(at: [recipientsIndexPath], with: .automatic) } - private func handleRecipientSelection(with indexPath: IndexPath) { - guard var recipient = contextToSend.recipient(at: indexPath) else { return } + private func handleRecipientSelection(with indexPath: IndexPath, type: RecipientType) { + guard let recipient = contextToSend.recipient(at: indexPath.row, type: type) else { return } - // TODO -// if recipient.state.isSelected { -// recipient.state = decorator.recipientIdleState -// contextToSend.recipients[indexPath.row].state = decorator.recipientIdleState -// evaluate(recipient: recipient) -// } else { -// contextToSend.recipients[indexPath.row].state = decorator.recipientSelectedState -// } + let indexPath = recipientsIndexPath(type: type, part: .list) + let isSelected = recipient.state.isSelected + let state = isSelected ? decorator.recipientIdleState : decorator.recipientSelectedState + contextToSend.update(recipient: recipient.email, type: type, state: state) -// node.reloadRows(at: [recipientsIndexPath], with: .automatic) + if isSelected { + evaluate(recipient: recipient) + } - // TODO -// if !(textField?.isFirstResponder() ?? true) { -// textField?.becomeFirstResponder() -// } -// textField?.reset() + node.reloadRows(at: [indexPath], with: .automatic) + + let textField = recipientsTextField(type: type) + if !(textField?.isFirstResponder() ?? true) { + textField?.becomeFirstResponder() + } + textField?.reset() } - private func handleRecipientAction(with indexPath: IndexPath) { - guard let recipient = contextToSend.recipient(at: indexPath) else { return } + private func handleRecipientAction(with indexPath: IndexPath, type: RecipientType) { + guard let recipient = contextToSend.recipient(at: indexPath.row, type: type) else { return } switch recipient.state { case .idle: - handleRecipientSelection(with: indexPath) + handleRecipientSelection(with: indexPath, type: type) case .keyFound, .keyExpired, .keyRevoked, .keyNotFound, .invalidEmail, .selected: break case let .error(_, isRetryError): @@ -1138,9 +1198,9 @@ extension ComposeViewController { ) evaluate(recipient: recipient) } else { - contextToSend.removeRecipient(at: indexPath) - // TODO - // node.reloadRows(at: [recipientsIndexPath], with: .fade) + let listIndexPath = recipientsIndexPath(type: type, part: .list) + contextToSend.remove(recipient: recipient.email, type: type) + node.reloadRows(at: [listIndexPath], with: .automatic) } } } @@ -1201,7 +1261,6 @@ extension ComposeViewController { private func updateState(with newState: State) { state = newState - print("UPDATE STATE \(newState)") switch state { case .main: node.reloadData() @@ -1209,7 +1268,7 @@ extension ComposeViewController { // TODO: filter sections let sections: [Section] if let type = selectedRecipientType { - let selectedRecipientSection = Section.recipientsSection(for: type) + let selectedRecipientSection = Section.recipientsSection(type: type) sections = Section.allCases.filter { $0 != selectedRecipientSection } } else { sections = Section.allCases diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index 4caba0e47..4424fb907 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -43,6 +43,10 @@ extension ComposeMessageContext { messagePassword != nil } + var hasCcOrBccRecipients: Bool { + recipients.first(where: { $0.type == .cc || $0.type == .bcc }) != nil + } + var hasRecipientsWithoutPubKey: Bool { recipients.first { $0.keyState == .empty } != nil } @@ -59,19 +63,8 @@ extension ComposeMessageContext { recipients(of: type).map(\.email) } - func recipient(at indexPath: IndexPath) -> ComposeMessageRecipient? { - // TODO - return nil -// guard let recipientType = RecipientType(rawValue: indexPath.section) else { return nil } -// -// switch recipientType { -// case .to: -// return to[indexPath.row] -// case .cc: -// return cc[indexPath.row] -// case .bcc: -// return bcc[indexPath.row] -// } + func recipient(at index: Int, type: RecipientType) -> ComposeMessageRecipient? { + recipients(of: type)[safe: index] } mutating func add(recipient: ComposeMessageRecipient) { @@ -79,15 +72,13 @@ extension ComposeMessageContext { } mutating func set(recipients: [ComposeMessageRecipient], for recipientType: RecipientType) { - // TODO: -// switch recipientType { -// case .to: -// self.recipients[.to] = recipients -// case .cc: -// self.recipients[.cc] = recipients -// case .bcc: -// self.recipients[.bcc] = recipients -// } + self.recipients.removeAll(where: { $0.type == recipientType }) + self.recipients += recipients + } + + mutating func update(recipient: String, type: RecipientType, state: RecipientState) { + guard let index = recipients.firstIndex(where: { $0.email == recipient && $0.type == type }) else { return } + recipients[index].state = state } mutating func updateRecipient(email: String, state: RecipientState, keyState: PubKeyState?) { @@ -98,45 +89,17 @@ extension ComposeMessageContext { } } - mutating func updateRecipient(at indexPath: IndexPath, state: RecipientState, keyState: PubKeyState?) { - // TODO -// guard let recipientType = RecipientType(rawValue: indexPath.section) else { return } -// -// switch recipientType { -// case .to: -// to[indexPath.row].state = state -// to[indexPath.row].keyState = keyState -// case .cc: -// cc[indexPath.row].state = state -// cc[indexPath.row].keyState = keyState -// case .bcc: -// bcc[indexPath.row].state = state -// bcc[indexPath.row].keyState = keyState -// } + mutating func remove(recipient: String, type: RecipientType) { + recipients = recipients.filter { $0.email != recipient && $0.type != type } } - mutating func removeRecipient(at indexPath: IndexPath) { - // TODO -// guard let recipientType = RecipientType(rawValue: indexPath.section) else { return } -// -// switch recipientType { -// case .to: -// to.remove(at: indexPath.row) -// case .cc: -// cc.remove(at: indexPath.row) -// case .bcc: -// bcc.remove(at: indexPath.row) -// } - } - - mutating func updateRecipient(email: String, state: RecipientState, keyState: PubKeyState) { - // TODO -// RecipientType.allCases.forEach { type in -// guard let index = recipients[type]?.firstIndex(where: { $0.email == email }) -// else { return } + mutating func update(recipient: String, state: RecipientState, keyState: PubKeyState?) { + RecipientType.allCases.forEach { type in + // TODO: +// guard let index = recipients[type]?.firstIndex(where: { $0.email == recipient }) else { return } // // recipients[type]?[index].state = state // recipients[type]?[index].keyState = keyState -// } + } } } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift new file mode 100644 index 000000000..45fbbf2a0 --- /dev/null +++ b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift @@ -0,0 +1,61 @@ +// +// RecipientEmailTextFieldNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 10/02/22 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +public final class RecipientEmailTextFieldNode: TextFieldCellNode { + private var buttonAction: (() -> Void)? + + private lazy var buttonNode: ASButtonNode = { + let configuration = UIImage.SymbolConfiguration(pointSize: 16, weight: .light) + let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) + let button = ASButtonNode() + button.setImage(image, for: .normal) + button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) + button.addTarget(self, action: #selector(onButtonTap), forControlEvents: .touchUpInside) + return button + }() + + public init(input: TextFieldCellNode.Input, action: TextFieldAction? = nil, buttonAction: (() -> Void)?) { + super.init(input: input, action: action) + + self.buttonAction = buttonAction + } + + public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + if buttonAction != nil { + textField.style.preferredSize = CGSize( + // TODO + width: constrainedSize.max.width - 32 - 32, + height: input.height + ) + + let stack = ASStackLayoutSpec.horizontal() + stack.children = [textField, buttonNode] + + return ASInsetLayoutSpec(insets: input.insets, child: stack) + } else { + textField.style.preferredSize = CGSize( + width: input.width ?? (constrainedSize.max.width - input.insets.width), + height: input.height + ) + + return ASInsetLayoutSpec(insets: input.insets, child: textField) + } + } + + @objc private func onButtonTap() { + buttonAction?() + } + + @discardableResult + public override func becomeFirstResponder() -> Bool { + textField.becomeFirstResponder() + return true + } +} diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index 8a38bd9c8..ccd5f7d10 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -24,8 +24,6 @@ final public class RecipientEmailsCellNode: CellNode { private var onAction: RecipientTap? - private var textFieldAction: TextFieldAction? - private lazy var layout: LeftAlignedCollectionViewFlowLayout = { let layout = LeftAlignedCollectionViewFlowLayout() layout.scrollDirection = .vertical @@ -35,7 +33,14 @@ final public class RecipientEmailsCellNode: CellNode { return layout }() - public let textField: TextFieldNode + private lazy var toggleButton: ASButtonNode = { + let configuration = UIImage.SymbolConfiguration(pointSize: 16, weight: .light) + let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) + let button = ASButtonNode() + button.setImage(image, for: .normal) + button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) + return button + }() public lazy var collectionNode: ASCollectionNode = { let node = ASCollectionNode(collectionViewLayout: layout) @@ -49,15 +54,6 @@ final public class RecipientEmailsCellNode: CellNode { public init(recipients: [RecipientInput], height: CGFloat, textFieldInput: TextFieldInput, textFieldAction: TextFieldAction?) { self.recipients = recipients self.collectionLayoutHeight = height - self.textFieldAction = textFieldAction - self.textField = TextFieldNode( - preferredHeight: 40, // input.height, - action: textFieldAction, - accessibilityIdentifier: "" // input.accessibilityIdentifier, - ) - self.textField.isLowercased = true - self.textField.keyboardType = .emailAddress - self.textField.attributedPlaceholderText = textFieldInput.placeholder super.init() collectionNode.dataSource = self collectionNode.delegate = self @@ -70,24 +66,12 @@ final public class RecipientEmailsCellNode: CellNode { } public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { - textField.style.preferredSize = CGSize( - width: 200, // input.width ?? (constrainedSize.max.width - input.insets.width), - height: 44 // input.height - ) - -// guard recipients.isNotEmpty else { -// return ASInsetLayoutSpec(insets: .zero, child: collectionNode) -// } - collectionNode.style.preferredSize.height = recipients.isEmpty ? 0 : collectionLayoutHeight collectionNode.style.preferredSize.width = constrainedSize.max.width - let stack = ASStackLayoutSpec.vertical() - stack.children = recipients.isEmpty ? [textField] : [collectionNode, textField] - return ASInsetLayoutSpec( - insets: UIEdgeInsets.deviceSpecificTextInsets(top: 0, bottom: 0), - child: stack + insets: .zero, // TODO + child: collectionNode ) } } diff --git a/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift b/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift index f75ff6826..5c194ac4e 100644 --- a/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift @@ -8,7 +8,7 @@ import AsyncDisplayKit -public final class TextFieldCellNode: CellNode { +public class TextFieldCellNode: CellNode { public struct Input { public var placeholder: NSAttributedString? public var isSecureTextEntry = false @@ -51,7 +51,7 @@ public final class TextFieldCellNode: CellNode { private var textFieldAction: TextFieldAction? - private let input: Input + let input: Input public let textField: TextFieldNode From aca76488ee7fd1696480517ee7b04f00038c1717 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 11 Feb 2022 16:37:49 +0200 Subject: [PATCH 03/19] cc and bcc updates --- .../Compose/ComposeViewController.swift | 126 +++++++----------- .../ComposeMessageContext.swift | 11 +- .../RecipientEmailTextFieldNode.swift | 26 +++- Gemfile.lock | 5 +- 4 files changed, 81 insertions(+), 87 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 5dc4218c6..97643b606 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -109,7 +109,8 @@ final class ComposeViewController: TableNodeViewController { navigationController?.navigationBar.frame.maxY ?? 0 } - private var selectedRecipientType: RecipientType? = .to + private var selectedRecipientType: RecipientType? + private var shouldShowAllRecipientTypes = false init( appContext: AppContextWithUser, @@ -587,7 +588,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case (.main, Section.to.rawValue): return RecipientPart.allCases.count case (.main, Section.cc.rawValue), (.main, Section.bcc.rawValue): - return selectedRecipientType == .to ? 0 : RecipientPart.allCases.count + return shouldShowAllRecipientTypes ? RecipientPart.allCases.count : 0 case (.main, Section.password.rawValue): return isMessagePasswordSupported && contextToSend.hasRecipientsWithoutPubKey ? 1 : 0 case (.main, Section.compose.rawValue): @@ -762,54 +763,23 @@ extension ComposeViewController { private func recipientsNode(type: RecipientType) -> ASCellNode { let recipients = contextToSend.recipients(of: type) - let calculatedHeight: CGFloat? - - switch type { - case .to: - calculatedHeight = calculatedRecipientsToPartHeight - case .cc: - calculatedHeight = calculatedRecipientsCcPartHeight - case .bcc: - calculatedHeight = calculatedRecipientsBccPartHeight - } - return RecipientEmailsCellNode( recipients: recipients.map(RecipientEmailsCellNode.RecipientInput.init), - height: calculatedHeight ?? Constants.minRecipientsPartHeight, + height: recipientsNodeHeight(type: type) ?? Constants.minRecipientsPartHeight, textFieldInput: RecipientEmailsCellNode.TextFieldInput( placeholder: type.inputPlaceholder.attributed( .regular(17), color: .lightGray ) - )) { [weak self] action in + ), + textFieldAction: { [weak self] action in self?.handle(textFieldAction: action, for: type) - } + }) .onLayoutHeightChanged { [weak self] layoutHeight in - // TODO: Improve - guard let self = self else { return } - let previousHeight: CGFloat? - - switch type { - case .to: - previousHeight = self.calculatedRecipientsToPartHeight - case .cc: - previousHeight = self.calculatedRecipientsCcPartHeight - case .bcc: - previousHeight = self.calculatedRecipientsBccPartHeight - } - - guard previousHeight != layoutHeight, layoutHeight > 0 else { - return - } - - switch type { - case .to: - self.calculatedRecipientsToPartHeight = layoutHeight - case .cc: - self.calculatedRecipientsCcPartHeight = layoutHeight - case .bcc: - self.calculatedRecipientsBccPartHeight = layoutHeight - } + self?.updateRecipientsNode( + layoutHeight: layoutHeight, + type: type + ) } .onItemSelect { [weak self] (action: RecipientEmailsCellNode.RecipientEmailTapAction) in switch action { @@ -821,6 +791,34 @@ extension ComposeViewController { } } + private func recipientsNodeHeight(type: RecipientType) -> CGFloat? { + switch type { + case .to: + return calculatedRecipientsToPartHeight + case .cc: + return calculatedRecipientsCcPartHeight + case .bcc: + return calculatedRecipientsBccPartHeight + } + } + + private func updateRecipientsNode(layoutHeight: CGFloat, type: RecipientType) { + let currentHeight = self.recipientsNodeHeight(type: type) + + guard currentHeight != layoutHeight, layoutHeight > 0 else { + return + } + + switch type { + case .to: + self.calculatedRecipientsToPartHeight = layoutHeight + case .cc: + self.calculatedRecipientsCcPartHeight = layoutHeight + case .bcc: + self.calculatedRecipientsBccPartHeight = layoutHeight + } + } + private func recipientInput(type: RecipientType) -> ASCellNode { return RecipientEmailTextFieldNode( input: decorator.styledTextFieldInput( @@ -1034,7 +1032,8 @@ extension ComposeViewController { private func toggleRecipientsList() { let sections: [Section] = [.cc, .bcc] - selectedRecipientType = selectedRecipientType == .to ? nil : .to + shouldShowAllRecipientTypes.toggle() + node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) } } @@ -1126,38 +1125,16 @@ extension ComposeViewController { contextToSend.recipients[$0].state = state contextToSend.recipients[$0].keyState = keyState -// -// if needsReload, selectedRecipientType == nil || selectedRecipientType == recipient.type { -// let section = Section.recipientsSection(for: recipient.type).rawValue -// print(section) -// node.reloadSections([section], with: .automatic) -// // node.reloadRows(at: [recipientsIndexPath], with: .automatic) -// } - } - // contextToSend.updateRecipient(email: email, state: state, keyState: keyState) - // TODO: -// let index: Int? = { -// switch context { -// case let .left(recipient): -// return recipients.firstIndex(of: recipient) -// case let .right(index): -// return index.row -// } -// }() -// -// guard let recipientIndex = index else { return } -// -// let recipient = contextToSend.recipients[recipientIndex] -// let needsReload = recipient.state != state || recipient.keyState != keyState -// -// guard needsReload else { return } -// -// contextToSend.recipients[recipientIndex].state = state -// contextToSend.recipients[recipientIndex].keyState = keyState -// - // TODO - // node.reloadSections([Section.password.rawValue], with: .automatic) -// node.reloadRows(at: [recipientsIndexPath], with: .automatic) + + if needsReload, selectedRecipientType == nil || selectedRecipientType == recipient.type { + let sections: [Section] = [.recipientsSection(type: recipient.type), .password] + node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) + // TODO + // node.reloadRows(at: [recipientsIndexPath], with: .automatic) + } + } + + node.reloadSections([Section.password.rawValue], with: .automatic) } private func handleRecipientSelection(with indexPath: IndexPath, type: RecipientType) { @@ -1265,7 +1242,6 @@ extension ComposeViewController { case .main: node.reloadData() case .searchEmails: - // TODO: filter sections let sections: [Section] if let type = selectedRecipientType { let selectedRecipientSection = Section.recipientsSection(type: type) diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index 4424fb907..2c111d982 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -94,12 +94,11 @@ extension ComposeMessageContext { } mutating func update(recipient: String, state: RecipientState, keyState: PubKeyState?) { - RecipientType.allCases.forEach { type in - // TODO: -// guard let index = recipients[type]?.firstIndex(where: { $0.email == recipient }) else { return } -// -// recipients[type]?[index].state = state -// recipients[type]?[index].keyState = keyState + recipients.indices.forEach { + guard recipients[$0].email == recipient else { return } + + recipients[$0].state = state + recipients[$0].keyState = keyState } } } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift index 45fbbf2a0..2cc3a8c36 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift @@ -12,15 +12,22 @@ public final class RecipientEmailTextFieldNode: TextFieldCellNode { private var buttonAction: (() -> Void)? private lazy var buttonNode: ASButtonNode = { - let configuration = UIImage.SymbolConfiguration(pointSize: 16, weight: .light) + let configuration = UIImage.SymbolConfiguration(pointSize: 14, weight: .light) let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) let button = ASButtonNode() button.setImage(image, for: .normal) + button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0) button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) button.addTarget(self, action: #selector(onButtonTap), forControlEvents: .touchUpInside) return button }() + var buttonIsRotated = false { + didSet { + updateButton() + } + } + public init(input: TextFieldCellNode.Input, action: TextFieldAction? = nil, buttonAction: (() -> Void)?) { super.init(input: input, action: action) @@ -28,10 +35,14 @@ public final class RecipientEmailTextFieldNode: TextFieldCellNode { } public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let textFieldWidth = input.width ?? (constrainedSize.max.width - input.insets.width) + if buttonAction != nil { + let buttonSize = CGSize(width: 40, height: 40) + + buttonNode.style.preferredSize = buttonSize textField.style.preferredSize = CGSize( - // TODO - width: constrainedSize.max.width - 32 - 32, + width: textFieldWidth - buttonSize.width - input.insets.right - 4, height: input.height ) @@ -41,7 +52,7 @@ public final class RecipientEmailTextFieldNode: TextFieldCellNode { return ASInsetLayoutSpec(insets: input.insets, child: stack) } else { textField.style.preferredSize = CGSize( - width: input.width ?? (constrainedSize.max.width - input.insets.width), + width: textFieldWidth, height: input.height ) @@ -49,7 +60,14 @@ public final class RecipientEmailTextFieldNode: TextFieldCellNode { } } + private func updateButton() { + UIView.animate(withDuration: 0.3) { + self.buttonNode.view.transform = CGAffineTransform(rotationAngle: self.buttonIsRotated ? .pi : 0) + } + } + @objc private func onButtonTap() { + buttonIsRotated.toggle() buttonAction?() } diff --git a/Gemfile.lock b/Gemfile.lock index 62628cb22..190409ce1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,7 +17,7 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.552.0) + aws-partitions (1.554.0) aws-sdk-core (3.126.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) @@ -183,7 +183,7 @@ GEM google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.36.0) + google-cloud-storage (1.36.1) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) @@ -288,6 +288,7 @@ GEM PLATFORMS arm64-darwin-21 + ruby x86_64-darwin-20 DEPENDENCIES From 1355695174e70d3c50a48bd1dc932a0b9b6724ab Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 14 Feb 2022 13:39:30 +0200 Subject: [PATCH 04/19] =?UTF-8?q?fix=20=E2=80=98toggle=20recipients?= =?UTF-8?q?=E2=80=99=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Compose/ComposeViewController.swift | 27 ++++---- .../RecipientEmailTextFieldNode.swift | 36 +++++++---- .../Cell Nodes/RecipientEmailsCellNode.swift | 61 +++++++++++++++++-- 3 files changed, 94 insertions(+), 30 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 4d81c7d29..061de79b7 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -762,19 +762,19 @@ extension ComposeViewController { private func recipientsNode(type: RecipientType) -> ASCellNode { let recipients = contextToSend.recipients(of: type) + let shouldShowToggleButton = type == .to + && recipients.isNotEmpty + && !contextToSend.hasCcOrBccRecipients return RecipientEmailsCellNode( recipients: recipients.map(RecipientEmailsCellNode.RecipientInput.init), height: recipientsNodeHeight(type: type) ?? Constants.minRecipientsPartHeight, - textFieldInput: RecipientEmailsCellNode.TextFieldInput( - placeholder: type.inputPlaceholder.attributed( - .regular(17), - color: .lightGray - ) - ), - textFieldAction: { [weak self] action in - self?.handle(textFieldAction: action, for: type) - }) + isToggleButtonRotated: shouldShowAllRecipientTypes, + toggleButtonAction: shouldShowToggleButton ? { [weak self] in + guard type == .to else { return } + self?.toggleRecipientsList() + } : nil + ) .onLayoutHeightChanged { [weak self] layoutHeight in self?.updateRecipientsNode( layoutHeight: layoutHeight, @@ -820,6 +820,10 @@ extension ComposeViewController { } private func recipientInput(type: RecipientType) -> ASCellNode { + let shouldShowToggleButton = type == .to + && contextToSend.recipients(of: .to).isEmpty + && !contextToSend.hasCcOrBccRecipients + return RecipientEmailTextFieldNode( input: decorator.styledTextFieldInput( with: type.inputPlaceholder, @@ -829,7 +833,8 @@ extension ComposeViewController { action: { [weak self] action in self?.handle(textFieldAction: action, for: type) }, - buttonAction: type == .to && !contextToSend.hasCcOrBccRecipients ? { [weak self] in + isToggleButtonRotated: shouldShowAllRecipientTypes, + toggleButtonAction: shouldShowToggleButton ? { [weak self] in guard type == .to else { return } self?.toggleRecipientsList() } : nil @@ -934,7 +939,7 @@ extension ComposeViewController { let textField = recipientsTextField(type: recipientType) textField?.reset() - // Set all recipients to idle state + // Set all selected recipients to idle state let idleRecipients: [ComposeMessageRecipient] = recipients.map { recipient in var recipient = recipient if recipient.state.isSelected { diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift index 2cc3a8c36..8f3979656 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift @@ -9,45 +9,55 @@ import AsyncDisplayKit public final class RecipientEmailTextFieldNode: TextFieldCellNode { - private var buttonAction: (() -> Void)? + private var toggleButtonAction: (() -> Void)? - private lazy var buttonNode: ASButtonNode = { + private lazy var toggleButtonNode: ASButtonNode = { let configuration = UIImage.SymbolConfiguration(pointSize: 14, weight: .light) let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) let button = ASButtonNode() button.setImage(image, for: .normal) button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0) button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) - button.addTarget(self, action: #selector(onButtonTap), forControlEvents: .touchUpInside) + button.addTarget(self, action: #selector(onToggleButtonTap), forControlEvents: .touchUpInside) return button }() - var buttonIsRotated = false { + var isToggleButtonRotated = false { didSet { updateButton() } } - public init(input: TextFieldCellNode.Input, action: TextFieldAction? = nil, buttonAction: (() -> Void)?) { + public init( + input: TextFieldCellNode.Input, + action: TextFieldAction? = nil, + isToggleButtonRotated: Bool, + toggleButtonAction: (() -> Void)? + ) { super.init(input: input, action: action) - self.buttonAction = buttonAction + self.isToggleButtonRotated = isToggleButtonRotated + self.toggleButtonAction = toggleButtonAction } public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let textFieldWidth = input.width ?? (constrainedSize.max.width - input.insets.width) - if buttonAction != nil { + if toggleButtonAction != nil { let buttonSize = CGSize(width: 40, height: 40) - buttonNode.style.preferredSize = buttonSize + toggleButtonNode.style.preferredSize = buttonSize textField.style.preferredSize = CGSize( width: textFieldWidth - buttonSize.width - input.insets.right - 4, height: input.height ) let stack = ASStackLayoutSpec.horizontal() - stack.children = [textField, buttonNode] + stack.children = [textField, toggleButtonNode] + + DispatchQueue.main.async { + self.toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: self.isToggleButtonRotated ? .pi : 0) + } return ASInsetLayoutSpec(insets: input.insets, child: stack) } else { @@ -62,13 +72,13 @@ public final class RecipientEmailTextFieldNode: TextFieldCellNode { private func updateButton() { UIView.animate(withDuration: 0.3) { - self.buttonNode.view.transform = CGAffineTransform(rotationAngle: self.buttonIsRotated ? .pi : 0) + self.toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: self.isToggleButtonRotated ? .pi : 0) } } - @objc private func onButtonTap() { - buttonIsRotated.toggle() - buttonAction?() + @objc private func onToggleButtonTap() { + isToggleButtonRotated.toggle() + toggleButtonAction?() } @discardableResult diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index ccd5f7d10..eb56beb1d 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -24,6 +24,24 @@ final public class RecipientEmailsCellNode: CellNode { private var onAction: RecipientTap? + private var toggleButtonAction: (() -> Void)? + private lazy var toggleButtonNode: ASButtonNode = { + let configuration = UIImage.SymbolConfiguration(pointSize: 14, weight: .light) + let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) + let button = ASButtonNode() + button.setImage(image, for: .normal) + button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0) + button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) + button.addTarget(self, action: #selector(onToggleButtonTap), forControlEvents: .touchUpInside) + return button + }() + + var isToggleButtonRotated = false { + didSet { + updateButton() + } + } + private lazy var layout: LeftAlignedCollectionViewFlowLayout = { let layout = LeftAlignedCollectionViewFlowLayout() layout.scrollDirection = .vertical @@ -51,7 +69,10 @@ final public class RecipientEmailsCellNode: CellNode { private var collectionLayoutHeight: CGFloat private var recipients: [RecipientInput] = [] - public init(recipients: [RecipientInput], height: CGFloat, textFieldInput: TextFieldInput, textFieldAction: TextFieldAction?) { + public init(recipients: [RecipientInput], + height: CGFloat, + isToggleButtonRotated: Bool, + toggleButtonAction: (() -> Void)?) { self.recipients = recipients self.collectionLayoutHeight = height super.init() @@ -63,16 +84,44 @@ final public class RecipientEmailsCellNode: CellNode { } automaticallyManagesSubnodes = true + + self.isToggleButtonRotated = isToggleButtonRotated + self.toggleButtonAction = toggleButtonAction } public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { collectionNode.style.preferredSize.height = recipients.isEmpty ? 0 : collectionLayoutHeight - collectionNode.style.preferredSize.width = constrainedSize.max.width - return ASInsetLayoutSpec( - insets: .zero, // TODO - child: collectionNode - ) + if toggleButtonAction != nil { + let buttonSize = CGSize(width: 40, height: 50) + + toggleButtonNode.style.preferredSize = buttonSize + + collectionNode.style.preferredSize.width = constrainedSize.max.width - buttonSize.width - 4 + + let stack = ASStackLayoutSpec.horizontal() + stack.children = [collectionNode, toggleButtonNode] + + DispatchQueue.main.async { + self.toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: self.isToggleButtonRotated ? .pi : 0) + } + + return ASInsetLayoutSpec(insets: .zero, child: stack) + } else { + collectionNode.style.preferredSize.width = constrainedSize.max.width + return ASInsetLayoutSpec(insets: .zero, child: collectionNode) + } + } + + private func updateButton() { + UIView.animate(withDuration: 0.3) { + self.toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: self.isToggleButtonRotated ? .pi : 0) + } + } + + @objc private func onToggleButtonTap() { + isToggleButtonRotated.toggle() + toggleButtonAction?() } } From f80b5d022091d03febe0f52dc4a197bb878f96db Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 14 Feb 2022 16:28:56 +0200 Subject: [PATCH 05/19] fix unit tests --- .../Compose/ComposeViewController.swift | 107 ++++++++++-------- .../Services/ComposeMessageServiceTests.swift | 12 +- 2 files changed, 63 insertions(+), 56 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 061de79b7..5c1032363 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -53,6 +53,19 @@ final class ComposeViewController: TableNodeViewController { private enum Section: Int, CaseIterable { case to, cc, bcc, password, compose, attachments + var recipientType: RecipientType? { + switch self { + case .to: + return .to + case .cc: + return .cc + case .bcc: + return .bcc + case .password, .compose, .attachments: + return nil + } + } + static func recipientsSection(type: RecipientType) -> Section { switch type { case .to: @@ -88,7 +101,7 @@ final class ComposeViewController: TableNodeViewController { private let email: String private var isMessagePasswordSupported: Bool { - return clientConfiguration.isUsingFes + clientConfiguration.isUsingFes } private let search = PassthroughSubject() @@ -236,12 +249,11 @@ final class ComposeViewController: TableNodeViewController { private func observeComposeUpdates() { composeMessageService.onStateChanged { [weak self] state in - DispatchQueue.main.async { - self?.updateSpinner(with: state) - } + self?.updateSpinner(with: state) } } + @MainActor private func updateSpinner(with state: ComposeMessageService.State) { switch state { case .progressChanged(let progress): @@ -595,12 +607,11 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { return ComposePart.allCases.count case (.main, Section.attachments.rawValue): return contextToSend.attachments.count - case (.searchEmails, Section.to.rawValue): - return selectedRecipientType == .to ? RecipientPart.allCases.count : 0 - case (.searchEmails, Section.cc.rawValue): - return selectedRecipientType == .cc ? RecipientPart.allCases.count : 0 - case (.searchEmails, Section.bcc.rawValue): - return selectedRecipientType == .bcc ? RecipientPart.allCases.count : 0 + case (.searchEmails, Section.to.rawValue), + (.searchEmails, Section.cc.rawValue), + (.searchEmails, Section.bcc.rawValue): + let recipientType = Section(rawValue: section)?.recipientType + return selectedRecipientType == recipientType ? RecipientPart.allCases.count : 0 case let (.searchEmails(emails), RecipientType.allCases.count): return emails.isNotEmpty ? emails.count + 1 : 2 case (.searchEmails, RecipientType.allCases.count + 1): @@ -1002,11 +1013,12 @@ extension ComposeViewController { let recipientsSection = Section.recipientsSection(type: recipientType) guard selectedRecipients.isEmpty else { + let sectionsToReload: [Section] = [.to, recipientsSection, .password] let notSelectedRecipients = recipients.filter { !$0.state.isSelected } contextToSend.set(recipients: notSelectedRecipients, for: recipientType) node.reloadSections( - [recipientsSection.rawValue, Section.password.rawValue], + IndexSet(sectionsToReload.map(\.rawValue).unique()), with: .automatic ) @@ -1019,7 +1031,9 @@ extension ComposeViewController { recipients.append(lastRecipient) contextToSend.set(recipients: recipients, for: recipientType) node.reloadRows(at: [indexPath], with: .fade) - node.reloadSections([Section.password.rawValue], with: .automatic) + + let sectionsToReload: [Section] = [.to, .password] + node.reloadSections(IndexSet(sectionsToReload.map(\.rawValue)), with: .automatic) } else { // dismiss keyboard if no recipients left textField.resignFirstResponder() @@ -1126,20 +1140,18 @@ extension ComposeViewController { state: RecipientState, keyState: PubKeyState? = nil ) { - contextToSend.recipients.indices.forEach { - guard contextToSend.recipients[$0].email == email else { return } + contextToSend.recipients.indices.forEach { index in + guard contextToSend.recipients[index].email == email else { return } - let recipient = contextToSend.recipients[$0] + let recipient = contextToSend.recipients[index] let needsReload = recipient.state != state || recipient.keyState != keyState - contextToSend.recipients[$0].state = state - contextToSend.recipients[$0].keyState = keyState + contextToSend.recipients[index].state = state + contextToSend.recipients[index].keyState = keyState if needsReload, selectedRecipientType == nil || selectedRecipientType == recipient.type { - let sections: [Section] = [.recipientsSection(type: recipient.type), .password] - node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) - // TODO - // node.reloadRows(at: [recipientsIndexPath], with: .automatic) + let section = Section.recipientsSection(type: recipient.type) + node.reloadSections(IndexSet([section.rawValue]), with: .automatic) } } @@ -1149,7 +1161,7 @@ extension ComposeViewController { private func handleRecipientSelection(with indexPath: IndexPath, type: RecipientType) { guard let recipient = contextToSend.recipient(at: indexPath.row, type: type) else { return } - let indexPath = recipientsIndexPath(type: type, part: .list) + let listIndexPath = recipientsIndexPath(type: type, part: .list) let isSelected = recipient.state.isSelected let state = isSelected ? decorator.recipientIdleState : decorator.recipientSelectedState contextToSend.update(recipient: recipient.email, type: type, state: state) @@ -1158,7 +1170,7 @@ extension ComposeViewController { evaluate(recipient: recipient) } - node.reloadRows(at: [indexPath], with: .automatic) + node.reloadRows(at: [listIndexPath], with: .automatic) let textField = recipientsTextField(type: type) if !(textField?.isFirstResponder() ?? true) { @@ -1287,34 +1299,29 @@ extension ComposeViewController: PHPickerViewControllerDelegate { } private func handleResults(_ results: [PHPickerResult]) { - let itemProvider = results.first?.itemProvider - if itemProvider?.hasItemConformingToTypeIdentifier("public.movie") == true { - itemProvider?.loadFileRepresentation( - forTypeIdentifier: "public.movie", - completionHandler: { [weak self] url, error in - DispatchQueue.main.async { - self?.handleRepresentation( - url: url, - error: error, - isVideo: true - ) - } - } - ) - } else { - itemProvider?.loadFileRepresentation( - forTypeIdentifier: "public.image", - completionHandler: { [weak self] url, error in - DispatchQueue.main.async { - self?.handleRepresentation( - url: url, - error: error, - isVideo: false - ) - } - } - ) + guard let itemProvider = results.first?.itemProvider else { return } + + enum MediaType: String { + case image, movie + + var identifier: String { "public.\(rawValue)" } } + + let isVideo = itemProvider.hasItemConformingToTypeIdentifier(MediaType.movie.identifier) + let mediaType: MediaType = isVideo ? .movie : .image + + itemProvider.loadFileRepresentation( + forTypeIdentifier: mediaType.identifier, + completionHandler: { [weak self] url, error in + DispatchQueue.main.async { + self?.handleRepresentation( + url: url, + error: error, + isVideo: isVideo + ) + } + } + ) } private func handleRepresentation(url: URL?, error: Error?, isVideo: Bool) { diff --git a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift index da644e63b..c38753bc7 100644 --- a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift @@ -16,9 +16,9 @@ class ComposeMessageServiceTests: XCTestCase { var sut: ComposeMessageService! let recipients: [ComposeMessageRecipient] = [ - ComposeMessageRecipient(email: "test@gmail.com", state: recipientIdleState), - ComposeMessageRecipient(email: "test2@gmail.com", state: recipientIdleState), - ComposeMessageRecipient(email: "test3@gmail.com", state: recipientIdleState) + ComposeMessageRecipient(email: "test@gmail.com", type: .to, state: recipientIdleState), + ComposeMessageRecipient(email: "test2@gmail.com", type: .to, state: recipientIdleState), + ComposeMessageRecipient(email: "test3@gmail.com", type: .to, state: recipientIdleState) ] let validKeyDetails = EncryptedStorageMock.createFakeKeyDetails(expiration: nil) let keypair = Keypair( @@ -77,9 +77,9 @@ class ComposeMessageServiceTests: XCTestCase { func testValidateMessageInputWithWhitespaceRecipients() async { let recipients: [ComposeMessageRecipient] = [ - ComposeMessageRecipient(email: " ", state: recipientIdleState), - ComposeMessageRecipient(email: " ", state: recipientIdleState), - ComposeMessageRecipient(email: "sdfff", state: recipientIdleState) + ComposeMessageRecipient(email: " ", type: .to, state: recipientIdleState), + ComposeMessageRecipient(email: " ", type: .to, state: recipientIdleState), + ComposeMessageRecipient(email: "sdfff", type: .to, state: recipientIdleState) ] do { _ = try await sut.validateAndProduceSendableMsg( From ecf888d38894b1ffe0299ac7e6bc2cbaced21e20 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 14 Feb 2022 17:07:07 +0200 Subject: [PATCH 06/19] code cleanup --- .../Controllers/Compose/ComposeViewController.swift | 8 ++++---- .../Controllers/Compose/ComposeViewDecorator.swift | 4 ++-- .../ComposeMessageContext.swift | 5 ++++- .../ComposeMessageService.swift | 9 +++++---- FlowCryptUI/Cell Nodes/RecipientEmailNode.swift | 2 +- FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift | 4 ++-- .../Cell Nodes/RecipientEmailsCellNodeInput.swift | 10 +--------- 7 files changed, 19 insertions(+), 23 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 5c1032363..a023c65ec 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -230,9 +230,9 @@ final class ComposeViewController: TableNodeViewController { } private func evaluateAllRecipients() { - contextToSend.recipients.forEach { - evaluate(recipient: $0) - } + for recipient in contextToSend.recipients { + evaluate(recipient: recipient) + } } func update(with message: Message) { @@ -778,7 +778,7 @@ extension ComposeViewController { && !contextToSend.hasCcOrBccRecipients return RecipientEmailsCellNode( - recipients: recipients.map(RecipientEmailsCellNode.RecipientInput.init), + recipients: recipients.map(RecipientEmailsCellNode.Input.init), height: recipientsNodeHeight(type: type) ?? Constants.minRecipientsPartHeight, isToggleButtonRotated: shouldShowAllRecipientTypes, toggleButtonAction: shouldShowToggleButton ? { [weak self] in diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 9c865c869..aeb261c4c 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -9,7 +9,7 @@ import FlowCryptUI import UIKit -typealias RecipientStateContext = RecipientEmailsCellNode.RecipientInput.StateContext +typealias RecipientStateContext = RecipientEmailsCellNode.Input.StateContext struct ComposeViewDecorator { let recipientIdleState: RecipientState = .idle(idleStateContext) @@ -269,7 +269,7 @@ extension ComposeViewDecorator { } // MARK: - RecipientEmailsCellNode.Input -extension RecipientEmailsCellNode.RecipientInput { +extension RecipientEmailsCellNode.Input { init(_ recipient: ComposeMessageRecipient) { self.init( email: recipient.email.lowercased().attributed( diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index 2c111d982..eabbe1dc7 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -77,7 +77,10 @@ extension ComposeMessageContext { } mutating func update(recipient: String, type: RecipientType, state: RecipientState) { - guard let index = recipients.firstIndex(where: { $0.email == recipient && $0.type == type }) else { return } + guard let index = recipients.firstIndex(where: { + $0.email == recipient && $0.type == type + }) else { return } + recipients[index].state = state } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 39276d50f..6fa74bd38 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -11,7 +11,7 @@ import Foundation import GoogleAPIClientForREST_Gmail import FlowCryptCommon -typealias RecipientState = RecipientEmailsCellNode.RecipientInput.State +typealias RecipientState = RecipientEmailsCellNode.Input.State protocol CoreComposeMessageType { func composeEmail(msg: SendableMsg, fmt: MsgFmt) async throws -> CoreRes.ComposeEmail @@ -78,11 +78,12 @@ final class ComposeMessageService { ) async throws -> SendableMsg { onStateChanged?(.validatingMessage) - guard contextToSend.recipients.isNotEmpty else { + let recipients = contextToSend.recipients + guard recipients.isNotEmpty else { throw MessageValidationError.emptyRecipient } - let emails = contextToSend.recipients.map(\.email) + let emails = recipients.map(\.email) let emptyEmails = emails.filter { !$0.hasContent } guard emails.isNotEmpty, emptyEmails.isEmpty else { @@ -111,7 +112,7 @@ final class ComposeMessageService { ? contextToSend.attachments.map { $0.toSendableMsgAttachment() } : [] - let recipientsWithPubKeys = try await getRecipientKeys(for: contextToSend.recipients) + let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) let validPubKeys = try validate( recipients: recipientsWithPubKeys, hasMessagePassword: contextToSend.hasMessagePassword diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift index 092275ed3..13f4b35f8 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift @@ -21,7 +21,7 @@ final class RecipientEmailNode: CellNode { } struct Input { - let recipient: RecipientEmailsCellNode.RecipientInput + let recipient: RecipientEmailsCellNode.Input let width: CGFloat } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index eb56beb1d..41fee0b83 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -67,9 +67,9 @@ final public class RecipientEmailsCellNode: CellNode { return node }() private var collectionLayoutHeight: CGFloat - private var recipients: [RecipientInput] = [] + private var recipients: [Input] = [] - public init(recipients: [RecipientInput], + public init(recipients: [Input], height: CGFloat, isToggleButtonRotated: Bool, toggleButtonAction: (() -> Void)?) { diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift index a502ad557..660e9b084 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift @@ -10,7 +10,7 @@ import UIKit // MARK: Input extension RecipientEmailsCellNode { - public struct RecipientInput { + public struct Input { public struct StateContext: Equatable { let backgroundColor, borderColor, textColor: UIColor let image: UIImage? @@ -107,12 +107,4 @@ extension RecipientEmailsCellNode { self.state = state } } - - public struct TextFieldInput { - let placeholder: NSAttributedString - - public init(placeholder: NSAttributedString) { - self.placeholder = placeholder - } - } } From 44765c9446944d36b6669a142c329e140d6f3856 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 15 Feb 2022 14:01:46 +0200 Subject: [PATCH 07/19] update compose screen sections --- .../Compose/ComposeViewController.swift | 189 +++++++++--------- .../ComposeMessageContext.swift | 2 +- .../ComposeMessageRecipient.swift | 2 +- .../RecipientEmailTextFieldNode.swift | 2 + 4 files changed, 95 insertions(+), 100 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index a023c65ec..f9b077891 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -18,20 +18,17 @@ import PhotosUI final class ComposeViewController: TableNodeViewController { private var calculatedRecipientsToPartHeight: CGFloat? { didSet { - let sections: [Section] = [.to, .password] - node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) + reload(sections: [.recipients(.to), .password]) } } private var calculatedRecipientsCcPartHeight: CGFloat? { didSet { - let sections: [Section] = [.to, .cc, .password] - node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) + reload(sections: [.recipients(.to), .recipients(.cc), .password]) } } private var calculatedRecipientsBccPartHeight: CGFloat? { didSet { - let sections: [Section] = [.to, .bcc, .password] - node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) + reload(sections: [.recipients(.to), .recipients(.bcc), .password]) } } @@ -50,31 +47,11 @@ final class ComposeViewController: TableNodeViewController { case main, searchEmails([String]) } - private enum Section: Int, CaseIterable { - case to, cc, bcc, password, compose, attachments + private enum Section: Hashable { + case recipients(RecipientType), password, compose, attachments, searchResults, contacts - var recipientType: RecipientType? { - switch self { - case .to: - return .to - case .cc: - return .cc - case .bcc: - return .bcc - case .password, .compose, .attachments: - return nil - } - } - - static func recipientsSection(type: RecipientType) -> Section { - switch type { - case .to: - return .to - case .cc: - return .cc - case .bcc: - return .bcc - } + static var recipientsSections: [Section] { + RecipientType.allCases.map { Section.recipients($0) } } } @@ -125,6 +102,8 @@ final class ComposeViewController: TableNodeViewController { private var selectedRecipientType: RecipientType? private var shouldShowAllRecipientTypes = false + private var sectionsList: [Section] = [] + init( appContext: AppContextWithUser, notificationCenter: NotificationCenter = .default, @@ -357,6 +336,8 @@ extension ComposeViewController { $0.view.contentInsetAdjustmentBehavior = .never $0.view.keyboardDismissMode = .interactive } + + updateState(with: .main) } private func setupQuote() { @@ -592,29 +573,28 @@ extension ComposeViewController { extension ComposeViewController: ASTableDelegate, ASTableDataSource { func numberOfSections(in _: ASTableNode) -> Int { - Section.allCases.count + sectionsList.count } func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int { - switch (state, section) { - case (.main, Section.to.rawValue): + guard let sectionItem = sectionsList[safe: section] else { return 0 } + + switch (state, sectionItem) { + case (.main, .recipients(.to)): return RecipientPart.allCases.count - case (.main, Section.cc.rawValue), (.main, Section.bcc.rawValue): + case (.main, .recipients(.cc)), (.main, .recipients(.bcc)): return shouldShowAllRecipientTypes ? RecipientPart.allCases.count : 0 - case (.main, Section.password.rawValue): + case (.main, .password): return isMessagePasswordSupported && contextToSend.hasRecipientsWithoutPubKey ? 1 : 0 - case (.main, Section.compose.rawValue): + case (.main, .compose): return ComposePart.allCases.count - case (.main, Section.attachments.rawValue): + case (.main, .attachments): return contextToSend.attachments.count - case (.searchEmails, Section.to.rawValue), - (.searchEmails, Section.cc.rawValue), - (.searchEmails, Section.bcc.rawValue): - let recipientType = Section(rawValue: section)?.recipientType - return selectedRecipientType == recipientType ? RecipientPart.allCases.count : 0 - case let (.searchEmails(emails), RecipientType.allCases.count): + case (.searchEmails, .recipients(let type)): + return selectedRecipientType == type ? RecipientPart.allCases.count : 0 + case let (.searchEmails(emails), .searchResults): return emails.isNotEmpty ? emails.count + 1 : 2 - case (.searchEmails, RecipientType.allCases.count + 1): + case (.searchEmails, .contacts): return cloudContactProvider.isContactsScopeEnabled ? 0 : 2 default: return 0 @@ -624,35 +604,37 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { // swiftlint:disable cyclomatic_complexity func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { return { [weak self] in - guard let self = self else { return ASCellNode() } + guard let self = self, + let section = self.sectionsList[safe: indexPath.section] + else { return ASCellNode() } - switch (self.state, indexPath.section) { - case (_, Section.to.rawValue), (_, Section.cc.rawValue), (_, Section.bcc.rawValue): + switch (self.state, section) { + case (_, .recipients(.to)), (_, .recipients(.cc)), (_, .recipients(.bcc)): let recipientType = RecipientType.allCases[indexPath.section] if indexPath.row == 0 { return self.recipientsNode(type: recipientType) } else { return self.recipientInput(type: recipientType) } - case (.main, Section.password.rawValue): + case (.main, .password): return self.messagePasswordNode() - case (.main, Section.compose.rawValue): + case (.main, .compose): guard let part = ComposePart(rawValue: indexPath.row) else { return ASCellNode() } switch part { case .subject: return self.subjectNode() case .text: return self.textNode() case .topDivider, .subjectDivider: return DividerCellNode() } - case (.main, Section.attachments.rawValue): + case (.main, .attachments): guard !self.contextToSend.attachments.isEmpty else { return ASCellNode() } return self.attachmentNode(for: indexPath.row) - case let (.searchEmails(emails), RecipientType.allCases.count): + case let (.searchEmails(emails), .searchResults): guard indexPath.row > 0 else { return DividerCellNode() } guard emails.isNotEmpty else { return self.noSearchResultsNode() } return InfoCellNode(input: self.decorator.styledRecipientInfo(with: emails[indexPath.row-1])) - case (.searchEmails, RecipientType.allCases.count + 1): + case (.searchEmails, .contacts): return indexPath.row == 0 ? DividerCellNode() : self.enableGoogleContactsNode() default: return ASCellNode() @@ -679,6 +661,14 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { navigationController?.pushViewController(controller, animated: true ) } } + + private func reload(sections: [Section]) { + let indexes = sectionsList.enumerated().compactMap { index, section in + sections.contains(section) ? index : nil + } + + node.reloadSections(IndexSet(indexes), with: .automatic) + } } // MARK: - Nodes @@ -846,7 +836,6 @@ extension ComposeViewController { }, isToggleButtonRotated: shouldShowAllRecipientTypes, toggleButtonAction: shouldShowToggleButton ? { [weak self] in - guard type == .to else { return } self?.toggleRecipientsList() } : nil ) @@ -876,7 +865,7 @@ extension ComposeViewController { ), onDeleteTap: { [weak self] in self?.contextToSend.attachments.safeRemove(at: index) - self?.node.reloadSections([Section.attachments.rawValue], with: .automatic) + self?.reload(sections: [.attachments]) } ) } @@ -945,7 +934,6 @@ extension ComposeViewController { else { return } let recipients = contextToSend.recipients(of: recipientType) - let indexPath = recipientsIndexPath(type: recipientType, part: .list) let textField = recipientsTextField(type: recipientType) textField?.reset() @@ -964,6 +952,8 @@ extension ComposeViewController { let newRecipient = ComposeMessageRecipient(email: text, type: recipientType, state: decorator.recipientIdleState) let indexOfRecipient: Int + let indexPath = recipientsIndexPath(type: recipientType, part: .list) + if let index = idleRecipients.firstIndex(where: { $0.email == newRecipient.email }) { // recipient already in list evaluate(recipient: newRecipient) @@ -971,20 +961,26 @@ extension ComposeViewController { } else { // add new recipient contextToSend.add(recipient: newRecipient) - node.reloadRows(at: [indexPath], with: .automatic) + + if let indexPath = indexPath { + node.reloadRows(at: [indexPath], with: .automatic) + } + evaluate(recipient: newRecipient) // scroll to the latest recipient indexOfRecipient = recipients.endIndex - 1 } - let collectionNode = (node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode)?.collectionNode - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - collectionNode?.scrollToItem( - at: IndexPath(row: indexOfRecipient, section: 0), - at: .bottom, - animated: true - ) + if let indexPath = indexPath, + let emailsNode = node.nodeForRow(at: indexPath) as? RecipientEmailsCellNode { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + emailsNode.collectionNode.scrollToItem( + at: IndexPath(row: indexOfRecipient, section: 0), + at: .bottom, + animated: true + ) + } } node.view.keyboardDismissMode = .interactive @@ -993,13 +989,13 @@ extension ComposeViewController { updateState(with: .main) } - private func recipientsIndexPath(type: RecipientType, part: RecipientPart) -> IndexPath { - let section = Section.recipientsSection(type: type) - return IndexPath(row: part.rawValue, section: section.rawValue) + private func recipientsIndexPath(type: RecipientType, part: RecipientPart) -> IndexPath? { + guard let section = sectionsList.firstIndex(of: .recipients(type)) else { return nil } + return IndexPath(row: part.rawValue, section: section) } private func recipientsTextField(type: RecipientType) -> TextFieldNode? { - let indexPath = recipientsIndexPath(type: type, part: .input) + guard let indexPath = recipientsIndexPath(type: type, part: .input) else { return nil } return (node.nodeForRow(at: indexPath) as? RecipientEmailTextFieldNode)?.textField } @@ -1008,19 +1004,12 @@ extension ComposeViewController { var recipients = contextToSend.recipients(of: recipientType) - let indexPath = recipientsIndexPath(type: recipientType, part: .input) let selectedRecipients = recipients.filter { $0.state.isSelected } - let recipientsSection = Section.recipientsSection(type: recipientType) guard selectedRecipients.isEmpty else { - let sectionsToReload: [Section] = [.to, recipientsSection, .password] let notSelectedRecipients = recipients.filter { !$0.state.isSelected } contextToSend.set(recipients: notSelectedRecipients, for: recipientType) - - node.reloadSections( - IndexSet(sectionsToReload.map(\.rawValue).unique()), - with: .automatic - ) + reload(sections: [.recipients(.to), .recipients(recipientType), .password]) return } @@ -1030,10 +1019,12 @@ extension ComposeViewController { lastRecipient.state = self.decorator.recipientSelectedState recipients.append(lastRecipient) contextToSend.set(recipients: recipients, for: recipientType) - node.reloadRows(at: [indexPath], with: .fade) - let sectionsToReload: [Section] = [.to, .password] - node.reloadSections(IndexSet(sectionsToReload.map(\.rawValue)), with: .automatic) + if let indexPath = recipientsIndexPath(type: recipientType, part: .input) { + node.reloadRows(at: [indexPath], with: .automatic) + } + + reload(sections: [.recipients(.to), .password]) } else { // dismiss keyboard if no recipients left textField.resignFirstResponder() @@ -1050,10 +1041,8 @@ extension ComposeViewController { } private func toggleRecipientsList() { - let sections: [Section] = [.cc, .bcc] shouldShowAllRecipientTypes.toggle() - - node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) + reload(sections: [.recipients(.cc), .recipients(.bcc)]) } } @@ -1150,18 +1139,14 @@ extension ComposeViewController { contextToSend.recipients[index].keyState = keyState if needsReload, selectedRecipientType == nil || selectedRecipientType == recipient.type { - let section = Section.recipientsSection(type: recipient.type) - node.reloadSections(IndexSet([section.rawValue]), with: .automatic) + reload(sections: [.recipients(recipient.type), .password]) } } - - node.reloadSections([Section.password.rawValue], with: .automatic) } private func handleRecipientSelection(with indexPath: IndexPath, type: RecipientType) { guard let recipient = contextToSend.recipient(at: indexPath.row, type: type) else { return } - let listIndexPath = recipientsIndexPath(type: type, part: .list) let isSelected = recipient.state.isSelected let state = isSelected ? decorator.recipientIdleState : decorator.recipientSelectedState contextToSend.update(recipient: recipient.email, type: type, state: state) @@ -1170,7 +1155,9 @@ extension ComposeViewController { evaluate(recipient: recipient) } - node.reloadRows(at: [listIndexPath], with: .automatic) + if let listIndexPath = recipientsIndexPath(type: type, part: .list) { + node.reloadRows(at: [listIndexPath], with: .automatic) + } let textField = recipientsTextField(type: type) if !(textField?.isFirstResponder() ?? true) { @@ -1196,9 +1183,11 @@ extension ComposeViewController { ) evaluate(recipient: recipient) } else { - let listIndexPath = recipientsIndexPath(type: type, part: .list) contextToSend.remove(recipient: recipient.email, type: type) - node.reloadRows(at: [listIndexPath], with: .automatic) + + if let listIndexPath = recipientsIndexPath(type: type, part: .list) { + node.reloadRows(at: [listIndexPath], with: .automatic) + } } } } @@ -1207,7 +1196,7 @@ extension ComposeViewController { private func setMessagePassword() { Task { contextToSend.messagePassword = await enterMessagePassword() - node.reloadSections([Section.password.rawValue], with: .automatic) + reload(sections: [.password]) } } @@ -1261,16 +1250,20 @@ extension ComposeViewController { switch state { case .main: + sectionsList = Section.recipientsSections + [.password, .compose, .attachments] node.reloadData() case .searchEmails: - let sections: [Section] + sectionsList = Section.recipientsSections + [.searchResults, .contacts] + + let sectionsToReload: [Section] + if let type = selectedRecipientType { - let selectedRecipientSection = Section.recipientsSection(type: type) - sections = Section.allCases.filter { $0 != selectedRecipientSection } + sectionsToReload = sectionsList.filter { $0 != .recipients(type) } } else { - sections = Section.allCases + sectionsToReload = sectionsList } - node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) + + reload(sections: sectionsToReload) } } } @@ -1285,7 +1278,7 @@ extension ComposeViewController: UIDocumentPickerDelegate { return } appendAttachmentIfAllowed(attachment) - node.reloadSections([Section.attachments.rawValue], with: .automatic) + reload(sections: [.attachments]) } } @@ -1338,7 +1331,7 @@ extension ComposeViewController: PHPickerViewControllerDelegate { } appendAttachmentIfAllowed(composeMessageAttachment) - node.reloadSections([Section.attachments.rawValue], with: .automatic) + reload(sections: [.attachments]) } } @@ -1362,7 +1355,7 @@ extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationCo return } appendAttachmentIfAllowed(attachment) - node.reloadSections([Section.attachments.rawValue], with: .automatic) + reload(sections: [.attachments]) } private func appendAttachmentIfAllowed(_ attachment: MessageAttachment) { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index eabbe1dc7..d1ef1f642 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -80,7 +80,7 @@ extension ComposeMessageContext { guard let index = recipients.firstIndex(where: { $0.email == recipient && $0.type == type }) else { return } - + recipients[index].state = state } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift index 4813bd857..4134f6bd0 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift @@ -21,7 +21,7 @@ extension ComposeMessageRecipient: Equatable { } } -enum RecipientType: String, CaseIterable { +enum RecipientType: String, CaseIterable, Hashable { case to, cc, bcc } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift index 8f3979656..fe3a0af04 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift @@ -38,6 +38,8 @@ public final class RecipientEmailTextFieldNode: TextFieldCellNode { self.isToggleButtonRotated = isToggleButtonRotated self.toggleButtonAction = toggleButtonAction + + self.textField.accessibilityIdentifier = input.accessibilityIdentifier } public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { From 0475ad9b69ea71ea3139e4bd0b3447ec613c0311 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 15 Feb 2022 17:37:30 +0200 Subject: [PATCH 08/19] improve recipient textfield focus --- .../Compose/ComposeViewController.swift | 54 +++++++++++++------ .../RecipientEmailTextFieldNode.swift | 1 + .../tests/screenobjects/new-message.screen.ts | 24 ++++++--- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index f9b077891..baa24e79a 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -202,6 +202,10 @@ final class ComposeViewController: TableNodeViewController { didLayoutSubviews = true node.contentInset.top = topContentInset + + if input.isForward || input.isIdle { + focusRecipientInput(type: .to) + } } deinit { @@ -644,11 +648,13 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { if case let .searchEmails(emails) = state, let recipientType = selectedRecipientType { - switch indexPath.section { - case RecipientType.allCases.count: + guard let section = sectionsList[safe: indexPath.section] else { return } + + switch section { + case .searchResults: let selectedEmail = emails[safe: indexPath.row-1] handleEndEditingAction(with: selectedEmail, for: recipientType) - case RecipientType.allCases.count + 1: + case .contacts: askForContactsPermission() default: break @@ -846,15 +852,6 @@ extension ComposeViewController { .onShouldChangeCharacters { [weak self] textField, character -> (Bool) in self?.shouldChange(with: textField, and: character, for: type) ?? true } - .then { - $0.isLowercased = true - - guard type == .to, - self.input.isForward || self.input.isIdle - else { return } - - $0.becomeFirstResponder() - } } private func attachmentNode(for index: Int) -> ASCellNode { @@ -893,6 +890,14 @@ extension ComposeViewController { // MARK: - Recipients Input extension ComposeViewController { + private func focusRecipientInput(type: RecipientType) { + guard let indexPath = recipientsIndexPath(type: type, part: .input), + let inputNode = node.nodeForRow(at: indexPath) as? RecipientEmailTextFieldNode + else { return } + + inputNode.textField.becomeFirstResponder() + } + private func shouldChange(with textField: UITextField, and character: String, for recipientType: RecipientType) -> Bool { func nextResponder() { guard let node = node.visibleNodes[safe: ComposePart.subject.rawValue] as? TextFieldCellNode else { return } @@ -1139,7 +1144,11 @@ extension ComposeViewController { contextToSend.recipients[index].keyState = keyState if needsReload, selectedRecipientType == nil || selectedRecipientType == recipient.type { - reload(sections: [.recipients(recipient.type), .password]) + reload(sections: [.password]) + + if let listIndexPath = recipientsIndexPath(type: recipient.type, part: .list) { + node.reloadRows(at: [listIndexPath], with: .automatic) + } } } } @@ -1252,18 +1261,31 @@ extension ComposeViewController { case .main: sectionsList = Section.recipientsSections + [.password, .compose, .attachments] node.reloadData() + + if let recipientType = selectedRecipientType { + focusRecipientInput(type: recipientType) + } case .searchEmails: + let previousSectionsCount = sectionsList.count sectionsList = Section.recipientsSections + [.searchResults, .contacts] - let sectionsToReload: [Section] + let deletedSectionsCount = previousSectionsCount - sectionsList.count + let sectionsToReload: [Section] if let type = selectedRecipientType { sectionsToReload = sectionsList.filter { $0 != .recipients(type) } } else { sectionsToReload = sectionsList } - reload(sections: sectionsToReload) + node.performBatchUpdates { + if deletedSectionsCount > 0 { + let sectionsToDelete = sectionsList.count.. { - await (await this.addRecipientField).setValue(recipient); + await (await this.addToRecipientField).setValue(recipient); await browser.pause(500); await (await $(SELECTORS.RETURN_BUTTON)).click() }; @@ -114,7 +124,7 @@ class NewMessageScreen extends BaseScreen { setAddRecipientByName = async (name: string, email: string) => { await browser.pause(500); // stability fix for transition animation - await (await this.addRecipientField).setValue(name); + await (await this.addToRecipientField).setValue(name); await ElementHelper.waitAndClick(await $(`~${email}`)); }; @@ -132,7 +142,7 @@ class NewMessageScreen extends BaseScreen { }; checkRecipientsTextFieldIsInvisible = async () => { - await ElementHelper.waitElementInvisible(await this.addRecipientField); + await ElementHelper.waitElementInvisible(await this.addToRecipientField); } checkRecipientsList = async(recipients: string[]) => { From 2455ff7b987eb75f3289df19b0ac0b76fc0cd9e0 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 15 Feb 2022 23:38:36 +0200 Subject: [PATCH 09/19] autofocus fixes --- .../Compose/ComposeViewController.swift | 31 ++++++------------- .../Cell Nodes/RecipientEmailsCellNode.swift | 2 +- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index baa24e79a..fc00ef6e3 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -99,7 +99,7 @@ final class ComposeViewController: TableNodeViewController { navigationController?.navigationBar.frame.maxY ?? 0 } - private var selectedRecipientType: RecipientType? + private var selectedRecipientType: RecipientType? = .to private var shouldShowAllRecipientTypes = false private var sectionsList: [Section] = [] @@ -202,10 +202,6 @@ final class ComposeViewController: TableNodeViewController { didLayoutSubviews = true node.contentInset.top = topContentInset - - if input.isForward || input.isIdle { - focusRecipientInput(type: .to) - } } deinit { @@ -664,7 +660,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { file: contextToSend.attachments[indexPath.row], shouldShowDownloadButton: false ) - navigationController?.pushViewController(controller, animated: true ) + navigationController?.pushViewController(controller, animated: true) } } @@ -852,6 +848,11 @@ extension ComposeViewController { .onShouldChangeCharacters { [weak self] textField, character -> (Bool) in self?.shouldChange(with: textField, and: character, for: type) ?? true } + .then { + if type == selectedRecipientType { + $0.becomeFirstResponder() + } + } } private func attachmentNode(for index: Int) -> ASCellNode { @@ -890,14 +891,6 @@ extension ComposeViewController { // MARK: - Recipients Input extension ComposeViewController { - private func focusRecipientInput(type: RecipientType) { - guard let indexPath = recipientsIndexPath(type: type, part: .input), - let inputNode = node.nodeForRow(at: indexPath) as? RecipientEmailTextFieldNode - else { return } - - inputNode.textField.becomeFirstResponder() - } - private func shouldChange(with textField: UITextField, and character: String, for recipientType: RecipientType) -> Bool { func nextResponder() { guard let node = node.visibleNodes[safe: ComposePart.subject.rawValue] as? TextFieldCellNode else { return } @@ -1019,17 +1012,15 @@ extension ComposeViewController { return } - if var lastRecipient = recipients.last { + if var lastRecipient = recipients.popLast() { // select last recipient in a list lastRecipient.state = self.decorator.recipientSelectedState recipients.append(lastRecipient) contextToSend.set(recipients: recipients, for: recipientType) - if let indexPath = recipientsIndexPath(type: recipientType, part: .input) { + if let indexPath = recipientsIndexPath(type: recipientType, part: .list) { node.reloadRows(at: [indexPath], with: .automatic) } - - reload(sections: [.recipients(.to), .password]) } else { // dismiss keyboard if no recipients left textField.resignFirstResponder() @@ -1261,10 +1252,6 @@ extension ComposeViewController { case .main: sectionsList = Section.recipientsSections + [.password, .compose, .attachments] node.reloadData() - - if let recipientType = selectedRecipientType { - focusRecipientInput(type: recipientType) - } case .searchEmails: let previousSectionsCount = sectionsList.count sectionsList = Section.recipientsSections + [.searchResults, .contacts] diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index 41fee0b83..2ce0e81ae 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -97,7 +97,7 @@ final public class RecipientEmailsCellNode: CellNode { toggleButtonNode.style.preferredSize = buttonSize - collectionNode.style.preferredSize.width = constrainedSize.max.width - buttonSize.width - 4 + collectionNode.style.preferredSize.width = max(0, constrainedSize.max.width - buttonSize.width - 4) let stack = ASStackLayoutSpec.horizontal() stack.children = [collectionNode, toggleButtonNode] From 767d7fbbd9cbacda6c298711786c0b3442ab4f0e Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 16 Feb 2022 15:30:23 +0200 Subject: [PATCH 10/19] add RecipientToggleButtonNode --- FlowCrypt.xcodeproj/project.pbxproj | 4 ++ .../Compose/ComposeViewController.swift | 16 +++-- .../RecipientEmailTextFieldNode.swift | 62 +++++------------ .../Cell Nodes/RecipientEmailsCellNode.swift | 66 +++++------------- .../RecipientToggleButtonNode.swift | 67 +++++++++++++++++++ 5 files changed, 112 insertions(+), 103 deletions(-) create mode 100644 FlowCryptUI/Cell Nodes/RecipientToggleButtonNode.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index b60645028..5f50c06f5 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 32DCAF95A6A329C3136B1C8E /* Imap+msg.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA55C094E9745AA1FD210 /* Imap+msg.swift */; }; 32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA9701B2D5052225A0414 /* SignInViewController.swift */; }; 50531BE42629B9A80039BAE9 /* AttachmentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */; }; + 51074E6427BD0C5800FBB124 /* RecipientToggleButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51074E6327BD0C5800FBB124 /* RecipientToggleButtonNode.swift */; }; 5109A77C272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5109A77B272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift */; }; 511D07E12769FBBA0050417B /* MessagePasswordCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */; }; 511D07E3276A2DF80050417B /* ButtonWithPaddingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E2276A2DF80050417B /* ButtonWithPaddingNode.swift */; }; @@ -501,6 +502,7 @@ 4753E9A27694B4D34C980FFA /* Pods_FlowCrypt.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FlowCrypt.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F928D493732294B4E521900 /* Pods-FlowCryptUIApplication.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUIApplication.release.xcconfig"; path = "Target Support Files/Pods-FlowCryptUIApplication/Pods-FlowCryptUIApplication.release.xcconfig"; sourceTree = ""; }; 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentNode.swift; sourceTree = ""; }; + 51074E6327BD0C5800FBB124 /* RecipientToggleButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientToggleButtonNode.swift; sourceTree = ""; }; 5109A77B272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftAlignedCollectionViewFlowLayout.swift; sourceTree = ""; }; 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePasswordCellNode.swift; sourceTree = ""; }; 511D07E2276A2DF80050417B /* ButtonWithPaddingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonWithPaddingNode.swift; sourceTree = ""; }; @@ -2050,6 +2052,7 @@ D26F132624509EB6009175BA /* RecipientEmailsCellNodeInput.swift */, D2531F3523FFEDA2007E5198 /* RecipientEmailNode.swift */, 5165ABCB27B526D100CCC379 /* RecipientEmailTextFieldNode.swift */, + 51074E6327BD0C5800FBB124 /* RecipientToggleButtonNode.swift */, 9F1797692368EE90002BF770 /* ButtonCellNode.swift */, 9F4453C3236B96F9005D7D05 /* DividerCellNode.swift */, 9F23EA4D237216FA0017DFED /* TextViewCellNode.swift */, @@ -2815,6 +2818,7 @@ D28655952423BFF60066F52E /* SideMenuOptionalView.swift in Sources */, D2717754242568A600BDA9A9 /* NavigationBarActionButton.swift in Sources */, D2A9CA3A2426198600E1D898 /* SignInDescriptionNode.swift in Sources */, + 51074E6427BD0C5800FBB124 /* RecipientToggleButtonNode.swift in Sources */, 5133B6742716E5EA00C95463 /* LabelCellNode.swift in Sources */, D211CE7123FC35AC00D1CE38 /* TextFieldCellNode.swift in Sources */, 9FA19890253C841F008C9CF2 /* TableViewController.swift in Sources */, diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index fc00ef6e3..d1428a141 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -921,8 +921,8 @@ extension ComposeViewController { switch textFieldAction { case let .deleteBackward(textField): handleBackspaceAction(with: textField, for: recipientType) case let .didEndEditing(text): handleEndEditingAction(with: text, for: recipientType) - case let .editingChanged(text): handleEditingChanged(with: text, for: recipientType) - case .didBeginEditing: handleDidBeginEditing() + case let .editingChanged(text): handleEditingChanged(with: text) + case .didBeginEditing: handleDidBeginEditing(recipientType: recipientType) } } @@ -1007,7 +1007,11 @@ extension ComposeViewController { guard selectedRecipients.isEmpty else { let notSelectedRecipients = recipients.filter { !$0.state.isSelected } contextToSend.set(recipients: notSelectedRecipients, for: recipientType) - reload(sections: [.recipients(.to), .recipients(recipientType), .password]) + reload(sections: [.recipients(.to), .password]) + + if let indexPath = recipientsIndexPath(type: recipientType, part: .list) { + node.reloadRows(at: [indexPath], with: .automatic) + } return } @@ -1027,12 +1031,12 @@ extension ComposeViewController { } } - private func handleEditingChanged(with text: String?, for recipientType: RecipientType) { - selectedRecipientType = recipientType + private func handleEditingChanged(with text: String?) { search.send(text ?? "") } - private func handleDidBeginEditing() { + private func handleDidBeginEditing(recipientType: RecipientType) { + selectedRecipientType = recipientType node.view.keyboardDismissMode = .none } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift index 4c09da87c..9dbd76eb0 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailTextFieldNode.swift @@ -8,23 +8,16 @@ import AsyncDisplayKit -public final class RecipientEmailTextFieldNode: TextFieldCellNode { - private var toggleButtonAction: (() -> Void)? +public final class RecipientEmailTextFieldNode: TextFieldCellNode, RecipientToggleButtonNode { + var toggleButtonAction: (() -> Void)? - private lazy var toggleButtonNode: ASButtonNode = { - let configuration = UIImage.SymbolConfiguration(pointSize: 14, weight: .light) - let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) - let button = ASButtonNode() - button.setImage(image, for: .normal) - button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0) - button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) - button.addTarget(self, action: #selector(onToggleButtonTap), forControlEvents: .touchUpInside) - return button + lazy var toggleButtonNode: ASButtonNode = { + createToggleButton() }() var isToggleButtonRotated = false { didSet { - updateButton() + updateToggleButton(animated: true) } } @@ -39,47 +32,22 @@ public final class RecipientEmailTextFieldNode: TextFieldCellNode { self.isLowercased = true self.isToggleButtonRotated = isToggleButtonRotated self.toggleButtonAction = toggleButtonAction - - self.textField.accessibilityIdentifier = input.accessibilityIdentifier } public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let textFieldWidth = input.width ?? (constrainedSize.max.width - input.insets.width) - - if toggleButtonAction != nil { - let buttonSize = CGSize(width: 40, height: 40) - - toggleButtonNode.style.preferredSize = buttonSize - textField.style.preferredSize = CGSize( - width: textFieldWidth - buttonSize.width - input.insets.right - 4, - height: input.height - ) - - let stack = ASStackLayoutSpec.horizontal() - stack.children = [textField, toggleButtonNode] - - DispatchQueue.main.async { - self.toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: self.isToggleButtonRotated ? .pi : 0) - } - - return ASInsetLayoutSpec(insets: input.insets, child: stack) - } else { - textField.style.preferredSize = CGSize( - width: textFieldWidth, - height: input.height - ) - - return ASInsetLayoutSpec(insets: input.insets, child: textField) - } - } - - private func updateButton() { - UIView.animate(withDuration: 0.3) { - self.toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: self.isToggleButtonRotated ? .pi : 0) - } + let textFieldSize = CGSize(width: textFieldWidth, height: input.height) + let buttonSize = CGSize(width: input.height, height: input.height) + + return createLayout( + contentNode: textField, + contentSize: textFieldSize, + insets: input.insets, + buttonSize: buttonSize + ) } - @objc private func onToggleButtonTap() { + func onToggleButtonTap() { isToggleButtonRotated.toggle() toggleButtonAction?() } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index 2ce0e81ae..711ced9cf 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -9,7 +9,7 @@ import AsyncDisplayKit import FlowCryptCommon -final public class RecipientEmailsCellNode: CellNode { +final public class RecipientEmailsCellNode: CellNode, RecipientToggleButtonNode { public typealias RecipientTap = (RecipientEmailTapAction) -> Void public enum RecipientEmailTapAction { @@ -24,21 +24,13 @@ final public class RecipientEmailsCellNode: CellNode { private var onAction: RecipientTap? - private var toggleButtonAction: (() -> Void)? - private lazy var toggleButtonNode: ASButtonNode = { - let configuration = UIImage.SymbolConfiguration(pointSize: 14, weight: .light) - let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) - let button = ASButtonNode() - button.setImage(image, for: .normal) - button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0) - button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) - button.addTarget(self, action: #selector(onToggleButtonTap), forControlEvents: .touchUpInside) - return button + lazy var toggleButtonNode: ASButtonNode = { + createToggleButton() }() - + var toggleButtonAction: (() -> Void)? var isToggleButtonRotated = false { didSet { - updateButton() + updateToggleButton(animated: true) } } @@ -51,15 +43,6 @@ final public class RecipientEmailsCellNode: CellNode { return layout }() - private lazy var toggleButton: ASButtonNode = { - let configuration = UIImage.SymbolConfiguration(pointSize: 16, weight: .light) - let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) - let button = ASButtonNode() - button.setImage(image, for: .normal) - button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) - return button - }() - public lazy var collectionNode: ASCollectionNode = { let node = ASCollectionNode(collectionViewLayout: layout) node.accessibilityIdentifier = "aid-recipients-list" @@ -90,36 +73,19 @@ final public class RecipientEmailsCellNode: CellNode { } public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { - collectionNode.style.preferredSize.height = recipients.isEmpty ? 0 : collectionLayoutHeight - - if toggleButtonAction != nil { - let buttonSize = CGSize(width: 40, height: 50) - - toggleButtonNode.style.preferredSize = buttonSize - - collectionNode.style.preferredSize.width = max(0, constrainedSize.max.width - buttonSize.width - 4) - - let stack = ASStackLayoutSpec.horizontal() - stack.children = [collectionNode, toggleButtonNode] - - DispatchQueue.main.async { - self.toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: self.isToggleButtonRotated ? .pi : 0) - } - - return ASInsetLayoutSpec(insets: .zero, child: stack) - } else { - collectionNode.style.preferredSize.width = constrainedSize.max.width - return ASInsetLayoutSpec(insets: .zero, child: collectionNode) - } - } - - private func updateButton() { - UIView.animate(withDuration: 0.3) { - self.toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: self.isToggleButtonRotated ? .pi : 0) - } + let collectionNodeHeight = recipients.isEmpty ? 0 : collectionLayoutHeight + let collectionNodeSize = CGSize(width: constrainedSize.max.width, height: collectionNodeHeight) + let buttonSize = CGSize(width: 40, height: 50) + + return createLayout( + contentNode: collectionNode, + contentSize: collectionNodeSize, + insets: .zero, + buttonSize: buttonSize + ) } - @objc private func onToggleButtonTap() { + func onToggleButtonTap() { isToggleButtonRotated.toggle() toggleButtonAction?() } diff --git a/FlowCryptUI/Cell Nodes/RecipientToggleButtonNode.swift b/FlowCryptUI/Cell Nodes/RecipientToggleButtonNode.swift new file mode 100644 index 000000000..c7ec357f6 --- /dev/null +++ b/FlowCryptUI/Cell Nodes/RecipientToggleButtonNode.swift @@ -0,0 +1,67 @@ +// +// RecipientToggleButtonNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 16/02/22 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +@objc protocol RecipientToggleButtonNode: AnyObject { + var isToggleButtonRotated: Bool { get } + var toggleButtonNode: ASButtonNode { get } + var toggleButtonAction: (() -> Void)? { get } + func onToggleButtonTap() +} + +extension RecipientToggleButtonNode { + func createToggleButton() -> ASButtonNode { + let configuration = UIImage.SymbolConfiguration(pointSize: 14, weight: .light) + let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) + let button = ASButtonNode() + button.setImage(image, for: .normal) + button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0) + button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) + button.addTarget(self, action: #selector(RecipientToggleButtonNode.onToggleButtonTap), forControlEvents: .touchUpInside) + return button + } + + func updateToggleButton(animated: Bool) { + func rotateButton(angle: CGFloat) { + toggleButtonNode.view.transform = CGAffineTransform(rotationAngle: angle) + } + + let angle = self.isToggleButtonRotated ? .pi : 0 + if animated { + UIView.animate(withDuration: 0.3) { + rotateButton(angle: angle) + } + } else { + rotateButton(angle: angle) + } + } + + func createLayout(contentNode: ASDisplayNode, contentSize: CGSize, insets: UIEdgeInsets, buttonSize: CGSize) -> ASInsetLayoutSpec { + if toggleButtonAction != nil { + toggleButtonNode.style.preferredSize = buttonSize + + DispatchQueue.main.async { + self.updateToggleButton(animated: false) + } + + let contentWidth = contentSize.width - buttonSize.width - insets.width / 2 - 4 + contentNode.style.preferredSize = CGSize( + width: max(0, contentWidth), + height: contentSize.height + ) + + let stack = ASStackLayoutSpec.horizontal() + stack.children = [contentNode, toggleButtonNode] + return ASInsetLayoutSpec(insets: insets, child: stack) + } else { + contentNode.style.preferredSize = contentSize + return ASInsetLayoutSpec(insets: insets, child: contentNode) + } + } +} From 490d1658a1758effde7b46b765c97e57c3a1294b Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 16 Feb 2022 17:27:22 +0200 Subject: [PATCH 11/19] update ui tests --- FlowCrypt/Controllers/Compose/ComposeViewController.swift | 5 +++-- FlowCrypt/Controllers/SetupImap/SetupImapViewDecorator.swift | 4 ++-- appium/tests/screenobjects/email-provider.screen.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index d1428a141..c27650f70 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -228,11 +228,12 @@ final class ComposeViewController: TableNodeViewController { private func observeComposeUpdates() { composeMessageService.onStateChanged { [weak self] state in - self?.updateSpinner(with: state) + DispatchQueue.main.async { + self?.updateSpinner(with: state) + } } } - @MainActor private func updateSpinner(with state: ComposeMessageService.State) { switch state { case .progressChanged(let progress): diff --git a/FlowCrypt/Controllers/SetupImap/SetupImapViewDecorator.swift b/FlowCrypt/Controllers/SetupImap/SetupImapViewDecorator.swift index ec8dd7e60..f8440068a 100644 --- a/FlowCrypt/Controllers/SetupImap/SetupImapViewDecorator.swift +++ b/FlowCrypt/Controllers/SetupImap/SetupImapViewDecorator.swift @@ -74,11 +74,11 @@ struct SetupImapViewDecorator { case .email: placeholder = "setup_imap_email".localized keyboardType = .emailAddress - accessibilityIdentifier = "Email" + accessibilityIdentifier = "aid-email-textfield" case .password: placeholder = "setup_imap_password".localized isSecure = true - accessibilityIdentifier = "Password" + accessibilityIdentifier = "aid-password-textfield" case .username: placeholder = "setup_imap_username".localized case .title: diff --git a/appium/tests/screenobjects/email-provider.screen.ts b/appium/tests/screenobjects/email-provider.screen.ts index 0a1a49ca2..ad9c1f4d3 100644 --- a/appium/tests/screenobjects/email-provider.screen.ts +++ b/appium/tests/screenobjects/email-provider.screen.ts @@ -6,8 +6,8 @@ const SELECTORS = { BACK_BTN: '~aid-back-button', EMAIL_PROVIDER_HEADER: '~navigationItemEmail Provider', CONNECT_BUTTON: '~Connect', - EMAIL_FIELD: '-ios class chain:**/XCUIElementTypeTextField[`name == "Email"`]', - PASSWORD_FIELD: '-ios class chain:**/XCUIElementTypeSecureTextField[`name == "Password"`]', + EMAIL_FIELD: '~aid-email-textfield', + PASSWORD_FIELD: '~aid-password-textfield', RETURN_BUTTON: '~Return', }; From ff6e876b2e57c1a81a5622dc0a0dfc4d8c014920 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 17 Feb 2022 14:32:12 +0200 Subject: [PATCH 12/19] add ui test for cc and bcc recipients --- .../Compose/ComposeViewController.swift | 10 +- .../Compose/ComposeViewDecorator.swift | 1 + .../Cell Nodes/RecipientEmailNode.swift | 4 +- .../Cell Nodes/RecipientEmailsCellNode.swift | 7 +- .../RecipientEmailsCellNodeInput.swift | 3 + .../RecipientToggleButtonNode.swift | 1 + .../tests/screenobjects/new-message.screen.ts | 100 ++++++++++-------- .../CheckComposeEmailAfterReopening.spec.ts | 6 +- 8 files changed, 77 insertions(+), 55 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index c27650f70..7cbe865cb 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -680,7 +680,7 @@ extension ComposeViewController { TextFieldCellNode( input: decorator.styledTextFieldInput( with: "compose_subject".localized, - accessibilityIdentifier: "subjectTextField" + accessibilityIdentifier: "aid-subject-text-field" ) ) { [weak self] event in switch event { @@ -721,7 +721,7 @@ extension ComposeViewController { return TextViewCellNode( decorator.styledTextViewInput( with: height, - accessibilityIdentifier: "messageTextView" + accessibilityIdentifier: "aid-message-text-view" ) ) { [weak self] event in guard let self = self else { return } @@ -772,13 +772,13 @@ extension ComposeViewController { return RecipientEmailsCellNode( recipients: recipients.map(RecipientEmailsCellNode.Input.init), + type: type.rawValue, height: recipientsNodeHeight(type: type) ?? Constants.minRecipientsPartHeight, isToggleButtonRotated: shouldShowAllRecipientTypes, toggleButtonAction: shouldShowToggleButton ? { [weak self] in guard type == .to else { return } self?.toggleRecipientsList() - } : nil - ) + } : nil) .onLayoutHeightChanged { [weak self] layoutHeight in self?.updateRecipientsNode( layoutHeight: layoutHeight, @@ -832,7 +832,7 @@ extension ComposeViewController { input: decorator.styledTextFieldInput( with: type.inputPlaceholder, keyboardType: .emailAddress, - accessibilityIdentifier: "aid-recipient-text-field-\(type.rawValue)" + accessibilityIdentifier: "aid-recipients-text-field-\(type.rawValue)" ), action: { [weak self] action in self?.handle(textFieldAction: action, for: type) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index aeb261c4c..36406b0a2 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -277,6 +277,7 @@ extension RecipientEmailsCellNode.Input { color: recipient.state.textColor, alignment: .left ), + type: recipient.type.rawValue, state: recipient.state ) } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift index 13f4b35f8..04d684a01 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift @@ -36,12 +36,12 @@ final class RecipientEmailNode: CellNode { super.init() if let stateAccessibilityIdentifier = input.recipient.state.accessibilityIdentifier { - accessibilityIdentifier = "aid-to-\(index)-\(stateAccessibilityIdentifier)" + accessibilityIdentifier = "aid-\(input.recipient.type)-\(index)-\(stateAccessibilityIdentifier)" } titleNode.attributedText = " ".attributed() + input.recipient.email + " ".attributed() titleNode.backgroundColor = input.recipient.state.backgroundColor - titleNode.accessibilityIdentifier = "aid-to-\(index)-label" + titleNode.accessibilityIdentifier = "aid-\(input.recipient.type)-\(index)-label" titleNode.cornerRadius = 8 titleNode.clipsToBounds = true diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index 711ced9cf..28ca131b6 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -45,20 +45,25 @@ final public class RecipientEmailsCellNode: CellNode, RecipientToggleButtonNode public lazy var collectionNode: ASCollectionNode = { let node = ASCollectionNode(collectionViewLayout: layout) - node.accessibilityIdentifier = "aid-recipients-list" + node.accessibilityIdentifier = "aid-recipients-list-\(type)" node.backgroundColor = .clear return node }() private var collectionLayoutHeight: CGFloat private var recipients: [Input] = [] + private let type: String public init(recipients: [Input], + type: String, height: CGFloat, isToggleButtonRotated: Bool, toggleButtonAction: (() -> Void)?) { self.recipients = recipients + self.type = type self.collectionLayoutHeight = height + super.init() + collectionNode.dataSource = self collectionNode.delegate = self diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift index 660e9b084..1a594dfb9 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift @@ -97,13 +97,16 @@ extension RecipientEmailsCellNode { } public let email: NSAttributedString + public let type: String public var state: State public init( email: NSAttributedString, + type: String, state: State ) { self.email = email + self.type = type self.state = state } } diff --git a/FlowCryptUI/Cell Nodes/RecipientToggleButtonNode.swift b/FlowCryptUI/Cell Nodes/RecipientToggleButtonNode.swift index c7ec357f6..13d655a6a 100644 --- a/FlowCryptUI/Cell Nodes/RecipientToggleButtonNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientToggleButtonNode.swift @@ -20,6 +20,7 @@ extension RecipientToggleButtonNode { let configuration = UIImage.SymbolConfiguration(pointSize: 14, weight: .light) let image = UIImage(systemName: "chevron.down", withConfiguration: configuration) let button = ASButtonNode() + button.accessibilityIdentifier = "aid-recipients-toggle-button" button.setImage(image, for: .normal) button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0) button.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.secondaryLabel) diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index 515540cec..40dad13ca 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -2,11 +2,9 @@ import BaseScreen from './base.screen'; import ElementHelper from "../helpers/ElementHelper"; const SELECTORS = { - ADD_RECIPIENT_FIELD_TO: '~aid-recipient-text-field-to', - ADD_RECIPIENT_FIELD_CC: '~aid-recipient-text-field-cc', - ADD_RECIPIENT_FIELD_BCC: '~aid-recipient-text-field-bcc', - SUBJECT_FIELD: '~subjectTextField', - COMPOSE_SECURITY_MESSAGE: '~messageTextView', + TOGGLE_RECIPIENTS_BUTTON: '~aid-recipients-toggle-button', + SUBJECT_FIELD: '~aid-subject-text-field', + COMPOSE_SECURITY_MESSAGE: '~aid-message-text-view', RECIPIENTS_LIST: '~aid-recipients-list', PASSWORD_CELL: '~aid-message-password-cell', ATTACHMENT_CELL: '~aid-attachment-cell-0', @@ -24,19 +22,11 @@ const SELECTORS = { class NewMessageScreen extends BaseScreen { constructor() { - super(SELECTORS.ADD_RECIPIENT_FIELD_TO); + super(SELECTORS.RECIPIENTS_LIST); } - get addToRecipientField() { - return $(SELECTORS.ADD_RECIPIENT_FIELD_TO); - } - - get addCcRecipientField() { - return $(SELECTORS.ADD_RECIPIENT_FIELD_CC); - } - - get addBccRecipientField() { - return $(SELECTORS.ADD_RECIPIENT_FIELD_BCC); + get toggleRecipientsButton() { + return $(SELECTORS.TOGGLE_RECIPIENTS_BUTTON); } get subjectField() { @@ -47,10 +37,6 @@ class NewMessageScreen extends BaseScreen { return $(SELECTORS.COMPOSE_SECURITY_MESSAGE); } - get recipientsList() { - return $(SELECTORS.RECIPIENTS_LIST); - } - get attachmentCell() { return $(SELECTORS.ATTACHMENT_CELL); } @@ -95,10 +81,20 @@ class NewMessageScreen extends BaseScreen { return $(SELECTORS.CANCEL_BUTTON); } - setAddRecipient = async (recipient: string) => { - await (await this.addToRecipientField).setValue(recipient); - await browser.pause(500); - await (await $(SELECTORS.RETURN_BUTTON)).click() + getRecipientsList = async (type: string) => { + return $(`~aid-recipients-list-${type}`); + } + + getRecipientsTextField = async (type: string) => { + return $(`~aid-recipients-text-field-${type}`); + } + + setAddRecipient = async (recipient?: string, type = 'to') => { + if (recipient) { + await (await this.getRecipientsTextField(type)).setValue(recipient); + await browser.pause(500); + await (await $(SELECTORS.RETURN_BUTTON)).click(); + } }; setSubject = async (subject: string) => { @@ -111,64 +107,74 @@ class NewMessageScreen extends BaseScreen { await ElementHelper.waitClickAndType(await this.composeSecurityMessage, message); }; - filledSubject = async (subject: string) => { - const selector = `**/XCUIElementTypeTextField[\`value == "${subject}"\`]`; - return await $(`-ios class chain:${selector}`); + checkSubject = async (subject: string) => { + expect(await this.subjectField).toHaveText(subject); }; - composeEmail = async (recipient: string, subject: string, message: string) => { + composeEmail = async (recipient: string, subject: string, message: string, cc?: string, bcc?: string) => { await this.setAddRecipient(recipient); + if (cc || bcc) { + await this.clickToggleRecipientsButton(); + await this.setAddRecipient(cc, 'cc'); + await this.setAddRecipient(bcc, 'bcc'); + } await this.setComposeSecurityMessage(message); await this.setSubject(subject); }; - setAddRecipientByName = async (name: string, email: string) => { + setAddRecipientByName = async (name: string, email: string, type = 'to') => { await browser.pause(500); // stability fix for transition animation - await (await this.addToRecipientField).setValue(name); + await (await this.getRecipientsTextField(type)).setValue(name); await ElementHelper.waitAndClick(await $(`~${email}`)); }; - checkFilledComposeEmailInfo = async (recipients: string[], subject: string, message: string, attachmentName?: string) => { + checkFilledComposeEmailInfo = async (recipients: string[], subject: string, message: string, attachmentName?: string, cc?: string[], bcc?: string[]) => { expect(this.composeSecurityMessage).toHaveTextContaining(message); - - const element = await this.filledSubject(subject); - await element.waitForDisplayed(); + this.checkSubject(subject); await this.checkRecipientsList(recipients); + if (cc) { + await this.checkRecipientsList(cc, 'cc'); + } + + if (bcc) { + await this.checkRecipientsList(bcc, 'bcc'); + } + if (attachmentName !== undefined) { await this.checkAddedAttachment(attachmentName); } }; - checkRecipientsTextFieldIsInvisible = async () => { - await ElementHelper.waitElementInvisible(await this.addToRecipientField); + checkRecipientsTextFieldIsInvisible = async (type = 'to') => { + await ElementHelper.waitElementInvisible(await this.getRecipientsTextField(type)); } - checkRecipientsList = async(recipients: string[]) => { + checkRecipientsList = async(recipients: string[], type = 'to') => { if (recipients.length === 0) { - await ElementHelper.waitElementInvisible(await $(`~aid-to-0-label`)); + await ElementHelper.waitElementInvisible(await $(`~aid-${type}-0-label`)); } else { for (const [index, recipient] of recipients.entries()) { - await this.checkAddedRecipient(recipient, index); + await this.checkAddedRecipient(recipient, index, type); } } } - checkAddedRecipient = async (recipient: string, order = 0) => { - const recipientCell = await $(`~aid-to-${order}-label`); + checkAddedRecipient = async (recipient: string, order = 0, type = 'to') => { + const recipientCell = await $(`~aid-${type}-${order}-label`); const name = await recipientCell.getValue(); expect(name).toEqual(` ${recipient} `); } - checkAddedRecipientColor = async (recipient: string, order: number, color: string) => { - const addedRecipientEl = await $(`~aid-to-${order}-${color}`); + checkAddedRecipientColor = async (recipient: string, order: number, color: string, type = 'to') => { + const addedRecipientEl = await $(`~aid-${type}-${order}-${color}`); await ElementHelper.waitElementVisible(addedRecipientEl); await this.checkAddedRecipient(recipient, order); } - deleteAddedRecipient = async (order: number) => { - const addedRecipientEl = await $(`~aid-to-${order}-label`); + deleteAddedRecipient = async (order: number, type = 'to') => { + const addedRecipientEl = await $(`~aid-${type}-${order}-label`); await ElementHelper.waitAndClick(addedRecipientEl); await driver.sendKeys(['\b']); // backspace } @@ -193,6 +199,10 @@ class NewMessageScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.sendButton); } + clickToggleRecipientsButton =async () => { + await ElementHelper.waitAndClick(await this.toggleRecipientsButton); + } + clickSetPasswordButton = async () => { await ElementHelper.waitAndClick(await this.setPasswordButton); } diff --git a/appium/tests/specs/live/composeEmail/CheckComposeEmailAfterReopening.spec.ts b/appium/tests/specs/live/composeEmail/CheckComposeEmailAfterReopening.spec.ts index c1c67ffaf..b0f4205e2 100644 --- a/appium/tests/specs/live/composeEmail/CheckComposeEmailAfterReopening.spec.ts +++ b/appium/tests/specs/live/composeEmail/CheckComposeEmailAfterReopening.spec.ts @@ -12,6 +12,8 @@ describe('COMPOSE EMAIL: ', () => { it('check filled compose email after reopening app and text autoscroll', async () => { const recipientEmail = CommonData.contact.email; + const ccRecipientEmail = CommonData.secondContact.email; + const bccRecipientEmail = CommonData.recipient.email; const emailSubject = CommonData.simpleEmail.subject; const emailText = CommonData.simpleEmail.message; const longEmailText = CommonData.longEmail.message; @@ -27,8 +29,8 @@ describe('COMPOSE EMAIL: ', () => { await NewMessageScreen.clickBackButton(); await MailFolderScreen.checkInboxScreen(); await MailFolderScreen.clickCreateEmail(); - await NewMessageScreen.composeEmail(recipientEmail, emailSubject, emailText); - await NewMessageScreen.checkFilledComposeEmailInfo([recipientEmail], emailSubject, emailText); + await NewMessageScreen.composeEmail(recipientEmail, emailSubject, emailText, ccRecipientEmail, bccRecipientEmail); + await NewMessageScreen.checkFilledComposeEmailInfo([recipientEmail], emailSubject, emailText, undefined, [ccRecipientEmail], [bccRecipientEmail]); await driver.background(3); From 711acd3b1bfdf0aa8d8930ce15f6f9a3dcc177c8 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 17 Feb 2022 15:19:20 +0200 Subject: [PATCH 13/19] code cleanup --- .../Controllers/Compose/ComposeViewController.swift | 13 +++++++------ .../ComposeMessageContext.swift | 8 ++++---- .../ComposeMessageService.swift | 6 +++--- appium/tests/screenobjects/new-message.screen.ts | 4 ++-- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 7cbe865cb..d43a71f2d 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -50,7 +50,7 @@ final class ComposeViewController: TableNodeViewController { private enum Section: Hashable { case recipients(RecipientType), password, compose, attachments, searchResults, contacts - static var recipientsSections: [Section] { + static let recipientsSections: [Section] { RecipientType.allCases.map { Section.recipients($0) } } } @@ -78,7 +78,7 @@ final class ComposeViewController: TableNodeViewController { private let email: String private var isMessagePasswordSupported: Bool { - clientConfiguration.isUsingFes + return clientConfiguration.isUsingFes } private let search = PassthroughSubject() @@ -765,7 +765,8 @@ extension ComposeViewController { } private func recipientsNode(type: RecipientType) -> ASCellNode { - let recipients = contextToSend.recipients(of: type) + let recipients = contextToSend.recipients(type: type) + let shouldShowToggleButton = type == .to && recipients.isNotEmpty && !contextToSend.hasCcOrBccRecipients @@ -825,7 +826,7 @@ extension ComposeViewController { private func recipientInput(type: RecipientType) -> ASCellNode { let shouldShowToggleButton = type == .to - && contextToSend.recipients(of: .to).isEmpty + && contextToSend.recipients(type: .to).isEmpty && !contextToSend.hasCcOrBccRecipients return RecipientEmailTextFieldNode( @@ -932,7 +933,7 @@ extension ComposeViewController { let text = text, text.isNotEmpty else { return } - let recipients = contextToSend.recipients(of: recipientType) + let recipients = contextToSend.recipients(type: recipientType) let textField = recipientsTextField(type: recipientType) textField?.reset() @@ -1001,7 +1002,7 @@ extension ComposeViewController { private func handleBackspaceAction(with textField: UITextField, for recipientType: RecipientType) { guard textField.text == "" else { return } - var recipients = contextToSend.recipients(of: recipientType) + var recipients = contextToSend.recipients(type: recipientType) let selectedRecipients = recipients.filter { $0.state.isSelected } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index d1ef1f642..9feb95d76 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -55,16 +55,16 @@ extension ComposeMessageContext { !hasRecipientsWithoutPubKey || hasMessagePassword } - func recipients(of type: RecipientType) -> [ComposeMessageRecipient] { + func recipients(type: RecipientType) -> [ComposeMessageRecipient] { recipients.filter { $0.type == type } } - func recipientEmails(of type: RecipientType) -> [String] { - recipients(of: type).map(\.email) + func recipientEmails(type: RecipientType) -> [String] { + recipients(type: type).map(\.email) } func recipient(at index: Int, type: RecipientType) -> ComposeMessageRecipient? { - recipients(of: type)[safe: index] + recipients(type: type)[safe: index] } mutating func add(recipient: ComposeMessageRecipient) { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 6fa74bd38..d07ce7934 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -134,9 +134,9 @@ final class ComposeMessageService { return SendableMsg( text: text, html: nil, - to: contextToSend.recipientEmails(of: .to), - cc: contextToSend.recipientEmails(of: .cc), - bcc: contextToSend.recipientEmails(of: .bcc), + to: contextToSend.recipientEmails(type: .to), + cc: contextToSend.recipientEmails(type: .cc), + bcc: contextToSend.recipientEmails(type: .bcc), from: sender, subject: subject, replyToMimeMsg: replyToMimeMsg, diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index 40dad13ca..94ad3ec70 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -129,8 +129,8 @@ class NewMessageScreen extends BaseScreen { }; checkFilledComposeEmailInfo = async (recipients: string[], subject: string, message: string, attachmentName?: string, cc?: string[], bcc?: string[]) => { - expect(this.composeSecurityMessage).toHaveTextContaining(message); - this.checkSubject(subject); + expect(await this.composeSecurityMessage).toHaveTextContaining(message); + await this.checkSubject(subject); await this.checkRecipientsList(recipients); From d5402cc0c27bb79d3c1396597f3c01826f6e8d5b Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 17 Feb 2022 15:27:47 +0200 Subject: [PATCH 14/19] fix --- FlowCrypt/Controllers/Compose/ComposeViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index d43a71f2d..1f495f076 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -50,7 +50,7 @@ final class ComposeViewController: TableNodeViewController { private enum Section: Hashable { case recipients(RecipientType), password, compose, attachments, searchResults, contacts - static let recipientsSections: [Section] { + static var recipientsSections: [Section] { RecipientType.allCases.map { Section.recipients($0) } } } From 83f148ec9c8fe3e28d239bd77a4c855d03bc6c58 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 18 Feb 2022 14:13:20 +0200 Subject: [PATCH 15/19] fix ipad layout --- FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift | 5 ++++- Gemfile.lock | 4 ++-- appium/tests/screenobjects/new-message.screen.ts | 9 ++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift index 28ca131b6..d42812358 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift @@ -82,10 +82,13 @@ final public class RecipientEmailsCellNode: CellNode, RecipientToggleButtonNode let collectionNodeSize = CGSize(width: constrainedSize.max.width, height: collectionNodeHeight) let buttonSize = CGSize(width: 40, height: 50) + var insets = UIEdgeInsets.deviceSpecificTextInsets(top: 0, bottom: 0) + insets.left -= 8 + return createLayout( contentNode: collectionNode, contentSize: collectionNodeSize, - insets: .zero, + insets: insets, buttonSize: buttonSize ) } diff --git a/Gemfile.lock b/Gemfile.lock index c00c3acbf..7cf828032 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,7 +17,7 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.554.0) + aws-partitions (1.555.0) aws-sdk-core (3.126.2) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) @@ -87,7 +87,7 @@ GEM ethon (0.15.0) ffi (>= 1.15.0) excon (0.91.0) - faraday (1.9.3) + faraday (1.10.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index 29e1e7077..de20fa897 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -107,8 +107,9 @@ class NewMessageScreen extends BaseScreen { await ElementHelper.waitClickAndType(await this.composeSecurityMessage, message); }; - checkSubject = async (subject: string) => { - expect(await this.subjectField).toHaveText(subject); + filledSubject = async (subject: string) => { + const selector = `**/XCUIElementTypeTextField[\`value == "${subject}"\`]`; + return await $(`-ios class chain:${selector}`); }; composeEmail = async (recipient: string, subject: string, message: string, cc?: string, bcc?: string) => { @@ -130,7 +131,9 @@ class NewMessageScreen extends BaseScreen { checkFilledComposeEmailInfo = async (recipients: string[], subject: string, message: string, attachmentName?: string, cc?: string[], bcc?: string[]) => { expect(await this.composeSecurityMessage).toHaveTextContaining(message); - await this.checkSubject(subject); + + const element = await this.filledSubject(subject); + await element.waitForDisplayed(); await this.checkRecipientsList(recipients); From a00204f11ef495cf68bf152b867236f8cdb4c6bc Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 18 Feb 2022 14:15:39 +0200 Subject: [PATCH 16/19] #1352 add simctl to semaphore --- .semaphore/semaphore.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 616856b66..1fa8bac71 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -30,10 +30,9 @@ blocks: - cache restore && make dependencies && cache store - mv ~/appium-env ~/git/flowcrypt-ios/appium/.env - sem-version node 16 && cache restore appium-npm && cd ./appium && npm i && cd .. && cache store appium-npm appium/node_modules - # temporary disabled because of https://github.com/ios-control/simctl/issues/30 - # - brew install ideviceinstaller - # - npm install ios-deploy -g --unsafe-perm=true --allow-root - # - npm install ios-sim -g --unsafe-perm=true --allow-root + - brew install ideviceinstaller + - npm install ios-deploy -g --unsafe-perm=true --allow-root + - npm install ios-sim -g --unsafe-perm=true --allow-root jobs: - name: Appium UI tests commands: From 4b1d8ce61cc08b5cae0e23119a0be3b5b43bbe7a Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 18 Feb 2022 15:07:19 +0200 Subject: [PATCH 17/19] Update swiftformat --- BuildTools/Package.resolved | 4 ++-- BuildTools/Package.swift | 2 +- FlowCrypt.xcodeproj/project.pbxproj | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BuildTools/Package.resolved b/BuildTools/Package.resolved index fd1fd9336..e93a7ca4b 100644 --- a/BuildTools/Package.resolved +++ b/BuildTools/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/nicklockwood/SwiftFormat", "state": { "branch": null, - "revision": "415c08ce2d63ff8bca95228939c92375882ea538", - "version": "0.49.2" + "revision": "f14f4f717e7e1d275acd7557d64c94cfef5723e6", + "version": "0.49.4" } } ] diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift index 7569f3ce6..567f2cfbd 100644 --- a/BuildTools/Package.swift +++ b/BuildTools/Package.swift @@ -6,7 +6,7 @@ let package = Package( name: "BuildTools", platforms: [.macOS(.v10_11)], dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.49.0"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.49.4"), ], targets: [.target(name: "BuildTools", path: "")] ) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 2fc11bca0..b37e0cb1c 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -2525,7 +2525,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd BuildTools\nSDKROOT=(xcrun --sdk macosx --show-sdk-path)\n#swift package update #Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\nswift run -c release swiftformat \"$SRCROOT\"\n"; + shellScript = "cd BuildTools\nSDKROOT=(xcrun --sdk macosx --show-sdk-path)\n#swift package update #Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\nswift run -c release swiftformat \"$SRCROOT\" --exclude appium,fastlane,Pods\n"; }; E531C3B50A9C90454C72F878 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; From 49b35c9914c642bb64e05dfee41e6e3a2343ab13 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 18 Feb 2022 15:07:58 +0200 Subject: [PATCH 18/19] update swiftformat config --- FlowCrypt.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index b37e0cb1c..0b185c4bc 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -2525,7 +2525,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd BuildTools\nSDKROOT=(xcrun --sdk macosx --show-sdk-path)\n#swift package update #Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\nswift run -c release swiftformat \"$SRCROOT\" --exclude appium,fastlane,Pods\n"; + shellScript = "cd BuildTools\nSDKROOT=(xcrun --sdk macosx --show-sdk-path)\n#swift package update #Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\nswift run -c release swiftformat \"$SRCROOT\" --exclude appium,fastlane,Pods,vendor\n"; }; E531C3B50A9C90454C72F878 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; From 5c126557e25283ca50e1f88d0d6e07b77ae4813c Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 18 Feb 2022 15:43:08 +0200 Subject: [PATCH 19/19] swiftformat fix --- .swiftformat | 1 + 1 file changed, 1 insertion(+) create mode 100644 .swiftformat diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 000000000..42b7b0039 --- /dev/null +++ b/.swiftformat @@ -0,0 +1 @@ +--exclude appium,Core,fastlane,Pods,vendor \ No newline at end of file