diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 84198663b..226c31fc6 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ 5180CB9127356D48001FC7EF /* MessageSubjectNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */; }; 5180CB9327357B67001FC7EF /* RawClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9227357B67001FC7EF /* RawClientConfiguration.swift */; }; 5180CB9527357BB0001FC7EF /* WkdApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9427357BB0001FC7EF /* WkdApi.swift */; }; + 5180CB97273724E9001FC7EF /* ThreadMessageSenderCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB96273724E9001FC7EF /* ThreadMessageSenderCellNode.swift */; }; 518389C82726D7DD00131B2C /* UIViewController+Spinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518389C72726D7DD00131B2C /* UIViewController+Spinner.swift */; }; 518389CA2726D8F700131B2C /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518389C92726D8F700131B2C /* UIApplicationExtension.swift */; }; 51B0C7712729861C00124663 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B0C7702729861C00124663 /* String+Extension.swift */; }; @@ -482,6 +483,7 @@ 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSubjectNode.swift; sourceTree = ""; }; 5180CB9227357B67001FC7EF /* RawClientConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawClientConfiguration.swift; sourceTree = ""; }; 5180CB9427357BB0001FC7EF /* WkdApi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WkdApi.swift; sourceTree = ""; }; + 5180CB96273724E9001FC7EF /* ThreadMessageSenderCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMessageSenderCellNode.swift; sourceTree = ""; }; 518389C72726D7DD00131B2C /* UIViewController+Spinner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Spinner.swift"; sourceTree = ""; }; 518389C92726D8F700131B2C /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; 51B0C7702729861C00124663 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; @@ -1966,6 +1968,7 @@ 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */, 9F82779B23737E2A00E19C07 /* MessageSubjectAndTimeNode.swift */, 9F82779D23737E3800E19C07 /* MessageTextSubjectNode.swift */, + 5180CB96273724E9001FC7EF /* ThreadMessageSenderCellNode.swift */, 9F56BD3123438B5B00A7371A /* InboxCellNode.swift */, 9F56BD3523438B9D00A7371A /* TextCellNode.swift */, D24ABA6223FDB4FF002EE9DD /* RecipientEmailsCellNode.swift */, @@ -2746,6 +2749,7 @@ 51DE2FEE2714DA0400916222 /* ContactKeyCellNode.swift in Sources */, D2A9CA432426210200E1D898 /* SetupTitleNode.swift in Sources */, D2F6D12F24324ACC00DB4065 /* SwitchCellNode.swift in Sources */, + 5180CB97273724E9001FC7EF /* ThreadMessageSenderCellNode.swift in Sources */, D2E26F6824F169E300612AF1 /* ContactCellNode.swift in Sources */, D2A9CA38242618DF00E1D898 /* LinkButtonNode.swift in Sources */, D24FAFA42520BF9100BF46C5 /* CheckBoxCircleView.swift in Sources */, diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 23f7bb755..f3af32520 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -96,8 +96,8 @@ "repositoryURL": "https://github.com/realm/realm-cocoa", "state": { "branch": null, - "revision": "9f43d0da902c55b493d6c8bb63203764caa8acbe", - "version": "10.18.0" + "revision": "328425bfc372ce77ec1f4f2701f61ececbb97d84", + "version": "10.19.0" } }, { @@ -105,8 +105,8 @@ "repositoryURL": "https://github.com/realm/realm-core", "state": { "branch": null, - "revision": "23f60515a00f076a9e3f2dc672fe1ae07601ee90", - "version": "11.4.1" + "revision": "b170db6a47789ff5f2fbc3eeed0220b4b0a3f6b7", + "version": "11.6.0" } }, { diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift index ece7fbbc1..02a58cd9a 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift @@ -9,16 +9,12 @@ import FlowCryptUI import UIKit -extension TextImageNode.Input { +extension ThreadMessageSenderCellNode.Input { init(threadMessage: ThreadDetailsViewController.Input) { let sender = threadMessage.rawMessage.sender ?? "message_unknown_sender".localized let date = DateFormatter().formatDate(threadMessage.rawMessage.date) let isMessageRead = threadMessage.rawMessage.isMessageRead - let collapseImage = #imageLiteral(resourceName: "arrow_up").tinted(.white) - let expandImage = #imageLiteral(resourceName: "arrow_down").tinted(.white) - let image = threadMessage.isExpanded ? expandImage : collapseImage - let style: NSAttributedString.Style = isMessageRead ? .regular(17) : .bold(17) @@ -32,12 +28,16 @@ extension TextImageNode.Input { : .mainTextUnreadColor self.init( - title: NSAttributedString.text(from: sender, style: style, color: textColor), - subtitle: NSAttributedString.text(from: date, style: style, color: dateColor), - image: image, - imageSize: CGSize(width: 16, height: 16), - nodeInsets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), - backgroundColor: .backgroundColor + sender: NSAttributedString.text(from: sender, style: style, color: textColor), + date: NSAttributedString.text(from: date, style: style, color: dateColor), + isExpanded: threadMessage.isExpanded, + buttonColor: .messageButtonColor ) } } + +extension UIColor { + static var messageButtonColor: UIColor { + .colorFor(darkStyle: .white, lightStyle: .main) + } +} diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 744ca9b87..de0c3c9bf 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -90,11 +90,11 @@ extension ThreadDetailsViewController { private func expandThreadMessage() { let indexOfSectionToExpand = thread.messages.firstIndex(where: { $0.isMessageRead == false }) ?? input.count - 1 let indexPath = IndexPath(row: 0, section: indexOfSectionToExpand + 1) - handleTap(at: indexPath) + handleExpandTap(at: indexPath) } - private func handleTap(at indexPath: IndexPath) { - guard let threadNode = node.nodeForRow(at: indexPath) as? TextImageNode else { + private func handleExpandTap(at indexPath: IndexPath) { + guard let threadNode = node.nodeForRow(at: indexPath) as? ThreadMessageSenderCellNode else { logger.logError("Fail to handle tap at \(indexPath)") return } @@ -102,12 +102,11 @@ extension ThreadDetailsViewController { UIView.animate( withDuration: 0.3, animations: { - threadNode.imageNode.view.transform = CGAffineTransform(rotationAngle: .pi) + threadNode.replyNode.view.alpha = self.input[indexPath.section-1].isExpanded ? 0 : 1 + threadNode.expandNode.view.transform = CGAffineTransform(rotationAngle: .pi) }, completion: { [weak self] _ in - guard let self = self else { - return - } + guard let self = self else { return } if let processedMessage = self.input[indexPath.section-1].processedMessage { self.handleReceived(message: processedMessage, at: indexPath) @@ -118,6 +117,28 @@ extension ThreadDetailsViewController { ) } + private func handleReplyTap(at indexPath: IndexPath) { + guard let email = DataService.shared.email, + let input = input[safe: indexPath.section-1], + let processedMessage = input.processedMessage + else { return } + + let replyInfo = ComposeMessageInput.ReplyInfo( + recipient: input.rawMessage.sender, + subject: input.rawMessage.subject, + mime: processedMessage.rawMimeData, + sentDate: input.rawMessage.date, + message: processedMessage.text, + threadId: input.rawMessage.threadId + ) + + let composeInput = ComposeMessageInput(type: .reply(replyInfo)) + navigationController?.pushViewController( + ComposeViewController(email: email, input: composeInput), + animated: true + ) + } + private func markAsRead(at index: Int) { guard let message = input[safe: index]?.rawMessage else { return @@ -344,11 +365,9 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { let section = self.input[indexPath.section-1] if indexPath.row == 0 { - return TextImageNode( + return ThreadMessageSenderCellNode( input: .init(threadMessage: section), - onTap: { [weak self] _ in - self?.handleTap(at: indexPath) - } + onReplyTap: { [weak self] _ in self?.handleReplyTap(at: indexPath) } ) } @@ -371,10 +390,10 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { - guard tableNode.nodeForRow(at: indexPath) is TextImageNode else { + guard tableNode.nodeForRow(at: indexPath) is ThreadMessageSenderCellNode else { return } - handleTap(at: indexPath) + handleExpandTap(at: indexPath) } } diff --git a/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift b/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift index 2de6e0f3b..cf165ebe2 100644 --- a/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageSubjectNode.swift @@ -25,7 +25,7 @@ public final class MessageSubjectNode: CellNode { public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { subjectNode.style.flexGrow = 1.0 return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 16, left: 8, bottom: 4, right: 8), + insets: UIEdgeInsets(top: 16, left: 16, bottom: 4, right: 16), child: subjectNode ) } diff --git a/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift b/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift index 697c7cea0..eb62ada22 100644 --- a/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift @@ -23,7 +23,7 @@ public final class MessageTextSubjectNode: CellNode { public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { textNode.style.flexGrow = 1.0 return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), + insets: UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16), child: textNode ) } diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift new file mode 100644 index 000000000..d1fe2749c --- /dev/null +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -0,0 +1,111 @@ +// +// ThreadMessageSenderCellNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 06/11/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit +import UIKit + +public final class ThreadMessageSenderCellNode: CellNode { + public struct Input { + public let sender: NSAttributedString + public let date: NSAttributedString? + public let isExpanded: Bool + public let buttonColor: UIColor + + public init(sender: NSAttributedString, + date: NSAttributedString, + isExpanded: Bool, + buttonColor: UIColor) { + self.sender = sender + self.date = date + self.isExpanded = isExpanded + self.buttonColor = buttonColor + } + + var replyImage: UIImage? { + return createButtonImage(systemName: "arrowshape.turn.up.left") + } + var expandImage: UIImage? { + let systemName = isExpanded ? "chevron.up" : "chevron.down" + return createButtonImage(systemName: systemName) + } + + private func createButtonImage(systemName: String, pointSize: CGFloat = 18) -> UIImage? { + let configuration = UIImage.SymbolConfiguration(pointSize: pointSize) + return UIImage(systemName: systemName, withConfiguration: configuration) + } + } + + private let senderNode = ASTextNode2() + private let dateNode = ASTextNode2() + public private(set) var replyNode = ASButtonNode() + public private(set) var expandNode = ASImageNode() + + private let input: ThreadMessageSenderCellNode.Input + private var onReplyTap: ((ThreadMessageSenderCellNode) -> Void)? + + public init(input: ThreadMessageSenderCellNode.Input, + onReplyTap: ((ThreadMessageSenderCellNode) -> Void)?) { + self.input = input + self.onReplyTap = onReplyTap + super.init() + automaticallyManagesSubnodes = true + + senderNode.attributedText = input.sender + dateNode.attributedText = input.date + + setupReplyNode() + setupExpandNode() + } + + private func setupReplyNode() { + replyNode.setImage(input.replyImage, for: .normal) + replyNode.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) + replyNode.contentMode = .center + replyNode.alpha = input.isExpanded ? 1 : 0 + replyNode.addTarget(self, action: #selector(onReplyNodeTap), forControlEvents: .touchUpInside) + } + + private func setupExpandNode() { + expandNode.image = input.expandImage + expandNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) + expandNode.contentMode = .right + } + + @objc private func onReplyNodeTap() { + onReplyTap?(self) + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + replyNode.style.preferredSize = CGSize(width: 44, height: 44) + expandNode.style.preferredSize = CGSize(width: 18, height: 44) + + let infoNode = ASStackLayoutSpec( + direction: .vertical, + spacing: 4, + justifyContent: .start, + alignItems: .start, + children: [senderNode, dateNode] + ) + + infoNode.style.flexGrow = 1 + infoNode.style.flexShrink = 1 + + let contentSpec = ASStackLayoutSpec( + direction: .horizontal, + spacing: 4, + justifyContent: .spaceBetween, + alignItems: .center, + children: [infoNode, replyNode, expandNode] + ) + + return ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 12), + child: contentSpec + ) + } +}