diff --git a/Modules/Sources/WordPressReader/Comments/Views/WebCommentContentRenderer.swift b/Modules/Sources/WordPressReader/Comments/Views/WebCommentContentRenderer.swift index 0b2b31fcdc02..a63e2380b553 100644 --- a/Modules/Sources/WordPressReader/Comments/Views/WebCommentContentRenderer.swift +++ b/Modules/Sources/WordPressReader/Comments/Views/WebCommentContentRenderer.swift @@ -33,6 +33,7 @@ public final class WebCommentContentRenderer: NSObject, CommentContentRenderer { private var cachedHead: String? private var comment: String? + private var currentNavigation: WKNavigation? private var lastReloadDate: Date? private var isReloadNeeded = false @@ -48,6 +49,7 @@ public final class WebCommentContentRenderer: NSObject, CommentContentRenderer { webView.scrollView.bounces = false webView.scrollView.showsVerticalScrollIndicator = false webView.scrollView.backgroundColor = .clear + webView.scrollView.isScrollEnabled = false NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } @@ -58,11 +60,12 @@ public final class WebCommentContentRenderer: NSObject, CommentContentRenderer { } private func actuallyRender(comment: String) { - webView.loadHTMLString(formattedHTMLString(for: comment), baseURL: nil) + currentNavigation = webView.loadHTMLString(formattedHTMLString(for: comment), baseURL: nil) } public func prepareForReuse() { comment = nil + currentNavigation = nil webView.stopLoading() } @@ -84,20 +87,22 @@ public final class WebCommentContentRenderer: NSObject, CommentContentRenderer { extension WebCommentContentRenderer: WKNavigationDelegate { public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - guard let comment else { + guard let comment, navigation === currentNavigation else { return } // Wait until the HTML document finished loading. // This also waits for all of resources within the HTML (images, video thumbnail images) to be fully loaded. webView.evaluateJavaScript("document.readyState") { complete, _ in - guard complete != nil, self.comment == comment else { + guard complete != nil, navigation === self.currentNavigation else { return } // To capture the content height, the methods to use is either `document.body.scrollHeight` or `document.documentElement.scrollHeight`. // `document.body` does not capture margins on tag, so we'll use `document.documentElement` instead. webView.evaluateJavaScript("document.documentElement.scrollHeight") { [weak self] height, _ in - guard let self, let height = height as? CGFloat else { + guard let self, + let height = height as? CGFloat, + navigation === self.currentNavigation else { return } diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 0cfcea46536a..3dd7fddbe35a 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -10,6 +10,8 @@ * [*] Reader: Collapse multiple spaces in site titles into one [#25094] * [*] Reader: Add a "Leave Comment" button to directly to the "Comments" under the article (one less tap to leave a comment) [#25155] * [*] Reader: You can now reply and like the comments directly under the artile and see more of them [#25155] +* [*] Reader: Collapse very long comments so it's easier to navigate large comment threads [#25155] +* [*] Reader: Fix an issue where in rare scenarios comments would be displayed with a wrong height [#25156] * [*] Post Settings: Fix post Visibility discoverability [#25127] * [*] Post Settings: Fix an issue with button "Done" sometimes disappearing when you entering password for post when publishing [#25138] * [*] Post Settings: Show the selected value for the "Access" field [#25138] diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift index e05885b9315e..07748d35943c 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift @@ -216,6 +216,10 @@ public class CommentDetailViewController: UIViewController, NoResultsViewHost { self.managedObjectContext = managedObjectContext super.init(nibName: nil, bundle: nil) + + if let post = comment.post as? ReaderPost { + helper.isP2Site = post.isP2Type + } } init(comment: Comment, @@ -229,6 +233,10 @@ public class CommentDetailViewController: UIViewController, NoResultsViewHost { self.managedObjectContext = managedObjectContext super.init(nibName: nil, bundle: nil) + + if let post = comment.post as? ReaderPost { + helper.isP2Site = post.isP2Type + } } public required init?(coder: NSCoder) { diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentComposerReplyCommentView.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentComposerReplyCommentView.swift index 4eac50f7e5ae..923c96dbfa83 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentComposerReplyCommentView.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentComposerReplyCommentView.swift @@ -13,6 +13,10 @@ final class CommentComposerReplyCommentView: UIView, UITableViewDataSource { super.init(frame: .zero) + if let post = comment.post as? ReaderPost { + helper.isP2Site = post.isP2Type + } + addSubview(tableView) tableView.pinEdges() diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentShowMoreOverlay.swift b/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentShowMoreOverlay.swift new file mode 100644 index 000000000000..4ddba5f82c2a --- /dev/null +++ b/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentShowMoreOverlay.swift @@ -0,0 +1,63 @@ +import UIKit + +final class CommentContentShowMoreOverlay: UIView { + var onTap: (() -> Void)? + + private let gradientLayer = CAGradientLayer() + private let button = UIButton() + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + private func setupView() { + isUserInteractionEnabled = true + + // Setup gradient + gradientLayer.colors = [ + UIColor.systemBackground.withAlphaComponent(0).cgColor, + UIColor.systemBackground.withAlphaComponent(0.9).cgColor, + UIColor.systemBackground.cgColor + ] + gradientLayer.locations = [0.0, 0.5, 1.0] + layer.addSublayer(gradientLayer) + + // Setup button + var configuration = UIButton.Configuration.plain() + configuration.attributedTitle = AttributedString(Strings.showMore, attributes: AttributeContainer([ + .font: UIFont.preferredFont(forTextStyle: .subheadline).withWeight(.medium) + ])) + configuration.image = UIImage(systemName: "chevron.up.chevron.down") + configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(font: UIFont.preferredFont(forTextStyle: .caption2).withWeight(.medium)) + configuration.imagePlacement = .leading + configuration.imagePadding = 6 + configuration.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 12, bottom: 9, trailing: 12) + + button.configuration = configuration + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) + button.maximumContentSizeCategory = .extraExtraExtraLarge + + addSubview(button) + button.pinEdges([.horizontal, .bottom]) + } + + @objc private func buttonTapped() { + onTap?() + } + + override func layoutSubviews() { + super.layoutSubviews() + gradientLayer.frame = bounds + } +} + +private enum Strings { + static let showMore = NSLocalizedString("reader.comments.showMore", value: "Show More", comment: "Button to expand a collapsed comment to show its full content.") +} diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift b/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift index ae8aed0e9240..9f16a0dd6c9e 100644 --- a/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift @@ -82,6 +82,8 @@ final class CommentContentTableViewCell: UITableViewCell, NibReusable { @IBOutlet private weak var contentContainerView: UIView! @IBOutlet private weak var contentContainerHeightConstraint: NSLayoutConstraint! + private var showMoreOverlay: CommentContentShowMoreOverlay? + @IBOutlet private weak var replyButton: UIButton! @IBOutlet private weak var likeButton: UIButton! @@ -101,6 +103,10 @@ final class CommentContentTableViewCell: UITableViewCell, NibReusable { private var viewModel: CommentCellViewModel? private var cancellables: [AnyCancellable] = [] + private static let maxCollapsedHeight: CGFloat = 280 + private var fullContentHeight: CGFloat? + private var isContentExpanded: Bool = false + // MARK: Like Button State /// Styling configuration based on `ReaderDisplaySetting`. The parameter is optional so that the styling approach @@ -140,6 +146,10 @@ final class CommentContentTableViewCell: UITableViewCell, NibReusable { contentLinkTapAction = nil onContentLoaded = nil + + fullContentHeight = nil + isContentExpanded = false + showMoreOverlay?.isHidden = true } override func awakeFromNib() { @@ -164,6 +174,7 @@ final class CommentContentTableViewCell: UITableViewCell, NibReusable { self.comment = comment self.viewModel = viewModel self.helper = helper + self.isContentExpanded = helper.isCommentExpanded(comment.objectID) self.onContentLoaded = onContentLoaded viewModel.$state.sink { [weak self] in @@ -256,14 +267,10 @@ final class CommentContentTableViewCell: UITableViewCell, NibReusable { extension CommentContentTableViewCell: CommentContentRendererDelegate { func renderer(_ renderer: CommentContentRenderer, asyncRenderCompletedWithHeight height: CGFloat, comment: String) { - if let constraint = contentContainerHeightConstraint { - if height != constraint.constant { - constraint.constant = height - helper?.setCachedContentHeight(height, for: comment) - onContentLoaded?(height) // We had the right size from the get-go - } - } else { - wpAssertionFailure("constraint missing") + if height != fullContentHeight { + helper?.setCachedContentHeight(height, for: comment) + configureContentView(contentHeight: height) + onContentLoaded?(height) } } @@ -399,6 +406,24 @@ private extension CommentContentTableViewCell { dateLabel?.textColor = style.dateTextColor } + private func getShowMoreOverlay() -> UIView { + if let showMoreOverlay { + return showMoreOverlay + } + + let overlay = CommentContentShowMoreOverlay() + overlay.onTap = { [weak self] in + self?.showMoreButtonTapped() + } + contentView.addSubview(overlay) + overlay.pinEdges([.horizontal, .bottom], to: contentContainerView) + NSLayoutConstraint.activate([ + overlay.heightAnchor.constraint(equalToConstant: 70) + ]) + showMoreOverlay = overlay + return overlay + } + private func configureAvatar(with avatar: CommentCellViewModel.Avatar?) { guard let avatar else { avatarImageView.wp.prepareForReuse() @@ -446,12 +471,7 @@ private extension CommentContentTableViewCell { return renderer }() - // reset height constraint to handle cases where the new content requires the webview to shrink. - contentContainerHeightConstraint?.isActive = true - // - warning: It's important to set height to the minimum supported - // value because `WKWebView` can only increase the content height and - // never decreases it when the content changes. - contentContainerHeightConstraint?.constant = helper.getCachedContentHeight(for: content) ?? 20 + configureContentView(contentHeight: helper.getCachedContentHeight(for: content)) let contentView = renderer.view if contentContainerView.subviews.first != contentView { @@ -460,9 +480,45 @@ private extension CommentContentTableViewCell { contentContainerView?.addSubview(contentView) contentView.pinEdges() } + renderer.render(comment: content) } + /// Updates the height of the content view according to the content + /// height (if known). + private func configureContentView(contentHeight: CGFloat?) { + fullContentHeight = contentHeight + + // - warning: It's important to set height to the minimum supported + // value because `WKWebView` can only increase the content height whe + // calculating content size (it will never decrease it). + let minContentHeight: CGFloat = 20 + let effectiveContentHeight = contentHeight ?? minContentHeight + + // P2 sites have a much higher percentage of long comments, so we can't + // collapse them as much as for regular posts + let threshold: CGFloat = (helper?.isP2Site ?? false) ? 280 : 120 + if let contentHeight, contentHeight < Self.maxCollapsedHeight + threshold { + // Expand automatically if the margin it too close, so there is no + // point hiding a few pixels + isContentExpanded = true + } + let displayHeight = isContentExpanded ? effectiveContentHeight : min(effectiveContentHeight, Self.maxCollapsedHeight) + + contentContainerHeightConstraint?.constant = displayHeight + + let isActuallyExpanded = displayHeight >= effectiveContentHeight + setShowMoreOverlayHidden(isActuallyExpanded) + } + + private func setShowMoreOverlayHidden(_ isHidden: Bool) { + if isHidden { + showMoreOverlay?.isHidden = true + } else { + getShowMoreOverlay().isHidden = false + } + } + // MARK: Button Actions @objc private func headerTapped() { @@ -501,6 +557,19 @@ private extension CommentContentTableViewCell { viewModel.buttonLikeTapped() } } + + @objc func showMoreButtonTapped() { + guard let fullContentHeight else { + return wpAssertionFailure("Invalid state") + } + isContentExpanded = true + if let commentID = viewModel?.comment.objectID { + helper?.setCommentExpanded(commentID, isExpanded: true) + } + contentContainerHeightConstraint?.constant = fullContentHeight + setShowMoreOverlayHidden(true) + onContentLoaded?(fullContentHeight) + } } // MARK: - Localization diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsHelper.swift b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsHelper.swift index eb397c5867fd..cac7c3e9aa28 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsHelper.swift +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsHelper.swift @@ -1,4 +1,5 @@ import Foundation +import CoreData import WordPressReader import WordPressUI @@ -6,6 +7,9 @@ import WordPressUI @MainActor @objc class ReaderCommentsHelper: NSObject { private var contentHeights: [String: CGFloat] = [:] + private var expandedComments: Set = [] + + var isP2Site: Bool = false func makeWebRenderer() -> WebCommentContentRenderer { let renderer = WebCommentContentRenderer() @@ -24,4 +28,16 @@ import WordPressUI func resetCachedContentHeights() { contentHeights.removeAll() } + + func isCommentExpanded(_ commentID: NSManagedObjectID) -> Bool { + expandedComments.contains(commentID) + } + + func setCommentExpanded(_ commentID: NSManagedObjectID, isExpanded: Bool) { + if isExpanded { + expandedComments.insert(commentID) + } else { + expandedComments.remove(commentID) + } + } } diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift index 284ced77b339..e1f20ba1e02f 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift @@ -52,6 +52,7 @@ final class ReaderCommentsViewController: UIViewController, WPContentSyncHelperD init(post: ReaderPost) { self.post = post super.init(nibName: nil, bundle: nil) + helper.isP2Site = post.isP2Type } init(postID: NSNumber, siteID: NSNumber) { @@ -157,6 +158,7 @@ final class ReaderCommentsViewController: UIViewController, WPContentSyncHelperD private func configure(with post: ReaderPost) { self.post = post + helper.isP2Site = post.isP2Type if post.isWPCom || post.isJetpack { let tableVC = ReaderCommentsTableViewController(post: post) diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailCommentsTableViewDelegate.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailCommentsTableViewDelegate.swift index 32b3048026c0..72f9b74d53fe 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailCommentsTableViewDelegate.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailCommentsTableViewDelegate.swift @@ -46,6 +46,7 @@ class ReaderDetailCommentsTableViewDelegate: NSObject, UITableViewDataSource, UI self.totalComments = totalComments self.presentingViewController = presentingViewController self.buttonDelegate = buttonDelegate + helper.isP2Site = post.isP2Type var items: [Item] = [] if post.commentsOpen {