From d9859b97dd08ce184ed839ec0a423508fcc3751b Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 6 May 2024 16:45:46 -0300 Subject: [PATCH 01/11] WIP tags feed like action implementation --- .../ui/reader/ReaderTagsFeedFragment.kt | 8 ++ .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 2 +- .../tagsfeed/ReaderTagsFeedViewModel.kt | 87 ++++++++++++++++++- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 4 +- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 14 ++- 5 files changed, 107 insertions(+), 8 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt index e9683331cb3..29694cebaf8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt @@ -81,6 +81,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme initViewModels(savedInstanceState) observeActionEvents() observeNavigationEvents() + observeRefreshPosts() } private fun initViewModels(savedInstanceState: Bundle?) { @@ -205,9 +206,16 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } + private fun observeRefreshPosts() { + viewModel.refreshPosts.observe(viewLifecycleOwner) { + viewModel.onRefreshPosts() + } + } + private fun showBookmarkSavedLocallyDialog( bookmarkDialog: ReaderNavigationEvents.ShowBookmarkedSavedOnlyLocallyDialog ) { + // TODO show bookmark saved dialog? bookmarkDialog.buttonLabel // if (bookmarksSavedLocallyDialog == null) { // MaterialAlertDialogBuilder(requireActivity()) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt index 341584d3e70..576ed66f92c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt @@ -18,7 +18,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( onTagClick: (ReaderTag) -> Unit, onSiteClick: (TagsFeedPostItem) -> Unit, onPostCardClick: (TagsFeedPostItem) -> Unit, - onPostLikeClick: () -> Unit, + onPostLikeClick: (TagsFeedPostItem) -> Unit, onPostMoreMenuClick: () -> Unit, ) = ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index 328a4f5dcfe..965723ded0f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -8,16 +8,22 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import org.wordpress.android.R import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderTag import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents +import org.wordpress.android.ui.reader.discover.ReaderPostCardActionType import org.wordpress.android.ui.reader.discover.ReaderPostCardActionsHandler import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository +import org.wordpress.android.ui.reader.repository.usecases.PostLikeUseCase import org.wordpress.android.ui.reader.tracker.ReaderTracker import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.util.AppLog import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent @@ -30,6 +36,7 @@ class ReaderTagsFeedViewModel @Inject constructor( private val readerPostRepository: ReaderPostRepository, private val readerTagsFeedUiStateMapper: ReaderTagsFeedUiStateMapper, private val readerPostCardActionsHandler: ReaderPostCardActionsHandler, + private val likeUseCase: PostLikeUseCase, private val readerPostTableWrapper: ReaderPostTableWrapper, ) : ScopedViewModel(bgDispatcher) { private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) @@ -41,6 +48,11 @@ class ReaderTagsFeedViewModel @Inject constructor( private val _navigationEvents = MediatorLiveData>() val navigationEvents: LiveData> = _navigationEvents + private val _refreshPosts = MediatorLiveData>() + val refreshPosts: LiveData> = _refreshPosts + + private var itemToBeRefreshed: TagsFeedPostItem? = null + private var hasInitialized = false /** @@ -53,6 +65,34 @@ class ReaderTagsFeedViewModel @Inject constructor( if (!hasInitialized) { hasInitialized = true initNavigationEvents() + initFollowStatusUpdatedEvents() + } + } + + fun onRefreshPosts() { + // Like, bookmark or block action status changed. + (_uiStateFlow.value as? UiState.Loaded?)?.let { uiState -> + itemToBeRefreshed?.let { item -> + launch { + findPost(item.postId, item.blogId)?.let { updatedPost -> + val hasPostChanged = item.isPostLiked != updatedPost.isLikedByCurrentUser + if (!hasPostChanged) { + return@launch + } + itemToBeRefreshed = null + uiState.data.filter { it.postList is PostList.Loaded } + .flatMap { (it.postList as PostList.Loaded).items } + .map { + if (it.postId == item.postId) { + it.isPostLiked = updatedPost.isLikedByCurrentUser + } else { + it + } + } + _uiStateFlow.update { uiState } + } + } + } } } @@ -84,6 +124,12 @@ class ReaderTagsFeedViewModel @Inject constructor( } } + private fun initFollowStatusUpdatedEvents() { + _refreshPosts.addSource(readerPostCardActionsHandler.refreshPosts) { event -> + _refreshPosts.value = event + } + } + /** * Fetch posts for a single tag. This method will emit a new state to [uiStateFlow] for different [UiState]s: * [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty], but only for the tag being fetched. @@ -185,8 +231,45 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun onPostLikeClick() { - // TODO + private fun onPostLikeClick(postItem: TagsFeedPostItem) { + // We can't immediately update the UI because ReaderPostCardActionsHandler doesn't return an error. + // If there's an error, this class directly shows a Snackbar with the error message. + itemToBeRefreshed = postItem + launch { + findPost(postItem.postId, postItem.blogId)?.let { +// readerPostCardActionsHandler.onAction( +// post = it, +// type = ReaderPostCardActionType.LIKE, +// isBookmarkList = false, +// source = ReaderTracker.SOURCE_TAGS_FEED, +// ) + likeUseCase.perform(it, !it.isLikedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED).collect { + when (it) { + is PostLikeUseCase.PostLikeState.Success -> { + // TODO + AppLog.e(AppLog.T.READER, "RL-> Post liked success") + } + is PostLikeUseCase.PostLikeState.Failed.NoNetwork -> { + // TODO + AppLog.e(AppLog.T.READER, "RL-> Post liked failed no network") +// _snackbarEvents.postValue(Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.no_network_message)))) + } + is PostLikeUseCase.PostLikeState.Failed.RequestFailed -> { + // TODO + AppLog.e(AppLog.T.READER, "RL-> Post liked failed request failed") +// _refreshPosts.postValue(Event(Unit)) +// _snackbarEvents.postValue( +// Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.reader_error_request_failed_title))) +// ) + } + else -> { + // no-op + AppLog.e(AppLog.T.READER, "RL-> Post liked else: $it") + } + } + } + } + } } private fun onPostMoreMenuClick() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt index eba913b2840..ad8a98ec758 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt @@ -411,12 +411,12 @@ data class TagsFeedPostItem( val postImageUrl: String, val postNumberOfLikesText: String, val postNumberOfCommentsText: String, - val isPostLiked: Boolean, + var isPostLiked: Boolean, val postId: Long, val blogId: Long, val onSiteClick: (TagsFeedPostItem) -> Unit, val onPostCardClick: (TagsFeedPostItem) -> Unit, - val onPostLikeClick: () -> Unit, + val onPostLikeClick: (TagsFeedPostItem) -> Unit, val onPostMoreMenuClick: () -> Unit, ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt index 76fc7c2b450..e362df3c85a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt @@ -182,7 +182,7 @@ fun ReaderTagsFeedPostListItem( TextButton( modifier = Modifier.defaultMinSize(minWidth = 1.dp), contentPadding = PaddingValues(0.dp), - onClick = { onPostLikeClick() }, + onClick = { onPostLikeClick(item) }, ) { Icon( modifier = Modifier.size(24.dp), @@ -200,11 +200,19 @@ fun ReaderTagsFeedPostListItem( R.string.reader_label_like } ), - tint = secondaryElementColor, + tint = if (isPostLiked) { + androidx.compose.material.MaterialTheme.colors.primary + } else { + secondaryElementColor + }, ) Text( text = stringResource(R.string.reader_label_like), - color = secondaryElementColor, + color = if (isPostLiked) { + androidx.compose.material.MaterialTheme.colors.primary + } else { + secondaryElementColor + }, ) } Spacer(Modifier.weight(1f)) From 430aac36876dc19af1a41cce648a8561e01c8bf9 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 6 May 2024 20:22:13 -0300 Subject: [PATCH 02/11] WIP update like button UI immediately after click --- .../ui/reader/ReaderTagsFeedFragment.kt | 8 - .../tagsfeed/ReaderTagsFeedViewModel.kt | 187 ++++++++++-------- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 2 +- 3 files changed, 111 insertions(+), 86 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt index 29694cebaf8..3c89c03e000 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt @@ -77,11 +77,9 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme ReaderTagsFeed(uiState) } } - initViewModels(savedInstanceState) observeActionEvents() observeNavigationEvents() - observeRefreshPosts() } private fun initViewModels(savedInstanceState: Bundle?) { @@ -206,12 +204,6 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } - private fun observeRefreshPosts() { - viewModel.refreshPosts.observe(viewLifecycleOwner) { - viewModel.onRefreshPosts() - } - } - private fun showBookmarkSavedLocallyDialog( bookmarkDialog: ReaderNavigationEvents.ShowBookmarkedSavedOnlyLocallyDialog ) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index 965723ded0f..342354fbbdd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -8,21 +8,17 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import org.wordpress.android.R import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderTag import org.wordpress.android.modules.BG_THREAD -import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents -import org.wordpress.android.ui.reader.discover.ReaderPostCardActionType import org.wordpress.android.ui.reader.discover.ReaderPostCardActionsHandler import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.ui.reader.repository.usecases.PostLikeUseCase import org.wordpress.android.ui.reader.tracker.ReaderTracker import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem -import org.wordpress.android.ui.utils.UiString import org.wordpress.android.util.AppLog import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel @@ -48,11 +44,6 @@ class ReaderTagsFeedViewModel @Inject constructor( private val _navigationEvents = MediatorLiveData>() val navigationEvents: LiveData> = _navigationEvents - private val _refreshPosts = MediatorLiveData>() - val refreshPosts: LiveData> = _refreshPosts - - private var itemToBeRefreshed: TagsFeedPostItem? = null - private var hasInitialized = false /** @@ -65,34 +56,6 @@ class ReaderTagsFeedViewModel @Inject constructor( if (!hasInitialized) { hasInitialized = true initNavigationEvents() - initFollowStatusUpdatedEvents() - } - } - - fun onRefreshPosts() { - // Like, bookmark or block action status changed. - (_uiStateFlow.value as? UiState.Loaded?)?.let { uiState -> - itemToBeRefreshed?.let { item -> - launch { - findPost(item.postId, item.blogId)?.let { updatedPost -> - val hasPostChanged = item.isPostLiked != updatedPost.isLikedByCurrentUser - if (!hasPostChanged) { - return@launch - } - itemToBeRefreshed = null - uiState.data.filter { it.postList is PostList.Loaded } - .flatMap { (it.postList as PostList.Loaded).items } - .map { - if (it.postId == item.postId) { - it.isPostLiked = updatedPost.isLikedByCurrentUser - } else { - it - } - } - _uiStateFlow.update { uiState } - } - } - } } } @@ -124,12 +87,6 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun initFollowStatusUpdatedEvents() { - _refreshPosts.addSource(readerPostCardActionsHandler.refreshPosts) { event -> - _refreshPosts.value = event - } - } - /** * Fetch posts for a single tag. This method will emit a new state to [uiStateFlow] for different [UiState]s: * [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty], but only for the tag being fetched. @@ -232,42 +189,118 @@ class ReaderTagsFeedViewModel @Inject constructor( } private fun onPostLikeClick(postItem: TagsFeedPostItem) { - // We can't immediately update the UI because ReaderPostCardActionsHandler doesn't return an error. - // If there's an error, this class directly shows a Snackbar with the error message. - itemToBeRefreshed = postItem - launch { - findPost(postItem.postId, postItem.blogId)?.let { -// readerPostCardActionsHandler.onAction( -// post = it, -// type = ReaderPostCardActionType.LIKE, -// isBookmarkList = false, -// source = ReaderTracker.SOURCE_TAGS_FEED, -// ) - likeUseCase.perform(it, !it.isLikedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED).collect { - when (it) { - is PostLikeUseCase.PostLikeState.Success -> { - // TODO - AppLog.e(AppLog.T.READER, "RL-> Post liked success") - } - is PostLikeUseCase.PostLikeState.Failed.NoNetwork -> { - // TODO - AppLog.e(AppLog.T.READER, "RL-> Post liked failed no network") -// _snackbarEvents.postValue(Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.no_network_message)))) - } - is PostLikeUseCase.PostLikeState.Failed.RequestFailed -> { - // TODO - AppLog.e(AppLog.T.READER, "RL-> Post liked failed request failed") -// _refreshPosts.postValue(Event(Unit)) -// _snackbarEvents.postValue( -// Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.reader_error_request_failed_title))) -// ) - } - else -> { - // no-op - AppLog.e(AppLog.T.READER, "RL-> Post liked else: $it") - } + AppLog.e(AppLog.T.READER, "RL-> onPostLikeClick - postItem isLiked = ${postItem.isPostLiked}") + // Immediately update the UI. If the request fails, show error and revert UI state. + updatePostItemUI(postItem, !postItem.isPostLiked) + +// +// // Like, bookmark or block action status changed. +// +// +// (_uiStateFlow.value as? UiState.Loaded?)?.let { uiState -> +// launch { +// findPost(item.postId, item.blogId)?.let { updatedPost -> +// val hasPostChanged = item.isPostLiked != updatedPost.isLikedByCurrentUser +// if (!hasPostChanged) { +// return@launch +// } +// uiState.data.filter { it.postList is PostList.Loaded } +// .flatMap { (it.postList as PostList.Loaded).items } +// .map { +// if (it.postId == item.postId) { +// it.isPostLiked = updatedPost.isLikedByCurrentUser +// } else { +// it +// } +// } +// _uiStateFlow.update { uiState } +// } +// } +// } +// +// +// launch { +// findPost(postItem.postId, postItem.blogId)?.let { +//// readerPostCardActionsHandler.onAction( +//// post = it, +//// type = ReaderPostCardActionType.LIKE, +//// isBookmarkList = false, +//// source = ReaderTracker.SOURCE_TAGS_FEED, +//// ) +// likeUseCase.perform(it, !it.isLikedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED).collect { +// when (it) { +// is PostLikeUseCase.PostLikeState.Success -> { +// // TODO +// AppLog.e(AppLog.T.READER, "RL-> Post liked success") +// } +// +// is PostLikeUseCase.PostLikeState.Failed.NoNetwork -> { +// // TODO +// AppLog.e(AppLog.T.READER, "RL-> Post liked failed no network") +//// _snackbarEvents.postValue(Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.no_network_message)))) +// } +// +// is PostLikeUseCase.PostLikeState.Failed.RequestFailed -> { +// // TODO +// AppLog.e(AppLog.T.READER, "RL-> Post liked failed request failed") +//// _refreshPosts.postValue(Event(Unit)) +//// _snackbarEvents.postValue( +//// Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.reader_error_request_failed_title))) +//// ) +// } +// +// else -> { +// // no-op +// AppLog.e(AppLog.T.READER, "RL-> Post liked else: $it") +// } +// } +// } +// } +// } + } + + private fun updatePostItemUI( + postItemToUpdate: TagsFeedPostItem, + isPostLikedUpdated: Boolean + ) { + val uiState = _uiStateFlow.value + if (uiState !is UiState.Loaded) { + return + } + + val tagFeedItemToUpdate: TagFeedItem = uiState.data.firstOrNull { + it.postList is PostList.Loaded && it.postList.items.contains(postItemToUpdate) + } ?: return + + uiState.data.indexOfFirst { it.tagChip == tagFeedItemToUpdate.tagChip }.let { tagFeedItemToUpdateIndex -> + if (tagFeedItemToUpdateIndex == -1) { + return + } + + if (tagFeedItemToUpdate.postList is PostList.Loaded) { + val updatedTagFeedItemPostListItems = tagFeedItemToUpdate.postList.items.toMutableList().apply { + val postItemToUpdateIndex = + indexOfFirst { it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId } + if (postItemToUpdateIndex != -1) { + removeAt(postItemToUpdateIndex) + add( + postItemToUpdateIndex, postItemToUpdate.copy( + isPostLiked = isPostLikedUpdated, + ) + ) } } + val updatedTagFeedItem = tagFeedItemToUpdate.copy( + postList = tagFeedItemToUpdate.postList.copy( + items = updatedTagFeedItemPostListItems + ) + ) + val updatedUiStateData = mutableListOf().apply { + addAll(uiState.data) + removeAt(tagFeedItemToUpdateIndex) + add(tagFeedItemToUpdateIndex, updatedTagFeedItem) + } + _uiStateFlow.value = uiState.copy(data = updatedUiStateData) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt index ad8a98ec758..54a9bebd274 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt @@ -411,7 +411,7 @@ data class TagsFeedPostItem( val postImageUrl: String, val postNumberOfLikesText: String, val postNumberOfCommentsText: String, - var isPostLiked: Boolean, + val isPostLiked: Boolean, val postId: Long, val blogId: Long, val onSiteClick: (TagsFeedPostItem) -> Unit, From 7e23abe34dc9c52bf86783b5083b7da9ae3a6b5f Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 6 May 2024 22:25:38 -0300 Subject: [PATCH 03/11] Disable like button after is tapped --- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 1 + .../viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt | 13 ++++++++++--- .../reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 6 ++++++ .../compose/tagsfeed/ReaderTagsFeedPostListItem.kt | 9 +++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt index 576ed66f92c..8f6cb3f34e2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt @@ -42,6 +42,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( numComments = it.numReplies ) else "", isPostLiked = it.isLikedByCurrentUser, + isLikeButtonEnabled = true, postId = it.postId, blogId = it.blogId, onSiteClick = onSiteClick, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index 342354fbbdd..95eacaf2236 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -190,8 +190,13 @@ class ReaderTagsFeedViewModel @Inject constructor( private fun onPostLikeClick(postItem: TagsFeedPostItem) { AppLog.e(AppLog.T.READER, "RL-> onPostLikeClick - postItem isLiked = ${postItem.isPostLiked}") - // Immediately update the UI. If the request fails, show error and revert UI state. - updatePostItemUI(postItem, !postItem.isPostLiked) + // Immediately update the UI and disable the like button. If the request fails, show error and revert UI state. + // If the request fails or succeeds, the like button is enabled again. + updatePostItemUI( + postItemToUpdate = postItem, + isPostLikedUpdated = !postItem.isPostLiked, + isLikeButtonEnabled = false + ) // // // Like, bookmark or block action status changed. @@ -261,7 +266,8 @@ class ReaderTagsFeedViewModel @Inject constructor( private fun updatePostItemUI( postItemToUpdate: TagsFeedPostItem, - isPostLikedUpdated: Boolean + isPostLikedUpdated: Boolean, + isLikeButtonEnabled: Boolean, ) { val uiState = _uiStateFlow.value if (uiState !is UiState.Loaded) { @@ -286,6 +292,7 @@ class ReaderTagsFeedViewModel @Inject constructor( add( postItemToUpdateIndex, postItemToUpdate.copy( isPostLiked = isPostLikedUpdated, + isLikeButtonEnabled = isLikeButtonEnabled, ) ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt index 54a9bebd274..0b84fb09ed4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt @@ -412,6 +412,7 @@ data class TagsFeedPostItem( val postNumberOfLikesText: String, val postNumberOfCommentsText: String, val isPostLiked: Boolean, + val isLikeButtonEnabled: Boolean, val postId: Long, val blogId: Long, val onSiteClick: (TagsFeedPostItem) -> Unit, @@ -436,6 +437,7 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "", isPostLiked = true, + isLikeButtonEnabled = true, postId = 123L, blogId = 123L, onSiteClick = {}, @@ -452,6 +454,7 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "", postNumberOfCommentsText = "3 comments", isPostLiked = true, + isLikeButtonEnabled = true, postId = 456L, blogId = 456L, onSiteClick = {}, @@ -468,6 +471,7 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "123 likes", postNumberOfCommentsText = "9 comments", isPostLiked = true, + isLikeButtonEnabled = true, postId = 789L, blogId = 789L, onSiteClick = {}, @@ -484,6 +488,7 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "1234 likes", postNumberOfCommentsText = "91 comments", isPostLiked = true, + isLikeButtonEnabled = true, postId = 1234L, blogId = 1234L, onSiteClick = {}, @@ -500,6 +505,7 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "12 likes", postNumberOfCommentsText = "34 comments", isPostLiked = true, + isLikeButtonEnabled = true, postId = 5678L, blogId = 5678L, onSiteClick = {}, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt index e362df3c85a..65fddc0e987 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt @@ -183,6 +183,7 @@ fun ReaderTagsFeedPostListItem( modifier = Modifier.defaultMinSize(minWidth = 1.dp), contentPadding = PaddingValues(0.dp), onClick = { onPostLikeClick(item) }, + enabled = isLikeButtonEnabled, ) { Icon( modifier = Modifier.size(24.dp), @@ -296,6 +297,7 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, + isLikeButtonEnabled = true, blogId = 123L, postId = 123L, onSiteClick = {}, @@ -330,6 +332,7 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, + isLikeButtonEnabled = true, blogId = 123L, postId = 123L, onSiteClick = {}, @@ -350,6 +353,7 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, + isLikeButtonEnabled = true, blogId = 123L, postId = 123L, onSiteClick = {}, @@ -370,6 +374,7 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, + isLikeButtonEnabled = true, blogId = 123L, postId = 123L, onSiteClick = {}, @@ -391,6 +396,7 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, + isLikeButtonEnabled = true, blogId = 123L, postId = 123L, onSiteClick = {}, @@ -412,6 +418,7 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, + isLikeButtonEnabled = true, blogId = 123L, postId = 123L, onSiteClick = {}, @@ -445,6 +452,7 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, + isLikeButtonEnabled = true, blogId = 123L, postId = 123L, onSiteClick = {}, @@ -477,6 +485,7 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, + isLikeButtonEnabled = true, blogId = 123L, postId = 123L, onSiteClick = {}, From 64a5b4ff0be290f84e6f5cc71390db21461c6cbe Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 6 May 2024 22:39:42 -0300 Subject: [PATCH 04/11] Simplify code and add comments --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index 95eacaf2236..779e21d6049 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -198,6 +198,10 @@ class ReaderTagsFeedViewModel @Inject constructor( isLikeButtonEnabled = false ) + // After updating the like button UI to the intended state and disabling the like button, send a request to the + // like endpoint by using the PostLikeUseCase + + // // // Like, bookmark or block action status changed. // @@ -269,21 +273,17 @@ class ReaderTagsFeedViewModel @Inject constructor( isPostLikedUpdated: Boolean, isLikeButtonEnabled: Boolean, ) { - val uiState = _uiStateFlow.value - if (uiState !is UiState.Loaded) { - return - } - + val uiState = _uiStateFlow.value as? UiState.Loaded ?: return + // Finds the TagFeedItem associated with the post that should be updated val tagFeedItemToUpdate: TagFeedItem = uiState.data.firstOrNull { - it.postList is PostList.Loaded && it.postList.items.contains(postItemToUpdate) - } ?: return - - uiState.data.indexOfFirst { it.tagChip == tagFeedItemToUpdate.tagChip }.let { tagFeedItemToUpdateIndex -> + it.postList is PostList.Loaded && it.postList.items.contains(postItemToUpdate) + } ?: return + uiState.data.indexOf(tagFeedItemToUpdate).let { tagFeedItemToUpdateIndex -> if (tagFeedItemToUpdateIndex == -1) { return } - if (tagFeedItemToUpdate.postList is PostList.Loaded) { + // Creates a new post list items collection with the post item updated values val updatedTagFeedItemPostListItems = tagFeedItemToUpdate.postList.items.toMutableList().apply { val postItemToUpdateIndex = indexOfFirst { it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId } @@ -297,16 +297,19 @@ class ReaderTagsFeedViewModel @Inject constructor( ) } } + // Creates a copy of the TagFeedItem with the updated post list items collection val updatedTagFeedItem = tagFeedItemToUpdate.copy( postList = tagFeedItemToUpdate.postList.copy( items = updatedTagFeedItemPostListItems ) ) + // Creates a new TagFeedItem collection with the updated TagFeedItem val updatedUiStateData = mutableListOf().apply { addAll(uiState.data) removeAt(tagFeedItemToUpdateIndex) add(tagFeedItemToUpdateIndex, updatedTagFeedItem) } + // Updates the UI state value with the updated TagFeedItem collection _uiStateFlow.value = uiState.copy(data = updatedUiStateData) } } From 668e716d53dcf5d2b2e1b95bd5289949dd0ac5c5 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 7 May 2024 00:14:19 -0300 Subject: [PATCH 05/11] WIP implement like action in reader tags feed: call like endpoint --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 124 ++++++++---------- 1 file changed, 55 insertions(+), 69 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index 779e21d6049..6bcf25efe71 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -192,80 +192,64 @@ class ReaderTagsFeedViewModel @Inject constructor( AppLog.e(AppLog.T.READER, "RL-> onPostLikeClick - postItem isLiked = ${postItem.isPostLiked}") // Immediately update the UI and disable the like button. If the request fails, show error and revert UI state. // If the request fails or succeeds, the like button is enabled again. + val isPostLikedUpdated = !postItem.isPostLiked updatePostItemUI( postItemToUpdate = postItem, - isPostLikedUpdated = !postItem.isPostLiked, - isLikeButtonEnabled = false + isPostLikedUpdated = isPostLikedUpdated, + isLikeButtonEnabled = false, ) // After updating the like button UI to the intended state and disabling the like button, send a request to the // like endpoint by using the PostLikeUseCase + launch { + findPost(postItem.postId, postItem.blogId)?.let { + likeUseCase.perform(it, !it.isLikedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED).collect { + when (it) { + is PostLikeUseCase.PostLikeState.Success -> { + // Re-enable like button without changing the current post item UI. + updatePostItemUI( + postItemToUpdate = postItem, + isPostLikedUpdated = isPostLikedUpdated, + isLikeButtonEnabled = true, + ) + } + + is PostLikeUseCase.PostLikeState.Failed.NoNetwork -> { + // Revert post item like button UI to the previous state and re-enable like button. + updatePostItemUI( + postItemToUpdate = postItem, + isPostLikedUpdated = !isPostLikedUpdated, + isLikeButtonEnabled = true, + ) - -// -// // Like, bookmark or block action status changed. -// -// -// (_uiStateFlow.value as? UiState.Loaded?)?.let { uiState -> -// launch { -// findPost(item.postId, item.blogId)?.let { updatedPost -> -// val hasPostChanged = item.isPostLiked != updatedPost.isLikedByCurrentUser -// if (!hasPostChanged) { -// return@launch -// } -// uiState.data.filter { it.postList is PostList.Loaded } -// .flatMap { (it.postList as PostList.Loaded).items } -// .map { -// if (it.postId == item.postId) { -// it.isPostLiked = updatedPost.isLikedByCurrentUser -// } else { -// it -// } -// } -// _uiStateFlow.update { uiState } -// } -// } -// } -// -// -// launch { -// findPost(postItem.postId, postItem.blogId)?.let { -//// readerPostCardActionsHandler.onAction( -//// post = it, -//// type = ReaderPostCardActionType.LIKE, -//// isBookmarkList = false, -//// source = ReaderTracker.SOURCE_TAGS_FEED, -//// ) -// likeUseCase.perform(it, !it.isLikedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED).collect { -// when (it) { -// is PostLikeUseCase.PostLikeState.Success -> { -// // TODO -// AppLog.e(AppLog.T.READER, "RL-> Post liked success") -// } -// -// is PostLikeUseCase.PostLikeState.Failed.NoNetwork -> { -// // TODO -// AppLog.e(AppLog.T.READER, "RL-> Post liked failed no network") -//// _snackbarEvents.postValue(Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.no_network_message)))) -// } -// -// is PostLikeUseCase.PostLikeState.Failed.RequestFailed -> { -// // TODO -// AppLog.e(AppLog.T.READER, "RL-> Post liked failed request failed") -//// _refreshPosts.postValue(Event(Unit)) -//// _snackbarEvents.postValue( -//// Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.reader_error_request_failed_title))) -//// ) -// } -// -// else -> { -// // no-op -// AppLog.e(AppLog.T.READER, "RL-> Post liked else: $it") -// } -// } -// } -// } -// } + AppLog.e(AppLog.T.READER, "RL-> Post liked failed no network") + // TODO show snackbar? +// _snackbarEvents.postValue(Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.no_network_message)))) + } + + is PostLikeUseCase.PostLikeState.Failed.RequestFailed -> { + // Revert post item like button UI to the previous state and re-enable like button. + updatePostItemUI( + postItemToUpdate = postItem, + isPostLikedUpdated = !isPostLikedUpdated, + isLikeButtonEnabled = true, + ) + AppLog.e(AppLog.T.READER, "RL-> Post liked failed request failed") + // TODO show snackbar? +// _refreshPosts.postValue(Event(Unit)) +// _snackbarEvents.postValue( +// Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.reader_error_request_failed_title))) +// ) + } + + else -> { + // no-op + AppLog.e(AppLog.T.READER, "RL-> Post liked else: $it") + } + } + } + } + } } private fun updatePostItemUI( @@ -275,8 +259,10 @@ class ReaderTagsFeedViewModel @Inject constructor( ) { val uiState = _uiStateFlow.value as? UiState.Loaded ?: return // Finds the TagFeedItem associated with the post that should be updated - val tagFeedItemToUpdate: TagFeedItem = uiState.data.firstOrNull { - it.postList is PostList.Loaded && it.postList.items.contains(postItemToUpdate) + val tagFeedItemToUpdate: TagFeedItem = uiState.data.firstOrNull { tagFeedItem -> + tagFeedItem.postList is PostList.Loaded && tagFeedItem.postList.items.firstOrNull { + it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId + } != null } ?: return uiState.data.indexOf(tagFeedItemToUpdate).let { tagFeedItemToUpdateIndex -> if (tagFeedItemToUpdateIndex == -1) { From cf95d1bbcf8ba687b632c19d10690da76b6e7655 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 7 May 2024 18:09:43 -0300 Subject: [PATCH 06/11] Extract like post remote to separate method --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 94 ++++++++++--------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index 1f055fd9d4d..a47ed4f91ec 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -206,6 +206,55 @@ class ReaderTagsFeedViewModel @Inject constructor( // After updating the like button UI to the intended state and disabling the like button, send a request to the // like endpoint by using the PostLikeUseCase + likePostRemote(postItem, isPostLikedUpdated) + } + + private fun updatePostItemUI( + postItemToUpdate: TagsFeedPostItem, + isPostLikedUpdated: Boolean, + isLikeButtonEnabled: Boolean, + ) { + val uiState = _uiStateFlow.value as? UiState.Loaded ?: return + // Finds the TagFeedItem associated with the post that should be updated. Return if the item is + // not found. + val tagFeedItemToUpdate: TagFeedItem = uiState.data.firstOrNull { tagFeedItem -> + tagFeedItem.postList is PostList.Loaded && tagFeedItem.postList.items.firstOrNull { + it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId + } != null + } ?: return + // Finds the index associated with the TagFeedItem to be updated found above. Return if the index is not found. + val tagFeedItemToUpdateIndex = uiState.data.indexOf(tagFeedItemToUpdate) + if (tagFeedItemToUpdateIndex != -1 && tagFeedItemToUpdate.postList is PostList.Loaded) { + // Creates a new post list items collection with the post item updated values + val updatedTagFeedItemPostListItems = tagFeedItemToUpdate.postList.items.toMutableList().apply { + val postItemToUpdateIndex = + indexOfFirst { + it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId + } + if (postItemToUpdateIndex != -1) { + this[postItemToUpdateIndex] = postItemToUpdate.copy( + isPostLiked = isPostLikedUpdated, + isLikeButtonEnabled = isLikeButtonEnabled, + ) + } + } + // Creates a copy of the TagFeedItem with the updated post list items collection + val updatedTagFeedItem = tagFeedItemToUpdate.copy( + postList = tagFeedItemToUpdate.postList.copy( + items = updatedTagFeedItemPostListItems + ) + ) + // Creates a new TagFeedItem collection with the updated TagFeedItem + val updatedUiStateData = mutableListOf().apply { + addAll(uiState.data) + this[tagFeedItemToUpdateIndex] = updatedTagFeedItem + } + // Updates the UI state value with the updated TagFeedItem collection + _uiStateFlow.value = uiState.copy(data = updatedUiStateData) + } + } + + private fun likePostRemote(postItem: TagsFeedPostItem, isPostLikedUpdated: Boolean) { launch { findPost(postItem.postId, postItem.blogId)?.let { postLikeUseCase.perform(it, !it.isLikedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED).collect { @@ -257,51 +306,6 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun updatePostItemUI( - postItemToUpdate: TagsFeedPostItem, - isPostLikedUpdated: Boolean, - isLikeButtonEnabled: Boolean, - ) { - val uiState = _uiStateFlow.value as? UiState.Loaded ?: return - // Finds the TagFeedItem associated with the post that should be updated. Return if the item is - // not found. - val tagFeedItemToUpdate: TagFeedItem = uiState.data.firstOrNull { tagFeedItem -> - tagFeedItem.postList is PostList.Loaded && tagFeedItem.postList.items.firstOrNull { - it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId - } != null - } ?: return - // Finds the index associated with the TagFeedItem to be updated found above. Return if the index is not found. - val tagFeedItemToUpdateIndex = uiState.data.indexOf(tagFeedItemToUpdate) - if (tagFeedItemToUpdateIndex != -1 && tagFeedItemToUpdate.postList is PostList.Loaded) { - // Creates a new post list items collection with the post item updated values - val updatedTagFeedItemPostListItems = tagFeedItemToUpdate.postList.items.toMutableList().apply { - val postItemToUpdateIndex = - indexOfFirst { - it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId - } - if (postItemToUpdateIndex != -1) { - this[postItemToUpdateIndex] = postItemToUpdate.copy( - isPostLiked = isPostLikedUpdated, - isLikeButtonEnabled = isLikeButtonEnabled, - ) - } - } - // Creates a copy of the TagFeedItem with the updated post list items collection - val updatedTagFeedItem = tagFeedItemToUpdate.copy( - postList = tagFeedItemToUpdate.postList.copy( - items = updatedTagFeedItemPostListItems - ) - ) - // Creates a new TagFeedItem collection with the updated TagFeedItem - val updatedUiStateData = mutableListOf().apply { - addAll(uiState.data) - this[tagFeedItemToUpdateIndex] = updatedTagFeedItem - } - // Updates the UI state value with the updated TagFeedItem collection - _uiStateFlow.value = uiState.copy(data = updatedUiStateData) - } - } - private fun onPostMoreMenuClick() { // TODO } From 1ed459339522edd71e2c6dccc947aa2fd0d9e6a5 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 7 May 2024 19:21:53 -0300 Subject: [PATCH 07/11] Implement like button error messages --- .../android/ui/reader/ReaderTagsFeedFragment.kt | 11 +++++++++++ .../tagsfeed/ReaderTagsFeedViewModel.kt | 17 ++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt index cfb749da5b1..5dea6134f1a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt @@ -9,6 +9,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.commitNow import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.databinding.ReaderTagFeedFragmentLayoutBinding @@ -29,6 +30,7 @@ import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewMod import org.wordpress.android.ui.reader.views.compose.tagsfeed.ReaderTagsFeed import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.viewmodel.observeEvent +import org.wordpress.android.widgets.WPSnackbar import javax.inject.Inject /** @@ -76,6 +78,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme observeSubFilterViewModel(savedInstanceState) observeActionEvents() observeNavigationEvents() + observeErrorMessageEvents() } private fun observeSubFilterViewModel(savedInstanceState: Bundle?) { @@ -248,6 +251,14 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } + private fun observeErrorMessageEvents() { + viewModel.errorMessageEvents.observeEvent(viewLifecycleOwner) { stringRes -> + activity?.findViewById(android.R.id.content)?.let { view -> + WPSnackbar.make(view, getString(stringRes), Snackbar.LENGTH_LONG).show() + } + } + } + private fun showBookmarkSavedLocallyDialog( bookmarkDialog: ReaderNavigationEvents.ShowBookmarkedSavedOnlyLocallyDialog ) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index a47ed4f91ec..ee51c0e699f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import org.wordpress.android.R import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderTag @@ -44,6 +45,9 @@ class ReaderTagsFeedViewModel @Inject constructor( private val _navigationEvents = MediatorLiveData>() val navigationEvents: LiveData> = _navigationEvents + private val _errorMessageEvents = MediatorLiveData>() + val errorMessageEvents: LiveData> = _errorMessageEvents + private var hasInitialized = false /** @@ -275,10 +279,7 @@ class ReaderTagsFeedViewModel @Inject constructor( isPostLikedUpdated = !isPostLikedUpdated, isLikeButtonEnabled = true, ) - - AppLog.e(AppLog.T.READER, "RL-> Post liked failed no network") - // TODO show snackbar? -// _snackbarEvents.postValue(Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.no_network_message)))) + _errorMessageEvents.postValue(Event(R.string.no_network_message)) } is PostLikeUseCase.PostLikeState.Failed.RequestFailed -> { @@ -288,17 +289,11 @@ class ReaderTagsFeedViewModel @Inject constructor( isPostLikedUpdated = !isPostLikedUpdated, isLikeButtonEnabled = true, ) - AppLog.e(AppLog.T.READER, "RL-> Post liked failed request failed") - // TODO show snackbar? -// _refreshPosts.postValue(Event(Unit)) -// _snackbarEvents.postValue( -// Event(SnackbarMessageHolder(UiString.UiStringRes(R.string.reader_error_request_failed_title))) -// ) + _errorMessageEvents.postValue(Event(R.string.reader_error_request_failed_title)) } else -> { // no-op - AppLog.e(AppLog.T.READER, "RL-> Post liked else: $it") } } } From 9c3cc3d51ff922c3904ab7311f46d85e9e8d4122 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 7 May 2024 22:34:51 -0300 Subject: [PATCH 08/11] WIP update ReaderTagsFeedViewModelTest like button tests --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 4 +- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 69 +++++++++++++++++-- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index ee51c0e699f..810760d6fe6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -197,8 +197,8 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun onPostLikeClick(postItem: TagsFeedPostItem) { - AppLog.e(AppLog.T.READER, "RL-> onPostLikeClick - postItem isLiked = ${postItem.isPostLiked}") + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun onPostLikeClick(postItem: TagsFeedPostItem) { // Immediately update the UI and disable the like button. If the request fails, show error and revert UI state. // If the request fails or succeeds, the like button is enabled again. val isPostLikedUpdated = !postItem.isPostLiked diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt index 79e683ad292..d054c2692ff 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt @@ -22,9 +22,9 @@ import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.ui.reader.ReaderTestUtils import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents import org.wordpress.android.ui.reader.discover.ReaderPostCardActionsHandler -import org.wordpress.android.ui.reader.ReaderTestUtils import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.ui.reader.repository.usecases.PostLikeUseCase @@ -34,6 +34,7 @@ import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewMod import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.viewmodel.Event import kotlin.test.assertIs +import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class ReaderTagsFeedViewModelTest : BaseUnitTest() { @@ -300,6 +301,59 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertThat(collectedUiStates).last().isInstanceOf(ReaderTagsFeedViewModel.UiState.Empty::class.java) } + @Test + fun `Should update UI immediately when like button is tapped`() = testCollectingUiStates { + // Given + val tagsFeedPostItem = TagsFeedPostItem( + siteName = "", + postDateLine = "", + postTitle = "", + postExcerpt = "", + postImageUrl = "", + postNumberOfLikesText = "", + postNumberOfCommentsText = "", + isPostLiked = false, + isLikeButtonEnabled = true, + postId = 123L, + blogId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {} + ) + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems(items = listOf(tagsFeedPostItem)) + val posts = ReaderPostList().apply { + add(ReaderPost()) + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag)).doSuspendableAnswer { + delay(100) + posts + } + + // When + viewModel.start(listOf(tag)) + advanceUntilIdle() + viewModel.onPostLikeClick(tagsFeedPostItem) + + // Then + val latestUiState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.Loaded + val latestUiStatePostList = (latestUiState.data.first().postList as ReaderTagsFeedViewModel.PostList.Loaded) + assertThat(latestUiStatePostList.items.first().isPostLiked).isEqualTo(!tagsFeedPostItem.isPostLiked) + } + + @Test + fun `Should send update like status request when like button is tapped if internet connection is available`() { + } + + @Test + fun `Should revert like button UI if update like status request fails (RequestFailed)`() { + } + + @Test + fun `Should revert like button UI if update like status request fails (NoNetwork)`() { + } + private fun mockMapLoadingTagFeedItems() { whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) .thenAnswer { @@ -318,10 +372,10 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } } - private fun mockMapLoadedTagFeedItems() { + private fun mockMapLoadedTagFeedItems(items: List = emptyList()) { whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any())) .thenAnswer { - getLoadedTagFeedItem(it.getArgument(0)) + getLoadedTagFeedItem(it.getArgument(0), items) } } @@ -332,10 +386,11 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } } - private fun getLoadedTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem( - ReaderTagsFeedViewModel.TagChip(tag, {}), - ReaderTagsFeedViewModel.PostList.Loaded(listOf()) - ) + private fun getLoadedTagFeedItem(tag: ReaderTag, items: List = emptyList()) = + ReaderTagsFeedViewModel.TagFeedItem( + ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.PostList.Loaded(items) + ) private fun getErrorTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem( ReaderTagsFeedViewModel.TagChip(tag, {}), From 44f9eac98ef1a946fc1c414a1c5e1fefa0b1721e Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 7 May 2024 22:58:05 -0300 Subject: [PATCH 09/11] finish updating ReaderTagsFeedViewModelTest like button tests --- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt index d054c2692ff..f81950465a4 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt @@ -3,6 +3,7 @@ package org.wordpress.android.ui.reader.viewmodels import androidx.lifecycle.MediatorLiveData import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope @@ -14,6 +15,7 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.doSuspendableAnswer +import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest @@ -34,7 +36,6 @@ import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewMod import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.viewmodel.Event import kotlin.test.assertIs -import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class ReaderTagsFeedViewModelTest : BaseUnitTest() { @@ -343,15 +344,46 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } @Test - fun `Should send update like status request when like button is tapped if internet connection is available`() { - } + fun `Should send update like status request when like button is tapped`() = testCollectingUiStates { + // Given + val tagsFeedPostItem = TagsFeedPostItem( + siteName = "", + postDateLine = "", + postTitle = "", + postExcerpt = "", + postImageUrl = "", + postNumberOfLikesText = "", + postNumberOfCommentsText = "", + isPostLiked = false, + isLikeButtonEnabled = true, + postId = 123L, + blogId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {} + ) + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems(items = listOf(tagsFeedPostItem)) + val posts = ReaderPostList().apply { + add(ReaderPost()) + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag)).doSuspendableAnswer { + delay(100) + posts + } + whenever(readerPostTableWrapper.getBlogPost(any(), any(), any())) + .thenReturn(ReaderPost()) + whenever(postLikeUseCase.perform(any(), any(), any())) + .thenReturn(flowOf()) - @Test - fun `Should revert like button UI if update like status request fails (RequestFailed)`() { - } + // When + viewModel.start(listOf(tag)) + advanceUntilIdle() + viewModel.onPostLikeClick(tagsFeedPostItem) - @Test - fun `Should revert like button UI if update like status request fails (NoNetwork)`() { + // Then + verify(postLikeUseCase).perform(any(), any(), any()) } private fun mockMapLoadingTagFeedItems() { From 7a5b826b87f68a16f8e6d3b260f4999a1e9c8a8f Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 7 May 2024 23:47:55 -0300 Subject: [PATCH 10/11] Fix detekt --- .../ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index 810760d6fe6..a6b19ec24e3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -20,7 +20,6 @@ import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.ui.reader.repository.usecases.PostLikeUseCase import org.wordpress.android.ui.reader.tracker.ReaderTracker import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem -import org.wordpress.android.util.AppLog import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent From 2a41ebcc9dbcc83012d54363498bbfe18ae32279 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 8 May 2024 11:33:52 -0300 Subject: [PATCH 11/11] Apply PR suggestion: break updatePostItemUI into smaller pieces --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index a6b19ec24e3..cec69fbe913 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -220,27 +220,18 @@ class ReaderTagsFeedViewModel @Inject constructor( val uiState = _uiStateFlow.value as? UiState.Loaded ?: return // Finds the TagFeedItem associated with the post that should be updated. Return if the item is // not found. - val tagFeedItemToUpdate: TagFeedItem = uiState.data.firstOrNull { tagFeedItem -> - tagFeedItem.postList is PostList.Loaded && tagFeedItem.postList.items.firstOrNull { - it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId - } != null - } ?: return + val tagFeedItemToUpdate = findTagFeedItemToUpdate(uiState, postItemToUpdate) ?: return + // Finds the index associated with the TagFeedItem to be updated found above. Return if the index is not found. val tagFeedItemToUpdateIndex = uiState.data.indexOf(tagFeedItemToUpdate) if (tagFeedItemToUpdateIndex != -1 && tagFeedItemToUpdate.postList is PostList.Loaded) { // Creates a new post list items collection with the post item updated values - val updatedTagFeedItemPostListItems = tagFeedItemToUpdate.postList.items.toMutableList().apply { - val postItemToUpdateIndex = - indexOfFirst { - it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId - } - if (postItemToUpdateIndex != -1) { - this[postItemToUpdateIndex] = postItemToUpdate.copy( - isPostLiked = isPostLikedUpdated, - isLikeButtonEnabled = isLikeButtonEnabled, - ) - } - } + val updatedTagFeedItemPostListItems = getPostListWithUpdatedPostItem( + postList = tagFeedItemToUpdate.postList, + postItemToUpdate = postItemToUpdate, + isPostLikedUpdated = isPostLikedUpdated, + isLikeButtonEnabled = isLikeButtonEnabled, + ) // Creates a copy of the TagFeedItem with the updated post list items collection val updatedTagFeedItem = tagFeedItemToUpdate.copy( postList = tagFeedItemToUpdate.postList.copy( @@ -257,6 +248,32 @@ class ReaderTagsFeedViewModel @Inject constructor( } } + private fun getPostListWithUpdatedPostItem( + postList: PostList.Loaded, + postItemToUpdate: TagsFeedPostItem, + isPostLikedUpdated: Boolean, + isLikeButtonEnabled: Boolean + ) = + postList.items.toMutableList().apply { + val postItemToUpdateIndex = + indexOfFirst { + it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId + } + if (postItemToUpdateIndex != -1) { + this[postItemToUpdateIndex] = postItemToUpdate.copy( + isPostLiked = isPostLikedUpdated, + isLikeButtonEnabled = isLikeButtonEnabled, + ) + } + } + + private fun findTagFeedItemToUpdate(uiState: UiState.Loaded, postItemToUpdate: TagsFeedPostItem) = + uiState.data.firstOrNull { tagFeedItem -> + tagFeedItem.postList is PostList.Loaded && tagFeedItem.postList.items.firstOrNull { + it.postId == postItemToUpdate.postId && it.blogId == postItemToUpdate.blogId + } != null + } + private fun likePostRemote(postItem: TagsFeedPostItem, isPostLikedUpdated: Boolean) { launch { findPost(postItem.postId, postItem.blogId)?.let {