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/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..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 @@ -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,39 @@ 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) { + 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 -> @@ -200,7 +236,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..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 @@ -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) + } } } } 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..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 @@ -255,7 +256,7 @@ class ReaderPostDetailViewModel @Inject constructor( updateFollowButtonUiState( currentUiState = currentUiState, isFollowed = post.isFollowedByCurrentUser, - isFollowEnabled = data.isChangeFinal + isFollowActionRunning = !data.isChangeFinal ) } } @@ -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) { @@ -608,11 +611,31 @@ 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 + + return if (currentFollowButtonState.isFollowActionRunning) { + val updatedFollowButtonUiState = newUiState.headerUiState.followButtonUiState.copy( + isFollowActionRunning = true + ) + val updatedHeaderUiState = newUiState.headerUiState.copy( + followButtonUiState = updatedFollowButtonUiState + ) + newUiState.copy(headerUiState = updatedHeaderUiState) + } else { + newUiState + } } private fun convertRelatedPostsToUiState( @@ -637,12 +660,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/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/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/ReaderSiteHeaderView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java index a3a6bcee1414..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 @@ -10,8 +10,11 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; +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; @@ -55,6 +58,7 @@ public interface OnBlogInfoLoadedListener { private boolean mIsFeed; private ReaderFollowButton mFollowButton; + @Nullable private ProgressBar mFollowProgress; private ReaderBlog mBlogInfo; private OnBlogInfoLoadedListener mBlogInfoListener; private OnFollowListener mFollowListener; @@ -84,6 +88,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 +249,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 +286,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 +318,7 @@ private void toggleFollowStatus(final View followButton, final String source) { } if (!result) { + setFollowButtonLoading(false); mFollowButton.setIsFollowed(!isAskingToFollow); } } 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_header_follow_container.xml b/WordPress/src/main/res/layout/reader_header_follow_container.xml index f0ffc881bfdd..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,20 +11,36 @@ 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" app:layout_constraintTop_toTopOf="parent" tools:text="123 posts • 1.2m followers" /> - + app:layout_constraintTop_toBottomOf="@+id/text_blog_follow_count"> + + + + + + 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"> + + + + + + - + 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..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 { @@ -745,7 +836,6 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { onItemClicked = onItemClicked, onFollowClicked = onFollowClicked, isFollowed = it.isFollowing, - isFollowEnabled = true, ) } ) 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 dc5eedbe4d52..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)) @@ -1197,7 +1281,6 @@ class ReaderPostDetailViewModelTest : BaseUnitTest() { FollowButtonUiState( onFollowButtonClicked = mock(), isFollowed = false, - isEnabled = true, isVisible = true ), "",