Skip to content

Commit

Permalink
Simplify ReaderDetailLikesView further
Browse files Browse the repository at this point in the history
  • Loading branch information
kean committed Dec 19, 2024
1 parent bf68b1e commit a4639df
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 128 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Foundation
import WordPressShared
import Combine

class ReaderDetailCoordinator {
final class ReaderDetailCoordinator {

/// Key for restoring the VC post
static let restorablePostObjectURLKey: String = "RestorablePostObjectURLKey"
Expand Down Expand Up @@ -43,6 +44,8 @@ class ReaderDetailCoordinator {
/// Called if the view controller's post fails to load
var postLoadFailureBlock: (() -> Void)? = nil

private var likesAvatarURLs: [String]?

/// An authenticator to ensure any request made to WP sites is properly authenticated
lazy var authenticator: RequestAuthenticator? = {
guard let account = try? WPAccount.lookupDefaultWordPressComAccount(in: coreDataStack.mainContext) else {
Expand Down Expand Up @@ -102,10 +105,7 @@ class ReaderDetailCoordinator {
}

private var followCommentsService: FollowCommentsService?

/// The total number of Likes for the post.
/// Passed to ReaderDetailLikesListController to display in the view title.
private var totalLikes = 0
private var likesObserver: AnyCancellable?

/// Initialize the Reader Detail Coordinator
///
Expand Down Expand Up @@ -156,41 +156,52 @@ class ReaderDetailCoordinator {
}
}

/// Fetch Likes for the current post.
/// Returns `ReaderDetailLikesView.maxAvatarsDisplayed` number of Likes.
///
func fetchLikes(for post: ReaderPost) {
guard let postID = post.postID else {
return
}
guard let postID = post.postID else { return }

// Fetch a full page of Likes but only return the `maxAvatarsDisplayed` number.
// That way the first page will already be cached if the user displays the full Likes list.
postService.getLikesFor(postID: postID, siteID: post.siteID, success: { [weak self] users, totalLikes, _ in
var filteredUsers = users
var currentLikeUser: LikeUser? = nil
let totalLikesExcludingSelf = totalLikes - (post.isLiked ? 1 : 0)
guard let self else { return }

// Split off current user's like from the list.
// Likes from self will always be placed in the last position, regardless of the when the post was liked.
var filteredUsers = users
if let userID = try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext)?.userID.int64Value,
let userIndex = filteredUsers.firstIndex(where: { $0.userID == userID }) {
currentLikeUser = filteredUsers.remove(at: userIndex)
filteredUsers.remove(at: userIndex)
}

self?.totalLikes = totalLikes
self?.view?.updateLikes(with: filteredUsers.prefix(ReaderDetailLikesView.maxAvatarsDisplayed).map { $0.avatarUrl },
totalLikes: totalLikesExcludingSelf)
// Only pass current user's avatar when we know *for sure* that the post is liked.
// This is to work around a possible race condition that causes an unliked post to have current user's LikeUser, which
// would cause a display bug in ReaderDetailLikesView. The race condition issue will be investigated separately.
self?.view?.updateSelfLike(with: post.isLiked ? currentLikeUser?.avatarUrl : nil)
}, failure: { [weak self] error in
self?.view?.updateLikes(with: [String](), totalLikes: 0)
self.likesAvatarURLs = filteredUsers.prefix(ReaderDetailLikesView.maxAvatarsDisplayed).map(\.avatarUrl)
self.updateLikesView()
self.startObservingLikes()
}, failure: { error in
DDLogError("Error fetching Likes for post detail: \(String(describing: error?.localizedDescription))")
})
}

private func startObservingLikes() {
guard let post else {
return wpAssertionFailure("post missing")
}

likesObserver = Publishers.CombineLatest(
post.publisher(for: \.likeCount, options: [.new]).removeDuplicates(),
post.publisher(for: \.isLiked, options: [.new]).removeDuplicates()
).sink { [weak self] _, _ in
self?.updateLikesView()
}
}

private func updateLikesView() {
guard let post, let likesAvatarURLs else { return }

let viewModel = ReaderDetailLikesViewModel(
likeCount: post.likeCount.intValue,
avatarURLs: likesAvatarURLs,
selfLikeAvatarURL: post.isLiked ? try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext)?.avatarURL : nil
)
view?.updateLikesView(with: viewModel)
}

/// Fetch Comments for the current post.
///
func fetchComments(for post: ReaderPost) {
Expand Down Expand Up @@ -588,8 +599,7 @@ class ReaderDetailCoordinator {
guard let post else {
return
}

let controller = ReaderDetailLikesListController(post: post, totalLikes: totalLikes)
let controller = ReaderDetailLikesListController(post: post, totalLikes: post.likeCount.intValue)
viewController?.navigationController?.pushViewController(controller, animated: true)
}

Expand Down Expand Up @@ -710,30 +720,19 @@ extension ReaderDetailCoordinator: ReaderDetailHeaderViewDelegate {
}
}

// MARK: - ReaderDetailFeaturedImageViewDelegate
extension ReaderDetailCoordinator: ReaderDetailFeaturedImageViewDelegate {
func didTapFeaturedImage(_ sender: AsyncImageView) {
showFeaturedImage(sender)
}
}

// MARK: - ReaderDetailLikesViewDelegate
extension ReaderDetailCoordinator: ReaderDetailLikesViewDelegate {
func didTapLikesView() {
showLikesList()
}
}

// MARK: - ReaderDetailToolbarDelegate
extension ReaderDetailCoordinator: ReaderDetailToolbarDelegate {
func didTapLikeButton(isLiked: Bool) {
guard let userAvatarURL = try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext)?.avatarURL else {
return
}

self.view?.updateSelfLike(with: isLiked ? userAvatarURL : nil)
}
}
extension ReaderDetailCoordinator: ReaderDetailToolbarDelegate {}

// MARK: - Private Definitions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,7 @@ protocol ReaderDetailView: AnyObject {
func showErrorWithWebAction()
func scroll(to: String)
func updateHeader()

/// Shows likes view containing avatars of users that liked the post.
/// The number of avatars displayed is limited to `ReaderDetailView.maxAvatarDisplayed` plus the current user's avatar.
/// Note that the current user's avatar is displayed through a different method.
///
/// - Seealso: `updateSelfLike(with avatarURLString: String?)`
/// - Parameters:
/// - avatarURLStrings: A list of URL strings for the liking users' avatars.
/// - totalLikes: The total number of likes for this post.
func updateLikes(with avatarURLStrings: [String], totalLikes: Int)

/// Updates the likes view to append an additional avatar for the current user, indicating that the post is liked by current user.
/// - Parameter avatarURLString: The URL string for the current user's avatar. Optional.
func updateSelfLike(with avatarURLString: String?)
func updateLikesView(with viewModel: ReaderDetailLikesViewModel)

/// Updates comments table to display the post's comments.
/// - Parameters:
Expand Down Expand Up @@ -374,38 +361,17 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView {
header.refreshFollowButton()
}

func updateLikes(with avatarURLStrings: [String], totalLikes: Int) {
// always configure likes summary view first regardless of totalLikes, since it can affected by self likes.
likesSummary.configure(with: avatarURLStrings, totalLikes: totalLikes)

guard totalLikes > 0 else {
func updateLikesView(with viewModel: ReaderDetailLikesViewModel) {
guard viewModel.likeCount > 0 else {
hideLikesView()
return
}

if likesSummary.superview == nil {
configureLikesSummary()
}

scrollView.layoutIfNeeded()
}

func updateSelfLike(with avatarURLString: String?) {
// only animate changes when the view is visible.
let shouldAnimate = isVisibleInScrollView(likesSummary)
guard let someURLString = avatarURLString else {
likesSummary.removeSelfAvatar(animated: shouldAnimate)
if likesSummary.totalLikesForDisplay == 0 {
hideLikesView()
}
return
}

if likesSummary.superview == nil {
configureLikesSummary()
}

likesSummary.addSelfAvatar(with: someURLString, animated: shouldAnimate)
likesSummary.configure(with: viewModel)
}

func updateComments(_ comments: [Comment], totalComments: Int) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final class ReaderDetailLikesView: UIView, NibLoadable {
didSet {
applyStyles()
if let viewModel {
configure(with: viewModel, animated: false)
configure(with: viewModel)
}
}
}
Expand All @@ -35,49 +35,16 @@ final class ReaderDetailLikesView: UIView, NibLoadable {
addTapGesture()
}

func configure(with viewModel: ReaderDetailLikesViewModel, animated: Bool) {
func configure(with viewModel: ReaderDetailLikesViewModel) {
self.viewModel = viewModel

summaryLabel.attributedText = makeHighlightedText(Strings.formattedLikeCount(viewModel.likeCount), displaySetting: displaySetting)

updateAvatars(with: viewModel.avatarURLs)

selfAvatarImageView.isHidden = viewModel.selfLikeAvatarURL == nil
if let avatarURL = viewModel.selfLikeAvatarURL {
addSelfAvatar(with: avatarURL, animated: animated)
} else {
removeSelfAvatar(animated: animated)
}
}

private func addSelfAvatar(with urlString: String, animated: Bool = false) {
downloadGravatar(for: selfAvatarImageView, withURL: urlString)

// pre-animation state
// set initial position from the left in LTR, or from the right in RTL.
selfAvatarImageView.alpha = 0
let directionalMultiplier: CGFloat = userInterfaceLayoutDirection() == .leftToRight ? -1.0 : 1.0
selfAvatarImageView.transform = CGAffineTransform(translationX: Constants.animationDeltaX * directionalMultiplier, y: 0)

UIView.animate(withDuration: animated ? Constants.animationDuration : 0) {
// post-animation state
self.selfAvatarImageView.alpha = 1
self.selfAvatarImageView.isHidden = false
self.selfAvatarImageView.transform = .identity
}
}

private func removeSelfAvatar(animated: Bool = false) {
// pre-animation state
selfAvatarImageView.alpha = 1
selfAvatarImageView.transform = .identity

UIView.animate(withDuration: animated ? Constants.animationDuration : 0) {
// post-animation state
// moves to the left in LTR, or to the right in RTL.
self.selfAvatarImageView.alpha = 0
self.selfAvatarImageView.isHidden = true
let directionalMultiplier: CGFloat = self.userInterfaceLayoutDirection() == .leftToRight ? -1.0 : 1.0
self.selfAvatarImageView.transform = CGAffineTransform(translationX: Constants.animationDeltaX * directionalMultiplier, y: 0)
downloadGravatar(for: selfAvatarImageView, withURL: avatarURL)
}
}

Expand Down Expand Up @@ -127,11 +94,6 @@ private extension ReaderDetailLikesView {
@objc func didTapView(_ gesture: UITapGestureRecognizer) {
delegate?.didTapLikesView()
}

struct Constants {
static let animationDuration: TimeInterval = 0.3
static let animationDeltaX: CGFloat = 16.0
}
}

private func makeHighlightedText(_ text: String, displaySetting: ReaderDisplaySetting) -> NSAttributedString {
Expand Down Expand Up @@ -162,10 +124,10 @@ private func makeHighlightedText(_ text: String, displaySetting: ReaderDisplaySe

struct ReaderDetailLikesViewModel {
/// A total like count, including your likes.
let likeCount: Int
var likeCount: Int
/// Avatar URLs excluding self-like view.
let avatarURLs: [String]
let selfLikeAvatarURL: String?
var avatarURLs: [String]
var selfLikeAvatarURL: String?
}

private enum Strings {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import UIKit
import WordPressUI

protocol ReaderDetailToolbarDelegate: AnyObject {
func didTapLikeButton(isLiked: Bool)
var notificationID: String? { get }
}

Expand Down Expand Up @@ -460,7 +459,6 @@ private extension ReaderDetailToolbar {
}

self?.configureLikeActionButton(true)
self?.delegate?.didTapLikeButton(isLiked: updatedPost.isLiked)
}

commentCountObserver = post?.observe(\.commentCount, options: [.old, .new]) { [weak self] _, change in
Expand Down
4 changes: 1 addition & 3 deletions WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -343,9 +343,7 @@ private class ReaderDetailViewMock: UIViewController, ReaderDetailView {

func updateHeader() { }

func updateLikes(with avatarURLStrings: [String], totalLikes: Int) { }

func updateSelfLike(with avatarURLString: String?) { }
func updateLikesView(with viewModel: ReaderDetailLikesViewModel) {}

func updateComments(_ comments: [Comment], totalComments: Int) { }

Expand Down

0 comments on commit a4639df

Please sign in to comment.