Skip to content

Commit

Permalink
Merge pull request #20791 from wordpress-mobile/issue/20707-tags-feed…
Browse files Browse the repository at this point in the history
…-ptr

[Tags Feed] Implement pull-to-refresh
  • Loading branch information
thomashorta committed May 10, 2024
2 parents 2aa1835 + 03d53da commit 78f1e2f
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 65 deletions.
Expand Up @@ -109,6 +109,10 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme
is ActionEvent.OpenTagPostsFeed -> {
subFilterViewModel.setSubfilterFromTag(it.readerTag)
}

ActionEvent.RefreshTagsFeed -> {
subFilterViewModel.updateTagsAndSites()
}
}
}
}
Expand Down
Expand Up @@ -35,7 +35,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor(
),
postTitle = it.title,
postExcerpt = it.excerpt,
postImageUrl = it.blogImageUrl,
postImageUrl = it.featuredImage,
postNumberOfLikesText = if (it.numLikes > 0) readerUtilsWrapper.getShortLikeLabelText(
numLikes = it.numLikes
) else "",
Expand Down Expand Up @@ -77,11 +77,13 @@ class ReaderTagsFeedUiStateMapper @Inject constructor(

fun mapInitialPostsUiState(
tags: List<ReaderTag>,
isRefreshing: Boolean,
onTagClick: (ReaderTag) -> Unit,
onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit,
onRefresh: () -> Unit,
): ReaderTagsFeedViewModel.UiState.Loaded =
ReaderTagsFeedViewModel.UiState.Loaded(
tags.map { tag ->
data = tags.map { tag ->
ReaderTagsFeedViewModel.TagFeedItem(
tagChip = ReaderTagsFeedViewModel.TagChip(
tag = tag,
Expand All @@ -90,7 +92,9 @@ class ReaderTagsFeedUiStateMapper @Inject constructor(
postList = ReaderTagsFeedViewModel.PostList.Initial,
onItemEnteredView = onItemEnteredView,
)
}
},
isRefreshing = isRefreshing,
onRefresh = onRefresh,
)

fun mapLoadingTagFeedItem(
Expand Down
Expand Up @@ -55,11 +55,11 @@ class ReaderTagsFeedViewModel @Inject constructor(
* [UiState]s: [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty].
*/
fun start(tags: List<ReaderTag>) {
// don't start again if the tags match
if (_uiStateFlow.value is UiState.Loaded &&
tags == (_uiStateFlow.value as UiState.Loaded).data.map { it.tagChip.tag }
) {
return
// don't start again if the tags match, unless the user requested a refresh
(_uiStateFlow.value as? UiState.Loaded)?.let { loadedState ->
if (!loadedState.isRefreshing && tags == loadedState.data.map { it.tagChip.tag }) {
return
}
}

if (tags.isEmpty()) {
Expand All @@ -74,7 +74,13 @@ class ReaderTagsFeedViewModel @Inject constructor(

// Initially add all tags to the list with the posts loading UI
_uiStateFlow.update {
readerTagsFeedUiStateMapper.mapInitialPostsUiState(tags, ::onTagClick, ::onItemEnteredView)
readerTagsFeedUiStateMapper.mapInitialPostsUiState(
tags,
false,
::onTagClick,
::onItemEnteredView,
::onRefresh
)
}
}

Expand Down Expand Up @@ -162,10 +168,18 @@ class ReaderTagsFeedViewModel @Inject constructor(
updatedLoadedData[existingIndex] = updatedItem
}

UiState.Loaded(updatedLoadedData)
(uiState as? UiState.Loaded)?.copy(data = updatedLoadedData) ?: UiState.Loaded(updatedLoadedData)
}
}

@VisibleForTesting
fun onRefresh() {
_uiStateFlow.update {
(it as? UiState.Loaded)?.copy(isRefreshing = true) ?: it
}
_actionEvents.value = ActionEvent.RefreshTagsFeed
}

@VisibleForTesting
fun onItemEnteredView(item: TagFeedItem) {
if (item.postList != PostList.Initial) {
Expand Down Expand Up @@ -286,15 +300,15 @@ class ReaderTagsFeedViewModel @Inject constructor(

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
}
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 {
postLikeUseCase.perform(it, !it.isLikedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED).collect {
findPost(postItem.postId, postItem.blogId)?.let { post ->
postLikeUseCase.perform(post, !post.isLikedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED).collect {
when (it) {
is PostLikeUseCase.PostLikeState.Success -> {
// Re-enable like button without changing the current post item UI.
Expand Down Expand Up @@ -348,11 +362,18 @@ class ReaderTagsFeedViewModel @Inject constructor(

sealed class ActionEvent {
data class OpenTagPostsFeed(val readerTag: ReaderTag) : ActionEvent()

data object RefreshTagsFeed : ActionEvent()
}

sealed class UiState {
data object Initial : UiState()
data class Loaded(val data: List<TagFeedItem>) : UiState()

data class Loaded(
val data: List<TagFeedItem>,
val isRefreshing: Boolean = false,
val onRefresh: () -> Unit = {},
) : UiState()

data object Loading : UiState()

Expand Down
Expand Up @@ -23,7 +23,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.ContentAlpha
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
Expand Down Expand Up @@ -58,6 +62,8 @@ import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterChip
import org.wordpress.android.ui.utils.UiString
import org.wordpress.android.util.AppLog

private const val LOADING_POSTS_COUNT = 5

@Composable
fun ReaderTagsFeed(uiState: UiState) {
Box(
Expand All @@ -77,46 +83,66 @@ fun ReaderTagsFeed(uiState: UiState) {
}
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun Loaded(uiState: UiState.Loaded) {
LazyColumn(
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.isRefreshing,
onRefresh = {
uiState.onRefresh()
}
)

Box(
modifier = Modifier
.fillMaxSize(),
.fillMaxSize()
.pullRefresh(state = pullRefreshState),
) {
items(
items = uiState.data,
) { item ->
val tagChip = item.tagChip
val postList = item.postList
LazyColumn(
modifier = Modifier
.fillMaxSize(),
) {
items(
items = uiState.data,
) { item ->
val tagChip = item.tagChip
val postList = item.postList

LaunchedEffect(Unit) {
item.onEnteredView()
}
LaunchedEffect(item.postList) {
item.onEnteredView()
}

val backgroundColor = if (isSystemInDarkTheme()) {
AppColor.White.copy(alpha = 0.12F)
} else {
AppColor.Black.copy(alpha = 0.08F)
}
Spacer(modifier = Modifier.height(Margin.Large.value))
// Tag chip UI
ReaderFilterChip(
modifier = Modifier.padding(
start = Margin.Large.value,
),
text = UiString.UiStringText(tagChip.tag.tagTitle),
onClick = { tagChip.onTagClick(tagChip.tag) },
height = 36.dp,
)
Spacer(modifier = Modifier.height(Margin.Large.value))
// Posts list UI
when (postList) {
is PostList.Initial, is PostList.Loading -> PostListLoading()
is PostList.Loaded -> PostListLoaded(postList, tagChip, backgroundColor)
is PostList.Error -> PostListError(backgroundColor, tagChip, postList)
val backgroundColor = if (isSystemInDarkTheme()) {
AppColor.White.copy(alpha = 0.12F)
} else {
AppColor.Black.copy(alpha = 0.08F)
}
Spacer(modifier = Modifier.height(Margin.Large.value))
// Tag chip UI
ReaderFilterChip(
modifier = Modifier.padding(
start = Margin.Large.value,
),
text = UiString.UiStringText(tagChip.tag.tagTitle),
onClick = { tagChip.onTagClick(tagChip.tag) },
height = 36.dp,
)
Spacer(modifier = Modifier.height(Margin.Large.value))
// Posts list UI
when (postList) {
is PostList.Initial, is PostList.Loading -> PostListLoading()
is PostList.Loaded -> PostListLoaded(postList, tagChip, backgroundColor)
is PostList.Error -> PostListError(backgroundColor, tagChip, postList)
}
Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value))
}
Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value))
}

PullRefreshIndicator(
refreshing = uiState.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}

Expand Down Expand Up @@ -148,14 +174,12 @@ private fun Loading() {
LazyRow(
modifier = Modifier
.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 12.dp),
userScrollEnabled = false,
horizontalArrangement = Arrangement.spacedBy(Margin.Large.value),
contentPadding = PaddingValues(horizontal = Margin.Large.value),
) {
item {
items(LOADING_POSTS_COUNT) {
ReaderTagsFeedPostListItemLoading()
Spacer(Modifier.width(12.dp))
ReaderTagsFeedPostListItemLoading()
Spacer(Modifier.width(12.dp))
}
}
}
Expand Down Expand Up @@ -240,16 +264,14 @@ private fun PostListLoading() {
modifier = Modifier
.fillMaxWidth(),
userScrollEnabled = false,
horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value),
contentPadding = PaddingValues(
start = Margin.Large.value,
end = Margin.Large.value
),
) {
item {
ReaderTagsFeedPostListItemLoading()
Spacer(Modifier.width(Margin.ExtraMediumLarge.value))
items(LOADING_POSTS_COUNT) {
ReaderTagsFeedPostListItemLoading()
Spacer(Modifier.width(Margin.ExtraMediumLarge.value))
}
}
}
Expand All @@ -272,9 +294,9 @@ private fun PostListLoaded(
items(
items = postList.items,
) { postItem ->
ReaderTagsFeedPostListItem(
item = postItem
)
ReaderTagsFeedPostListItem(
item = postItem
)
}
item {
val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black
Expand Down

0 comments on commit 78f1e2f

Please sign in to comment.