Skip to content

Commit b3e2433

Browse files
adalpariclaude[bot]
authored andcommitted
CMM-955 reader subscribe button unresponsive on quick tap (#22373)
* Adding a loading spinner to the recommended blogs subscribe button * Fixing the loading wrong state * Fixing it for the post details screen * Fixing blog screen subscription as well * detekt * lint fix * Using suspend function to iterate over cards * Removing the withContext, but keeping the scope usage * Some refactoring * Adding tests * 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> * Compile fix * chore: trigger CI --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
1 parent 048f9d6 commit b3e2433

17 files changed

+364
-47
lines changed

WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,8 @@ private void renderTagHeader(
348348
new FollowButtonUiState(
349349
onFollowButtonClicked,
350350
ReaderTagTable.isFollowedTagName(currentTag.getTagSlug()),
351-
isFollowButtonEnabled,
352-
true
351+
isFollowButtonEnabled, // isVisible
352+
false // isFollowActionRunning
353353
)
354354
);
355355
tagHolder.onBind(uiState);

WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ sealed class ReaderCardUiState {
122122
val description: String?,
123123
val iconUrl: String?,
124124
val isFollowed: Boolean,
125-
val isFollowEnabled: Boolean,
125+
val isFollowActionRunning: Boolean = false,
126126
val onItemClicked: (Long, Long, Boolean) -> Unit,
127127
val onFollowClicked: (ReaderRecommendedBlogUiState) -> Unit
128128
) {

WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,11 @@ class ReaderDiscoverViewModel @Inject constructor(
157157
emptyList()
158158
}
159159

160+
val newCards = convertCardsToUiStates(posts)
161+
val cardsWithPreservedState = preserveFollowActionRunningState(newCards)
162+
160163
_uiState.value = DiscoverUiState.ContentUiState(
161-
announcement + convertCardsToUiStates(posts),
164+
announcement + cardsWithPreservedState,
162165
reloadProgressVisibility = false,
163166
loadMoreProgressVisibility = false,
164167
)
@@ -175,6 +178,39 @@ class ReaderDiscoverViewModel @Inject constructor(
175178
}
176179
}
177180

181+
private fun preserveFollowActionRunningState(newCards: List<ReaderCardUiState>): List<ReaderCardUiState> {
182+
val currentUiState = _uiState.value as? DiscoverUiState.ContentUiState ?: return newCards
183+
184+
return newCards.map { card ->
185+
if (card is ReaderCardUiState.ReaderRecommendedBlogsCardUiState) {
186+
preserveBlogCardFollowState(card, currentUiState)
187+
} else {
188+
card
189+
}
190+
}
191+
}
192+
193+
private fun preserveBlogCardFollowState(
194+
card: ReaderCardUiState.ReaderRecommendedBlogsCardUiState,
195+
currentUiState: DiscoverUiState.ContentUiState
196+
): ReaderCardUiState.ReaderRecommendedBlogsCardUiState {
197+
val currentBlogs = currentUiState.cards
198+
.filterIsInstance<ReaderCardUiState.ReaderRecommendedBlogsCardUiState>()
199+
.flatMap { it.blogs }
200+
201+
val updatedBlogs = card.blogs.map { blog ->
202+
val currentBlog = currentBlogs.find {
203+
it.blogId == blog.blogId && it.feedId == blog.feedId
204+
}
205+
if (currentBlog?.isFollowActionRunning == true) {
206+
blog.copy(isFollowActionRunning = true)
207+
} else {
208+
blog
209+
}
210+
}
211+
return card.copy(blogs = updatedBlogs)
212+
}
213+
178214
private fun dismissAnnouncementCard() {
179215
readerAnnouncementHelper.dismissReaderAnnouncement()
180216
_uiState.value = (_uiState.value as? DiscoverUiState.ContentUiState)?.let { contentUiState ->
@@ -200,7 +236,10 @@ class ReaderDiscoverViewModel @Inject constructor(
200236
for (j in mutableBlogs.indices) {
201237
val blog = mutableBlogs[j]
202238
if (blog.blogId == data.blogId && blog.feedId == data.feedId) {
203-
mutableBlogs[j] = blog.copy(isFollowed = data.following, isFollowEnabled = data.isChangeFinal)
239+
mutableBlogs[j] = blog.copy(
240+
isFollowed = data.following,
241+
isFollowActionRunning = !data.isChangeFinal
242+
)
204243
hasChangedBlogs = true
205244
}
206245
}

WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderPostUiStateBuilder.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,6 @@ class ReaderPostUiStateBuilder @Inject constructor(
198198
description = it.description.ifEmpty { null },
199199
iconUrl = it.imageUrl,
200200
isFollowed = it.isFollowing,
201-
isFollowEnabled = true,
202201
onFollowClicked = onFollowClicked,
203202
onItemClicked = onItemClicked
204203
)

WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderRecommendedBlogViewHolder.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.wordpress.android.ui.reader.discover.viewholders
22

3+
import android.view.View
34
import android.view.ViewGroup
45
import androidx.recyclerview.widget.RecyclerView
56
import org.wordpress.android.databinding.ReaderRecommendedBlogItemBinding
@@ -29,12 +30,16 @@ class ReaderRecommendedBlogViewHolder(
2930
uiState: ReaderRecommendedBlogUiState,
3031
binding: ReaderRecommendedBlogItemBinding
3132
) {
32-
with(binding.siteFollowButton) {
33-
isEnabled = uiState.isFollowEnabled
34-
setIsFollowed(uiState.isFollowed)
35-
contentDescription = context.getString(uiState.followContentDescription.stringRes)
36-
setOnClickListener {
37-
uiState.onFollowClicked(uiState)
33+
with(binding) {
34+
siteFollowProgress.visibility = if (uiState.isFollowActionRunning) View.VISIBLE else View.GONE
35+
siteFollowButton.apply {
36+
setIsLoading(uiState.isFollowActionRunning)
37+
isEnabled = !uiState.isFollowActionRunning
38+
setIsFollowed(uiState.isFollowed)
39+
contentDescription = context.getString(uiState.followContentDescription.stringRes)
40+
setOnClickListener {
41+
uiState.onFollowClicked(uiState)
42+
}
3843
}
3944
}
4045
}

WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope
99
import dagger.hilt.android.lifecycle.HiltViewModel
1010
import kotlinx.coroutines.CoroutineDispatcher
1111
import kotlinx.coroutines.Job
12+
import kotlinx.coroutines.launch
1213
import kotlinx.coroutines.withContext
1314
import org.greenrobot.eventbus.Subscribe
1415
import org.greenrobot.eventbus.ThreadMode.MAIN
@@ -255,7 +256,7 @@ class ReaderPostDetailViewModel @Inject constructor(
255256
updateFollowButtonUiState(
256257
currentUiState = currentUiState,
257258
isFollowed = post.isFollowedByCurrentUser,
258-
isFollowEnabled = data.isChangeFinal
259+
isFollowActionRunning = !data.isChangeFinal
259260
)
260261
}
261262
}
@@ -529,7 +530,9 @@ class ReaderPostDetailViewModel @Inject constructor(
529530
}
530531

531532
fun onUpdatePost(post: ReaderPost) {
532-
_uiState.value = convertPostToUiState(post)
533+
viewModelScope.launch {
534+
_uiState.value = convertPostToUiState(post)
535+
}
533536
}
534537

535538
fun onTagItemClicked(tagSlug: String) {
@@ -608,11 +611,31 @@ class ReaderPostDetailViewModel @Inject constructor(
608611
private fun convertPostToUiState(
609612
post: ReaderPost
610613
): ReaderPostDetailsUiState {
611-
return postDetailUiStateBuilder.mapPostToUiState(
614+
val newUiState = postDetailUiStateBuilder.mapPostToUiState(
612615
post = post,
613616
onButtonClicked = this@ReaderPostDetailViewModel::onButtonClicked,
614617
onHeaderAction = { action -> onHeaderAction(post, action) },
615618
)
619+
return preserveFollowActionRunningState(newUiState)
620+
}
621+
622+
private fun preserveFollowActionRunningState(
623+
newUiState: ReaderPostDetailsUiState
624+
): ReaderPostDetailsUiState {
625+
val currentUiState = _uiState.value as? ReaderPostDetailsUiState ?: return newUiState
626+
val currentFollowButtonState = currentUiState.headerUiState.followButtonUiState
627+
628+
return if (currentFollowButtonState.isFollowActionRunning) {
629+
val updatedFollowButtonUiState = newUiState.headerUiState.followButtonUiState.copy(
630+
isFollowActionRunning = true
631+
)
632+
val updatedHeaderUiState = newUiState.headerUiState.copy(
633+
followButtonUiState = updatedFollowButtonUiState
634+
)
635+
newUiState.copy(headerUiState = updatedHeaderUiState)
636+
} else {
637+
newUiState
638+
}
616639
}
617640

618641
private fun convertRelatedPostsToUiState(
@@ -637,12 +660,15 @@ class ReaderPostDetailViewModel @Inject constructor(
637660
private fun updateFollowButtonUiState(
638661
currentUiState: ReaderPostDetailsUiState,
639662
isFollowed: Boolean,
640-
isFollowEnabled: Boolean,
663+
isFollowActionRunning: Boolean,
641664
) {
642665
val updatedFollowButtonUiState = currentUiState
643666
.headerUiState
644667
.followButtonUiState
645-
.copy(isFollowed = isFollowed, isEnabled = isFollowEnabled)
668+
.copy(
669+
isFollowed = isFollowed,
670+
isFollowActionRunning = isFollowActionRunning
671+
)
646672

647673
val updatedHeaderUiState = currentUiState
648674
.headerUiState

WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class ReaderFollowButton @JvmOverloads constructor(
2424
) : MaterialButton(context, attrs, defStyleAttr) {
2525
private var isFollowed = false
2626
private var showCaption = false
27+
private var isLoading = false
2728

2829
init {
2930
initView(context, attrs)
@@ -58,6 +59,14 @@ class ReaderFollowButton @JvmOverloads constructor(
5859
setIsFollowed(isFollowed, true)
5960
}
6061

62+
fun setIsLoading(loading: Boolean) {
63+
if (isLoading == loading) return
64+
isLoading = loading
65+
isEnabled = !loading
66+
}
67+
68+
fun getIsLoading(): Boolean = isLoading
69+
6170
@SuppressLint("Recycle")
6271
private fun setIsFollowed(isFollowed: Boolean, animateChanges: Boolean) {
6372
if (isFollowed == this.isFollowed && isSelected == isFollowed) {

WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor(
5858

5959
uiHelpers.setTextOrHide(layoutBlogSection.blogSectionTextBlogName, uiState.blogSectionUiState.blogName)
6060

61-
headerFollowButton.update(uiState.followButtonUiState)
61+
updateFollowButton(uiState.followButtonUiState)
6262

6363
updateAvatars(uiState.blogSectionUiState)
6464
updateBlogSectionClick(uiState.blogSectionUiState)
@@ -113,11 +113,17 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor(
113113
}
114114
}
115115

116-
private fun ReaderFollowButton.update(followButtonUiState: FollowButtonUiState) {
117-
isEnabled = followButtonUiState.isEnabled
118-
setVisible(followButtonUiState.isVisible)
119-
setIsFollowed(followButtonUiState.isFollowed)
120-
setOnClickListener { followButtonUiState.onFollowButtonClicked?.invoke() }
116+
private fun ReaderPostDetailHeaderViewBinding.updateFollowButton(
117+
followButtonUiState: FollowButtonUiState
118+
) {
119+
headerFollowButtonContainer.setVisible(followButtonUiState.isVisible)
120+
headerFollowProgress.setVisible(followButtonUiState.isFollowActionRunning)
121+
headerFollowButton.apply {
122+
setIsLoading(followButtonUiState.isFollowActionRunning)
123+
isEnabled = !followButtonUiState.isFollowActionRunning
124+
setIsFollowed(followButtonUiState.isFollowed)
125+
setOnClickListener { followButtonUiState.onFollowButtonClicked?.invoke() }
126+
}
121127
}
122128

123129
private fun setAuthorAndDate(authorName: String?, dateLine: String) = with(binding.layoutBlogSection) {

WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderSiteHeaderView.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
import android.view.ViewGroup;
1111
import android.widget.ImageView;
1212
import android.widget.LinearLayout;
13+
import android.widget.ProgressBar;
1314
import android.widget.TextView;
1415

16+
import androidx.annotation.Nullable;
17+
1518
import org.wordpress.android.R;
1619
import org.wordpress.android.WordPress;
1720
import org.wordpress.android.datasets.ReaderBlogTable;
@@ -55,6 +58,7 @@ public interface OnBlogInfoLoadedListener {
5558
private boolean mIsFeed;
5659

5760
private ReaderFollowButton mFollowButton;
61+
@Nullable private ProgressBar mFollowProgress;
5862
private ReaderBlog mBlogInfo;
5963
private OnBlogInfoLoadedListener mBlogInfoListener;
6064
private OnFollowListener mFollowListener;
@@ -84,6 +88,7 @@ public ReaderSiteHeaderView(Context context, AttributeSet attrs, int defStyleAtt
8488
private void initView(Context context) {
8589
final View view = inflate(context, R.layout.reader_site_header_view, this);
8690
mFollowButton = view.findViewById(R.id.follow_button);
91+
mFollowProgress = view.findViewById(R.id.follow_button_progress);
8792
view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
8893
}
8994

@@ -246,12 +251,17 @@ private void showBlavatarImage(ReaderBlog blogInfo, ImageView blavatarImg) {
246251
PhotonUtils.getPhotonImageUrl(blogInfo.getImageUrl(), mBlavatarSz, mBlavatarSz, Quality.HIGH));
247252
}
248253

254+
private void setFollowButtonLoading(boolean isLoading) {
255+
mFollowButton.setIsLoading(isLoading);
256+
mFollowProgress.setVisibility(isLoading ? View.VISIBLE : View.GONE);
257+
}
258+
249259
private void toggleFollowStatus(final View followButton, final String source) {
250260
if (!NetworkUtils.checkConnection(getContext())) {
251261
return;
252262
}
253-
// disable follow button until API call returns
254-
mFollowButton.setEnabled(false);
263+
// disable follow button and show loading indicator until API call returns
264+
setFollowButtonLoading(true);
255265

256266
final boolean isAskingToFollow;
257267
if (mIsFeed) {
@@ -278,7 +288,7 @@ private void toggleFollowStatus(final View followButton, final String source) {
278288
if (getContext() == null) {
279289
return;
280290
}
281-
mFollowButton.setEnabled(true);
291+
setFollowButtonLoading(false);
282292
if (!succeeded) {
283293
int errResId = isAskingToFollow ? R.string.reader_toast_err_unable_to_follow_blog
284294
: R.string.reader_toast_err_unable_to_unfollow_blog;
@@ -310,6 +320,7 @@ private void toggleFollowStatus(final View followButton, final String source) {
310320
}
311321

312322
if (!result) {
323+
setFollowButtonLoading(false);
313324
mFollowButton.setIsFollowed(!isAskingToFollow);
314325
}
315326
}

WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagHeaderView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class ReaderTagHeaderView @JvmOverloads constructor(
4848
with(uiState.followButtonUiState) {
4949
val followButton = binding.followContainer.followButton
5050
followButton.setIsFollowed(isFollowed)
51-
followButton.isEnabled = isEnabled
51+
followButton.isEnabled = !isFollowActionRunning
5252
onFollowBtnClicked = onFollowButtonClicked
5353
}
5454
}

0 commit comments

Comments
 (0)