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
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* [*] Stats: Fix an issue where tapping on a bar in a bar chart would somtimes select an adjacent bar [#25374]
* [*] Stats: Improve bar selection. When you tap on a bar, it will now select this subrange without fully navigating it, which makes it much easier to go through multiple subranges. [#25374]
* [*] Stats: Usability improvements [#25404]
* [*] Reader: Fix an issue with the "Comments" screen sometimes failing to scroll to the comment selected from the "Notifications" [#25389]
* [*] Reader: Fix an issue with pan gesture failing to dismiss Lightbox [#25372]
* [*] Reader: Fix color of links in comments [#25390]
* [*] Reader: Fix color or links in dark mode on custom themes [#25391]
Expand Down
2 changes: 1 addition & 1 deletion WordPress/Classes/Services/CommentService.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

@import WordPressShared;

NSUInteger const WPTopLevelHierarchicalCommentsPerPage = 20;
NSUInteger const WPTopLevelHierarchicalCommentsPerPage = 40;
NSInteger const WPNumberOfCommentsToSync = 100;
static NSTimeInterval const CommentsRefreshTimeoutInSeconds = 60 * 5; // 5 minutes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,24 @@ final class CommentContentTableViewCell: UITableViewCell, NibReusable {
containerStackTrailingConstraint.constant = 0
}

/// Emphasizes the cell and draws the user's attention with a flash that settles at the target highlight level.
func flashHighlight() {
isEmphasized = true

guard let highlightedBackgroundView else {
return wpAssertionFailure("background view not configured")
}

let originalBackgroundColor = highlightedBackgroundView.backgroundColor
UIView.animate(withDuration: 0.33, delay: 0.5, options: .curveEaseOut) {
highlightedBackgroundView.backgroundColor = UIAppColor.blue.withAlphaComponent(0.09)
} completion: { _ in
UIView.animate(withDuration: 0.33, delay: 1.5, options: .curveEaseOut) {
highlightedBackgroundView.backgroundColor = originalBackgroundColor
}
}
}

func hideActions() {
replyButton.isHidden = true
likeButton.isHidden = true
Expand Down Expand Up @@ -233,7 +251,7 @@ final class CommentContentTableViewCell: UITableViewCell, NibReusable {
}
for level in 0..<depth {
let separatorView = depthSeparators[level] ?? {
let separatorView = SeparatorView.vertical()
let separatorView = SeparatorView.vertical(width: 1.0)
depthSeparators[level] = separatorView
contentView.addSubview(separatorView)
separatorView.pinEdges([.top, .bottom])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ final class ReaderCommentsTableViewController: UIViewController, UITableViewData
}

@objc func scrollToComment(withID commentID: NSNumber) -> Bool {
scrollToComment(withID: commentID, animated: true)
}

func scrollToComment(withID commentID: NSNumber, animated: Bool) -> Bool {
let comments = fetchResultsController.fetchedObjects ?? []
guard let comment = comments.first(where: { $0.commentID == commentID.int32Value }) else {
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ final class ReaderCommentsViewController: UIViewController, WPContentSyncHelperD
private var commentModified = false
private var highlightedIndexPath: IndexPath?

private var navigationOverlayView: UIView?
private var navigationPagesLoaded = 0
private var navigationCommentID: Int32?
private var onNavigationCommentRendered: (() -> Void)?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wonder if these properties can be put into a dedicated struct, so that you can nil the struct instead of all these individual properties.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I looked into it, but it seems they are all serving slightly different purposes and are never updated at the same time.

  • navigationOverlayView should probably be separate as it's a dedicated view
  • onNavigationCommentRendered is set when the comment is found
  • navigationPagesLoaded is a separate counter for loading extra pages

There was some code clearing them out at once in viewDidDisappear. I opted to remove it as there isn't really a need to do that.


private var syncHelper: WPContentSyncHelper?
private var followCommentsService: FollowCommentsService?
private var readerCommentsFollowPresenter: ReaderCommentsFollowPresenter?
Expand Down Expand Up @@ -362,7 +367,8 @@ final class ReaderCommentsViewController: UIViewController, WPContentSyncHelperD
let isModerationEnabled = comment.allowsModeration()
cell.accessoryButton.showsMenuAsPrimaryAction = isModerationEnabled
cell.accessoryButton.menu = isModerationEnabled ? menu(for: comment, indexPath: indexPath, tableView: tableView, sourceView: cell.accessoryButton) : nil
cell.configure(viewModel: viewModel, helper: helper) { [weak tableView] _ in
let commentID = comment.commentID
cell.configure(viewModel: viewModel, helper: helper) { [weak self, weak tableView] _ in
guard let tableView else { return }

if tableView.alpha == 0 {
Expand All @@ -373,6 +379,7 @@ final class ReaderCommentsViewController: UIViewController, WPContentSyncHelperD
UIView.setAnimationsEnabled(false)
tableView.performBatchUpdates({})
UIView.setAnimationsEnabled(true)
self?.commentRenderedIfNeeded(commentID: commentID)
}

cell.isEmphasized = indexPath == highlightedIndexPath
Expand Down Expand Up @@ -427,15 +434,89 @@ final class ReaderCommentsViewController: UIViewController, WPContentSyncHelperD
self.highlightedIndexPath = indexPath
}

// Shows an overlay while searching for the target comment across pages (up to 5).
// Once found, waits for the cell's WebKit content to finish rendering, then
// fades the overlay out and flashes the cell to draw the user's attention.
private func navigateToCommentIDIfNeeded() {
guard let navigateToCommentID, let tableVC else {
return
guard let navigateToCommentID, let tableVC else { return }

showNavigationOverlay()

if tableVC.scrollToComment(withID: navigateToCommentID, animated: false) {
let commentID = navigateToCommentID.int32Value
self.navigateToCommentID = nil
self.navigationPagesLoaded = 0
setupNavigationReveal(commentID: commentID, in: tableVC)
} else if navigationPagesLoaded < 5, let syncHelper, syncHelper.hasMoreContent {
navigationPagesLoaded += 1
syncHelper.syncMoreContent()
} else {
self.navigateToCommentID = nil
self.navigationPagesLoaded = 0
hideNavigationOverlay(completion: nil)
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
if tableVC.scrollToComment(withID: navigateToCommentID) {
self.navigateToCommentID = nil
}

private func setupNavigationReveal(commentID: Int32, in tableVC: ReaderCommentsTableViewController) {
navigationCommentID = commentID

let reveal: () -> Void = { [weak self, weak tableVC] in
guard let self, let tableVC, self.navigationCommentID == commentID else { return }
self.navigationCommentID = nil
self.onNavigationCommentRendered = nil

guard let indexPath = self.highlightedIndexPath else { return }
tableVC.tableView.scrollToRow(at: indexPath, at: .top, animated: false)

DispatchQueue.main.asyncAfter(deadline: .now() + 0.33) { [weak self, weak tableVC] in
guard let self, let tableVC else { return }
// The initial scroll occasionally fails due to the async rendering
tableVC.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
self.hideNavigationOverlay {
(tableVC.tableView.cellForRow(at: indexPath) as? CommentContentTableViewCell)?.flashHighlight()
}
}
}

onNavigationCommentRendered = reveal

// Safety timeout in case the render callback never fires
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
guard self?.navigationCommentID == commentID else { return }
reveal()
}
}

func commentRenderedIfNeeded(commentID: Int32) {
guard commentID == navigationCommentID else { return }
onNavigationCommentRendered?()
}

private func showNavigationOverlay() {
guard navigationOverlayView == nil else { return }
let overlay = UIView()
overlay.backgroundColor = .systemBackground
let spinner = UIActivityIndicatorView(style: .medium)
spinner.startAnimating()
overlay.addSubview(spinner)
spinner.pinCenter()
view.addSubview(overlay)
overlay.pinEdges()
navigationOverlayView = overlay
}

private func hideNavigationOverlay(completion: (() -> Void)?) {
guard let overlay = navigationOverlayView else {
completion?()
return
}
navigationOverlayView = nil
UIView.animate(withDuration: 0.33) {
overlay.alpha = 0
} completion: { _ in
overlay.removeFromSuperview()
completion?()
}
}

// MARK: - Tracking
Expand Down