From 69fc6e68dd20e6a678102da7a22bc6ab35b3359e Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 27 Nov 2025 16:27:35 +0100 Subject: [PATCH 01/13] Adding a loading spinner to the recommended blogs subscribe button --- .../ui/reader/discover/ReaderCardUiState.kt | 2 +- .../discover/ReaderDiscoverViewModel.kt | 5 +++- .../discover/ReaderPostUiStateBuilder.kt | 1 - .../ReaderRecommendedBlogViewHolder.kt | 18 ++++++++---- .../ui/reader/views/ReaderFollowButton.kt | 9 ++++++ .../layout/reader_recommended_blog_item.xml | 28 +++++++++++++++---- WordPress/src/main/res/values/dimens.xml | 1 + .../discover/ReaderDiscoverViewModelTest.kt | 1 - 8 files changed, 49 insertions(+), 16 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt index a8a9084605cd..f421249ecd02 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt @@ -122,7 +122,7 @@ sealed class ReaderCardUiState { val description: String?, val iconUrl: String?, val isFollowed: Boolean, - val isFollowEnabled: Boolean, + val isFollowActionRunning: Boolean = false, val onItemClicked: (Long, Long, Boolean) -> Unit, val onFollowClicked: (ReaderRecommendedBlogUiState) -> Unit ) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt index 559cce8a0a8a..3477a7550215 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt @@ -200,7 +200,10 @@ class ReaderDiscoverViewModel @Inject constructor( for (j in mutableBlogs.indices) { val blog = mutableBlogs[j] if (blog.blogId == data.blogId && blog.feedId == data.feedId) { - mutableBlogs[j] = blog.copy(isFollowed = data.following, isFollowEnabled = data.isChangeFinal) + mutableBlogs[j] = blog.copy( + isFollowed = data.following, + isFollowActionRunning = !data.isChangeFinal + ) hasChangedBlogs = true } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilder.kt index 579b4755f368..825625646329 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilder.kt @@ -200,7 +200,6 @@ class ReaderPostUiStateBuilder @Inject constructor( description = it.description.ifEmpty { null }, iconUrl = it.imageUrl, isFollowed = it.isFollowing, - isFollowEnabled = true, onFollowClicked = onFollowClicked, onItemClicked = onItemClicked ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderRecommendedBlogViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderRecommendedBlogViewHolder.kt index 4c39cbb75d39..e54e47bc7694 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderRecommendedBlogViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderRecommendedBlogViewHolder.kt @@ -1,5 +1,6 @@ package org.wordpress.android.ui.reader.discover.viewholders +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.wordpress.android.databinding.ReaderRecommendedBlogItemBinding @@ -29,12 +30,16 @@ class ReaderRecommendedBlogViewHolder( uiState: ReaderRecommendedBlogUiState, binding: ReaderRecommendedBlogItemBinding ) { - with(binding.siteFollowButton) { - isEnabled = uiState.isFollowEnabled - setIsFollowed(uiState.isFollowed) - contentDescription = context.getString(uiState.followContentDescription.stringRes) - setOnClickListener { - uiState.onFollowClicked(uiState) + with(binding) { + siteFollowProgress.visibility = if (uiState.isFollowActionRunning) View.VISIBLE else View.GONE + siteFollowButton.apply { + setIsLoading(uiState.isFollowActionRunning) + isEnabled = !uiState.isFollowActionRunning + setIsFollowed(uiState.isFollowed) + contentDescription = context.getString(uiState.followContentDescription.stringRes) + setOnClickListener { + uiState.onFollowClicked(uiState) + } } } } @@ -50,4 +55,5 @@ class ReaderRecommendedBlogViewHolder( imageManager.cancelRequestAndClearImageView(siteIcon) } } + } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.kt index 30b3338c7545..088ab37992b0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.kt @@ -24,6 +24,7 @@ class ReaderFollowButton @JvmOverloads constructor( ) : MaterialButton(context, attrs, defStyleAttr) { private var isFollowed = false private var showCaption = false + private var isLoading = false init { initView(context, attrs) @@ -58,6 +59,14 @@ class ReaderFollowButton @JvmOverloads constructor( setIsFollowed(isFollowed, true) } + fun setIsLoading(loading: Boolean) { + if (isLoading == loading) return + isLoading = loading + isEnabled = !loading + } + + fun getIsLoading(): Boolean = isLoading + @SuppressLint("Recycle") private fun setIsFollowed(isFollowed: Boolean, animateChanges: Boolean) { if (isFollowed == this.isFollowed && isSelected == isFollowed) { diff --git a/WordPress/src/main/res/layout/reader_recommended_blog_item.xml b/WordPress/src/main/res/layout/reader_recommended_blog_item.xml index 54ed1bc6c9ae..05a36427924c 100644 --- a/WordPress/src/main/res/layout/reader_recommended_blog_item.xml +++ b/WordPress/src/main/res/layout/reader_recommended_blog_item.xml @@ -34,7 +34,7 @@ android:textColor="?attr/colorOnSurface" android:textSize="@dimen/text_sz_medium" app:layout_constraintBottom_toTopOf="@+id/site_url" - app:layout_constraintEnd_toStartOf="@id/site_follow_button" + app:layout_constraintEnd_toStartOf="@id/site_follow_container" app:layout_constraintStart_toEndOf="@id/site_icon" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" @@ -55,14 +55,30 @@ app:layout_constraintTop_toBottomOf="@+id/site_name" tools:text="site.com site.com site.com site.com site.com site.com site.com site.com " /> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index 18cac035775e..9b4398bb534d 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -192,6 +192,7 @@ 36dp 24dp 36dp + 24dp 24dp diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt index 176629483ccc..cb4b833ca30c 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt @@ -745,7 +745,6 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { onItemClicked = onItemClicked, onFollowClicked = onFollowClicked, isFollowed = it.isFollowing, - isFollowEnabled = true, ) } ) From 288ff57f0b20734e101b1fce5fad9040054ca4b6 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 27 Nov 2025 16:47:09 +0100 Subject: [PATCH 02/13] Fixing the loading wrong state --- .../discover/ReaderDiscoverViewModel.kt | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt index 3477a7550215..0fe16a07417c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt @@ -157,8 +157,11 @@ class ReaderDiscoverViewModel @Inject constructor( emptyList() } + val newCards = convertCardsToUiStates(posts) + val cardsWithPreservedState = preserveFollowActionRunningState(newCards) + _uiState.value = DiscoverUiState.ContentUiState( - announcement + convertCardsToUiStates(posts), + announcement + cardsWithPreservedState, reloadProgressVisibility = false, loadMoreProgressVisibility = false, ) @@ -175,6 +178,30 @@ class ReaderDiscoverViewModel @Inject constructor( } } + private fun preserveFollowActionRunningState(newCards: List): List { + val currentUiState = _uiState.value as? DiscoverUiState.ContentUiState ?: return newCards + + return newCards.map { card -> + if (card is ReaderCardUiState.ReaderRecommendedBlogsCardUiState) { + val updatedBlogs = card.blogs.map { blog -> + val currentBlog = currentUiState.cards + .filterIsInstance() + .flatMap { it.blogs } + .find { it.blogId == blog.blogId && it.feedId == blog.feedId } + + if (currentBlog != null && currentBlog.isFollowActionRunning) { + blog.copy(isFollowActionRunning = true) + } else { + blog + } + } + card.copy(blogs = updatedBlogs) + } else { + card + } + } + } + private fun dismissAnnouncementCard() { readerAnnouncementHelper.dismissReaderAnnouncement() _uiState.value = (_uiState.value as? DiscoverUiState.ContentUiState)?.let { contentUiState -> From bfa8eb9afb022a37ac30d73359e9b31f7829fb8e Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 27 Nov 2025 17:31:28 +0100 Subject: [PATCH 03/13] Fixing it for the post details screen --- .../ui/reader/adapters/ReaderPostAdapter.java | 4 +-- .../viewmodels/ReaderPostDetailViewModel.kt | 30 ++++++++++++++++--- .../views/ReaderPostDetailHeaderView.kt | 18 +++++++---- ...aderPostDetailsHeaderViewUiStateBuilder.kt | 1 - .../ui/reader/views/ReaderTagHeaderView.kt | 2 +- .../views/uistates/FollowButtonUiState.kt | 4 +-- .../layout/reader_post_detail_header_view.xml | 26 ++++++++++++---- .../ReaderPostDetailViewModelTest.kt | 1 - 8 files changed, 64 insertions(+), 22 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java index 5f17855b93e5..705262f26773 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java @@ -348,8 +348,8 @@ private void renderTagHeader( new FollowButtonUiState( onFollowButtonClicked, ReaderTagTable.isFollowedTagName(currentTag.getTagSlug()), - isFollowButtonEnabled, - true + isFollowButtonEnabled, // isVisible + false // isFollowActionRunning ) ); tagHolder.onBind(uiState); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt index 465124161f4d..7c60c7e58411 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt @@ -255,7 +255,7 @@ class ReaderPostDetailViewModel @Inject constructor( updateFollowButtonUiState( currentUiState = currentUiState, isFollowed = post.isFollowedByCurrentUser, - isFollowEnabled = data.isChangeFinal + isFollowActionRunning = !data.isChangeFinal ) } } @@ -608,11 +608,30 @@ class ReaderPostDetailViewModel @Inject constructor( private fun convertPostToUiState( post: ReaderPost ): ReaderPostDetailsUiState { - return postDetailUiStateBuilder.mapPostToUiState( + val newUiState = postDetailUiStateBuilder.mapPostToUiState( post = post, onButtonClicked = this@ReaderPostDetailViewModel::onButtonClicked, onHeaderAction = { action -> onHeaderAction(post, action) }, ) + return preserveFollowActionRunningState(newUiState) + } + + private fun preserveFollowActionRunningState( + newUiState: ReaderPostDetailsUiState + ): ReaderPostDetailsUiState { + val currentUiState = _uiState.value as? ReaderPostDetailsUiState ?: return newUiState + val currentFollowButtonState = currentUiState.headerUiState.followButtonUiState + + if (currentFollowButtonState.isFollowActionRunning) { + val updatedFollowButtonUiState = newUiState.headerUiState.followButtonUiState.copy( + isFollowActionRunning = true + ) + val updatedHeaderUiState = newUiState.headerUiState.copy( + followButtonUiState = updatedFollowButtonUiState + ) + return newUiState.copy(headerUiState = updatedHeaderUiState) + } + return newUiState } private fun convertRelatedPostsToUiState( @@ -637,12 +656,15 @@ class ReaderPostDetailViewModel @Inject constructor( private fun updateFollowButtonUiState( currentUiState: ReaderPostDetailsUiState, isFollowed: Boolean, - isFollowEnabled: Boolean, + isFollowActionRunning: Boolean, ) { val updatedFollowButtonUiState = currentUiState .headerUiState .followButtonUiState - .copy(isFollowed = isFollowed, isEnabled = isFollowEnabled) + .copy( + isFollowed = isFollowed, + isFollowActionRunning = isFollowActionRunning + ) val updatedHeaderUiState = currentUiState .headerUiState diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.kt index 611ac23f0e9c..5540d5d8a9a3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.kt @@ -58,7 +58,7 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( uiHelpers.setTextOrHide(layoutBlogSection.blogSectionTextBlogName, uiState.blogSectionUiState.blogName) - headerFollowButton.update(uiState.followButtonUiState) + updateFollowButton(uiState.followButtonUiState) updateAvatars(uiState.blogSectionUiState) updateBlogSectionClick(uiState.blogSectionUiState) @@ -113,11 +113,17 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( } } - private fun ReaderFollowButton.update(followButtonUiState: FollowButtonUiState) { - isEnabled = followButtonUiState.isEnabled - setVisible(followButtonUiState.isVisible) - setIsFollowed(followButtonUiState.isFollowed) - setOnClickListener { followButtonUiState.onFollowButtonClicked?.invoke() } + private fun ReaderPostDetailHeaderViewBinding.updateFollowButton( + followButtonUiState: FollowButtonUiState + ) { + headerFollowButtonContainer.setVisible(followButtonUiState.isVisible) + headerFollowProgress.setVisible(followButtonUiState.isFollowActionRunning) + headerFollowButton.apply { + setIsLoading(followButtonUiState.isFollowActionRunning) + isEnabled = !followButtonUiState.isFollowActionRunning + setIsFollowed(followButtonUiState.isFollowed) + setOnClickListener { followButtonUiState.onFollowButtonClicked?.invoke() } + } } private fun setAuthorAndDate(authorName: String?, dateLine: String) = with(binding.layoutBlogSection) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailsHeaderViewUiStateBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailsHeaderViewUiStateBuilder.kt index 7872badb82e3..af19d449e3f3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailsHeaderViewUiStateBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailsHeaderViewUiStateBuilder.kt @@ -74,7 +74,6 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( return FollowButtonUiState( onFollowButtonClicked = onFollowClicked, isFollowed = post.isFollowedByCurrentUser, - isEnabled = hasAccessToken, isVisible = hasAccessToken ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagHeaderView.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagHeaderView.kt index d1b3eb6f0f9d..f2d63c2f139f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagHeaderView.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagHeaderView.kt @@ -48,7 +48,7 @@ class ReaderTagHeaderView @JvmOverloads constructor( with(uiState.followButtonUiState) { val followButton = binding.followContainer.followButton followButton.setIsFollowed(isFollowed) - followButton.isEnabled = isEnabled + followButton.isEnabled = !isFollowActionRunning onFollowBtnClicked = onFollowButtonClicked } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/FollowButtonUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/FollowButtonUiState.kt index 0f67e89d1b04..c45951f02fd9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/FollowButtonUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/FollowButtonUiState.kt @@ -3,6 +3,6 @@ package org.wordpress.android.ui.reader.views.uistates data class FollowButtonUiState( val onFollowButtonClicked: (() -> Unit)?, val isFollowed: Boolean, - val isEnabled: Boolean, - val isVisible: Boolean = true + val isVisible: Boolean = true, + val isFollowActionRunning: Boolean = false ) diff --git a/WordPress/src/main/res/layout/reader_post_detail_header_view.xml b/WordPress/src/main/res/layout/reader_post_detail_header_view.xml index a7e0c6f1d30e..6dd8d802ac2f 100644 --- a/WordPress/src/main/res/layout/reader_post_detail_header_view.xml +++ b/WordPress/src/main/res/layout/reader_post_detail_header_view.xml @@ -18,19 +18,35 @@ android:clickable="true" android:focusable="true" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/header_follow_button" + app:layout_constraintEnd_toStartOf="@id/header_follow_button_container" app:layout_constraintTop_toTopOf="parent" /> - + tools:visibility="visible"> + + + + + + Date: Thu, 27 Nov 2025 17:47:59 +0100 Subject: [PATCH 04/13] Fixing blog screen subscription as well --- .../ui/reader/views/ReaderSiteHeaderView.java | 15 +++++++++--- .../layout/reader_header_follow_container.xml | 24 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java index a3a6bcee1414..3ffef9f5205f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java @@ -10,6 +10,7 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.ProgressBar; import android.widget.TextView; import org.wordpress.android.R; @@ -55,6 +56,7 @@ public interface OnBlogInfoLoadedListener { private boolean mIsFeed; private ReaderFollowButton mFollowButton; + private ProgressBar mFollowProgress; private ReaderBlog mBlogInfo; private OnBlogInfoLoadedListener mBlogInfoListener; private OnFollowListener mFollowListener; @@ -84,6 +86,7 @@ public ReaderSiteHeaderView(Context context, AttributeSet attrs, int defStyleAtt private void initView(Context context) { final View view = inflate(context, R.layout.reader_site_header_view, this); mFollowButton = view.findViewById(R.id.follow_button); + mFollowProgress = view.findViewById(R.id.follow_button_progress); view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); } @@ -244,12 +247,17 @@ private void showBlavatarImage(ReaderBlog blogInfo, ImageView blavatarImg) { PhotonUtils.getPhotonImageUrl(blogInfo.getImageUrl(), mBlavatarSz, mBlavatarSz, Quality.HIGH)); } + private void setFollowButtonLoading(boolean isLoading) { + mFollowButton.setIsLoading(isLoading); + mFollowProgress.setVisibility(isLoading ? View.VISIBLE : View.GONE); + } + private void toggleFollowStatus(final View followButton, final String source) { if (!NetworkUtils.checkConnection(getContext())) { return; } - // disable follow button until API call returns - mFollowButton.setEnabled(false); + // disable follow button and show loading indicator until API call returns + setFollowButtonLoading(true); final boolean isAskingToFollow; if (mIsFeed) { @@ -276,7 +284,7 @@ private void toggleFollowStatus(final View followButton, final String source) { if (getContext() == null) { return; } - mFollowButton.setEnabled(true); + setFollowButtonLoading(false); if (!succeeded) { int errResId = isAskingToFollow ? R.string.reader_toast_err_unable_to_follow_blog : R.string.reader_toast_err_unable_to_unfollow_blog; @@ -308,6 +316,7 @@ private void toggleFollowStatus(final View followButton, final String source) { } if (!result) { + setFollowButtonLoading(false); mFollowButton.setIsFollowed(!isAskingToFollow); } } diff --git a/WordPress/src/main/res/layout/reader_header_follow_container.xml b/WordPress/src/main/res/layout/reader_header_follow_container.xml index f0ffc881bfdd..4e247e384aa9 100644 --- a/WordPress/src/main/res/layout/reader_header_follow_container.xml +++ b/WordPress/src/main/res/layout/reader_header_follow_container.xml @@ -18,13 +18,29 @@ app:layout_constraintTop_toTopOf="parent" tools:text="123 posts • 1.2m followers" /> - + app:layout_constraintTop_toBottomOf="@+id/text_blog_follow_count"> + + + + + + From 82e1b005b928eb793a97bf8a216bb25aca853a54 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 27 Nov 2025 17:51:42 +0100 Subject: [PATCH 05/13] detekt --- .../viewholders/ReaderRecommendedBlogViewHolder.kt | 1 - .../ui/reader/viewmodels/ReaderPostDetailViewModel.kt | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderRecommendedBlogViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderRecommendedBlogViewHolder.kt index e54e47bc7694..3dfae16f6eb5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderRecommendedBlogViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderRecommendedBlogViewHolder.kt @@ -55,5 +55,4 @@ class ReaderRecommendedBlogViewHolder( imageManager.cancelRequestAndClearImageView(siteIcon) } } - } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt index 7c60c7e58411..577c20c3b602 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt @@ -622,16 +622,17 @@ class ReaderPostDetailViewModel @Inject constructor( val currentUiState = _uiState.value as? ReaderPostDetailsUiState ?: return newUiState val currentFollowButtonState = currentUiState.headerUiState.followButtonUiState - if (currentFollowButtonState.isFollowActionRunning) { + return if (currentFollowButtonState.isFollowActionRunning) { val updatedFollowButtonUiState = newUiState.headerUiState.followButtonUiState.copy( isFollowActionRunning = true ) val updatedHeaderUiState = newUiState.headerUiState.copy( followButtonUiState = updatedFollowButtonUiState ) - return newUiState.copy(headerUiState = updatedHeaderUiState) + newUiState.copy(headerUiState = updatedHeaderUiState) + } else { + newUiState } - return newUiState } private fun convertRelatedPostsToUiState( From 329b5132b8a0e85b60b1c3adb7ce05280e5d8542 Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 10:08:09 +0100 Subject: [PATCH 06/13] lint fix --- .../src/main/res/layout/reader_header_follow_container.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/res/layout/reader_header_follow_container.xml b/WordPress/src/main/res/layout/reader_header_follow_container.xml index 4e247e384aa9..729a43f2d34d 100644 --- a/WordPress/src/main/res/layout/reader_header_follow_container.xml +++ b/WordPress/src/main/res/layout/reader_header_follow_container.xml @@ -11,7 +11,7 @@ android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textColor="?attr/colorOnSurface" - app:layout_constraintBottom_toTopOf="@+id/follow_button" + app:layout_constraintBottom_toTopOf="@+id/follow_button_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" android:layout_marginBottom="@dimen/margin_extra_large" From efa3b3bc0b7d9a787725302a566c5969e84fe17b Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 11:07:10 +0100 Subject: [PATCH 07/13] Using suspend function to iterate over cards --- .../discover/ReaderDiscoverViewModel.kt | 40 ++++++++++--------- .../viewmodels/ReaderPostDetailViewModel.kt | 25 +++++++----- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt index 0fe16a07417c..50de995e8f19 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.models.ReaderPost @@ -178,29 +179,30 @@ class ReaderDiscoverViewModel @Inject constructor( } } - private fun preserveFollowActionRunningState(newCards: List): List { - val currentUiState = _uiState.value as? DiscoverUiState.ContentUiState ?: return newCards - - return newCards.map { card -> - if (card is ReaderCardUiState.ReaderRecommendedBlogsCardUiState) { - val updatedBlogs = card.blogs.map { blog -> - val currentBlog = currentUiState.cards - .filterIsInstance() - .flatMap { it.blogs } - .find { it.blogId == blog.blogId && it.feedId == blog.feedId } - - if (currentBlog != null && currentBlog.isFollowActionRunning) { - blog.copy(isFollowActionRunning = true) - } else { - blog + private suspend fun preserveFollowActionRunningState(newCards: List): List = + withContext(ioDispatcher) { + val currentUiState = _uiState.value as? DiscoverUiState.ContentUiState ?: return@withContext newCards + + newCards.map { card -> + if (card is ReaderCardUiState.ReaderRecommendedBlogsCardUiState) { + val updatedBlogs = card.blogs.map { blog -> + val currentBlog = currentUiState.cards + .filterIsInstance() + .flatMap { it.blogs } + .find { it.blogId == blog.blogId && it.feedId == blog.feedId } + + if (currentBlog != null && currentBlog.isFollowActionRunning) { + blog.copy(isFollowActionRunning = true) + } else { + blog + } } + card.copy(blogs = updatedBlogs) + } else { + card } - card.copy(blogs = updatedBlogs) - } else { - card } } - } private fun dismissAnnouncementCard() { readerAnnouncementHelper.dismissReaderAnnouncement() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt index 577c20c3b602..3d40cf3e2407 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode.MAIN @@ -529,7 +530,9 @@ class ReaderPostDetailViewModel @Inject constructor( } fun onUpdatePost(post: ReaderPost) { - _uiState.value = convertPostToUiState(post) + viewModelScope.launch { + _uiState.value = convertPostToUiState(post) + } } fun onTagItemClicked(tagSlug: String) { @@ -605,7 +608,7 @@ class ReaderPostDetailViewModel @Inject constructor( ) } - private fun convertPostToUiState( + private suspend fun convertPostToUiState( post: ReaderPost ): ReaderPostDetailsUiState { val newUiState = postDetailUiStateBuilder.mapPostToUiState( @@ -616,13 +619,13 @@ class ReaderPostDetailViewModel @Inject constructor( return preserveFollowActionRunningState(newUiState) } - private fun preserveFollowActionRunningState( + private suspend fun preserveFollowActionRunningState( newUiState: ReaderPostDetailsUiState - ): ReaderPostDetailsUiState { - val currentUiState = _uiState.value as? ReaderPostDetailsUiState ?: return newUiState + ): ReaderPostDetailsUiState = withContext(ioDispatcher) { + val currentUiState = _uiState.value as? ReaderPostDetailsUiState ?: return@withContext newUiState val currentFollowButtonState = currentUiState.headerUiState.followButtonUiState - return if (currentFollowButtonState.isFollowActionRunning) { + if (currentFollowButtonState.isFollowActionRunning) { val updatedFollowButtonUiState = newUiState.headerUiState.followButtonUiState.copy( isFollowActionRunning = true ) @@ -647,10 +650,12 @@ class ReaderPostDetailViewModel @Inject constructor( ) private fun updatePostDetailsUi() { - post?.let { - readerTracker.trackPost(Stat.READER_ARTICLE_RENDERED, it) - _navigationEvents.postValue(Event(ShowPostInWebView(it))) - _uiState.value = convertPostToUiState(it) + viewModelScope.launch { + post?.let { + readerTracker.trackPost(Stat.READER_ARTICLE_RENDERED, it) + _navigationEvents.postValue(Event(ShowPostInWebView(it))) + _uiState.value = convertPostToUiState(it) + } } } From 7f8fa183f194355c4db48a70d93096171a80b16e Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 11:20:04 +0100 Subject: [PATCH 08/13] Removing the withContext, but keeping the scope usage --- .../discover/ReaderDiscoverViewModel.kt | 40 +++++++++---------- .../viewmodels/ReaderPostDetailViewModel.kt | 21 +++++----- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt index 50de995e8f19..0fe16a07417c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.models.ReaderPost @@ -179,30 +178,29 @@ class ReaderDiscoverViewModel @Inject constructor( } } - private suspend fun preserveFollowActionRunningState(newCards: List): List = - withContext(ioDispatcher) { - val currentUiState = _uiState.value as? DiscoverUiState.ContentUiState ?: return@withContext newCards - - newCards.map { card -> - if (card is ReaderCardUiState.ReaderRecommendedBlogsCardUiState) { - val updatedBlogs = card.blogs.map { blog -> - val currentBlog = currentUiState.cards - .filterIsInstance() - .flatMap { it.blogs } - .find { it.blogId == blog.blogId && it.feedId == blog.feedId } - - if (currentBlog != null && currentBlog.isFollowActionRunning) { - blog.copy(isFollowActionRunning = true) - } else { - blog - } + private fun preserveFollowActionRunningState(newCards: List): List { + val currentUiState = _uiState.value as? DiscoverUiState.ContentUiState ?: return newCards + + return newCards.map { card -> + if (card is ReaderCardUiState.ReaderRecommendedBlogsCardUiState) { + val updatedBlogs = card.blogs.map { blog -> + val currentBlog = currentUiState.cards + .filterIsInstance() + .flatMap { it.blogs } + .find { it.blogId == blog.blogId && it.feedId == blog.feedId } + + if (currentBlog != null && currentBlog.isFollowActionRunning) { + blog.copy(isFollowActionRunning = true) + } else { + blog } - card.copy(blogs = updatedBlogs) - } else { - card } + card.copy(blogs = updatedBlogs) + } else { + card } } + } private fun dismissAnnouncementCard() { readerAnnouncementHelper.dismissReaderAnnouncement() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt index 3d40cf3e2407..902aff2ad0d8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt @@ -9,7 +9,6 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode.MAIN @@ -608,7 +607,7 @@ class ReaderPostDetailViewModel @Inject constructor( ) } - private suspend fun convertPostToUiState( + private fun convertPostToUiState( post: ReaderPost ): ReaderPostDetailsUiState { val newUiState = postDetailUiStateBuilder.mapPostToUiState( @@ -619,13 +618,13 @@ class ReaderPostDetailViewModel @Inject constructor( return preserveFollowActionRunningState(newUiState) } - private suspend fun preserveFollowActionRunningState( + private fun preserveFollowActionRunningState( newUiState: ReaderPostDetailsUiState - ): ReaderPostDetailsUiState = withContext(ioDispatcher) { - val currentUiState = _uiState.value as? ReaderPostDetailsUiState ?: return@withContext newUiState + ): ReaderPostDetailsUiState { + val currentUiState = _uiState.value as? ReaderPostDetailsUiState ?: return newUiState val currentFollowButtonState = currentUiState.headerUiState.followButtonUiState - if (currentFollowButtonState.isFollowActionRunning) { + return if (currentFollowButtonState.isFollowActionRunning) { val updatedFollowButtonUiState = newUiState.headerUiState.followButtonUiState.copy( isFollowActionRunning = true ) @@ -650,12 +649,10 @@ class ReaderPostDetailViewModel @Inject constructor( ) private fun updatePostDetailsUi() { - viewModelScope.launch { - post?.let { - readerTracker.trackPost(Stat.READER_ARTICLE_RENDERED, it) - _navigationEvents.postValue(Event(ShowPostInWebView(it))) - _uiState.value = convertPostToUiState(it) - } + post?.let { + readerTracker.trackPost(Stat.READER_ARTICLE_RENDERED, it) + _navigationEvents.postValue(Event(ShowPostInWebView(it))) + _uiState.value = convertPostToUiState(it) } } From 62d2d7bf2ed085f10e328837af711fb750dcdca7 Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 11:22:17 +0100 Subject: [PATCH 09/13] Some refactoring --- .../discover/ReaderDiscoverViewModel.kt | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt index 0fe16a07417c..cffca043b89a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt @@ -183,25 +183,34 @@ class ReaderDiscoverViewModel @Inject constructor( return newCards.map { card -> if (card is ReaderCardUiState.ReaderRecommendedBlogsCardUiState) { - val updatedBlogs = card.blogs.map { blog -> - val currentBlog = currentUiState.cards - .filterIsInstance() - .flatMap { it.blogs } - .find { it.blogId == blog.blogId && it.feedId == blog.feedId } - - if (currentBlog != null && currentBlog.isFollowActionRunning) { - blog.copy(isFollowActionRunning = true) - } else { - blog - } - } - card.copy(blogs = updatedBlogs) + preserveBlogCardFollowState(card, currentUiState) } else { card } } } + private fun preserveBlogCardFollowState( + card: ReaderCardUiState.ReaderRecommendedBlogsCardUiState, + currentUiState: DiscoverUiState.ContentUiState + ): ReaderCardUiState.ReaderRecommendedBlogsCardUiState { + val currentBlogs = currentUiState.cards + .filterIsInstance() + .flatMap { it.blogs } + + val updatedBlogs = card.blogs.map { blog -> + val currentBlog = currentBlogs.find { + it.blogId == blog.blogId && it.feedId == blog.feedId + } + if (currentBlog?.isFollowActionRunning == true) { + blog.copy(isFollowActionRunning = true) + } else { + blog + } + } + return card.copy(blogs = updatedBlogs) + } + private fun dismissAnnouncementCard() { readerAnnouncementHelper.dismissReaderAnnouncement() _uiState.value = (_uiState.value as? DiscoverUiState.ContentUiState)?.let { contentUiState -> From 6367a29dd6ee5e4bc667695f4f5ddd9e72f1cd6d Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 11:26:22 +0100 Subject: [PATCH 10/13] Adding tests --- .../viewmodels/ReaderPostDetailViewModel.kt | 1 + .../discover/ReaderDiscoverViewModelTest.kt | 91 +++++++++++++++++++ .../ReaderPostDetailViewModelTest.kt | 84 +++++++++++++++++ 3 files changed, 176 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt index 902aff2ad0d8..db2393b5aee7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode.MAIN diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt index cb4b833ca30c..7519ae8fa099 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt @@ -625,6 +625,97 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { verify(readerDiscoverDataProvider).refreshCards() } + @Test + fun `When follow status changes and is not final, isFollowActionRunning is true`() = test { + // Arrange + val (uiStates) = init(autoUpdateFeed = false) + fakeDiscoverFeed.value = ReaderDiscoverCards(createReaderRecommendedBlogsCardList()) + + // Act + fakeFollowStatusChangedFeed.value = FollowStatusChanged( + blogId = 1L, + feedId = 0L, + following = true, + isChangeFinal = false + ) + + // Assert + val contentUiState = uiStates.last() as ContentUiState + val blogCard = contentUiState.cards.first() as ReaderRecommendedBlogsCardUiState + assertThat(blogCard.blogs[0].isFollowActionRunning).isTrue + } + + @Test + fun `When follow status changes and is final, isFollowActionRunning is false`() = test { + // Arrange + val (uiStates) = init(autoUpdateFeed = false) + fakeDiscoverFeed.value = ReaderDiscoverCards(createReaderRecommendedBlogsCardList()) + + // Act + fakeFollowStatusChangedFeed.value = FollowStatusChanged( + blogId = 1L, + feedId = 0L, + following = true, + isChangeFinal = true + ) + + // Assert + val contentUiState = uiStates.last() as ContentUiState + val blogCard = contentUiState.cards.first() as ReaderRecommendedBlogsCardUiState + assertThat(blogCard.blogs[0].isFollowActionRunning).isFalse + } + + @Test + fun `When feed updates during follow action, isFollowActionRunning state is preserved`() = test { + // Arrange + val (uiStates) = init(autoUpdateFeed = false) + fakeDiscoverFeed.value = ReaderDiscoverCards(createReaderRecommendedBlogsCardList()) + + // Simulate follow action starting (not final) + fakeFollowStatusChangedFeed.value = FollowStatusChanged( + blogId = 1L, + feedId = 0L, + following = true, + isChangeFinal = false + ) + + // Act - simulate feed refresh while follow action is running + fakeDiscoverFeed.value = ReaderDiscoverCards(createReaderRecommendedBlogsCardList()) + + // Assert - the running state should be preserved + val contentUiState = uiStates.last() as ContentUiState + val blogCard = contentUiState.cards.first() as ReaderRecommendedBlogsCardUiState + assertThat(blogCard.blogs[0].isFollowActionRunning).isTrue + } + + @Test + fun `When follow action completes after feed refresh, isFollowActionRunning becomes false`() = test { + // Arrange + val (uiStates) = init(autoUpdateFeed = false) + fakeDiscoverFeed.value = ReaderDiscoverCards(createReaderRecommendedBlogsCardList()) + + // Simulate follow action starting (not final) + fakeFollowStatusChangedFeed.value = FollowStatusChanged( + blogId = 1L, + feedId = 0L, + following = true, + isChangeFinal = false + ) + + // Act - complete the follow action + fakeFollowStatusChangedFeed.value = FollowStatusChanged( + blogId = 1L, + feedId = 0L, + following = true, + isChangeFinal = true + ) + + // Assert + val contentUiState = uiStates.last() as ContentUiState + val blogCard = contentUiState.cards.first() as ReaderRecommendedBlogsCardUiState + assertThat(blogCard.blogs[0].isFollowActionRunning).isFalse + } + private fun init(autoUpdateFeed: Boolean = true): Observers { val uiStates = mutableListOf() viewModel.uiState.observeForever { diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt index 8fefbc40af5d..bca19a8668c7 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt @@ -1141,6 +1141,90 @@ class ReaderPostDetailViewModelTest : BaseUnitTest() { verify(readerTracker).track(AnalyticsTracker.Stat.READER_ARTICLE_TEXT_HIGHLIGHTED) } + /* FOLLOW BUTTON LOADING STATE */ + @Test + fun `when follow status changes and is not final, isFollowActionRunning is true`() = test { + // Arrange + val observers = init() + + // Act + fakePostFollowStatusChangedFeed.value = FollowStatusChanged( + blogId = readerPost.blogId, + feedId = readerPost.feedId, + following = true, + isChangeFinal = false + ) + + // Assert + val uiState = observers.uiStates.last() as ReaderPostDetailsUiState + assertThat(uiState.headerUiState.followButtonUiState.isFollowActionRunning).isTrue + } + + @Test + fun `when follow status changes and is final, isFollowActionRunning is false`() = test { + // Arrange + val observers = init() + + // Act + fakePostFollowStatusChangedFeed.value = FollowStatusChanged( + blogId = readerPost.blogId, + feedId = readerPost.feedId, + following = true, + isChangeFinal = true + ) + + // Assert + val uiState = observers.uiStates.last() as ReaderPostDetailsUiState + assertThat(uiState.headerUiState.followButtonUiState.isFollowActionRunning).isFalse + } + + @Test + fun `when post updates during follow action, isFollowActionRunning state is preserved`() = test { + // Arrange + val observers = init() + + // Simulate follow action starting (not final) + fakePostFollowStatusChangedFeed.value = FollowStatusChanged( + blogId = readerPost.blogId, + feedId = readerPost.feedId, + following = true, + isChangeFinal = false + ) + + // Act - simulate post update while follow action is running + viewModel.onUpdatePost(readerPost) + + // Assert - the running state should be preserved + val uiState = observers.uiStates.last() as ReaderPostDetailsUiState + assertThat(uiState.headerUiState.followButtonUiState.isFollowActionRunning).isTrue + } + + @Test + fun `when follow action completes, isFollowActionRunning becomes false`() = test { + // Arrange + val observers = init() + + // Simulate follow action starting (not final) + fakePostFollowStatusChangedFeed.value = FollowStatusChanged( + blogId = readerPost.blogId, + feedId = readerPost.feedId, + following = true, + isChangeFinal = false + ) + + // Act - complete the follow action + fakePostFollowStatusChangedFeed.value = FollowStatusChanged( + blogId = readerPost.blogId, + feedId = readerPost.feedId, + following = true, + isChangeFinal = true + ) + + // Assert + val uiState = observers.uiStates.last() as ReaderPostDetailsUiState + assertThat(uiState.headerUiState.followButtonUiState.isFollowActionRunning).isFalse + } + private fun testWithoutLocalPost(block: suspend CoroutineScope.() -> T) { test { whenever(readerGetPostUseCase.get(any(), any(), any())).thenReturn(Pair(null, false)) From 2c2f17bb3812e966d43e6e83ab1403d19a2aa440 Mon Sep 17 00:00:00 2001 From: Adalberto Plaza Date: Fri, 28 Nov 2025 11:15:33 +0100 Subject: [PATCH 11/13] Update WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .../wordpress/android/ui/reader/views/ReaderSiteHeaderView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java index 3ffef9f5205f..090d4e1bdb83 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java @@ -56,7 +56,7 @@ public interface OnBlogInfoLoadedListener { private boolean mIsFeed; private ReaderFollowButton mFollowButton; - private ProgressBar mFollowProgress; + @Nullable private ProgressBar mFollowProgress; private ReaderBlog mBlogInfo; private OnBlogInfoLoadedListener mBlogInfoListener; private OnFollowListener mFollowListener; From 3cb26b11faea454ab953ace44b2864af28ef2fd0 Mon Sep 17 00:00:00 2001 From: adalpari Date: Fri, 28 Nov 2025 11:50:37 +0100 Subject: [PATCH 12/13] Compile fix --- .../wordpress/android/ui/reader/views/ReaderSiteHeaderView.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java index 090d4e1bdb83..39beba228e77 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java @@ -13,6 +13,8 @@ import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.Nullable; + import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.datasets.ReaderBlogTable; From 464ba63e719ce039c788d04ae3304d374ec9a0a2 Mon Sep 17 00:00:00 2001 From: adalpari Date: Mon, 1 Dec 2025 11:43:06 +0100 Subject: [PATCH 13/13] chore: trigger CI