Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,8 @@ private void renderTagHeader(
new FollowButtonUiState(
onFollowButtonClicked,
ReaderTagTable.isFollowedTagName(currentTag.getTagSlug()),
isFollowButtonEnabled,
true
isFollowButtonEnabled, // isVisible
false // isFollowActionRunning
)
);
tagHolder.onBind(uiState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -175,6 +178,39 @@ class ReaderDiscoverViewModel @Inject constructor(
}
}

private fun preserveFollowActionRunningState(newCards: List<ReaderCardUiState>): List<ReaderCardUiState> {
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<ReaderCardUiState.ReaderRecommendedBlogsCardUiState>()
.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 ->
Expand All @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -255,7 +256,7 @@ class ReaderPostDetailViewModel @Inject constructor(
updateFollowButtonUiState(
currentUiState = currentUiState,
isFollowed = post.isFollowedByCurrentUser,
isFollowEnabled = data.isChangeFinal
isFollowActionRunning = !data.isChangeFinal
)
}
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor(
return FollowButtonUiState(
onFollowButtonClicked = onFollowClicked,
isFollowed = post.isFollowedByCurrentUser,
isEnabled = hasAccessToken,
isVisible = hasAccessToken
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -308,6 +318,7 @@ private void toggleFollowStatus(final View followButton, final String source) {
}

if (!result) {
setFollowButtonLoading(false);
mFollowButton.setIsFollowed(!isAskingToFollow);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
26 changes: 21 additions & 5 deletions WordPress/src/main/res/layout/reader_header_follow_container.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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" />

<org.wordpress.android.ui.reader.views.ReaderFollowButton
android:id="@+id/follow_button"
style="@style/Reader.Follow.Button.New"
<FrameLayout
android:id="@+id/follow_button_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_blog_follow_count" />
app:layout_constraintTop_toBottomOf="@+id/text_blog_follow_count">

<org.wordpress.android.ui.reader.views.ReaderFollowButton
android:id="@+id/follow_button"
style="@style/Reader.Follow.Button.New"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />

<ProgressBar
android:id="@+id/follow_button_progress"
android:layout_width="@dimen/reader_follow_button_progress_size"
android:layout_height="@dimen/reader_follow_button_progress_size"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />

</FrameLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
Loading