Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tags IA] Fetch posts for multiple tags #20684

Merged
merged 21 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import org.wordpress.android.datasets.ReaderPostTable
import org.wordpress.android.models.ReaderPost
import org.wordpress.android.models.ReaderPostList
import org.wordpress.android.models.ReaderTag
import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResult
import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId
import javax.inject.Inject

@Reusable
Expand All @@ -30,6 +32,23 @@ class ReaderPostTableWrapper @Inject constructor() {

fun getNumPostsWithTag(readerTag: ReaderTag): Int = ReaderPostTable.getNumPostsWithTag(readerTag)

fun addOrUpdatePosts(readerTag: ReaderTag, posts: ReaderPostList) =
fun addOrUpdatePosts(readerTag: ReaderTag?, posts: ReaderPostList) =
ReaderPostTable.addOrUpdatePosts(readerTag, posts)

fun deletePostsWithTag(tag: ReaderTag) = ReaderPostTable.deletePostsWithTag(tag)

fun comparePosts(posts: ReaderPostList): UpdateResult = ReaderPostTable.comparePosts(posts)

fun updateBookmarkedPostPseudoId(posts: ReaderPostList) = ReaderPostTable.updateBookmarkedPostPseudoId(posts)

fun setGapMarkerForTag(blogId: Long, postId: Long, tag: ReaderTag) =
ReaderPostTable.setGapMarkerForTag(blogId, postId, tag)

fun removeGapMarkerForTag(tag: ReaderTag) = ReaderPostTable.removeGapMarkerForTag(tag)

fun deletePostsBeforeGapMarkerForTag(tag: ReaderTag) = ReaderPostTable.deletePostsBeforeGapMarkerForTag(tag)

fun hasOverlap(posts: ReaderPostList?, tag: ReaderTag): Boolean = ReaderPostTable.hasOverlap(posts, tag)

fun getGapMarkerIdsForTag(tag: ReaderTag): ReaderBlogIdPostId? = ReaderPostTable.getGapMarkerIdsForTag(tag)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.wordpress.android.ui.bloggingprompts

import org.wordpress.android.models.ReaderTag
import org.wordpress.android.models.ReaderTagType
import org.wordpress.android.ui.reader.services.post.ReaderPostLogic
import org.wordpress.android.ui.reader.repository.ReaderPostRepository
import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper
import javax.inject.Inject

Expand All @@ -23,7 +23,7 @@ class BloggingPromptsPostTagProvider @Inject constructor(
promptIdTag,
promptIdTag,
promptIdTag,
ReaderPostLogic.formatFullEndpointForTag(promptIdTag),
ReaderPostRepository.formatFullEndpointForTag(promptIdTag),
ReaderTagType.FOLLOWED,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,10 @@ class AppPrefsWrapper @Inject constructor() {
fun getShouldHideDynamicCard(id: String, ): Boolean =
AppPrefs.getShouldHideDynamicCard(id)

fun shouldUpdateBookmarkPostsPseudoIds(tag: ReaderTag?): Boolean = AppPrefs.shouldUpdateBookmarkPostsPseudoIds(tag)

fun setBookmarkPostsPseudoIdsUpdated() = AppPrefs.setBookmarkPostsPseudoIdsUpdated()

fun getAllPrefs(): Map<String, Any?> = AppPrefs.getAllPrefs()

fun setString(prefKey: PrefKey, value: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,15 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView
}

childFragmentManager.beginTransaction().apply {
val fragment = if (uiState.selectedReaderTag.isDiscover) {
ReaderDiscoverFragment()
} else {
ReaderPostListFragment.newInstanceForTag(
uiState.selectedReaderTag,
val selectedTag = uiState.selectedReaderTag
val fragment = when {
selectedTag.isDiscover -> ReaderDiscoverFragment()
selectedTag.isTags -> ReaderTagsFeedFragment.newInstance(selectedTag)
else -> ReaderPostListFragment.newInstanceForTag(
selectedTag,
ReaderTypes.ReaderPostListType.TAG_FOLLOWED,
true,
uiState.selectedReaderTag.isFilterable
selectedTag.isFilterable
)
}
replace(R.id.container, fragment, uiState.selectedReaderTag.tagSlug)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package org.wordpress.android.ui.reader

import android.os.Bundle
import android.view.View
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.R
import org.wordpress.android.databinding.ReaderTagFeedFragmentLayoutBinding
import org.wordpress.android.models.ReaderTag
import org.wordpress.android.ui.ViewPagerFragment
import org.wordpress.android.ui.compose.theme.AppThemeWithoutBackground
import org.wordpress.android.ui.main.WPMainActivity
import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter
import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel
import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel.Companion.getViewModelKeyForTag
import org.wordpress.android.ui.reader.subfilter.SubfilterListItem
import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel
import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel
import org.wordpress.android.util.NetworkUtils
import org.wordpress.android.util.extensions.getSerializableCompat
import javax.inject.Inject

/**
* Initial implementation of ReaderTagFeedFragment with the idea of it containing both a ComposeView, which will host
thomashorta marked this conversation as resolved.
Show resolved Hide resolved
* all Compose content related to the new Tags Feed as well as an internal ReaderPostListFragment, which will be used
* to display "filtered" content based on the currently selected tag on the top app bar filter.
*
* It might be tricky to get this working properly since a lot of places expect the ReaderPostListFragment to be the
* main content of the ReaderFragment (e.g.: initializing the SubFilterViewModel), so a few changes might be needed.
*/
@AndroidEntryPoint
class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragment_layout),
WPMainActivity.OnScrollToTopListener {
private val tagsFeedTag by lazy {
// TODO maybe we can just create a static function somewhere that returns the Tags Feed ReaderTag, since it's
thomashorta marked this conversation as resolved.
Show resolved Hide resolved
// used in multiple places, client-side only, and always the same.
requireArguments().getSerializableCompat<ReaderTag>(ARG_TAGS_FEED_TAG)!!
}

@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var subFilterViewModel: SubFilterViewModel

private val viewModel: ReaderTagsFeedViewModel by viewModels()
private val readerViewModel: ReaderViewModel by viewModels(
ownerProducer = { requireParentFragment() }
thomashorta marked this conversation as resolved.
Show resolved Hide resolved
)

// binding
private lateinit var binding: ReaderTagFeedFragmentLayoutBinding

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = ReaderTagFeedFragmentLayoutBinding.bind(view)

binding.composeView.setContent {
AppThemeWithoutBackground {
val uiState by viewModel.uiStateFlow.collectAsState()
ReaderTagsFeedScreen(
uiState = uiState,
onRetryClicked = viewModel::fetchTag,
)
}
}

initViewModels(savedInstanceState)
}

private fun initViewModels(savedInstanceState: Bundle?) {
subFilterViewModel = ViewModelProvider(this, viewModelFactory).get(
getViewModelKeyForTag(tagsFeedTag),
SubFilterViewModel::class.java
)
subFilterViewModel.start(tagsFeedTag, tagsFeedTag, savedInstanceState)

subFilterViewModel.updateTagsAndSites.observe(viewLifecycleOwner) { event ->
event.applyIfNotHandled {
if (NetworkUtils.isNetworkAvailable(activity)) {
ReaderUpdateServiceStarter.startService(activity, this)
}
}
}

subFilterViewModel.subFilters.observe(viewLifecycleOwner) { subFilters ->
readerViewModel.showTopBarFilterGroup(
tagsFeedTag,
subFilters
)

val tags = subFilters.filterIsInstance<SubfilterListItem.Tag>().map { it.tag }
viewModel.fetchAll(tags)
}
subFilterViewModel.updateTagsAndSites()
}

override fun getScrollableViewForUniqueIdProvision(): View {
return binding.composeView
}

override fun onScrollToTop() {
// TODO scroll current content to top
}

companion object {
private const val ARG_TAGS_FEED_TAG = "tags_feed_tag"

fun newInstance(
feedTag: ReaderTag
): ReaderTagsFeedFragment = ReaderTagsFeedFragment().apply {
arguments = Bundle().apply {
putSerializable(ARG_TAGS_FEED_TAG, feedTag)
}
}
}
}

/**
* Throwaway UI code just for testing the initial Tags Feed fetching code.
* TODO remove this and replace with the final Compose content.
*/
@Composable
private fun ReaderTagsFeedScreen(
uiState: ReaderTagsFeedViewModel.UiState,
onRetryClicked: (ReaderTag) -> Unit,
) {
Column(
modifier = Modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
uiState.tagStates.forEach { (tag, fetchState) ->
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = tag.tagTitle,
style = MaterialTheme.typography.h4,
)

when (fetchState) {
is ReaderTagsFeedViewModel.FetchState.Loading -> {
Text(
text = "Loading...",
style = MaterialTheme.typography.body1,
)
}

is ReaderTagsFeedViewModel.FetchState.Error -> {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = "Error loading posts.",
style = MaterialTheme.typography.body1,
)

Text(
text = "Retry",
style = MaterialTheme.typography.body1,
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colors.primary,
modifier = Modifier
.padding(start = 8.dp)
.clickable { onRetryClicked(tag) },
)
}
}

is ReaderTagsFeedViewModel.FetchState.Success -> {
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
) {
fetchState.posts.forEach { post ->
Column(
modifier = Modifier
.width(300.dp)
.background(
MaterialTheme.colors.surface,
RoundedCornerShape(4.dp)
)
.padding(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = post.title,
style = MaterialTheme.typography.h5,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)

Text(
text = post.excerpt,
style = MaterialTheme.typography.body1,
maxLines = 4,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.wordpress.android.ui.reader.exceptions

class ReaderPostFetchException(
message: String = "Failed to fetch post(s).",
) : RuntimeException(message)
Loading
Loading