Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}
Expand All @@ -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()
}

Expand All @@ -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 <body> 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
}

Expand Down
2 changes: 2 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
@@ -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.")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand All @@ -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
Expand Down Expand Up @@ -140,6 +146,10 @@ final class CommentContentTableViewCell: UITableViewCell, NibReusable {
contentLinkTapAction = nil

onContentLoaded = nil

fullContentHeight = nil
isContentExpanded = false
showMoreOverlay?.isHidden = true
}

override func awakeFromNib() {
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this constraint may cause conflicts if the font is super large? Instead of hard-code a height here, would setting the button.contentInsets or the margins between the button and its parent view work?

])
showMoreOverlay = overlay
return overlay
}

private func configureAvatar(with avatar: CommentCellViewModel.Avatar?) {
guard let avatar else {
avatarImageView.wp.prepareForReuse()
Expand Down Expand Up @@ -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 {
Expand All @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import Foundation
import CoreData
import WordPressReader
import WordPressUI

/// A collection of utilities for managing rendering for comments.
@MainActor
@objc class ReaderCommentsHelper: NSObject {
private var contentHeights: [String: CGFloat] = [:]
private var expandedComments: Set<NSManagedObjectID> = []

var isP2Site: Bool = false

func makeWebRenderer() -> WebCommentContentRenderer {
let renderer = WebCommentContentRenderer()
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down