From 87d44ded33a8de416712a1255c391a9df05e6838 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 8 Nov 2021 13:12:17 +0200 Subject: [PATCH 1/3] issue #936 add reply button to threads --- FlowCrypt.xcodeproj/project.pbxproj | 4 + .../xcshareddata/swiftpm/Package.resolved | 8 +- .../Threads/ThreadDetailsDecorator.swift | 15 +--- .../Threads/ThreadDetailsViewController.swift | 46 +++++++--- .../Cell Nodes/MessageSubjectNode.swift | 2 +- .../Cell Nodes/MessageTextSubjectNode.swift | 2 +- .../ThreadMessageSenderCellNode.swift | 90 +++++++++++++++++++ 7 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift 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..978e5be18 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,9 @@ 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 ) } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 744ca9b87..2e2705cf9 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,12 @@ 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.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) + 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 +118,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 +366,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?.handleExpandTap(at: indexPath) } ) } @@ -371,10 +391,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..0171e9199 --- /dev/null +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -0,0 +1,90 @@ +// +// 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 init(sender: NSAttributedString, date: NSAttributedString, isExpanded: Bool) { + self.sender = sender + self.date = date + self.isExpanded = isExpanded + } + + var expandImageName: String { isExpanded ? "chevron.up" : "chevron.down" } + } + + private let senderNode = ASTextNode2() + private let dateNode = ASTextNode2() + public private(set) var replyNode = ASImageNode() + 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 + + replyNode.image = UIImage(systemName: "arrowshape.turn.up.left", + withConfiguration: UIImage.SymbolConfiguration(pointSize: 28)) + replyNode.contentMode = .center + replyNode.tintColor = .textColor + replyNode.alpha = input.isExpanded ? 1 : 0 + expandNode.image = UIImage(systemName: input.expandImageName, + withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .light)) + expandNode.contentMode = .right + expandNode.tintColor = .textColor + + replyNode.addTarget(self, action: #selector(onReplyNodeTap), forControlEvents: .touchUpInside) + } + + @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 + ) + } +} From c9eed1a63629aeecc965a83da7278e3aa1c83c3f Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 8 Nov 2021 14:50:00 +0200 Subject: [PATCH 2/3] issue #936 update thread buttons color --- .../Threads/ThreadDetailsDecorator.swift | 9 +++++++- .../Threads/ThreadDetailsViewController.swift | 3 +-- .../ThreadMessageSenderCellNode.swift | 22 ++++++++++++++----- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift index 978e5be18..02a58cd9a 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift @@ -30,7 +30,14 @@ extension ThreadMessageSenderCellNode.Input { self.init( sender: NSAttributedString.text(from: sender, style: style, color: textColor), date: NSAttributedString.text(from: date, style: style, color: dateColor), - isExpanded: threadMessage.isExpanded + 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 2e2705cf9..de0c3c9bf 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -103,7 +103,6 @@ extension ThreadDetailsViewController { withDuration: 0.3, animations: { threadNode.replyNode.view.alpha = self.input[indexPath.section-1].isExpanded ? 0 : 1 - threadNode.expandNode.view.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) threadNode.expandNode.view.transform = CGAffineTransform(rotationAngle: .pi) }, completion: { [weak self] _ in @@ -368,7 +367,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { if indexPath.row == 0 { return ThreadMessageSenderCellNode( input: .init(threadMessage: section), - onReplyTap: { [weak self] _ in self?.handleExpandTap(at: indexPath) } + onReplyTap: { [weak self] _ in self?.handleReplyTap(at: indexPath) } ) } diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift index 0171e9199..6d4fda8d6 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -14,11 +14,16 @@ public final class ThreadMessageSenderCellNode: CellNode { public let sender: NSAttributedString public let date: NSAttributedString? public let isExpanded: Bool + public let buttonColor: UIColor - public init(sender: NSAttributedString, date: NSAttributedString, isExpanded: Bool) { + public init(sender: NSAttributedString, + date: NSAttributedString, + isExpanded: Bool, + buttonColor: UIColor) { self.sender = sender self.date = date self.isExpanded = isExpanded + self.buttonColor = buttonColor } var expandImageName: String { isExpanded ? "chevron.up" : "chevron.down" } @@ -42,17 +47,24 @@ public final class ThreadMessageSenderCellNode: CellNode { senderNode.attributedText = input.sender dateNode.attributedText = input.date + setupReplyNode() + setupExpandNode() + } + + private func setupReplyNode() { replyNode.image = UIImage(systemName: "arrowshape.turn.up.left", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28)) replyNode.contentMode = .center - replyNode.tintColor = .textColor + replyNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) replyNode.alpha = input.isExpanded ? 1 : 0 + replyNode.addTarget(self, action: #selector(onReplyNodeTap), forControlEvents: .touchUpInside) + } + + private func setupExpandNode() { expandNode.image = UIImage(systemName: input.expandImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .light)) expandNode.contentMode = .right - expandNode.tintColor = .textColor - - replyNode.addTarget(self, action: #selector(onReplyNodeTap), forControlEvents: .touchUpInside) + expandNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) } @objc private func onReplyNodeTap() { From 308f1ff504e3cb1ef354c023950316415842e0a9 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 8 Nov 2021 15:59:17 +0200 Subject: [PATCH 3/3] issue #936 update thread buttons layout --- .../ThreadMessageSenderCellNode.swift | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift index 6d4fda8d6..d1fe2749c 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -26,12 +26,23 @@ public final class ThreadMessageSenderCellNode: CellNode { self.buttonColor = buttonColor } - var expandImageName: String { isExpanded ? "chevron.up" : "chevron.down" } + 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 = ASImageNode() + public private(set) var replyNode = ASButtonNode() public private(set) var expandNode = ASImageNode() private let input: ThreadMessageSenderCellNode.Input @@ -52,19 +63,17 @@ public final class ThreadMessageSenderCellNode: CellNode { } private func setupReplyNode() { - replyNode.image = UIImage(systemName: "arrowshape.turn.up.left", - withConfiguration: UIImage.SymbolConfiguration(pointSize: 28)) + replyNode.setImage(input.replyImage, for: .normal) + replyNode.imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) replyNode.contentMode = .center - replyNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) replyNode.alpha = input.isExpanded ? 1 : 0 replyNode.addTarget(self, action: #selector(onReplyNodeTap), forControlEvents: .touchUpInside) } private func setupExpandNode() { - expandNode.image = UIImage(systemName: input.expandImageName, - withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .light)) - expandNode.contentMode = .right + expandNode.image = input.expandImage expandNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(input.buttonColor) + expandNode.contentMode = .right } @objc private func onReplyNodeTap() {