From 0c75a65a71697912a1e6ea769a928c0448042ad0 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:18:56 -0300 Subject: [PATCH 001/237] Add Tags item to dropdown menu --- .../android/ui/reader/utils/ReaderTopBarMenuHelper.kt | 10 ++++++++++ .../src/main/res/drawable/ic_reader_tags_24dp.xml | 9 +++++++++ WordPress/src/main/res/values/strings.xml | 1 + 3 files changed, 20 insertions(+) create mode 100644 WordPress/src/main/res/drawable/ic_reader_tags_24dp.xml diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt index 26e2f876a83c..d0e6aa33e981 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt @@ -37,6 +37,8 @@ class ReaderTopBarMenuHelper @Inject constructor() { text = readerTagsList[followedP2sIndex].tagTitle, )) } + // "Tags" should be the last item before "Lists", that's why the index determined by `size` + add(createTagsItem(getMenuItemIdFromReaderTagIndex(size))) readerTagsList .foldIndexed(SparseArrayCompat()) { index, sparseArray, readerTag -> if (readerTag.tagType == ReaderTagType.CUSTOM_LIST) { @@ -98,6 +100,14 @@ class ReaderTopBarMenuHelper @Inject constructor() { ) } + private fun createTagsItem(id: String): MenuElementData.Item.Single { + return MenuElementData.Item.Single( + id = id, + text = UiString.UiStringRes(R.string.reader_dropdown_menu_tags), + leadingIcon = R.drawable.ic_reader_tags_24dp, + ) + } + private fun createCustomListsItem(customLists: SparseArrayCompat): MenuElementData.Item.SubMenu { val customListsMenuItems = mutableListOf() customLists.forEach { index, readerTag -> diff --git a/WordPress/src/main/res/drawable/ic_reader_tags_24dp.xml b/WordPress/src/main/res/drawable/ic_reader_tags_24dp.xml new file mode 100644 index 000000000000..66e347a7ec51 --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_reader_tags_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index a98f8033b020..be60787b0c81 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1690,6 +1690,7 @@ Saved Liked Automattic + Tags Lists 0 Blogs 1 Blog From f824a0415380f2dd81ed3379bc73474c691ff0e5 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:33:07 -0300 Subject: [PATCH 002/237] Create local ReaderTagType called TAGS --- .../wordpress/android/models/ReaderTag.java | 4 ++++ .../android/models/ReaderTagType.java | 3 ++- .../reader/usecases/LoadReaderTabsUseCase.kt | 24 +++++++++++++++---- .../ui/reader/utils/ReaderTopBarMenuHelper.kt | 5 ++-- .../ui/reader/viewmodels/ReaderViewModel.kt | 2 +- WordPress/src/main/res/values/strings.xml | 1 + .../reader/viewmodels/ReaderViewModelTest.kt | 8 +++---- 7 files changed, 34 insertions(+), 13 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java index a8e8018e9814..dbd7b537e8cf 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java @@ -184,6 +184,10 @@ public boolean isBookmarked() { return tagType == ReaderTagType.BOOKMARKED; } + public boolean isTags() { + return tagType == ReaderTagType.TAGS; + } + public boolean isDiscover() { return tagType == ReaderTagType.DEFAULT && getEndpoint().endsWith(DISCOVER_PATH); } diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java index d7d84f191168..cb3486efc523 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java @@ -7,7 +7,8 @@ public enum ReaderTagType { CUSTOM_LIST, SEARCH, INTERESTS, - DISCOVER_POST_CARDS; + DISCOVER_POST_CARDS, + TAGS; private static final int INT_DEFAULT = 0; private static final int INT_FOLLOWED = 1; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/LoadReaderTabsUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/LoadReaderTabsUseCase.kt index e6d6aea13e5c..276fcb8fcb13 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/LoadReaderTabsUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/LoadReaderTabsUseCase.kt @@ -3,24 +3,29 @@ package org.wordpress.android.ui.reader.usecases import dagger.Reusable import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext +import org.wordpress.android.R import org.wordpress.android.datasets.ReaderTagTable +import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagList +import org.wordpress.android.models.ReaderTagType import org.wordpress.android.models.containsFollowingTag import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.reader.utils.ReaderUtils import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper +import org.wordpress.android.util.StringProvider import javax.inject.Inject import javax.inject.Named /** - * Loads list of tags that should be displayed as tabs in the entry-point Reader screen. + * Loads list of items that should be displayed in the Reader dropdown menu. */ @Reusable class LoadReaderTabsUseCase @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, - private val readerUtilsWrapper: ReaderUtilsWrapper + private val readerUtilsWrapper: ReaderUtilsWrapper, + private val stringProvider: StringProvider, ) { - suspend fun loadTabs(): ReaderTagList { + suspend fun load(): ReaderTagList { return withContext(bgDispatcher) { val tagList = ReaderTagTable.getDefaultTags() @@ -28,9 +33,18 @@ class LoadReaderTabsUseCase @Inject constructor( for users who created custom lists in the past.*/ tagList.addAll(ReaderTagTable.getCustomListTags()) - tagList.addAll(ReaderTagTable.getBookmarkTags()) // Add "Saved" tab manually + tagList.addAll(ReaderTagTable.getBookmarkTags()) // Add "Saved" item manually - // Add "Following" tab manually when on self-hosted site + // Add "Tags" item manually + tagList.add(ReaderTag( + "", + stringProvider.getString(R.string.reader_tags_display_name), + stringProvider.getString(R.string.reader_tags_display_name), + "", + ReaderTagType.TAGS + )) + + // Add "Subscriptions" item manually when on self-hosted site if (!tagList.containsFollowingTag()) { tagList.add(readerUtilsWrapper.getDefaultTagFromDbOrCreateInMemory()) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt index d0e6aa33e981..466c642f271f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt @@ -37,8 +37,9 @@ class ReaderTopBarMenuHelper @Inject constructor() { text = readerTagsList[followedP2sIndex].tagTitle, )) } - // "Tags" should be the last item before "Lists", that's why the index determined by `size` - add(createTagsItem(getMenuItemIdFromReaderTagIndex(size))) + readerTagsList.indexOrNull { it.isTags }?.let { tagsIndex -> + add(createTagsItem(getMenuItemIdFromReaderTagIndex(tagsIndex))) + } readerTagsList .foldIndexed(SparseArrayCompat()) { index, sparseArray, readerTag -> if (readerTag.tagType == ReaderTagType.CUSTOM_LIST) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 5806d169043a..a8552bb70ac5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -140,7 +140,7 @@ class ReaderViewModel @Inject constructor( @JvmOverloads fun loadTabs(savedInstanceState: Bundle? = null) { launch { - val tagList = loadReaderTabsUseCase.loadTabs() + val tagList = loadReaderTabsUseCase.load() if (tagList.isNotEmpty() && readerTagsList != tagList) { updateReaderTagsList(tagList) updateTopBarUiState(savedInstanceState) diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index be60787b0c81..6969e14b0513 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2105,6 +2105,7 @@ Discover Likes Subscribed + Tags now diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index 2f437f24465c..4838e1d3099f 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -156,7 +156,7 @@ class ReaderViewModelTest : BaseUnitTest() { viewModel.uiState.observeForever { state = it } - whenever(loadReaderTabsUseCase.loadTabs()).thenReturn(ReaderTagList()) + whenever(loadReaderTabsUseCase.load()).thenReturn(ReaderTagList()) // Act triggerContentDisplay() // Assert @@ -562,14 +562,14 @@ class ReaderViewModelTest : BaseUnitTest() { private fun testWithEmptyTags(block: suspend CoroutineScope.() -> T) { test { - whenever(loadReaderTabsUseCase.loadTabs()).thenReturn(emptyReaderTagList) + whenever(loadReaderTabsUseCase.load()).thenReturn(emptyReaderTagList) block() } } private fun testWithNonEmptyTags(block: suspend CoroutineScope.() -> T) { test { - whenever(loadReaderTabsUseCase.loadTabs()).thenReturn(nonEmptyReaderTagList) + whenever(loadReaderTabsUseCase.load()).thenReturn(nonEmptyReaderTagList) block() } } @@ -579,7 +579,7 @@ class ReaderViewModelTest : BaseUnitTest() { block: suspend CoroutineScope.() -> T ) { test { - whenever(loadReaderTabsUseCase.loadTabs()).thenReturn(readerTags) + whenever(loadReaderTabsUseCase.load()).thenReturn(readerTags) block() } } From 719602b50dae9e6e881c82b630a5a1c5de49fa28 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:36:45 -0300 Subject: [PATCH 003/237] Rename LoadReaderTabsUseCase to LoadReaderItemsUseCase --- ...derTabsUseCase.kt => LoadReaderItemsUseCase.kt} | 2 +- .../ui/reader/viewmodels/ReaderViewModel.kt | 6 +++--- .../ui/reader/viewmodels/ReaderViewModelTest.kt | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) rename WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/{LoadReaderTabsUseCase.kt => LoadReaderItemsUseCase.kt} (97%) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/LoadReaderTabsUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/LoadReaderItemsUseCase.kt similarity index 97% rename from WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/LoadReaderTabsUseCase.kt rename to WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/LoadReaderItemsUseCase.kt index 276fcb8fcb13..0194392aa29d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/LoadReaderTabsUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/LoadReaderItemsUseCase.kt @@ -20,7 +20,7 @@ import javax.inject.Named * Loads list of items that should be displayed in the Reader dropdown menu. */ @Reusable -class LoadReaderTabsUseCase @Inject constructor( +class LoadReaderItemsUseCase @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val readerUtilsWrapper: ReaderUtilsWrapper, private val stringProvider: StringProvider, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index a8552bb70ac5..71da3851da95 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -39,7 +39,7 @@ import org.wordpress.android.ui.reader.subfilter.SubfilterListItem import org.wordpress.android.ui.reader.tracker.ReaderTab import org.wordpress.android.ui.reader.tracker.ReaderTracker import org.wordpress.android.ui.reader.tracker.ReaderTrackerType.MAIN_READER -import org.wordpress.android.ui.reader.usecases.LoadReaderTabsUseCase +import org.wordpress.android.ui.reader.usecases.LoadReaderItemsUseCase import org.wordpress.android.ui.reader.utils.DateProvider import org.wordpress.android.ui.reader.utils.ReaderTopBarMenuHelper import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState @@ -69,7 +69,7 @@ class ReaderViewModel @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val appPrefsWrapper: AppPrefsWrapper, private val dateProvider: DateProvider, - private val loadReaderTabsUseCase: LoadReaderTabsUseCase, + private val loadReaderItemsUseCase: LoadReaderItemsUseCase, private val readerTracker: ReaderTracker, private val accountStore: AccountStore, private val quickStartRepository: QuickStartRepository, @@ -140,7 +140,7 @@ class ReaderViewModel @Inject constructor( @JvmOverloads fun loadTabs(savedInstanceState: Bundle? = null) { launch { - val tagList = loadReaderTabsUseCase.load() + val tagList = loadReaderItemsUseCase.load() if (tagList.isNotEmpty() && readerTagsList != tagList) { updateReaderTagsList(tagList) updateTopBarUiState(savedInstanceState) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index 4838e1d3099f..bc6e73735645 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -33,7 +33,7 @@ import org.wordpress.android.ui.quickstart.QuickStartEvent import org.wordpress.android.ui.quickstart.QuickStartType import org.wordpress.android.ui.reader.utils.ReaderTopBarMenuHelper import org.wordpress.android.ui.reader.tracker.ReaderTracker -import org.wordpress.android.ui.reader.usecases.LoadReaderTabsUseCase +import org.wordpress.android.ui.reader.usecases.LoadReaderItemsUseCase import org.wordpress.android.ui.reader.utils.DateProvider import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.QuickStartReaderPrompt import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState @@ -59,7 +59,7 @@ class ReaderViewModelTest : BaseUnitTest() { lateinit var dateProvider: DateProvider @Mock - lateinit var loadReaderTabsUseCase: LoadReaderTabsUseCase + lateinit var loadReaderItemsUseCase: LoadReaderItemsUseCase @Mock lateinit var readerTracker: ReaderTracker @@ -100,7 +100,7 @@ class ReaderViewModelTest : BaseUnitTest() { testDispatcher(), appPrefsWrapper, dateProvider, - loadReaderTabsUseCase, + loadReaderItemsUseCase, readerTracker, accountStore, quickStartRepository, @@ -156,7 +156,7 @@ class ReaderViewModelTest : BaseUnitTest() { viewModel.uiState.observeForever { state = it } - whenever(loadReaderTabsUseCase.load()).thenReturn(ReaderTagList()) + whenever(loadReaderItemsUseCase.load()).thenReturn(ReaderTagList()) // Act triggerContentDisplay() // Assert @@ -562,14 +562,14 @@ class ReaderViewModelTest : BaseUnitTest() { private fun testWithEmptyTags(block: suspend CoroutineScope.() -> T) { test { - whenever(loadReaderTabsUseCase.load()).thenReturn(emptyReaderTagList) + whenever(loadReaderItemsUseCase.load()).thenReturn(emptyReaderTagList) block() } } private fun testWithNonEmptyTags(block: suspend CoroutineScope.() -> T) { test { - whenever(loadReaderTabsUseCase.load()).thenReturn(nonEmptyReaderTagList) + whenever(loadReaderItemsUseCase.load()).thenReturn(nonEmptyReaderTagList) block() } } @@ -579,7 +579,7 @@ class ReaderViewModelTest : BaseUnitTest() { block: suspend CoroutineScope.() -> T ) { test { - whenever(loadReaderTabsUseCase.load()).thenReturn(readerTags) + whenever(loadReaderItemsUseCase.load()).thenReturn(readerTags) block() } } From 891ad4b9d59195f04f1e35bf5bf6be4177b4ee4c Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 3 Apr 2024 18:12:22 -0300 Subject: [PATCH 004/237] Create HorizontalPostListItem file --- .../compose/horizontalpostlist/HorizontalPostListItem.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt new file mode 100644 index 000000000000..893ec558eb52 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -0,0 +1,7 @@ +package org.wordpress.android.ui.reader.views.compose.horizontalpostlist + +import androidx.compose.runtime.Composable + +@Composable +fun HorizontalPostListItem() { +} From 54e5b11805ffcefdca33730a2773a7aca06a7acb Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:27:26 -0300 Subject: [PATCH 005/237] Implement ReaderTagsFeedFeatureConfig --- WordPress/build.gradle | 1 + .../util/config/ReaderTagsFeedFeatureConfig.kt | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/util/config/ReaderTagsFeedFeatureConfig.kt diff --git a/WordPress/build.gradle b/WordPress/build.gradle index e3f33b5c6cf4..8b9c6ad61d76 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -150,6 +150,7 @@ android { buildConfigField "boolean", "READER_DISCOVER_NEW_ENDPOINT", "false" buildConfigField "boolean", "READER_READING_PREFERENCES", "false" buildConfigField "boolean", "READER_READING_PREFERENCES_FEEDBACK", "false" + buildConfigField "boolean", "READER_TAGS_FEED", "false" // Override these constants in jetpack product flavor to enable/ disable features buildConfigField "boolean", "ENABLE_SITE_CREATION", "true" diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/ReaderTagsFeedFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderTagsFeedFeatureConfig.kt new file mode 100644 index 000000000000..a41e596b5b2f --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderTagsFeedFeatureConfig.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.Feature +import javax.inject.Inject + +private const val READER_TAGS_FEED_REMOTE_FIELD = "reader_tags_feed" + +@Feature(remoteField = READER_TAGS_FEED_REMOTE_FIELD, defaultValue = false) +class ReaderTagsFeedFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.READER_TAGS_FEED, + READER_TAGS_FEED_REMOTE_FIELD, +) From 97c74f8e2c28f576fbae4a4889f6a10e241901e1 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:48:30 -0300 Subject: [PATCH 006/237] Show Tags feed in Reader dropdown menu only if feature flag is enabled --- .../android/ui/reader/utils/ReaderTopBarMenuHelper.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt index 466c642f271f..ba94c014f5c5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelper.kt @@ -10,10 +10,13 @@ import org.wordpress.android.models.ReaderTagList import org.wordpress.android.models.ReaderTagType import org.wordpress.android.ui.compose.components.menu.dropdown.MenuElementData import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig import org.wordpress.android.util.extensions.indexOrNull import javax.inject.Inject -class ReaderTopBarMenuHelper @Inject constructor() { +class ReaderTopBarMenuHelper @Inject constructor( + private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig +) { fun createMenu(readerTagsList: ReaderTagList): List { return mutableListOf().apply { readerTagsList.indexOrNull { it.isDiscover }?.let { discoverIndex -> @@ -37,8 +40,10 @@ class ReaderTopBarMenuHelper @Inject constructor() { text = readerTagsList[followedP2sIndex].tagTitle, )) } - readerTagsList.indexOrNull { it.isTags }?.let { tagsIndex -> - add(createTagsItem(getMenuItemIdFromReaderTagIndex(tagsIndex))) + if (readerTagsFeedFeatureConfig.isEnabled()) { + readerTagsList.indexOrNull { it.isTags }?.let { tagsIndex -> + add(createTagsItem(getMenuItemIdFromReaderTagIndex(tagsIndex))) + } } readerTagsList .foldIndexed(SparseArrayCompat()) { index, sparseArray, readerTag -> From 8eabf34eaeea059d89f4be792936bbbb95285fa9 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 4 Apr 2024 18:36:37 -0300 Subject: [PATCH 007/237] Update unit tests --- .../utils/ReaderTopBarMenuHelperTest.kt | 73 ++++++++++++++++++- .../reader/viewmodels/ReaderViewModelTest.kt | 5 +- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelperTest.kt index 0279f25bee2d..03e57616424e 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderTopBarMenuHelperTest.kt @@ -4,6 +4,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import org.wordpress.android.R import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagList @@ -11,12 +12,18 @@ import org.wordpress.android.models.ReaderTagType import org.wordpress.android.ui.compose.components.menu.dropdown.MenuElementData import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig class ReaderTopBarMenuHelperTest { - val helper = ReaderTopBarMenuHelper() + private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig = mock() + val helper = ReaderTopBarMenuHelper( + readerTagsFeedFeatureConfig = readerTagsFeedFeatureConfig + ) @Test - fun `GIVEN all tags are available WHEN createMenu THEN all items are created correctly`() { + fun `GIVEN all tags are available and tags FF disabled WHEN createMenu THEN all items are created correctly`() { + whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(false) + val tags = ReaderTagList().apply { add(mockFollowingTag()) // item 0 add(mockDiscoverTag()) // item 1 @@ -65,6 +72,62 @@ class ReaderTopBarMenuHelperTest { assertThat(customList3Item.text).isEqualTo(UiStringText("custom-list-3")) } + @Test + fun `GIVEN all tags are available and tags FF enabled WHEN createMenu THEN all items are created correctly`() { + whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(true) + + val tags = ReaderTagList().apply { + add(mockFollowingTag()) // item 0 + add(mockDiscoverTag()) // item 1 + add(mockSavedTag()) // item 2 + add(mockLikedTag()) // item 3 + add(mockTagsTag()) // item 4 + add(mockA8CTag()) // item 5 + add(mockFollowedP2sTag()) // item 6 + add(createCustomListTag("custom-list-1")) // item 7 + add(createCustomListTag("custom-list-2")) // item 8 + add(createCustomListTag("custom-list-3")) // item 9 + } + + val menu = helper.createMenu(tags) + + // compare the menu items one by one to check their indices + val discoverItem = menu.findSingleItem { it.id == "1" }!! + assertThat(discoverItem.text).isEqualTo(UiStringRes(R.string.reader_dropdown_menu_discover)) + + val subscriptionsItem = menu.findSingleItem { it.id == "0" }!! + assertThat(subscriptionsItem.text).isEqualTo(UiStringRes(R.string.reader_dropdown_menu_subscriptions)) + + val savedItem = menu.findSingleItem { it.id == "2" }!! + assertThat(savedItem.text).isEqualTo(UiStringRes(R.string.reader_dropdown_menu_saved)) + + val likedItem = menu.findSingleItem { it.id == "3" }!! + assertThat(likedItem.text).isEqualTo(UiStringRes(R.string.reader_dropdown_menu_liked)) + + val tagsItem = menu.findSingleItem { it.id == "4" }!! + assertThat(tagsItem.text).isEqualTo(UiStringRes(R.string.reader_dropdown_menu_tags)) + + val a8cItem = menu.findSingleItem { it.id == "5" }!! + assertThat(a8cItem.text).isEqualTo(UiStringRes(R.string.reader_dropdown_menu_automattic)) + + val followedP2sItem = menu.findSingleItem { it.id == "6" }!! + assertThat(followedP2sItem.text).isEqualTo(UiStringText("Followed P2s")) + + assertThat(menu).contains(MenuElementData.Divider) + + val customListsItem = menu.findSubMenu()!! + assertThat(customListsItem.text).isEqualTo(UiStringRes(R.string.reader_dropdown_menu_lists)) + + val customList1Item = customListsItem.children.findSingleItem { it.id == "7" }!! + assertThat(customList1Item.text).isEqualTo(UiStringText("custom-list-1")) + + val customList2Item = customListsItem.children.findSingleItem { it.id == "8" }!! + assertThat(customList2Item.text).isEqualTo(UiStringText("custom-list-2")) + + val customList3Item = customListsItem.children.findSingleItem { it.id == "9" }!! + assertThat(customList3Item.text).isEqualTo(UiStringText("custom-list-3")) + } + @Test fun `GIVEN discover not present WHEN createMenu THEN discover menu item not created`() { val tags = ReaderTagList().apply { @@ -275,6 +338,12 @@ class ReaderTopBarMenuHelperTest { } } + private fun mockTagsTag(): ReaderTag { + return mock { + on { isTags } doReturn true + } + } + private fun createCustomListTag(title: String): ReaderTag { return ReaderTag( title, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index bc6e73735645..bb9c557587bd 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -42,6 +42,7 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.TopBarUiState import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.UrlUtilsWrapper +import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig import org.wordpress.android.viewmodel.Event import java.util.Date @@ -85,7 +86,9 @@ class ReaderViewModelTest : BaseUnitTest() { @Mock lateinit var jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil - private val readerTopBarMenuHelper: ReaderTopBarMenuHelper = ReaderTopBarMenuHelper() + private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig = mock() + + private val readerTopBarMenuHelper: ReaderTopBarMenuHelper = ReaderTopBarMenuHelper(readerTagsFeedFeatureConfig) private val emptyReaderTagList = ReaderTagList() From 08d851f75f4a387732cf202e37127265fdcdb428 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 4 Apr 2024 18:58:11 -0300 Subject: [PATCH 008/237] Display tags chip in subscriptions feed based on tags feed feature flag --- .../android/ui/reader/ReaderFragment.kt | 1 + .../ui/reader/viewmodels/ReaderViewModel.kt | 4 +++ .../reader/views/compose/ReaderTopAppBar.kt | 6 +++++ .../compose/filter/ReaderFilterChipGroup.kt | 26 +++++++++++-------- .../reader/viewmodels/ReaderViewModelTest.kt | 3 ++- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 23e1b6835c44..f8e1861e7984 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -137,6 +137,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView onClearFilterClick = ::clearFilter, isSearchVisible = state.isSearchActionVisible, onSearchClick = viewModel::onSearchActionClicked, + showTagsChip = state.showTagsChip, ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 71da3851da95..bd144a85a2f0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -52,6 +52,7 @@ import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.util.QuickStartUtils import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.UrlUtilsWrapper +import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig import org.wordpress.android.util.distinct import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel @@ -79,6 +80,7 @@ class ReaderViewModel @Inject constructor( private val jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil, private val readerTopBarMenuHelper: ReaderTopBarMenuHelper, private val urlUtilsWrapper: UrlUtilsWrapper, + private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig, // todo: annnmarie removed this private val getFollowedTagsUseCase: GetFollowedTagsUseCase ) : ScopedViewModel(mainDispatcher) { private var initialized: Boolean = false @@ -372,6 +374,7 @@ class ReaderViewModel @Inject constructor( selectedItem = selectedItem, filterUiState = filterUiState, onDropdownMenuClick = ::onDropdownMenuClick, + showTagsChip = !readerTagsFeedFeatureConfig.isEnabled(), isSearchActionVisible = isSearchSupported(), ) ) @@ -523,6 +526,7 @@ class ReaderViewModel @Inject constructor( val selectedItem: MenuElementData.Item.Single, val filterUiState: FilterUiState? = null, val onDropdownMenuClick: () -> Unit, + val showTagsChip: Boolean, val isSearchActionVisible: Boolean = false, ) { @Parcelize diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderTopAppBar.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderTopAppBar.kt index 6578d1ba6b5d..b937c01f0f77 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderTopAppBar.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderTopAppBar.kt @@ -56,6 +56,7 @@ fun ReaderTopAppBar( onClearFilterClick: () -> Unit, isSearchVisible: Boolean, onSearchClick: () -> Unit = {}, + showTagsChip: Boolean, ) { var selectedItem by remember { mutableStateOf(topBarUiState.selectedItem) } var isFilterShown by remember { mutableStateOf(topBarUiState.filterUiState != null) } @@ -120,6 +121,7 @@ fun ReaderTopAppBar( modifier = Modifier // use padding instead of Spacer for a nicer animation .padding(start = Margin.Medium.value), + showTagsChip = showTagsChip, ) } } @@ -149,6 +151,7 @@ private fun Filter( onFilterClick: (ReaderFilterType) -> Unit, onClearFilterClick: () -> Unit, modifier: Modifier = Modifier, + showTagsChip: Boolean, ) { ReaderFilterChipGroup( modifier = modifier, @@ -161,6 +164,7 @@ private fun Filter( onSelectedItemClick = { filterUiState.selectedItem?.type?.let(onFilterClick) }, onSelectedItemDismissClick = onClearFilterClick, chipHeight = chipHeight, + showTagsChip = showTagsChip, ) } @@ -211,6 +215,7 @@ fun ReaderTopAppBarPreview() { menuItems = menuItems, selectedItem = menuItems.first() as MenuElementData.Item.Single, onDropdownMenuClick = {}, + showTagsChip = true, ) ) } @@ -232,6 +237,7 @@ fun ReaderTopAppBarPreview() { onClearFilterClick = {}, isSearchVisible = true, onSearchClick = {}, + showTagsChip = true, ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt index 649c08fdf3e8..3625bdbc701f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt @@ -60,6 +60,7 @@ fun ReaderFilterChipGroup( showBlogsFilter: Boolean = blogsFilterCount > 0, showTagsFilter: Boolean = tagsFilterCount > 0, chipHeight: Dp = 36.dp, + showTagsChip: Boolean, ) { Row( modifier = modifier, @@ -97,17 +98,19 @@ fun ReaderFilterChipGroup( } // tags filter chip - AnimatedVisibility( - modifier = Modifier.clip(roundedShape), - visible = isTagChipVisible, - ) { - ReaderFilterChip( - text = tagChipText, - onClick = if (isTagSelected) onSelectedItemClick else ({ onFilterClick(ReaderFilterType.TAG) }), - onDismissClick = if (isTagSelected) onSelectedItemDismissClick else null, - isSelectedItem = isTagSelected, - height = chipHeight, - ) + if (showTagsChip) { + AnimatedVisibility( + modifier = Modifier.clip(roundedShape), + visible = isTagChipVisible, + ) { + ReaderFilterChip( + text = tagChipText, + onClick = if (isTagSelected) onSelectedItemClick else ({ onFilterClick(ReaderFilterType.TAG) }), + onDismissClick = if (isTagSelected) onSelectedItemDismissClick else null, + isSelectedItem = isTagSelected, + height = chipHeight, + ) + } } AnimatedVisibility(visible = isBlogChipVisible && isTagChipVisible) { @@ -242,6 +245,7 @@ fun ReaderFilterChipGroupPreview() { onSelectedItemDismissClick = { selectedItem = null }, + showTagsChip = true, ) } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index bb9c557587bd..ccfb6f7479a3 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -112,7 +112,8 @@ class ReaderViewModelTest : BaseUnitTest() { snackbarSequencer, jetpackFeatureRemovalOverlayUtil, readerTopBarMenuHelper, - urlUtilsWrapper + urlUtilsWrapper, + readerTagsFeedFeatureConfig, ) whenever(dateProvider.getCurrentDate()).thenReturn(Date(DUMMY_CURRENT_TIME)) From 3fd813473ecddc444e2f78cd0a4b794adcf3fb55 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:31:09 -0300 Subject: [PATCH 009/237] Fix detekt --- .../ui/reader/views/compose/filter/ReaderFilterChipGroup.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt index 3625bdbc701f..7e823d2dfe4b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt @@ -48,6 +48,7 @@ import androidx.compose.material3.MaterialTheme as Material3Theme private val roundedShape = RoundedCornerShape(100) +@Suppress("CyclomaticComplexMethod") @Composable fun ReaderFilterChipGroup( blogsFilterCount: Int, From 522cc73cd508e0160e555258f3acd8f80e35aebb Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:41:28 -0300 Subject: [PATCH 010/237] WIP HorizontalPostListItem --- .../HorizontalPostListItem.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 893ec558eb52..8d0909da89f3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -1,7 +1,34 @@ package org.wordpress.android.ui.reader.views.compose.horizontalpostlist +import android.content.res.Configuration +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.wordpress.android.ui.compose.theme.AppTheme @Composable fun HorizontalPostListItem() { + Column(modifier = Modifier.width(240.dp)) { + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun HorizontalPostListItemPreview() { + AppTheme { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + HorizontalPostListItem() + } + } } From 4706ab0dc7a6f2174295470fdf08f9a77b17de4a Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 8 Apr 2024 18:13:34 -0300 Subject: [PATCH 011/237] Revert "Merge pull request #20600 from wordpress-mobile/issue/20594-hide-tags-chip-subscriptions-feed" This reverts commit 8ea1d0c5af118766711e66834bca14ca79a66e10, reversing changes made to 33927465b6f3df7a3fcea60a25408c2db1f66086. --- .../android/ui/reader/ReaderFragment.kt | 1 - .../ui/reader/viewmodels/ReaderViewModel.kt | 4 --- .../reader/views/compose/ReaderTopAppBar.kt | 6 ----- .../compose/filter/ReaderFilterChipGroup.kt | 27 ++++++++----------- .../reader/viewmodels/ReaderViewModelTest.kt | 3 +-- 5 files changed, 12 insertions(+), 29 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index f8e1861e7984..23e1b6835c44 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -137,7 +137,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView onClearFilterClick = ::clearFilter, isSearchVisible = state.isSearchActionVisible, onSearchClick = viewModel::onSearchActionClicked, - showTagsChip = state.showTagsChip, ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index bd144a85a2f0..71da3851da95 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -52,7 +52,6 @@ import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.util.QuickStartUtils import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.UrlUtilsWrapper -import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig import org.wordpress.android.util.distinct import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel @@ -80,7 +79,6 @@ class ReaderViewModel @Inject constructor( private val jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil, private val readerTopBarMenuHelper: ReaderTopBarMenuHelper, private val urlUtilsWrapper: UrlUtilsWrapper, - private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig, // todo: annnmarie removed this private val getFollowedTagsUseCase: GetFollowedTagsUseCase ) : ScopedViewModel(mainDispatcher) { private var initialized: Boolean = false @@ -374,7 +372,6 @@ class ReaderViewModel @Inject constructor( selectedItem = selectedItem, filterUiState = filterUiState, onDropdownMenuClick = ::onDropdownMenuClick, - showTagsChip = !readerTagsFeedFeatureConfig.isEnabled(), isSearchActionVisible = isSearchSupported(), ) ) @@ -526,7 +523,6 @@ class ReaderViewModel @Inject constructor( val selectedItem: MenuElementData.Item.Single, val filterUiState: FilterUiState? = null, val onDropdownMenuClick: () -> Unit, - val showTagsChip: Boolean, val isSearchActionVisible: Boolean = false, ) { @Parcelize diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderTopAppBar.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderTopAppBar.kt index b937c01f0f77..6578d1ba6b5d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderTopAppBar.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderTopAppBar.kt @@ -56,7 +56,6 @@ fun ReaderTopAppBar( onClearFilterClick: () -> Unit, isSearchVisible: Boolean, onSearchClick: () -> Unit = {}, - showTagsChip: Boolean, ) { var selectedItem by remember { mutableStateOf(topBarUiState.selectedItem) } var isFilterShown by remember { mutableStateOf(topBarUiState.filterUiState != null) } @@ -121,7 +120,6 @@ fun ReaderTopAppBar( modifier = Modifier // use padding instead of Spacer for a nicer animation .padding(start = Margin.Medium.value), - showTagsChip = showTagsChip, ) } } @@ -151,7 +149,6 @@ private fun Filter( onFilterClick: (ReaderFilterType) -> Unit, onClearFilterClick: () -> Unit, modifier: Modifier = Modifier, - showTagsChip: Boolean, ) { ReaderFilterChipGroup( modifier = modifier, @@ -164,7 +161,6 @@ private fun Filter( onSelectedItemClick = { filterUiState.selectedItem?.type?.let(onFilterClick) }, onSelectedItemDismissClick = onClearFilterClick, chipHeight = chipHeight, - showTagsChip = showTagsChip, ) } @@ -215,7 +211,6 @@ fun ReaderTopAppBarPreview() { menuItems = menuItems, selectedItem = menuItems.first() as MenuElementData.Item.Single, onDropdownMenuClick = {}, - showTagsChip = true, ) ) } @@ -237,7 +232,6 @@ fun ReaderTopAppBarPreview() { onClearFilterClick = {}, isSearchVisible = true, onSearchClick = {}, - showTagsChip = true, ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt index 7e823d2dfe4b..649c08fdf3e8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/filter/ReaderFilterChipGroup.kt @@ -48,7 +48,6 @@ import androidx.compose.material3.MaterialTheme as Material3Theme private val roundedShape = RoundedCornerShape(100) -@Suppress("CyclomaticComplexMethod") @Composable fun ReaderFilterChipGroup( blogsFilterCount: Int, @@ -61,7 +60,6 @@ fun ReaderFilterChipGroup( showBlogsFilter: Boolean = blogsFilterCount > 0, showTagsFilter: Boolean = tagsFilterCount > 0, chipHeight: Dp = 36.dp, - showTagsChip: Boolean, ) { Row( modifier = modifier, @@ -99,19 +97,17 @@ fun ReaderFilterChipGroup( } // tags filter chip - if (showTagsChip) { - AnimatedVisibility( - modifier = Modifier.clip(roundedShape), - visible = isTagChipVisible, - ) { - ReaderFilterChip( - text = tagChipText, - onClick = if (isTagSelected) onSelectedItemClick else ({ onFilterClick(ReaderFilterType.TAG) }), - onDismissClick = if (isTagSelected) onSelectedItemDismissClick else null, - isSelectedItem = isTagSelected, - height = chipHeight, - ) - } + AnimatedVisibility( + modifier = Modifier.clip(roundedShape), + visible = isTagChipVisible, + ) { + ReaderFilterChip( + text = tagChipText, + onClick = if (isTagSelected) onSelectedItemClick else ({ onFilterClick(ReaderFilterType.TAG) }), + onDismissClick = if (isTagSelected) onSelectedItemDismissClick else null, + isSelectedItem = isTagSelected, + height = chipHeight, + ) } AnimatedVisibility(visible = isBlogChipVisible && isTagChipVisible) { @@ -246,7 +242,6 @@ fun ReaderFilterChipGroupPreview() { onSelectedItemDismissClick = { selectedItem = null }, - showTagsChip = true, ) } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index ccfb6f7479a3..bb9c557587bd 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -112,8 +112,7 @@ class ReaderViewModelTest : BaseUnitTest() { snackbarSequencer, jetpackFeatureRemovalOverlayUtil, readerTopBarMenuHelper, - urlUtilsWrapper, - readerTagsFeedFeatureConfig, + urlUtilsWrapper ) whenever(dateProvider.getCurrentDate()).thenReturn(Date(DUMMY_CURRENT_TIME)) From 2cf30fe791b11a8056808f1d6d6bc13dd8ad8bbf Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 8 Apr 2024 18:32:24 -0300 Subject: [PATCH 012/237] Make Tags feed filterable --- .../src/main/java/org/wordpress/android/models/ReaderTag.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java index dbd7b537e8cf..4676fb991291 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java @@ -208,7 +208,7 @@ public boolean isA8C() { } public boolean isFilterable() { - return this.isFollowedSites() || this.isA8C() || this.isP2(); + return this.isFollowedSites() || this.isA8C() || this.isP2() || this.isTags(); } public boolean isListTopic() { From e1a74cf4a2ba113304e0a3f1ed58736c602f5f7a Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 8 Apr 2024 18:33:55 -0300 Subject: [PATCH 013/237] Show tags filter in tags feed and hide from subscriptions feed --- .../android/ui/reader/viewmodels/ReaderViewModel.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 71da3851da95..97106e3b3326 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -25,7 +25,6 @@ import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagList import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD -import org.wordpress.android.ui.Organization import org.wordpress.android.ui.compose.components.menu.dropdown.MenuElementData import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil import org.wordpress.android.ui.jetpackoverlay.JetpackOverlayConnectedFeature.READER @@ -52,6 +51,7 @@ import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.util.QuickStartUtils import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.UrlUtilsWrapper +import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig import org.wordpress.android.util.distinct import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel @@ -79,6 +79,7 @@ class ReaderViewModel @Inject constructor( private val jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil, private val readerTopBarMenuHelper: ReaderTopBarMenuHelper, private val urlUtilsWrapper: UrlUtilsWrapper, + private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig, // todo: annnmarie removed this private val getFollowedTagsUseCase: GetFollowedTagsUseCase ) : ScopedViewModel(mainDispatcher) { private var initialized: Boolean = false @@ -511,11 +512,14 @@ class ReaderViewModel @Inject constructor( } private fun shouldShowBlogsFilter(readerTag: ReaderTag): Boolean { - return readerTag.isFilterable + return readerTag.isFilterable && readerTag.isFollowedSites } private fun shouldShowTagsFilter(readerTag: ReaderTag): Boolean { - return readerTag.isFilterable && readerTag.organization == Organization.NO_ORGANIZATION + val showForFollowedSites = readerTag.isFollowedSites && !readerTagsFeedFeatureConfig.isEnabled() + val showForTags = readerTag.isTags + + return readerTag.isFilterable && (showForFollowedSites || showForTags) } data class TopBarUiState( From 26cbe022d6bbf00d3db6c83692086cc25809d034 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 8 Apr 2024 18:51:32 -0300 Subject: [PATCH 014/237] Fix unit tests --- .../ui/reader/viewmodels/ReaderViewModelTest.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index bb9c557587bd..02a0a53f8ed6 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -31,10 +31,10 @@ import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.quickstart.QuickStartEvent import org.wordpress.android.ui.quickstart.QuickStartType -import org.wordpress.android.ui.reader.utils.ReaderTopBarMenuHelper import org.wordpress.android.ui.reader.tracker.ReaderTracker import org.wordpress.android.ui.reader.usecases.LoadReaderItemsUseCase import org.wordpress.android.ui.reader.utils.DateProvider +import org.wordpress.android.ui.reader.utils.ReaderTopBarMenuHelper import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.QuickStartReaderPrompt import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState @@ -86,10 +86,8 @@ class ReaderViewModelTest : BaseUnitTest() { @Mock lateinit var jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil - private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig = mock() - - private val readerTopBarMenuHelper: ReaderTopBarMenuHelper = ReaderTopBarMenuHelper(readerTagsFeedFeatureConfig) - + @Mock + lateinit var readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig private val emptyReaderTagList = ReaderTagList() private val nonEmptyReaderTagList = createNonMockedNonEmptyReaderTagList() @@ -111,8 +109,9 @@ class ReaderViewModelTest : BaseUnitTest() { jetpackBrandingUtils, snackbarSequencer, jetpackFeatureRemovalOverlayUtil, - readerTopBarMenuHelper, - urlUtilsWrapper + ReaderTopBarMenuHelper(readerTagsFeedFeatureConfig), + urlUtilsWrapper, + readerTagsFeedFeatureConfig, ) whenever(dateProvider.getCurrentDate()).thenReturn(Date(DUMMY_CURRENT_TIME)) From ebe30f0ca9cf8c8c0d134148ee8a6b804c6abf6f Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:12:16 -0300 Subject: [PATCH 015/237] Implement HorizontalPostListItemSiteImage --- .../HorizontalPostListItemSiteImage.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemSiteImage.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemSiteImage.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemSiteImage.kt new file mode 100644 index 000000000000..31db0cb39e55 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemSiteImage.kt @@ -0,0 +1,57 @@ +package org.wordpress.android.ui.reader.views.compose.horizontalpostlist + +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.wordpress.android.R +import org.wordpress.android.ui.compose.theme.AppTheme + +@Composable +fun HorizontalPostListItemSiteImage( + imageUrl: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .error(R.drawable.bg_oval_placeholder_image_32dp) + .crossfade(true) + .build(), + contentDescription = null, + modifier = modifier + .size(20.dp) + .clip(CircleShape) + .clickable { onClick() } + ) +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun HorizontalPostListItemSiteImagePreview() { + AppTheme { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + HorizontalPostListItemSiteImage( + imageUrl = "https://picsum.photos/200/300", + onClick = {}, + ) + } + } +} From 903ca2ae2832b1edfab83df3e14b2a041ff8f6ec Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 9 Apr 2024 19:41:45 -0300 Subject: [PATCH 016/237] WIP Implement first row of HorizontalPostListItem --- .../HorizontalPostListItem.kt | 92 ++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 8d0909da89f3..03314c42c1e1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -3,18 +3,101 @@ package org.wordpress.android.ui.reader.views.compose.horizontalpostlist import android.content.res.Configuration import androidx.compose.foundation.layout.Box 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.size import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.wordpress.android.R +import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.unit.Margin @Composable -fun HorizontalPostListItem() { +fun HorizontalPostListItem( + postTitle: String, + postDateLine: String, + onPostSiteImageClick: () -> Unit, + onPostMoreMenuClick: () -> Unit, +) { Column(modifier = Modifier.width(240.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + val secondaryElementColor = MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.6F + ) + // Site image + HorizontalPostListItemSiteImage( + modifier = Modifier.padding( + horizontal = Margin.Small.value + ), + imageUrl = "", + onClick = { onPostSiteImageClick() }, + ) + // Site name + Text( + modifier = Modifier.weight(1F), + text = postTitle, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + color = AppColor.Black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + // "•" separator + Text( + modifier = Modifier.padding( + horizontal = Margin.Small.value + ), + text = "•", + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = secondaryElementColor, + ) + // Time since it was posted + Text( + text = postDateLine, + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = secondaryElementColor, + ) + // More menu ("...") + IconButton( + modifier = Modifier + .size(24.dp) + .padding( + horizontal = Margin.Small.value + ), + onClick = { + onPostMoreMenuClick() + }, + ) { + Icon( + painter = painterResource(R.drawable.ic_more_ellipsis_horizontal_squares), + contentDescription = stringResource( + R.string.show_more_desc + ), + tint = secondaryElementColor, + ) + } + } } } @@ -28,7 +111,12 @@ fun HorizontalPostListItemPreview() { .fillMaxWidth() .fillMaxHeight() ) { - HorizontalPostListItem() + HorizontalPostListItem( + postTitle = "This is a really long post title used for testing", + postDateLine = "1h", + onPostMoreMenuClick = {}, + onPostSiteImageClick = {}, + ) } } } From d64f07eb175114a7ab9c03d58d897f2e0987bf4a Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 9 Apr 2024 19:58:02 -0300 Subject: [PATCH 017/237] Implement post title --- .../HorizontalPostListItem.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 03314c42c1e1..9f9fa190ecd5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -30,8 +30,9 @@ import org.wordpress.android.ui.compose.unit.Margin @Composable fun HorizontalPostListItem( - postTitle: String, + siteName: String, postDateLine: String, + postTitle: String, onPostSiteImageClick: () -> Unit, onPostMoreMenuClick: () -> Unit, ) { @@ -46,7 +47,7 @@ fun HorizontalPostListItem( // Site image HorizontalPostListItemSiteImage( modifier = Modifier.padding( - horizontal = Margin.Small.value + end = Margin.Small.value ), imageUrl = "", onClick = { onPostSiteImageClick() }, @@ -54,7 +55,7 @@ fun HorizontalPostListItem( // Site name Text( modifier = Modifier.weight(1F), - text = postTitle, + text = siteName, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = AppColor.Black, @@ -98,6 +99,18 @@ fun HorizontalPostListItem( ) } } + // Post title + Text( + modifier = Modifier.padding(top = Margin.Medium.value), + text = postTitle, + fontSize = 20.sp, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = AppColor.Black, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + lineHeight = 25.sp, + ) } } @@ -112,8 +125,9 @@ fun HorizontalPostListItemPreview() { .fillMaxHeight() ) { HorizontalPostListItem( - postTitle = "This is a really long post title used for testing", + siteName = "This is a really long site name used for testing", postDateLine = "1h", + postTitle = "This is a really really really long post title used for testing", onPostMoreMenuClick = {}, onPostSiteImageClick = {}, ) From 1e488285715514f81588cb68bdea6721a5986997 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 9 Apr 2024 20:16:28 -0300 Subject: [PATCH 018/237] Add post excerpt, update styles --- .../HorizontalPostListItem.kt | 65 +++++++------------ 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 9f9fa190ecd5..6964c5a5b02c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -7,23 +7,16 @@ 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.size import androidx.compose.foundation.layout.width -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin @@ -33,9 +26,12 @@ fun HorizontalPostListItem( siteName: String, postDateLine: String, postTitle: String, + postExcerpt: String, onPostSiteImageClick: () -> Unit, - onPostMoreMenuClick: () -> Unit, ) { + val black87AlphaColor = AppColor.Black.copy( + alpha = 0.87F + ) Column(modifier = Modifier.width(240.dp)) { Row( modifier = Modifier.fillMaxWidth(), @@ -56,9 +52,8 @@ fun HorizontalPostListItem( Text( modifier = Modifier.weight(1F), text = siteName, - fontSize = 13.sp, - fontWeight = FontWeight.SemiBold, - color = AppColor.Black, + style = MaterialTheme.typography.labelLarge, + color = black87AlphaColor, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -68,48 +63,33 @@ fun HorizontalPostListItem( horizontal = Margin.Small.value ), text = "•", - fontSize = 13.sp, - fontWeight = FontWeight.Normal, + style = MaterialTheme.typography.bodyMedium, color = secondaryElementColor, ) // Time since it was posted Text( text = postDateLine, - fontSize = 13.sp, - fontWeight = FontWeight.Normal, + style = MaterialTheme.typography.bodyMedium, color = secondaryElementColor, ) - // More menu ("...") - IconButton( - modifier = Modifier - .size(24.dp) - .padding( - horizontal = Margin.Small.value - ), - onClick = { - onPostMoreMenuClick() - }, - ) { - Icon( - painter = painterResource(R.drawable.ic_more_ellipsis_horizontal_squares), - contentDescription = stringResource( - R.string.show_more_desc - ), - tint = secondaryElementColor, - ) - } } // Post title Text( modifier = Modifier.padding(top = Margin.Medium.value), text = postTitle, - fontSize = 20.sp, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleMedium, color = AppColor.Black, maxLines = 2, overflow = TextOverflow.Ellipsis, - lineHeight = 25.sp, + ) + // Post excerpt + Text( + modifier = Modifier.padding(top = Margin.Small.value), + text = postExcerpt, + style = MaterialTheme.typography.bodySmall, + color = black87AlphaColor, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) } } @@ -125,10 +105,13 @@ fun HorizontalPostListItemPreview() { .fillMaxHeight() ) { HorizontalPostListItem( - siteName = "This is a really long site name used for testing", + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + " urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", - postTitle = "This is a really really really long post title used for testing", - onPostMoreMenuClick = {}, + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien" + + " sed urna fermentum posuere. Vivamus in pretium nisl.", onPostSiteImageClick = {}, ) } From 2f5c8883b44c7ecc7d3e7e0f4f83085ad8a333ba Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 9 Apr 2024 20:42:07 -0300 Subject: [PATCH 019/237] Implement post image UI element --- .../HorizontalPostListItem.kt | 89 ++++++++++++++++--- .../HorizontalPostListItemSiteImage.kt | 57 ------------ 2 files changed, 76 insertions(+), 70 deletions(-) delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemSiteImage.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 6964c5a5b02c..7f2337634f33 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -1,22 +1,32 @@ package org.wordpress.android.ui.reader.views.compose.horizontalpostlist import android.content.res.Configuration +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box 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.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin @@ -24,12 +34,15 @@ import org.wordpress.android.ui.compose.unit.Margin @Composable fun HorizontalPostListItem( siteName: String, + siteImageUrl: String, postDateLine: String, postTitle: String, postExcerpt: String, - onPostSiteImageClick: () -> Unit, + postImageUrl: String, + onSiteImageClick: () -> Unit, + onPostImageClick: () -> Unit, ) { - val black87AlphaColor = AppColor.Black.copy( + val primaryElementColor = AppColor.Black.copy( alpha = 0.87F ) Column(modifier = Modifier.width(240.dp)) { @@ -37,26 +50,26 @@ fun HorizontalPostListItem( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - val secondaryElementColor = MaterialTheme.colorScheme.onSurface.copy( - alpha = 0.6F - ) // Site image - HorizontalPostListItemSiteImage( + SiteImage( modifier = Modifier.padding( - end = Margin.Small.value + end = Margin.Small.value, ), - imageUrl = "", - onClick = { onPostSiteImageClick() }, + imageUrl = siteImageUrl, + onClick = { onSiteImageClick() }, ) // Site name Text( modifier = Modifier.weight(1F), text = siteName, style = MaterialTheme.typography.labelLarge, - color = black87AlphaColor, + color = primaryElementColor, maxLines = 1, overflow = TextOverflow.Ellipsis, ) + val secondaryElementColor = MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.6F + ) // "•" separator Text( modifier = Modifier.padding( @@ -87,13 +100,60 @@ fun HorizontalPostListItem( modifier = Modifier.padding(top = Margin.Small.value), text = postExcerpt, style = MaterialTheme.typography.bodySmall, - color = black87AlphaColor, + color = primaryElementColor, maxLines = 2, overflow = TextOverflow.Ellipsis, ) + // Post image + PostImage( + imageUrl = postImageUrl, + onClick = onPostImageClick, + ) } } +@Composable +fun SiteImage( + imageUrl: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + AsyncImage( + modifier = modifier + .size(20.dp) + .clip(CircleShape) + .clickable { onClick() }, + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .error(R.drawable.bg_oval_placeholder_image_32dp) + .crossfade(true) + .build(), + contentDescription = null + ) +} + +@Composable +fun PostImage( + imageUrl: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + AsyncImage( + modifier = modifier + .width(240.dp) + .height(150.dp) + .clip(RoundedCornerShape(corner = CornerSize(8.dp))) + .clickable { onClick() }, + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + // TODO RenanLukas: placeholder + // .error(R.drawable.bg_oval_placeholder_image_32dp) + .crossfade(true) + .build(), + contentDescription = null + ) +} + @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -107,12 +167,15 @@ fun HorizontalPostListItemPreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + " urna fermentum posuere. Vivamus in pretium nisl.", + siteImageUrl = "", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + "sed urna fermentum posuere. Vivamus in pretium nisl.", postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien" + " sed urna fermentum posuere. Vivamus in pretium nisl.", - onPostSiteImageClick = {}, + postImageUrl = "", + onSiteImageClick = {}, + onPostImageClick = {}, ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemSiteImage.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemSiteImage.kt deleted file mode 100644 index 31db0cb39e55..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemSiteImage.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.wordpress.android.ui.reader.views.compose.horizontalpostlist - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.request.ImageRequest -import org.wordpress.android.R -import org.wordpress.android.ui.compose.theme.AppTheme - -@Composable -fun HorizontalPostListItemSiteImage( - imageUrl: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) - .error(R.drawable.bg_oval_placeholder_image_32dp) - .crossfade(true) - .build(), - contentDescription = null, - modifier = modifier - .size(20.dp) - .clip(CircleShape) - .clickable { onClick() } - ) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun HorizontalPostListItemSiteImagePreview() { - AppTheme { - Box( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - ) { - HorizontalPostListItemSiteImage( - imageUrl = "https://picsum.photos/200/300", - onClick = {}, - ) - } - } -} From efae10d7df67cd39db0622747b5ec90546df196c Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 9 Apr 2024 20:44:03 -0300 Subject: [PATCH 020/237] Fix post excerpt margin --- .../compose/horizontalpostlist/HorizontalPostListItem.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 7f2337634f33..df0c2e0edd96 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -97,7 +97,10 @@ fun HorizontalPostListItem( ) // Post excerpt Text( - modifier = Modifier.padding(top = Margin.Small.value), + modifier = Modifier.padding( + top = Margin.Small.value, + bottom = Margin.Small.value, + ), text = postExcerpt, style = MaterialTheme.typography.bodySmall, color = primaryElementColor, From 1aee1d5b9f8696a970787e05410f37cbb49dadc5 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 9 Apr 2024 20:54:17 -0300 Subject: [PATCH 021/237] Implement number of likes and comments row --- .../HorizontalPostListItem.kt | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index df0c2e0edd96..ab3666ba5b86 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -39,12 +39,17 @@ fun HorizontalPostListItem( postTitle: String, postExcerpt: String, postImageUrl: String, + postNumberOfLikesText: String, + postNumberOfCommentsText: String, onSiteImageClick: () -> Unit, onPostImageClick: () -> Unit, ) { val primaryElementColor = AppColor.Black.copy( alpha = 0.87F ) + val secondaryElementColor = MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.6F + ) Column(modifier = Modifier.width(240.dp)) { Row( modifier = Modifier.fillMaxWidth(), @@ -67,9 +72,6 @@ fun HorizontalPostListItem( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - val secondaryElementColor = MaterialTheme.colorScheme.onSurface.copy( - alpha = 0.6F - ) // "•" separator Text( modifier = Modifier.padding( @@ -112,6 +114,32 @@ fun HorizontalPostListItem( imageUrl = postImageUrl, onClick = onPostImageClick, ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + // Number of likes + Text( + text = postNumberOfLikesText, + style = MaterialTheme.typography.bodyMedium, + color = secondaryElementColor, + ) + // "•" separator + Text( + modifier = Modifier.padding( + horizontal = Margin.Small.value + ), + text = "•", + style = MaterialTheme.typography.bodyMedium, + color = secondaryElementColor, + ) + // Number of comments + Text( + text = postNumberOfCommentsText, + style = MaterialTheme.typography.bodyMedium, + color = secondaryElementColor, + ) + } } } @@ -177,6 +205,8 @@ fun HorizontalPostListItemPreview() { postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien" + " sed urna fermentum posuere. Vivamus in pretium nisl.", postImageUrl = "", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", onSiteImageClick = {}, onPostImageClick = {}, ) From e3715a3d5377ff9c6d2dd211911d7ed8516bff75 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 9 Apr 2024 21:48:36 -0300 Subject: [PATCH 022/237] Implement like and more actions --- .../HorizontalPostListItem.kt | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index ab3666ba5b86..0e5c1c86b5b8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -4,7 +4,10 @@ import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -14,13 +17,19 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -43,6 +52,8 @@ fun HorizontalPostListItem( postNumberOfCommentsText: String, onSiteImageClick: () -> Unit, onPostImageClick: () -> Unit, + onPostLikeClick: () -> Unit, + onPostMoreMenuClick: () -> Unit, ) { val primaryElementColor = AppColor.Black.copy( alpha = 0.87F @@ -101,7 +112,7 @@ fun HorizontalPostListItem( Text( modifier = Modifier.padding( top = Margin.Small.value, - bottom = Margin.Small.value, + bottom = Margin.Medium.value, ), text = postExcerpt, style = MaterialTheme.typography.bodySmall, @@ -115,7 +126,9 @@ fun HorizontalPostListItem( onClick = onPostImageClick, ) Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(top = Margin.Medium.value), verticalAlignment = Alignment.CenterVertically, ) { // Number of likes @@ -140,6 +153,42 @@ fun HorizontalPostListItem( color = secondaryElementColor, ) } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + // Like action + TextButton( + modifier = Modifier.defaultMinSize(minHeight = 24.dp, minWidth = 24.dp), + contentPadding = PaddingValues(0.dp), + onClick = { onPostLikeClick() }, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_reader_liked_24dp), + contentDescription = null, + tint = secondaryElementColor, + ) + Text( + text = stringResource(R.string.reader_label_like), + color = secondaryElementColor, + ) + } + Spacer(Modifier.weight(1f)) + // More menu ("…") + IconButton( + modifier = Modifier.size(24.dp), + onClick = { + onPostMoreMenuClick() + }, + ) { + Icon( + painter = painterResource(R.drawable.ic_more_ellipsis_horizontal_squares), + contentDescription = stringResource(R.string.show_more_desc), + tint = secondaryElementColor, + ) + } + } } } @@ -209,6 +258,8 @@ fun HorizontalPostListItemPreview() { postNumberOfCommentsText = "4 comments", onSiteImageClick = {}, onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, ) } } From 5e50e18ad35d4bfece8ae2692818dc20112c7b7d Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 9 Apr 2024 22:03:05 -0300 Subject: [PATCH 023/237] Implement UI for post liked and not liked --- .../HorizontalPostListItem.kt | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 0e5c1c86b5b8..2a47f2e260ac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -50,6 +49,7 @@ fun HorizontalPostListItem( postImageUrl: String, postNumberOfLikesText: String, postNumberOfCommentsText: String, + isPostLiked: Boolean, onSiteImageClick: () -> Unit, onPostImageClick: () -> Unit, onPostLikeClick: () -> Unit, @@ -159,14 +159,26 @@ fun HorizontalPostListItem( ) { // Like action TextButton( - modifier = Modifier.defaultMinSize(minHeight = 24.dp, minWidth = 24.dp), + modifier = Modifier.defaultMinSize(minHeight = 24.dp), contentPadding = PaddingValues(0.dp), onClick = { onPostLikeClick() }, ) { Icon( modifier = Modifier.size(24.dp), - painter = painterResource(R.drawable.ic_reader_liked_24dp), - contentDescription = null, + painter = painterResource( + if (isPostLiked) { + R.drawable.ic_like_fill_new_24dp + } else { + R.drawable.ic_like_outline_new_24dp + } + ), + contentDescription = stringResource( + if (isPostLiked) { + R.string.mnu_comment_liked + } else { + R.string.reader_label_like + } + ), tint = secondaryElementColor, ) Text( @@ -256,6 +268,7 @@ fun HorizontalPostListItemPreview() { postImageUrl = "", postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", + isPostLiked = true, onSiteImageClick = {}, onPostImageClick = {}, onPostLikeClick = {}, From 56d609e4d0c6dbe4a14dc71c13353be28bb1bc2f Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 10 Apr 2024 16:32:54 -0300 Subject: [PATCH 024/237] Initial Fragment and ViewModel for ReaderTagsFeed This breaks the top bar filter behavior of the tags feed for now, since that requires the SubFilterViewModel to be properly initialized and communicate with the TopBar (via ReaderViewModel), which happens ONLY inside the ReaderPostListFragment for now. --- .../android/ui/reader/ReaderFragment.kt | 13 +++--- .../ui/reader/ReaderTagsFeedFragment.kt | 41 +++++++++++++++++++ .../viewmodels/ReaderTagsFeedViewModel.kt | 7 ++++ .../reader_tag_feed_fragment_layout.xml | 17 ++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt create mode 100644 WordPress/src/main/res/layout/reader_tag_feed_fragment_layout.xml diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 23e1b6835c44..50f5c0071217 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -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() + else -> ReaderPostListFragment.newInstanceForTag( + selectedTag, ReaderTypes.ReaderPostListType.TAG_FOLLOWED, true, - uiState.selectedReaderTag.isFilterable + selectedTag.isFilterable ) } replace(R.id.container, fragment, uiState.selectedReaderTag.tagSlug) 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 new file mode 100644 index 000000000000..c4ee6df6bb14 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt @@ -0,0 +1,41 @@ +package org.wordpress.android.ui.reader + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.R +import org.wordpress.android.databinding.ReaderTagFeedFragmentLayoutBinding +import org.wordpress.android.ui.ViewPagerFragment +import org.wordpress.android.ui.main.WPMainActivity +import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel + +/** + * Initial implementation of ReaderTagFeedFragment with the idea of it containing both a ComposeView, which will host + * 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 viewModel: ReaderTagsFeedViewModel by viewModels() + + // binding + private lateinit var binding: ReaderTagFeedFragmentLayoutBinding + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = ReaderTagFeedFragmentLayoutBinding.bind(view) + } + + override fun getScrollableViewForUniqueIdProvision(): View { + return binding.composeView + } + + override fun onScrollToTop() { + // TODO scroll current content to top + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt new file mode 100644 index 000000000000..bc38dbbb331b --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -0,0 +1,7 @@ +package org.wordpress.android.ui.reader.viewmodels + +import androidx.lifecycle.ViewModel + +class ReaderTagsFeedViewModel : ViewModel() { + // TODO implement ViewModel +} diff --git a/WordPress/src/main/res/layout/reader_tag_feed_fragment_layout.xml b/WordPress/src/main/res/layout/reader_tag_feed_fragment_layout.xml new file mode 100644 index 000000000000..c2b216966121 --- /dev/null +++ b/WordPress/src/main/res/layout/reader_tag_feed_fragment_layout.xml @@ -0,0 +1,17 @@ + + + + + + + + From a59396a1642b544d8e0e921d403d8dffa7863ebe Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:24:33 -0300 Subject: [PATCH 025/237] Update HorizontalPostListItem layout with different design for posts without image --- .../HorizontalPostListItem.kt | 82 +++++++++++++++---- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 2a47f2e260ac..0252949adfc7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import org.wordpress.android.R +import org.wordpress.android.ui.compose.modifiers.conditionalThen import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin @@ -46,7 +47,7 @@ fun HorizontalPostListItem( postDateLine: String, postTitle: String, postExcerpt: String, - postImageUrl: String, + postImageUrl: String?, postNumberOfLikesText: String, postNumberOfCommentsText: String, isPostLiked: Boolean, @@ -61,7 +62,9 @@ fun HorizontalPostListItem( val secondaryElementColor = MaterialTheme.colorScheme.onSurface.copy( alpha = 0.6F ) - Column(modifier = Modifier.width(240.dp)) { + Column(modifier = Modifier + .width(240.dp) + .height(340.dp)) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -110,21 +113,29 @@ fun HorizontalPostListItem( ) // Post excerpt Text( - modifier = Modifier.padding( - top = Margin.Small.value, - bottom = Margin.Medium.value, - ), + modifier = Modifier + .padding( + top = Margin.Small.value, + bottom = Margin.Medium.value, + ) + .conditionalThen( + predicate = postImageUrl == null, + other = Modifier.height(180.dp) + ), text = postExcerpt, style = MaterialTheme.typography.bodySmall, color = primaryElementColor, - maxLines = 2, + maxLines = if (postImageUrl != null) 2 else Int.MAX_VALUE, overflow = TextOverflow.Ellipsis, ) // Post image - PostImage( - imageUrl = postImageUrl, - onClick = onPostImageClick, - ) + postImageUrl?.let { + PostImage( + imageUrl = it, + onClick = onPostImageClick, + ) + } + Spacer(Modifier.weight(1f)) Row( modifier = Modifier .fillMaxWidth() @@ -232,14 +243,11 @@ fun PostImage( ) { AsyncImage( modifier = modifier - .width(240.dp) .height(150.dp) .clip(RoundedCornerShape(corner = CornerSize(8.dp))) .clickable { onClick() }, model = ImageRequest.Builder(LocalContext.current) .data(imageUrl) - // TODO RenanLukas: placeholder - // .error(R.drawable.bg_oval_placeholder_image_32dp) .crossfade(true) .build(), contentDescription = null @@ -249,7 +257,7 @@ fun PostImage( @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun HorizontalPostListItemPreview() { +fun HorizontalPostListItemWithPostImagePreview() { AppTheme { Box( modifier = Modifier @@ -265,7 +273,49 @@ fun HorizontalPostListItemPreview() { "sed urna fermentum posuere. Vivamus in pretium nisl.", postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien" + " sed urna fermentum posuere. Vivamus in pretium nisl.", - postImageUrl = "", + postImageUrl = "postImageUrl", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + onSiteImageClick = {}, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun HorizontalPostListItemWithoutPostImagePreview() { + AppTheme { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + HorizontalPostListItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + " urna fermentum posuere. Vivamus in pretium nisl.", + siteImageUrl = "", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + " in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem " + + "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna " + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + + "pretium nisl.", + postImageUrl = null, postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, From 9d9f69e6cc7b16c07de154659a11b0e256c24e5d Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:14:47 -0300 Subject: [PATCH 026/237] Fix HorizontalPostListItem dark theme colors --- .../compose/horizontalpostlist/HorizontalPostListItem.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 0252949adfc7..5d307fa5b47e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -2,6 +2,7 @@ package org.wordpress.android.ui.reader.views.compose.horizontalpostlist import android.content.res.Configuration import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -56,10 +57,11 @@ fun HorizontalPostListItem( onPostLikeClick: () -> Unit, onPostMoreMenuClick: () -> Unit, ) { - val primaryElementColor = AppColor.Black.copy( + val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black + val primaryElementColor = baseColor.copy( alpha = 0.87F ) - val secondaryElementColor = MaterialTheme.colorScheme.onSurface.copy( + val secondaryElementColor = baseColor.copy( alpha = 0.6F ) Column(modifier = Modifier @@ -107,7 +109,7 @@ fun HorizontalPostListItem( modifier = Modifier.padding(top = Margin.Medium.value), text = postTitle, style = MaterialTheme.typography.titleMedium, - color = AppColor.Black, + color = baseColor, maxLines = 2, overflow = TextOverflow.Ellipsis, ) From a7d6970a53ccca1215bc30c1189cedce9feb9081 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 11 Apr 2024 18:30:47 -0300 Subject: [PATCH 027/237] Fix HorizontalPostListItem post image scale type and size --- .../compose/horizontalpostlist/HorizontalPostListItem.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 5d307fa5b47e..91c57c024fba 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -127,7 +128,7 @@ fun HorizontalPostListItem( text = postExcerpt, style = MaterialTheme.typography.bodySmall, color = primaryElementColor, - maxLines = if (postImageUrl != null) 2 else Int.MAX_VALUE, + maxLines = if (postImageUrl != null) 3 else Int.MAX_VALUE, overflow = TextOverflow.Ellipsis, ) // Post image @@ -245,6 +246,7 @@ fun PostImage( ) { AsyncImage( modifier = modifier + .fillMaxWidth() .height(150.dp) .clip(RoundedCornerShape(corner = CornerSize(8.dp))) .clickable { onClick() }, @@ -252,7 +254,8 @@ fun PostImage( .data(imageUrl) .crossfade(true) .build(), - contentDescription = null + contentDescription = null, + contentScale = ContentScale.Crop, ) } From 93412a46792e36f4fda6076569d67caeacea25a6 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 11 Apr 2024 19:09:54 -0300 Subject: [PATCH 028/237] Fix blog avatar scale type in HorizontalPostListItem --- .../HorizontalPostListItem.kt | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 91c57c024fba..72b2032db808 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -45,7 +45,7 @@ import org.wordpress.android.ui.compose.unit.Margin @Composable fun HorizontalPostListItem( siteName: String, - siteImageUrl: String, + blogAvatarUrl: String, postDateLine: String, postTitle: String, postExcerpt: String, @@ -53,7 +53,7 @@ fun HorizontalPostListItem( postNumberOfLikesText: String, postNumberOfCommentsText: String, isPostLiked: Boolean, - onSiteImageClick: () -> Unit, + onBlogAvatarClick: () -> Unit, onPostImageClick: () -> Unit, onPostLikeClick: () -> Unit, onPostMoreMenuClick: () -> Unit, @@ -72,13 +72,13 @@ fun HorizontalPostListItem( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - // Site image - SiteImage( + // Blog avatar + BlogAvatar( modifier = Modifier.padding( - end = Margin.Small.value, + end = Margin.Medium.value, ), - imageUrl = siteImageUrl, - onClick = { onSiteImageClick() }, + imageUrl = blogAvatarUrl, + onClick = { onBlogAvatarClick() }, ) // Site name Text( @@ -219,7 +219,7 @@ fun HorizontalPostListItem( } @Composable -fun SiteImage( +fun BlogAvatar( imageUrl: String, onClick: () -> Unit, modifier: Modifier = Modifier, @@ -234,7 +234,8 @@ fun SiteImage( .error(R.drawable.bg_oval_placeholder_image_32dp) .crossfade(true) .build(), - contentDescription = null + contentDescription = null, + contentScale = ContentScale.Crop, ) } @@ -272,7 +273,7 @@ fun HorizontalPostListItemWithPostImagePreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + " urna fermentum posuere. Vivamus in pretium nisl.", - siteImageUrl = "", + blogAvatarUrl = "", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + "sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -282,7 +283,7 @@ fun HorizontalPostListItemWithPostImagePreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onSiteImageClick = {}, + onBlogAvatarClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -304,7 +305,7 @@ fun HorizontalPostListItemWithoutPostImagePreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + " urna fermentum posuere. Vivamus in pretium nisl.", - siteImageUrl = "", + blogAvatarUrl = "", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + "sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -324,7 +325,7 @@ fun HorizontalPostListItemWithoutPostImagePreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onSiteImageClick = {}, + onBlogAvatarClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, From b7fa18cf8e6216fcb51e0e83edbde584a8c3fdbb Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:36:52 -0300 Subject: [PATCH 029/237] Fix like button height and update preview to display multiple scenarios --- .../HorizontalPostListItem.kt | 264 +++++++++++++----- 1 file changed, 200 insertions(+), 64 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 72b2032db808..aeec85b91492 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -3,6 +3,7 @@ package org.wordpress.android.ui.reader.views.compose.horizontalpostlist import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -128,13 +130,13 @@ fun HorizontalPostListItem( text = postExcerpt, style = MaterialTheme.typography.bodySmall, color = primaryElementColor, - maxLines = if (postImageUrl != null) 3 else Int.MAX_VALUE, + maxLines = if (!postImageUrl.isNullOrBlank()) 3 else Int.MAX_VALUE, overflow = TextOverflow.Ellipsis, ) // Post image - postImageUrl?.let { + if (!postImageUrl.isNullOrBlank()) { PostImage( - imageUrl = it, + imageUrl = postImageUrl, onClick = onPostImageClick, ) } @@ -168,7 +170,9 @@ fun HorizontalPostListItem( ) } Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .height(24.dp), verticalAlignment = Alignment.CenterVertically, ) { // Like action @@ -270,66 +274,198 @@ fun HorizontalPostListItemWithPostImagePreview() { .fillMaxWidth() .fillMaxHeight() ) { - HorizontalPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - " urna fermentum posuere. Vivamus in pretium nisl.", - blogAvatarUrl = "", - postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien" + - " sed urna fermentum posuere. Vivamus in pretium nisl.", - postImageUrl = "postImageUrl", - postNumberOfLikesText = "15 likes", - postNumberOfCommentsText = "4 comments", - isPostLiked = true, - onBlogAvatarClick = {}, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ) - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun HorizontalPostListItemWithoutPostImagePreview() { - AppTheme { - Box( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - ) { - HorizontalPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - " urna fermentum posuere. Vivamus in pretium nisl.", - blogAvatarUrl = "", - postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - " in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + - "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + - "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - " Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem " + - "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna " + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + - "pretium nisl.", - postImageUrl = null, - postNumberOfLikesText = "15 likes", - postNumberOfCommentsText = "4 comments", - isPostLiked = true, - onBlogAvatarClick = {}, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ) + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 16.dp), + contentPadding = PaddingValues(horizontal = 12.dp), + ) { + item { + HorizontalPostListItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + " urna fermentum posuere. Vivamus in pretium nisl.", + blogAvatarUrl = "https://picsum.photos/200/300", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + " in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem " + + "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna " + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + + "pretium nisl.", + postImageUrl = "https://picsum.photos/200/300", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + onBlogAvatarClick = {}, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) + Spacer(Modifier.width(12.dp)) + HorizontalPostListItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + " urna fermentum posuere. Vivamus in pretium nisl.", + blogAvatarUrl = "https://picsum.photos/200/300", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + " in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem " + + "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna " + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + + "pretium nisl.", + postImageUrl = null, + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + onBlogAvatarClick = {}, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) + Spacer(Modifier.width(12.dp)) + HorizontalPostListItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + " urna fermentum posuere. Vivamus in pretium nisl.", + blogAvatarUrl = "https://picsum.photos/200/300", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet.", + postExcerpt = "Lorem ipsum dolor sit amet.", + postImageUrl = "https://picsum.photos/200/300", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + onBlogAvatarClick = {}, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) + Spacer(Modifier.width(12.dp)) + HorizontalPostListItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + " urna fermentum posuere. Vivamus in pretium nisl.", + blogAvatarUrl = "https://picsum.photos/200/300", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet.", + postExcerpt = "Lorem ipsum dolor sit amet.", + postImageUrl = null, + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + onBlogAvatarClick = {}, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) + Spacer(Modifier.width(12.dp)) + HorizontalPostListItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + " urna fermentum posuere. Vivamus in pretium nisl.", + blogAvatarUrl = "https://picsum.photos/200/300", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet.", + postImageUrl = "https://picsum.photos/200/300", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + onBlogAvatarClick = {}, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) + Spacer(Modifier.width(12.dp)) + HorizontalPostListItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + " urna fermentum posuere. Vivamus in pretium nisl.", + blogAvatarUrl = "https://picsum.photos/200/300", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet.", + postImageUrl = null, + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + onBlogAvatarClick = {}, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) + Spacer(Modifier.width(12.dp)) + HorizontalPostListItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + " urna fermentum posuere. Vivamus in pretium nisl.", + blogAvatarUrl = "https://picsum.photos/200/300", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + " in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem " + + "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna " + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + + "pretium nisl.", + postImageUrl = "https://picsum.photos/200/300", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + onBlogAvatarClick = {}, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) + Spacer(Modifier.width(12.dp)) + HorizontalPostListItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + " urna fermentum posuere. Vivamus in pretium nisl.", + blogAvatarUrl = "https://picsum.photos/200/300", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + " in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem " + + "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna " + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + + "pretium nisl.", + postImageUrl = null, + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + onBlogAvatarClick = {}, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) + } + } } } } From b56006c33d32a1bd91579b488e9429e15aba033a Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:40:10 -0300 Subject: [PATCH 030/237] Rename HorizontalPostListItem preview --- .../views/compose/horizontalpostlist/HorizontalPostListItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index aeec85b91492..be004fced2fa 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -267,7 +267,7 @@ fun PostImage( @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun HorizontalPostListItemWithPostImagePreview() { +fun HorizontalPostListItemPreview() { AppTheme { Box( modifier = Modifier From 0e9b967798166472992f66ef2943e42df03a80b4 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:05:10 -0300 Subject: [PATCH 031/237] Fix detekt --- .../HorizontalPostListItem.kt | 143 +++++++++--------- 1 file changed, 73 insertions(+), 70 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index be004fced2fa..54f085f48ee1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -3,7 +3,6 @@ package org.wordpress.android.ui.reader.views.compose.horizontalpostlist import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -282,24 +281,25 @@ fun HorizontalPostListItemPreview() { ) { item { HorizontalPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - " urna fermentum posuere. Vivamus in pretium nisl.", + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + " pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + + "Lorem ipsum dolor sit amet consectetur adipiscing elit. Integer pellentesque sapien " + "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - " in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + - "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + - "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - " Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem " + - "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna " + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + - "pretium nisl.", + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + + " Vivamus in pretium nisl. Lorem ipsum dolor " + + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + " adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in" + + "pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem" + + "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + "urna fermentum posuere. Vivamus in pretium nisl.", postImageUrl = "https://picsum.photos/200/300", postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", @@ -311,22 +311,24 @@ fun HorizontalPostListItemPreview() { ) Spacer(Modifier.width(12.dp)) HorizontalPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - " urna fermentum posuere. Vivamus in pretium nisl.", + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - " in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + - "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + - "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - " Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem " + - "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna " + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem" + + "ipsum dolor sit amet, " + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + + "Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + "Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + + "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + "urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + + " consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + "pretium nisl.", postImageUrl = null, @@ -340,8 +342,8 @@ fun HorizontalPostListItemPreview() { ) Spacer(Modifier.width(12.dp)) HorizontalPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - " urna fermentum posuere. Vivamus in pretium nisl.", + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet.", @@ -357,8 +359,8 @@ fun HorizontalPostListItemPreview() { ) Spacer(Modifier.width(12.dp)) HorizontalPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - " urna fermentum posuere. Vivamus in pretium nisl.", + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet.", @@ -374,12 +376,12 @@ fun HorizontalPostListItemPreview() { ) Spacer(Modifier.width(12.dp)) HorizontalPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - " urna fermentum posuere. Vivamus in pretium nisl.", + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl.", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postExcerpt = "Lorem ipsum dolor sit amet.", postImageUrl = "https://picsum.photos/200/300", postNumberOfLikesText = "15 likes", @@ -392,12 +394,12 @@ fun HorizontalPostListItemPreview() { ) Spacer(Modifier.width(12.dp)) HorizontalPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - " urna fermentum posuere. Vivamus in pretium nisl.", + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl.", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postExcerpt = "Lorem ipsum dolor sit amet.", postImageUrl = null, postNumberOfLikesText = "15 likes", @@ -410,23 +412,24 @@ fun HorizontalPostListItemPreview() { ) Spacer(Modifier.width(12.dp)) HorizontalPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - " urna fermentum posuere. Vivamus in pretium nisl.", + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - " in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + - "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + - "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - " Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem " + - "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna " + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + - "pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit" + + "amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + + "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + "urna fermentum posuere. Vivamus in pretium nisl. Lorem " + + "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + "urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + + "posuere. Vivamus in pretium nisl.", postImageUrl = "https://picsum.photos/200/300", postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", @@ -438,23 +441,23 @@ fun HorizontalPostListItemPreview() { ) Spacer(Modifier.width(12.dp)) HorizontalPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - " urna fermentum posuere. Vivamus in pretium nisl.", + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - " in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + - "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + - "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - " Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem " + - "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna " + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + - "pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor" + + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + + "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postImageUrl = null, postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", From 65cee90f72389d573969ed1db2eec89e84cc54a9 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 12 Apr 2024 20:35:51 -0300 Subject: [PATCH 032/237] Implement HorizontalPostListItemLoading --- .../HorizontalPostListItemLoading.kt | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt new file mode 100644 index 000000000000..1d7875a4cd12 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt @@ -0,0 +1,119 @@ +package org.wordpress.android.ui.reader.views.compose.horizontalpostlist + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.wordpress.android.ui.compose.theme.AppColor +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.unit.Margin + +@Composable +fun HorizontalPostListItemLoading() { + val loadingColor = AppColor.Black.copy( + alpha = 0.08F + ) + Column( + modifier = Modifier + .width(240.dp) + .height(340.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(20.dp) + .aspectRatio(1f) + .background(loadingColor, shape = CircleShape), + ) + Box( + modifier = Modifier + .padding(start = Margin.Small.value) + .width(99.dp) + .height(8.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(loadingColor), + ) + } + Box( + modifier = Modifier + .padding(top = Margin.Large.value) + .width(204.dp) + .height(18.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(loadingColor), + ) + Box( + modifier = Modifier + .padding(top = Margin.Large.value) + .width(140.dp) + .height(18.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(loadingColor), + ) + Box( + modifier = Modifier + .padding(top = Margin.Large.value) + .fillMaxWidth() + .height(150.dp) + .clip(shape = RoundedCornerShape(8.dp)) + .background(loadingColor), + ) + Box( + modifier = Modifier + .padding( + start = Margin.Small.value, + top = Margin.Large.value, + ) + .width(170.dp) + .height(8.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(loadingColor), + ) + Box( + modifier = Modifier + .padding( + start = Margin.Small.value, + top = Margin.Large.value, + ) + .width(170.dp) + .height(8.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(loadingColor), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun HorizontalPostListItemLoadingPreview() { + AppTheme { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + HorizontalPostListItemLoading() + } + } +} From 1834b154dc7ba966ef552bee67e777819fbcf565 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 12 Apr 2024 21:10:24 -0300 Subject: [PATCH 033/237] Implement shimmer effect, update Compose preview --- .../ui/compose/components/shimmer/Shimmer.kt | 85 +++++++++++++++++++ .../compose/components/shimmer/ShimmerBox.kt | 26 ++++++ .../HorizontalPostListItemLoading.kt | 45 +++++++--- 3 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt new file mode 100644 index 000000000000..ab7bb410c9ca --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt @@ -0,0 +1,85 @@ +package org.wordpress.android.ui.compose.components.shimmer + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import org.wordpress.android.ui.compose.theme.AppColor + +/** + * Taken from https://github.com/canerkaseler/jetpack-compose-shimmer-loading-animation + */ +internal fun Modifier.shimmerLoadingAnimation( + isLoadingCompleted: Boolean = true, + isLightModeActive: Boolean = true, + widthOfShadowBrush: Int = 500, + angleOfAxisY: Float = 270f, + durationMillis: Int = 1000, +): Modifier { + if (isLoadingCompleted) { + return this + } else { + return composed { + val shimmerColors = ShimmerAnimationData(isLightMode = isLightModeActive).getColours() + + val transition = rememberInfiniteTransition(label = "") + + val translateAnimation = transition.animateFloat( + initialValue = 0f, + targetValue = (durationMillis + widthOfShadowBrush).toFloat(), + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = durationMillis, + easing = LinearEasing, + ), + repeatMode = RepeatMode.Restart, + ), + label = "Shimmer loading animation", + ) + + this.background( + brush = Brush.linearGradient( + colors = shimmerColors, + start = Offset(x = translateAnimation.value - widthOfShadowBrush, y = 0.0f), + end = Offset(x = translateAnimation.value, y = angleOfAxisY), + ), + ) + } + } +} + +internal data class ShimmerAnimationData( + private val isLightMode: Boolean +) { + fun getColours(): List { + return if (isLightMode) { + val color = AppColor.White + + listOf( + color.copy(alpha = 0.3f), + color.copy(alpha = 0.5f), + color.copy(alpha = 1.0f), + color.copy(alpha = 0.5f), + color.copy(alpha = 0.3f), + ) + } else { + val color = AppColor.Black + + listOf( + color.copy(alpha = 0.0f), + color.copy(alpha = 0.3f), + color.copy(alpha = 0.5f), + color.copy(alpha = 0.3f), + color.copy(alpha = 0.0f), + ) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt new file mode 100644 index 000000000000..4602d885a1c6 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt @@ -0,0 +1,26 @@ +package org.wordpress.android.ui.compose.components.shimmer + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun ShimmerBox( + modifier: Modifier = Modifier, + isLoadingCompleted: Boolean = true, + isLightModeActive: Boolean = true, + widthOfShadowBrush: Int = 500, + angleOfAxisY: Float = 270f, + durationMillis: Int = 1000, +) { + Box( + modifier = modifier + .shimmerLoadingAnimation( + isLoadingCompleted = isLoadingCompleted, + isLightModeActive = isLightModeActive, + widthOfShadowBrush = widthOfShadowBrush, + angleOfAxisY = angleOfAxisY, + durationMillis = durationMillis, + ) + ) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt index 1d7875a4cd12..9bd3cb933836 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt @@ -4,7 +4,9 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -12,8 +14,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -21,6 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import org.wordpress.android.ui.compose.components.shimmer.ShimmerBox import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin @@ -33,52 +36,57 @@ fun HorizontalPostListItemLoading() { Column( modifier = Modifier .width(240.dp) - .height(340.dp) + .height(340.dp), ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - Box( + ShimmerBox( modifier = Modifier .size(20.dp) .aspectRatio(1f) .background(loadingColor, shape = CircleShape), + isLoadingCompleted = false, ) - Box( + ShimmerBox( modifier = Modifier .padding(start = Margin.Small.value) .width(99.dp) .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)) .background(loadingColor), + isLoadingCompleted = false, ) } - Box( + ShimmerBox( modifier = Modifier .padding(top = Margin.Large.value) .width(204.dp) .height(18.dp) .clip(shape = RoundedCornerShape(16.dp)) .background(loadingColor), + isLoadingCompleted = false, ) - Box( + ShimmerBox( modifier = Modifier .padding(top = Margin.Large.value) .width(140.dp) .height(18.dp) .clip(shape = RoundedCornerShape(16.dp)) .background(loadingColor), + isLoadingCompleted = false, ) - Box( + ShimmerBox( modifier = Modifier .padding(top = Margin.Large.value) .fillMaxWidth() .height(150.dp) .clip(shape = RoundedCornerShape(8.dp)) .background(loadingColor), + isLoadingCompleted = false, ) - Box( + ShimmerBox( modifier = Modifier .padding( start = Margin.Small.value, @@ -88,8 +96,9 @@ fun HorizontalPostListItemLoading() { .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)) .background(loadingColor), + isLoadingCompleted = false, ) - Box( + ShimmerBox( modifier = Modifier .padding( start = Margin.Small.value, @@ -99,6 +108,7 @@ fun HorizontalPostListItemLoading() { .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)) .background(loadingColor), + isLoadingCompleted = false, ) } } @@ -113,7 +123,22 @@ fun HorizontalPostListItemLoadingPreview() { .fillMaxWidth() .fillMaxHeight() ) { - HorizontalPostListItemLoading() + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 16.dp), + contentPadding = PaddingValues(horizontal = 12.dp), + ) { + item { + HorizontalPostListItemLoading() + Spacer(Modifier.width(12.dp)) + HorizontalPostListItemLoading() + Spacer(Modifier.width(12.dp)) + HorizontalPostListItemLoading() + Spacer(Modifier.width(12.dp)) + HorizontalPostListItemLoading() + } + } } } } From 07194ea4e6c8401895371d2e0beca321c3130725 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:36:10 -0300 Subject: [PATCH 034/237] Remove blog avatar, fix excerpt max lines --- .../HorizontalPostListItem.kt | 50 +------------------ 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 54f085f48ee1..1fb1a0c3649f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -46,7 +45,6 @@ import org.wordpress.android.ui.compose.unit.Margin @Composable fun HorizontalPostListItem( siteName: String, - blogAvatarUrl: String, postDateLine: String, postTitle: String, postExcerpt: String, @@ -54,7 +52,6 @@ fun HorizontalPostListItem( postNumberOfLikesText: String, postNumberOfCommentsText: String, isPostLiked: Boolean, - onBlogAvatarClick: () -> Unit, onPostImageClick: () -> Unit, onPostLikeClick: () -> Unit, onPostMoreMenuClick: () -> Unit, @@ -73,14 +70,6 @@ fun HorizontalPostListItem( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - // Blog avatar - BlogAvatar( - modifier = Modifier.padding( - end = Margin.Medium.value, - ), - imageUrl = blogAvatarUrl, - onClick = { onBlogAvatarClick() }, - ) // Site name Text( modifier = Modifier.weight(1F), @@ -129,7 +118,7 @@ fun HorizontalPostListItem( text = postExcerpt, style = MaterialTheme.typography.bodySmall, color = primaryElementColor, - maxLines = if (!postImageUrl.isNullOrBlank()) 3 else Int.MAX_VALUE, + maxLines = if (!postImageUrl.isNullOrBlank()) 2 else Int.MAX_VALUE, overflow = TextOverflow.Ellipsis, ) // Post image @@ -221,27 +210,6 @@ fun HorizontalPostListItem( } } -@Composable -fun BlogAvatar( - imageUrl: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - AsyncImage( - modifier = modifier - .size(20.dp) - .clip(CircleShape) - .clickable { onClick() }, - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) - .error(R.drawable.bg_oval_placeholder_image_32dp) - .crossfade(true) - .build(), - contentDescription = null, - contentScale = ContentScale.Crop, - ) -} - @Composable fun PostImage( imageUrl: String, @@ -283,7 +251,6 @@ fun HorizontalPostListItemPreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + " pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -304,7 +271,6 @@ fun HorizontalPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onBlogAvatarClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -313,7 +279,6 @@ fun HorizontalPostListItemPreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -335,7 +300,6 @@ fun HorizontalPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onBlogAvatarClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -344,7 +308,6 @@ fun HorizontalPostListItemPreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet.", postExcerpt = "Lorem ipsum dolor sit amet.", @@ -352,7 +315,6 @@ fun HorizontalPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onBlogAvatarClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -361,7 +323,6 @@ fun HorizontalPostListItemPreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet.", postExcerpt = "Lorem ipsum dolor sit amet.", @@ -369,7 +330,6 @@ fun HorizontalPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onBlogAvatarClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -378,7 +338,6 @@ fun HorizontalPostListItemPreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -387,7 +346,6 @@ fun HorizontalPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onBlogAvatarClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -396,7 +354,6 @@ fun HorizontalPostListItemPreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -405,7 +362,6 @@ fun HorizontalPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onBlogAvatarClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -414,7 +370,6 @@ fun HorizontalPostListItemPreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet.", postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + @@ -434,7 +389,6 @@ fun HorizontalPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onBlogAvatarClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -443,7 +397,6 @@ fun HorizontalPostListItemPreview() { HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - blogAvatarUrl = "https://picsum.photos/200/300", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet.", postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + @@ -462,7 +415,6 @@ fun HorizontalPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onBlogAvatarClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, From b970e1dd1eee9584a374e63ea9853f28c9bf182a Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:23:44 -0300 Subject: [PATCH 035/237] Remove blog avatar from loading --- .../horizontalpostlist/HorizontalPostListItemLoading.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt index 9bd3cb933836..cb3f5677d2ac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt @@ -42,13 +42,6 @@ fun HorizontalPostListItemLoading() { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - ShimmerBox( - modifier = Modifier - .size(20.dp) - .aspectRatio(1f) - .background(loadingColor, shape = CircleShape), - isLoadingCompleted = false, - ) ShimmerBox( modifier = Modifier .padding(start = Margin.Small.value) From 4616094f52a2d3c13a066f1ac3e4c55abe63b70b Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 15 Apr 2024 18:41:15 -0300 Subject: [PATCH 036/237] Fix spacing between number of likes and like button in HorizontalPostListItem --- .../HorizontalPostListItem.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 1fb1a0c3649f..7fcc39add67f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -131,8 +130,7 @@ fun HorizontalPostListItem( Spacer(Modifier.weight(1f)) Row( modifier = Modifier - .fillMaxWidth() - .padding(top = Margin.Medium.value), + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { // Number of likes @@ -141,6 +139,7 @@ fun HorizontalPostListItem( style = MaterialTheme.typography.bodyMedium, color = secondaryElementColor, ) + Spacer(Modifier.height(Margin.Medium.value)) // "•" separator Text( modifier = Modifier.padding( @@ -157,6 +156,7 @@ fun HorizontalPostListItem( color = secondaryElementColor, ) } + Spacer(Modifier.height(Margin.Medium.value)) Row( modifier = Modifier .fillMaxWidth() @@ -165,7 +165,7 @@ fun HorizontalPostListItem( ) { // Like action TextButton( - modifier = Modifier.defaultMinSize(minHeight = 24.dp), + modifier = Modifier.height(24.dp), contentPadding = PaddingValues(0.dp), onClick = { onPostLikeClick() }, ) { @@ -240,12 +240,12 @@ fun HorizontalPostListItemPreview() { modifier = Modifier .fillMaxWidth() .fillMaxHeight() + .padding(top = 16.dp, bottom = 16.dp) ) { LazyRow( modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, bottom = 16.dp), - contentPadding = PaddingValues(horizontal = 12.dp), + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 24.dp), ) { item { HorizontalPostListItem( @@ -275,7 +275,7 @@ fun HorizontalPostListItemPreview() { onPostLikeClick = {}, onPostMoreMenuClick = {}, ) - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(24.dp)) HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -304,7 +304,7 @@ fun HorizontalPostListItemPreview() { onPostLikeClick = {}, onPostMoreMenuClick = {}, ) - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(24.dp)) HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -319,7 +319,7 @@ fun HorizontalPostListItemPreview() { onPostLikeClick = {}, onPostMoreMenuClick = {}, ) - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(24.dp)) HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -334,7 +334,7 @@ fun HorizontalPostListItemPreview() { onPostLikeClick = {}, onPostMoreMenuClick = {}, ) - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(24.dp)) HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -350,7 +350,7 @@ fun HorizontalPostListItemPreview() { onPostLikeClick = {}, onPostMoreMenuClick = {}, ) - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(24.dp)) HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -366,7 +366,7 @@ fun HorizontalPostListItemPreview() { onPostLikeClick = {}, onPostMoreMenuClick = {}, ) - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(24.dp)) HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", @@ -393,7 +393,7 @@ fun HorizontalPostListItemPreview() { onPostLikeClick = {}, onPostMoreMenuClick = {}, ) - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(24.dp)) HorizontalPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", From 907d5df3603ea1dc087591d1f9bff11d25fe2b51 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 15 Apr 2024 19:10:14 -0300 Subject: [PATCH 037/237] Fix detekt, remove extra padding --- .../horizontalpostlist/HorizontalPostListItemLoading.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt index cb3f5677d2ac..f4da915ae614 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt @@ -7,15 +7,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -44,7 +41,6 @@ fun HorizontalPostListItemLoading() { ) { ShimmerBox( modifier = Modifier - .padding(start = Margin.Small.value) .width(99.dp) .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)) From eeffadd86108dd7bb77c39e10ff47fb2b7485cc8 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 15 Apr 2024 19:18:17 -0300 Subject: [PATCH 038/237] Create HorizontalPostList file --- .../views/compose/horizontalpostlist/HorizontalPostList.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostList.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostList.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostList.kt new file mode 100644 index 000000000000..e8d425cfd2bb --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostList.kt @@ -0,0 +1,7 @@ +package org.wordpress.android.ui.reader.views.compose.horizontalpostlist + +import androidx.compose.runtime.Composable + +@Composable +fun HorizontalPostList() { +} From f1b2bafa943cea58298dda91537452a1e573d77d Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:18:13 -0300 Subject: [PATCH 039/237] Fix like icon start padding --- .../compose/horizontalpostlist/HorizontalPostListItem.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt index 7fcc39add67f..dd09bde9b50e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -161,11 +162,10 @@ fun HorizontalPostListItem( modifier = Modifier .fillMaxWidth() .height(24.dp), - verticalAlignment = Alignment.CenterVertically, ) { // Like action TextButton( - modifier = Modifier.height(24.dp), + modifier = Modifier.defaultMinSize(minWidth = 1.dp), contentPadding = PaddingValues(0.dp), onClick = { onPostLikeClick() }, ) { From d0ea2ae081d2b560cf5ba19d360d2bae0e8f58fc Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 16 Apr 2024 16:36:59 -0300 Subject: [PATCH 040/237] Create ReaderPostRepository --- .../BloggingPromptsPostTagProvider.kt | 4 +- .../reader/repository/ReaderPostRepository.kt | 289 +++++++++++++++++ .../services/post/ReaderPostJobService.java | 10 +- .../reader/services/post/ReaderPostLogic.java | 293 +----------------- .../services/post/ReaderPostLogicFactory.kt | 14 + .../services/post/ReaderPostService.java | 10 +- .../BloggingPromptsPostTagProviderTest.kt | 6 +- 7 files changed, 326 insertions(+), 300 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactory.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt index 9bac2972def3..2fcff83c7a75 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt @@ -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 @@ -23,7 +23,7 @@ class BloggingPromptsPostTagProvider @Inject constructor( promptIdTag, promptIdTag, promptIdTag, - ReaderPostLogic.formatFullEndpointForTag(promptIdTag), + ReaderPostRepository.formatFullEndpointForTag(promptIdTag), ReaderTagType.FOLLOWED, ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt new file mode 100644 index 000000000000..755c03195ed7 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt @@ -0,0 +1,289 @@ +package org.wordpress.android.ui.reader.repository + +import android.text.TextUtils +import com.android.volley.VolleyError +import com.wordpress.rest.RestRequest +import org.json.JSONObject +import org.wordpress.android.WordPress.Companion.getRestClientUtilsV1_2 +import org.wordpress.android.datasets.ReaderPostTable +import org.wordpress.android.datasets.ReaderTagTable +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.prefs.AppPrefs +import org.wordpress.android.ui.reader.ReaderConstants +import org.wordpress.android.ui.reader.actions.ReaderActions +import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResultListener +import org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter +import org.wordpress.android.ui.reader.utils.ReaderUtils +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.LocaleManagerWrapper +import org.wordpress.android.util.UrlUtils +import java.util.Locale +import javax.inject.Inject + +class ReaderPostRepository @Inject constructor( + private val localeManagerWrapper: LocaleManagerWrapper +) { + fun requestPostsWithTag( + tag: ReaderTag, + updateAction: ReaderPostServiceStarter.UpdateAction, + resultListener: UpdateResultListener + ) { + val path = getRelativeEndpointForTag(tag) + if (path.isNullOrBlank()) { + resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED) + return + } + val sb = StringBuilder(path) + + // append #posts to retrieve + sb.append("?number=").append(ReaderConstants.READER_MAX_POSTS_TO_REQUEST) + + // return newest posts first (this is the default, but make it explicit since it's important) + sb.append("&order=DESC") + + val beforeDate: String? = when (updateAction) { + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER -> { + // request posts older than the oldest existing post with this tag + ReaderPostTable.getOldestDateWithTag(tag) + } + + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP -> { + // request posts older than the post with the gap marker for this tag + ReaderPostTable.getGapMarkerDateForTag(tag) + } + + ReaderPostServiceStarter.UpdateAction.REQUEST_NEWER, + ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH -> null + } + + if (!TextUtils.isEmpty(beforeDate)) { + sb.append("&before=").append(UrlUtils.urlEncode(beforeDate)) + } + sb.append("&meta=site,likes") + sb.append("&lang=").append(localeManagerWrapper.getLanguage()) + + val listener = RestRequest.Listener { jsonObject: JSONObject? -> + // remember when this tag was updated if newer posts were requested + if (updateAction == ReaderPostServiceStarter.UpdateAction.REQUEST_NEWER || + updateAction == ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH + ) { + ReaderTagTable.setTagLastUpdated(tag) + } + handleUpdatePostsResponse(tag, jsonObject, updateAction, resultListener) + } + + val errorListener = RestRequest.ErrorListener { volleyError: VolleyError? -> + AppLog.e(AppLog.T.READER, volleyError) + resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED) + } + + getRestClientUtilsV1_2().get(sb.toString(), null, null, listener, errorListener) + } + + fun requestPostsForBlog( + blogId: Long, + updateAction: ReaderPostServiceStarter.UpdateAction, + resultListener: UpdateResultListener + ) { + var path = "read/sites/$blogId/posts/?meta=site,likes" + + // append the date of the oldest cached post in this blog when requesting older posts + if (updateAction == ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER) { + val dateOldest = ReaderPostTable.getOldestPubDateInBlog(blogId) + if (!TextUtils.isEmpty(dateOldest)) { + path += "&before=" + UrlUtils.urlEncode(dateOldest) + } + } + val listener = RestRequest.Listener { jsonObject -> + handleUpdatePostsResponse( + null, + jsonObject, + updateAction, + resultListener + ) + } + val errorListener = RestRequest.ErrorListener { volleyError -> + AppLog.e(AppLog.T.READER, volleyError) + resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED) + } + AppLog.d(AppLog.T.READER, "updating posts in blog $blogId") + getRestClientUtilsV1_2().getWithLocale(path, null, null, listener, errorListener) + } + + fun requestPostsForFeed( + feedId: Long, + updateAction: ReaderPostServiceStarter.UpdateAction, + resultListener: UpdateResultListener + ) { + var path = "read/feed/$feedId/posts/?meta=site,likes" + if (updateAction == ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER) { + val dateOldest = ReaderPostTable.getOldestPubDateInFeed(feedId) + if (!TextUtils.isEmpty(dateOldest)) { + path += "&before=" + UrlUtils.urlEncode(dateOldest) + } + } + val listener = RestRequest.Listener { jsonObject -> + handleUpdatePostsResponse( + null, + jsonObject, + updateAction, + resultListener + ) + } + val errorListener = RestRequest.ErrorListener { volleyError -> + AppLog.e(AppLog.T.READER, volleyError) + resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED) + } + AppLog.d(AppLog.T.READER, "updating posts in feed $feedId") + getRestClientUtilsV1_2().getWithLocale(path, null, null, listener, errorListener) + } + + /* + * called after requesting posts with a specific tag or in a specific blog/feed + */ + private fun handleUpdatePostsResponse( + tag: ReaderTag?, + jsonObject: JSONObject?, + updateAction: ReaderPostServiceStarter.UpdateAction, + resultListener: UpdateResultListener + ) { + if (jsonObject == null) { + resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED) + return + } + object : Thread() { + override fun run() { + val serverPosts = ReaderPostList.fromJson(jsonObject) + val updateResult = ReaderPostTable.comparePosts(serverPosts) + if (updateResult.isNewOrChanged) { + // gap detection - only applies to posts with a specific tag + var postWithGap: ReaderPost? = null + if (tag != null) { + when (updateAction) { + ReaderPostServiceStarter.UpdateAction.REQUEST_NEWER -> { + // if there's no overlap between server and local (ie: all server + // posts are new), assume there's a gap between server and local + // provided that local posts exist + val numServerPosts = serverPosts.size + if (numServerPosts >= 2 && ReaderPostTable.getNumPostsWithTag(tag) > 0 && + !ReaderPostTable.hasOverlap( + serverPosts, + tag + ) + ) { + // treat the second to last server post as having a gap + postWithGap = serverPosts[numServerPosts - 2] + // remove the last server post to deal with the edge case of + // there actually not being a gap between local & server + serverPosts.removeAt(numServerPosts - 1) + val gapMarker = ReaderPostTable.getGapMarkerIdsForTag(tag) + if (gapMarker != null) { + // We mustn't have two gapMarkers at the same time. Therefor we need to + // delete all posts before the current gapMarker and clear the gapMarker flag. + ReaderPostTable.deletePostsBeforeGapMarkerForTag(tag) + ReaderPostTable.removeGapMarkerForTag(tag) + } + } + } + + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP -> { + // if service was started as a request to fill a gap, delete existing posts + // before the one with the gap marker, then remove the existing gap marker + ReaderPostTable.deletePostsBeforeGapMarkerForTag(tag) + ReaderPostTable.removeGapMarkerForTag(tag) + } + + ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH -> ReaderPostTable.deletePostsWithTag( + tag + ) + + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER -> {} + } + } + ReaderPostTable.addOrUpdatePosts(tag, serverPosts) + if (AppPrefs.shouldUpdateBookmarkPostsPseudoIds(tag)) { + ReaderPostTable.updateBookmarkedPostPseudoId(serverPosts) + AppPrefs.setBookmarkPostsPseudoIdsUpdated() + } + + // gap marker must be set after saving server posts + if (postWithGap != null) { + ReaderPostTable.setGapMarkerForTag(postWithGap.blogId, postWithGap.postId, tag) + AppLog.d(AppLog.T.READER, "added gap marker to tag " + tag!!.tagNameForLog) + } + } else if (updateResult == ReaderActions.UpdateResult.UNCHANGED + && updateAction == ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP + ) { + // edge case - request to fill gap returned nothing new, so remove the gap marker + ReaderPostTable.removeGapMarkerForTag(tag) + AppLog.w(AppLog.T.READER, "attempt to fill gap returned nothing new") + } + AppLog.d( + AppLog.T.READER, + "requested posts response = $updateResult" + ) + resultListener.onUpdateResult(updateResult) + } + }.start() + } + + /* + * returns the endpoint to use when requesting posts with the passed tag + */ + private fun getRelativeEndpointForTag(tag: ReaderTag): String? { + val endpoint = tag.endpoint?.takeIf { it.isNotBlank() } // if passed tag has an assigned endpoint, use it + ?: ReaderTagTable.getEndpointForTag(tag)?.takeIf { it.isNotBlank() } // check the db for the endpoint + + return endpoint + ?.let { getRelativeEndpoint(it) } + ?: if (tag.tagType == ReaderTagType.DEFAULT) { + // never hand craft the endpoint for default tags, since these MUST be updated using their endpoints + null + } else { + formatRelativeEndpointForTag(tag.tagSlug) + } + } + + private fun formatRelativeEndpointForTag(tagSlug: String): String { + return String.format(Locale.US, "read/tags/%s/posts", ReaderUtils.sanitizeWithDashes(tagSlug)) + } + + /* + * returns the passed endpoint without the unnecessary path - this is + * needed because as of 20-Feb-2015 the /read/menu/ call returns the + * full path but we don't want to use the full path since it may change + * between API versions (as it did when we moved from v1 to v1.1) + * + * ex: https://public-api.wordpress.com/rest/v1/read/tags/fitness/posts + * becomes just read/tags/fitness/posts + */ + @Suppress("MagicNumber") + private fun getRelativeEndpoint(endpoint: String): String { + return endpoint.takeIf { it.startsWith("http") } + ?.let { + var pos = it.indexOf("/read/") + if (pos > -1) { + return@let it.substring(pos + 1) + } + pos = it.indexOf("/v1/") + if (pos > -1) { + return@let it.substring(pos + 4) + } + return@let it + } + ?: endpoint + } + + companion object { + private fun formatRelativeEndpointForTag(tagSlug: String): String { + return String.format(Locale.US, "read/tags/%s/posts", ReaderUtils.sanitizeWithDashes(tagSlug)) + } + + fun formatFullEndpointForTag(tagSlug: String): String { + return (getRestClientUtilsV1_2().restClient.endpointURL + formatRelativeEndpointForTag(tagSlug)) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostJobService.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostJobService.java index 70a2ed80efb8..b0cfb5d519db 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostJobService.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostJobService.java @@ -10,12 +10,9 @@ import org.wordpress.android.ui.reader.ReaderEvents; import org.wordpress.android.ui.reader.services.ServiceCompletionListener; import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.LocaleManagerWrapper; import javax.inject.Inject; -import dagger.hilt.android.AndroidEntryPoint; - import static org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.ARG_ACTION; import static org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.ARG_BLOG_ID; import static org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.ARG_FEED_ID; @@ -26,6 +23,8 @@ import static org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.ARG_TAG_PARAM_TITLE; import static org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.UpdateAction; +import dagger.hilt.android.AndroidEntryPoint; + /** * service which updates posts with specific tags or in specific blogs/feeds - relies on * EventBus to alert of update status @@ -33,10 +32,9 @@ @AndroidEntryPoint public class ReaderPostJobService extends JobService implements ServiceCompletionListener { + @Inject ReaderPostLogicFactory mPostLogicFactory; private ReaderPostLogic mReaderPostLogic; - @Inject LocaleManagerWrapper mLocaleManagerWrapper; - @Override public boolean onStartJob(JobParameters params) { AppLog.i(AppLog.T.READER, "reader post job service > started"); UpdateAction action; @@ -74,7 +72,7 @@ public class ReaderPostJobService extends JobService implements ServiceCompletio @Override public void onCreate() { super.onCreate(); - mReaderPostLogic = new ReaderPostLogic(this, mLocaleManagerWrapper); + mReaderPostLogic = mPostLogicFactory.create(this); AppLog.i(AppLog.T.READER, "reader post job service > created"); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogic.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogic.java index 212d32230fab..d3b9dd20a8dd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogic.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogic.java @@ -1,43 +1,26 @@ package org.wordpress.android.ui.reader.services.post; -import android.text.TextUtils; - import androidx.annotation.NonNull; -import com.android.volley.VolleyError; -import com.wordpress.rest.RestRequest; - import org.greenrobot.eventbus.EventBus; -import org.json.JSONObject; -import org.wordpress.android.WordPress; -import org.wordpress.android.datasets.ReaderPostTable; -import org.wordpress.android.datasets.ReaderTagTable; -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.prefs.AppPrefs; -import org.wordpress.android.ui.reader.ReaderConstants; import org.wordpress.android.ui.reader.ReaderEvents; import org.wordpress.android.ui.reader.actions.ReaderActions; -import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId; +import org.wordpress.android.ui.reader.repository.ReaderPostRepository; import org.wordpress.android.ui.reader.services.ServiceCompletionListener; import org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.UpdateAction; -import org.wordpress.android.ui.reader.utils.ReaderUtils; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.LocaleManagerWrapper; -import org.wordpress.android.util.StringUtils; -import org.wordpress.android.util.UrlUtils; public class ReaderPostLogic { - private ServiceCompletionListener mCompletionListener; - private final LocaleManagerWrapper mLocaleManagerWrapper; + @NonNull + private final ServiceCompletionListener mCompletionListener; + @NonNull + private final ReaderPostRepository mReaderPostRepository; private Object mListenerCompanion; public ReaderPostLogic(@NonNull final ServiceCompletionListener listener, - @NonNull final LocaleManagerWrapper localeManagerWrapper) { + @NonNull final ReaderPostRepository readerPostRepository) { mCompletionListener = listener; - mLocaleManagerWrapper = localeManagerWrapper; + mReaderPostRepository = readerPostRepository; } public void performTask(Object companion, UpdateAction action, @@ -55,9 +38,8 @@ public void performTask(Object companion, UpdateAction action, } } - private void updatePostsWithTag(final ReaderTag tag, final UpdateAction action) { - requestPostsWithTag( + mReaderPostRepository.requestPostsWithTag( tag, action, new ReaderActions.UpdateResultListener() { @@ -77,7 +59,7 @@ public void onUpdateResult(ReaderActions.UpdateResult result) { mCompletionListener.onCompleted(mListenerCompanion); } }; - requestPostsForBlog(blogId, action, listener); + mReaderPostRepository.requestPostsForBlog(blogId, action, listener); } private void updatePostsInFeed(long feedId, final UpdateAction action) { @@ -88,261 +70,6 @@ public void onUpdateResult(ReaderActions.UpdateResult result) { mCompletionListener.onCompleted(mListenerCompanion); } }; - requestPostsForFeed(feedId, action, listener); - } - - private void requestPostsWithTag(final ReaderTag tag, - final UpdateAction updateAction, - final ReaderActions.UpdateResultListener resultListener) { - String path = getRelativeEndpointForTag(tag); - if (TextUtils.isEmpty(path)) { - resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED); - return; - } - - StringBuilder sb = new StringBuilder(path); - - // append #posts to retrieve - sb.append("?number=").append(ReaderConstants.READER_MAX_POSTS_TO_REQUEST); - - // return newest posts first (this is the default, but make it explicit since it's important) - sb.append("&order=DESC"); - - String beforeDate; - switch (updateAction) { - case REQUEST_OLDER: - // request posts older than the oldest existing post with this tag - beforeDate = ReaderPostTable.getOldestDateWithTag(tag); - break; - case REQUEST_OLDER_THAN_GAP: - // request posts older than the post with the gap marker for this tag - beforeDate = ReaderPostTable.getGapMarkerDateForTag(tag); - break; - case REQUEST_NEWER: - case REQUEST_REFRESH: - default: - beforeDate = null; - break; - } - if (!TextUtils.isEmpty(beforeDate)) { - sb.append("&before=").append(UrlUtils.urlEncode(beforeDate)); - } - - sb.append("&meta=site,likes"); - - sb.append("&lang=").append(mLocaleManagerWrapper.getLanguage()); - - com.wordpress.rest.RestRequest.Listener listener = jsonObject -> { - // remember when this tag was updated if newer posts were requested - if (updateAction == UpdateAction.REQUEST_NEWER || updateAction == UpdateAction.REQUEST_REFRESH) { - ReaderTagTable.setTagLastUpdated(tag); - } - handleUpdatePostsResponse(tag, jsonObject, updateAction, resultListener); - }; - RestRequest.ErrorListener errorListener = volleyError -> { - AppLog.e(AppLog.T.READER, volleyError); - resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED); - }; - - WordPress.getRestClientUtilsV1_2().get(sb.toString(), null, null, listener, errorListener); - } - - private static void requestPostsForBlog(final long blogId, - final UpdateAction updateAction, - final ReaderActions.UpdateResultListener resultListener) { - String path = "read/sites/" + blogId + "/posts/?meta=site,likes"; - - // append the date of the oldest cached post in this blog when requesting older posts - if (updateAction == UpdateAction.REQUEST_OLDER) { - String dateOldest = ReaderPostTable.getOldestPubDateInBlog(blogId); - if (!TextUtils.isEmpty(dateOldest)) { - path += "&before=" + UrlUtils.urlEncode(dateOldest); - } - } - - com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { - @Override - public void onResponse(JSONObject jsonObject) { - handleUpdatePostsResponse(null, jsonObject, updateAction, resultListener); - } - }; - RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { - @Override - public void onErrorResponse(VolleyError volleyError) { - AppLog.e(AppLog.T.READER, volleyError); - resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED); - } - }; - AppLog.d(AppLog.T.READER, "updating posts in blog " + blogId); - WordPress.getRestClientUtilsV1_2().getWithLocale(path, null, null, listener, errorListener); - } - - private static void requestPostsForFeed(final long feedId, - final UpdateAction updateAction, - final ReaderActions.UpdateResultListener resultListener) { - String path = "read/feed/" + feedId + "/posts/?meta=site,likes"; - if (updateAction == UpdateAction.REQUEST_OLDER) { - String dateOldest = ReaderPostTable.getOldestPubDateInFeed(feedId); - if (!TextUtils.isEmpty(dateOldest)) { - path += "&before=" + UrlUtils.urlEncode(dateOldest); - } - } - - com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() { - @Override - public void onResponse(JSONObject jsonObject) { - handleUpdatePostsResponse(null, jsonObject, updateAction, resultListener); - } - }; - RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { - @Override - public void onErrorResponse(VolleyError volleyError) { - AppLog.e(AppLog.T.READER, volleyError); - resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED); - } - }; - - AppLog.d(AppLog.T.READER, "updating posts in feed " + feedId); - WordPress.getRestClientUtilsV1_2().getWithLocale(path, null, null, listener, errorListener); - } - - /* - * called after requesting posts with a specific tag or in a specific blog/feed - */ - private static void handleUpdatePostsResponse(final ReaderTag tag, - final JSONObject jsonObject, - final UpdateAction updateAction, - final ReaderActions.UpdateResultListener resultListener) { - if (jsonObject == null) { - resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED); - return; - } - - new Thread() { - @Override - public void run() { - ReaderPostList serverPosts = ReaderPostList.fromJson(jsonObject); - ReaderActions.UpdateResult updateResult = ReaderPostTable.comparePosts(serverPosts); - if (updateResult.isNewOrChanged()) { - // gap detection - only applies to posts with a specific tag - ReaderPost postWithGap = null; - if (tag != null) { - switch (updateAction) { - case REQUEST_NEWER: - // if there's no overlap between server and local (ie: all server - // posts are new), assume there's a gap between server and local - // provided that local posts exist - int numServerPosts = serverPosts.size(); - if (numServerPosts >= 2 - && ReaderPostTable.getNumPostsWithTag(tag) > 0 - && !ReaderPostTable.hasOverlap(serverPosts, tag)) { - // treat the second to last server post as having a gap - postWithGap = serverPosts.get(numServerPosts - 2); - // remove the last server post to deal with the edge case of - // there actually not being a gap between local & server - serverPosts.remove(numServerPosts - 1); - ReaderBlogIdPostId gapMarker = ReaderPostTable.getGapMarkerIdsForTag(tag); - if (gapMarker != null) { - // We mustn't have two gapMarkers at the same time. Therefor we need to - // delete all posts before the current gapMarker and clear the gapMarker flag. - ReaderPostTable.deletePostsBeforeGapMarkerForTag(tag); - ReaderPostTable.removeGapMarkerForTag(tag); - } - } - break; - case REQUEST_OLDER_THAN_GAP: - // if service was started as a request to fill a gap, delete existing posts - // before the one with the gap marker, then remove the existing gap marker - ReaderPostTable.deletePostsBeforeGapMarkerForTag(tag); - ReaderPostTable.removeGapMarkerForTag(tag); - break; - case REQUEST_REFRESH: - ReaderPostTable.deletePostsWithTag(tag); - break; - case REQUEST_OLDER: - // no-op - break; - } - } - ReaderPostTable.addOrUpdatePosts(tag, serverPosts); - if (AppPrefs.shouldUpdateBookmarkPostsPseudoIds(tag)) { - ReaderPostTable.updateBookmarkedPostPseudoId(serverPosts); - AppPrefs.setBookmarkPostsPseudoIdsUpdated(); - } - - // gap marker must be set after saving server posts - if (postWithGap != null) { - ReaderPostTable.setGapMarkerForTag(postWithGap.blogId, postWithGap.postId, tag); - AppLog.d(AppLog.T.READER, "added gap marker to tag " + tag.getTagNameForLog()); - } - } else if (updateResult == ReaderActions.UpdateResult.UNCHANGED - && updateAction == UpdateAction.REQUEST_OLDER_THAN_GAP) { - // edge case - request to fill gap returned nothing new, so remove the gap marker - ReaderPostTable.removeGapMarkerForTag(tag); - AppLog.w(AppLog.T.READER, "attempt to fill gap returned nothing new"); - } - AppLog.d(AppLog.T.READER, "requested posts response = " + updateResult.toString()); - resultListener.onUpdateResult(updateResult); - } - }.start(); - } - - /* - * returns the endpoint to use when requesting posts with the passed tag - */ - private static String getRelativeEndpointForTag(ReaderTag tag) { - if (tag == null) { - return null; - } - - // if passed tag has an assigned endpoint, return it and be done - if (!TextUtils.isEmpty(tag.getEndpoint())) { - return getRelativeEndpoint(tag.getEndpoint()); - } - - // check the db for the endpoint - String endpoint = ReaderTagTable.getEndpointForTag(tag); - if (!TextUtils.isEmpty(endpoint)) { - return getRelativeEndpoint(endpoint); - } - - // never hand craft the endpoint for default tags, since these MUST be updated - // using their stored endpoints - if (tag.tagType == ReaderTagType.DEFAULT) { - return null; - } - return formatRelativeEndpointForTag(tag.getTagSlug()); - } - - private static String formatRelativeEndpointForTag(@NonNull final String tagSlug) { - return String.format("read/tags/%s/posts", ReaderUtils.sanitizeWithDashes(tagSlug)); - } - - public static String formatFullEndpointForTag(@NonNull final String tagSlug) { - return WordPress.getRestClientUtilsV1_2().getRestClient().getEndpointURL() - + formatRelativeEndpointForTag(tagSlug); - } - - /* - * returns the passed endpoint without the unnecessary path - this is - * needed because as of 20-Feb-2015 the /read/menu/ call returns the - * full path but we don't want to use the full path since it may change - * between API versions (as it did when we moved from v1 to v1.1) - * - * ex: https://public-api.wordpress.com/rest/v1/read/tags/fitness/posts - * becomes just read/tags/fitness/posts - */ - private static String getRelativeEndpoint(final String endpoint) { - if (endpoint != null && endpoint.startsWith("http")) { - int pos = endpoint.indexOf("/read/"); - if (pos > -1) { - return endpoint.substring(pos + 1, endpoint.length()); - } - pos = endpoint.indexOf("/v1/"); - if (pos > -1) { - return endpoint.substring(pos + 4, endpoint.length()); - } - } - return StringUtils.notNullStr(endpoint); + mReaderPostRepository.requestPostsForFeed(feedId, action, listener); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactory.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactory.kt new file mode 100644 index 000000000000..6d64d4b5117d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactory.kt @@ -0,0 +1,14 @@ +package org.wordpress.android.ui.reader.services.post + +import org.wordpress.android.ui.reader.repository.ReaderPostRepository +import org.wordpress.android.ui.reader.services.ServiceCompletionListener +import javax.inject.Inject + +class ReaderPostLogicFactory @Inject constructor( + private val readerPostRepository: ReaderPostRepository, +) { + fun create(listener: ServiceCompletionListener) = ReaderPostLogic( + listener, + readerPostRepository, + ) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostService.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostService.java index 5753d9ce4cb7..8312575d0124 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostService.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostService.java @@ -9,18 +9,17 @@ import org.wordpress.android.ui.reader.ReaderEvents; import org.wordpress.android.ui.reader.services.ServiceCompletionListener; import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.LocaleManagerWrapper; import javax.inject.Inject; -import dagger.hilt.android.AndroidEntryPoint; - import static org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.ARG_ACTION; import static org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.ARG_BLOG_ID; import static org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.ARG_FEED_ID; import static org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.ARG_TAG; import static org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter.UpdateAction; +import dagger.hilt.android.AndroidEntryPoint; + /** * service which updates posts with specific tags or in specific blogs/feeds - relies on * EventBus to alert of update status @@ -28,10 +27,9 @@ @AndroidEntryPoint public class ReaderPostService extends Service implements ServiceCompletionListener { + @Inject ReaderPostLogicFactory mPostLogicFactory; private ReaderPostLogic mReaderPostLogic; - @Inject LocaleManagerWrapper mLocaleManagerWrapper; - @Override public IBinder onBind(Intent intent) { return null; @@ -40,7 +38,7 @@ public IBinder onBind(Intent intent) { @Override public void onCreate() { super.onCreate(); - mReaderPostLogic = new ReaderPostLogic(this, mLocaleManagerWrapper); + mReaderPostLogic = mPostLogicFactory.create(this); AppLog.i(AppLog.T.READER, "reader post service > created"); } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProviderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProviderTest.kt index 73250ba0c7f5..5b1fc3546c74 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProviderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProviderTest.kt @@ -10,7 +10,7 @@ import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest 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 kotlin.test.assertEquals @@ -47,7 +47,7 @@ class BloggingPromptsPostTagProviderTest : BaseUnitTest() { BLOGGING_PROMPT_ID_TAG, BLOGGING_PROMPT_ID_TAG, BLOGGING_PROMPT_ID_TAG, - ReaderPostLogic.formatFullEndpointForTag(BLOGGING_PROMPT_ID_TAG), + ReaderPostRepository.formatFullEndpointForTag(BLOGGING_PROMPT_ID_TAG), ReaderTagType.FOLLOWED, ) val actual = tagProvider.promptSearchReaderTag("valid-url") @@ -61,7 +61,7 @@ class BloggingPromptsPostTagProviderTest : BaseUnitTest() { BLOGGING_PROMPT_TAG, BLOGGING_PROMPT_TAG, BLOGGING_PROMPT_TAG, - ReaderPostLogic.formatFullEndpointForTag(BLOGGING_PROMPT_TAG), + ReaderPostRepository.formatFullEndpointForTag(BLOGGING_PROMPT_TAG), ReaderTagType.FOLLOWED, ) val actual = tagProvider.promptSearchReaderTag("invalid-url") From dbb0f831a8d31b3c2e71bc66e6c1da75655d6584 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:12:42 -0300 Subject: [PATCH 041/237] Refactor reader tags feed components package --- .../horizontalpostlist/HorizontalPostList.kt | 7 ----- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 8 ++++++ .../horizontalpostlist/HorizontalPostList.kt | 28 +++++++++++++++++++ .../HorizontalPostListItem.kt | 2 +- .../HorizontalPostListItemLoading.kt | 2 +- 5 files changed, 38 insertions(+), 9 deletions(-) delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostList.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostList.kt rename WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/{ => tagsfeed}/horizontalpostlist/HorizontalPostListItem.kt (99%) rename WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/{ => tagsfeed}/horizontalpostlist/HorizontalPostListItemLoading.kt (98%) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostList.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostList.kt deleted file mode 100644 index e8d425cfd2bb..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostList.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.wordpress.android.ui.reader.views.compose.horizontalpostlist - -import androidx.compose.runtime.Composable - -@Composable -fun HorizontalPostList() { -} 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 new file mode 100644 index 000000000000..e51f6c6cf396 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt @@ -0,0 +1,8 @@ +package org.wordpress.android.ui.reader.views.compose.tagsfeed + +import androidx.compose.runtime.Composable + +@Composable +fun ReaderTagsFeed() { + +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostList.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostList.kt new file mode 100644 index 000000000000..8716a5d989e2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostList.kt @@ -0,0 +1,28 @@ +package org.wordpress.android.ui.reader.views.compose.tagsfeed.horizontalpostlist + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun HorizontalPostList() { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(top = 16.dp, bottom = 16.dp) + ) { + LazyRow( + modifier = Modifier + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 24.dp), + ) { + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItem.kt similarity index 99% rename from WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt rename to WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItem.kt index dd09bde9b50e..ece139a2e233 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItem.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.reader.views.compose.horizontalpostlist +package org.wordpress.android.ui.reader.views.compose.tagsfeed.horizontalpostlist import android.content.res.Configuration import androidx.compose.foundation.clickable diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItemLoading.kt similarity index 98% rename from WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt rename to WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItemLoading.kt index f4da915ae614..822739da0bc5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItemLoading.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.reader.views.compose.horizontalpostlist +package org.wordpress.android.ui.reader.views.compose.tagsfeed.horizontalpostlist import android.content.res.Configuration import androidx.compose.foundation.background From 8145b0abd5093e7e8b9e14543c08ec3309a8ece5 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 16 Apr 2024 17:13:24 -0300 Subject: [PATCH 042/237] Replace StringUtils by kotlin extensions --- .../android/ui/reader/repository/ReaderPostRepository.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt index 755c03195ed7..448ca5860b1e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt @@ -1,6 +1,5 @@ package org.wordpress.android.ui.reader.repository -import android.text.TextUtils import com.android.volley.VolleyError import com.wordpress.rest.RestRequest import org.json.JSONObject @@ -59,7 +58,7 @@ class ReaderPostRepository @Inject constructor( ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH -> null } - if (!TextUtils.isEmpty(beforeDate)) { + if (!beforeDate.isNullOrBlank()) { sb.append("&before=").append(UrlUtils.urlEncode(beforeDate)) } sb.append("&meta=site,likes") @@ -93,7 +92,7 @@ class ReaderPostRepository @Inject constructor( // append the date of the oldest cached post in this blog when requesting older posts if (updateAction == ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER) { val dateOldest = ReaderPostTable.getOldestPubDateInBlog(blogId) - if (!TextUtils.isEmpty(dateOldest)) { + if (!dateOldest.isNullOrBlank()) { path += "&before=" + UrlUtils.urlEncode(dateOldest) } } @@ -121,7 +120,7 @@ class ReaderPostRepository @Inject constructor( var path = "read/feed/$feedId/posts/?meta=site,likes" if (updateAction == ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER) { val dateOldest = ReaderPostTable.getOldestPubDateInFeed(feedId) - if (!TextUtils.isEmpty(dateOldest)) { + if (!dateOldest.isNullOrBlank()) { path += "&before=" + UrlUtils.urlEncode(dateOldest) } } From 212400e55d30b522b0fff74b43e5b8917c866cb0 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:12:02 -0300 Subject: [PATCH 043/237] Move shimmer color to generic component --- .../ui/compose/components/shimmer/Shimmer.kt | 6 ++++++ .../compose/components/shimmer/ShimmerBox.kt | 2 ++ .../HorizontalPostListItemLoading.kt | 21 ++++++------------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt index ab7bb410c9ca..7782a19ede72 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt @@ -14,6 +14,12 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import org.wordpress.android.ui.compose.theme.AppColor +object Shimmer { + val color = AppColor.Black.copy( + alpha = 0.08F + ) +} + /** * Taken from https://github.com/canerkaseler/jetpack-compose-shimmer-loading-animation */ diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt index 4602d885a1c6..20f9c174331e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt @@ -1,5 +1,6 @@ package org.wordpress.android.ui.compose.components.shimmer +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -15,6 +16,7 @@ fun ShimmerBox( ) { Box( modifier = modifier + .background(Shimmer.color) .shimmerLoadingAnimation( isLoadingCompleted = isLoadingCompleted, isLightModeActive = isLightModeActive, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt index f4da915ae614..c236b8ead01a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt @@ -27,9 +27,6 @@ import org.wordpress.android.ui.compose.unit.Margin @Composable fun HorizontalPostListItemLoading() { - val loadingColor = AppColor.Black.copy( - alpha = 0.08F - ) Column( modifier = Modifier .width(240.dp) @@ -43,8 +40,7 @@ fun HorizontalPostListItemLoading() { modifier = Modifier .width(99.dp) .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(loadingColor), + .clip(shape = RoundedCornerShape(16.dp)), isLoadingCompleted = false, ) } @@ -53,8 +49,7 @@ fun HorizontalPostListItemLoading() { .padding(top = Margin.Large.value) .width(204.dp) .height(18.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(loadingColor), + .clip(shape = RoundedCornerShape(16.dp)), isLoadingCompleted = false, ) ShimmerBox( @@ -62,8 +57,7 @@ fun HorizontalPostListItemLoading() { .padding(top = Margin.Large.value) .width(140.dp) .height(18.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(loadingColor), + .clip(shape = RoundedCornerShape(16.dp)), isLoadingCompleted = false, ) ShimmerBox( @@ -71,8 +65,7 @@ fun HorizontalPostListItemLoading() { .padding(top = Margin.Large.value) .fillMaxWidth() .height(150.dp) - .clip(shape = RoundedCornerShape(8.dp)) - .background(loadingColor), + .clip(shape = RoundedCornerShape(8.dp)), isLoadingCompleted = false, ) ShimmerBox( @@ -83,8 +76,7 @@ fun HorizontalPostListItemLoading() { ) .width(170.dp) .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(loadingColor), + .clip(shape = RoundedCornerShape(16.dp)), isLoadingCompleted = false, ) ShimmerBox( @@ -95,8 +87,7 @@ fun HorizontalPostListItemLoading() { ) .width(170.dp) .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(loadingColor), + .clip(shape = RoundedCornerShape(16.dp)), isLoadingCompleted = false, ) } From 538fc5b11e9cbcbee0248b93ffcb8681d696e9f5 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 16 Apr 2024 19:15:00 -0300 Subject: [PATCH 044/237] WIP ReaderTagsFeed UI --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 60 ++++++++++++++++++- ...tItem.kt => ReaderTagsFeedPostListItem.kt} | 2 +- ...t => ReaderTagsFeedPostListItemLoading.kt} | 4 +- .../horizontalpostlist/HorizontalPostList.kt | 28 --------- 4 files changed, 61 insertions(+), 33 deletions(-) rename WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/{horizontalpostlist/HorizontalPostListItem.kt => ReaderTagsFeedPostListItem.kt} (99%) rename WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/{horizontalpostlist/HorizontalPostListItemLoading.kt => ReaderTagsFeedPostListItemLoading.kt} (95%) delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostList.kt 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 e51f6c6cf396..55b7b1a926dc 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 @@ -1,8 +1,66 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.wordpress.android.models.ReaderPost +import org.wordpress.android.models.ReaderTag +import org.wordpress.android.ui.compose.theme.AppTheme @Composable -fun ReaderTagsFeed() { +fun ReaderTagsFeed(uiState: UiState) { + AppTheme { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + ) { + when (uiState) { + is UiState.Loaded -> Loaded(uiState.tags) + is UiState.Loading -> { + } + is UiState.Error -> { + + } + } + } + } +} + +@Composable +private fun Loaded(items: List) { + LazyRow( + modifier = Modifier + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 24.dp), + ) { + } +} + + +// TODO move to VM +sealed class UiState { + // TODO Loaded parameters + data class Loaded( + val tags: List, + ) : UiState() + + object Loading : UiState() +} + +sealed class TagsFeedItem { + data class Success( + val tag: ReaderTag, + val posts: List + ) + + data class Error( + val tag: ReaderTag, + ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt similarity index 99% rename from WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItem.kt rename to WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt index ece139a2e233..fd847dffffe5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.reader.views.compose.tagsfeed.horizontalpostlist +package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration import androidx.compose.foundation.clickable diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt similarity index 95% rename from WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItemLoading.kt rename to WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt index 02545bf7172a..64adaadd7b22 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt @@ -1,7 +1,6 @@ -package org.wordpress.android.ui.reader.views.compose.tagsfeed.horizontalpostlist +package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -21,7 +20,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.ui.compose.components.shimmer.ShimmerBox -import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostList.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostList.kt deleted file mode 100644 index 8716a5d989e2..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/horizontalpostlist/HorizontalPostList.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.wordpress.android.ui.reader.views.compose.tagsfeed.horizontalpostlist - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun HorizontalPostList() { - Box( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(top = 16.dp, bottom = 16.dp) - ) { - LazyRow( - modifier = Modifier - .fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 24.dp), - ) { - } - } -} From 6475dd1797077bc92cda233f3798c9f4361e6f04 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 16 Apr 2024 20:53:11 -0300 Subject: [PATCH 045/237] WIP Reader tags feed UI states --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 136 +++++++++++++++--- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 20 +-- .../ReaderTagsFeedPostListItemLoading.kt | 12 +- 3 files changed, 131 insertions(+), 37 deletions(-) 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 55b7b1a926dc..ccac5c6de576 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 @@ -1,32 +1,77 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed +import android.content.res.Configuration import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderTag import org.wordpress.android.ui.compose.theme.AppTheme @Composable fun ReaderTagsFeed(uiState: UiState) { - AppTheme { - Box( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(), - ) { - when (uiState) { - is UiState.Loaded -> Loaded(uiState.tags) - is UiState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + ) { + when (uiState) { + is UiState.Loaded -> Loaded(uiState) + is UiState.Loading -> Loading() + is UiState.Empty -> Empty() + } + } +} +@Composable +private fun Loaded(uiState: UiState.Loaded) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + ) { + items( + items = uiState.items, + key = { it.tag.tagDisplayName }, + ) { tagsFeedItem -> + when (tagsFeedItem) { + // If item is Success, show posts list + is TagsFeedItem.Success -> { + LazyRow( + modifier = Modifier + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 24.dp), + ) { + items( + items = tagsFeedItem.posts, + ) { postItem -> + with(postItem) { + ReaderTagsFeedPostListItem( + siteName = siteName, + postDateLine = postDateLine, + postTitle = postTitle, + postExcerpt = postExcerpt, + postImageUrl = postImageUrl, + postNumberOfLikesText = postNumberOfLikesText, + postNumberOfCommentsText = postNumberOfCommentsText, + isPostLiked = isPostLiked, + onPostImageClick = onPostImageClick, + onPostLikeClick = onPostLikeClick, + onPostMoreMenuClick = onPostMoreMenuClick, + ) + } + } + } } - is UiState.Error -> { - + // If item is Error, show error UI and retry button + is TagsFeedItem.Error -> { } } } @@ -34,7 +79,7 @@ fun ReaderTagsFeed(uiState: UiState) { } @Composable -private fun Loaded(items: List) { +private fun Loading() { LazyRow( modifier = Modifier .fillMaxWidth(), @@ -43,24 +88,73 @@ private fun Loaded(items: List) { } } +// TODO empty state (https://github.com/wordpress-mobile/WordPress-Android/issues/20584) +@Composable +private fun Empty() { +} + // TODO move to VM sealed class UiState { - // TODO Loaded parameters + // TODO review Loaded parameters data class Loaded( - val tags: List, + val items: List, ) : UiState() object Loading : UiState() + + object Empty : UiState() } -sealed class TagsFeedItem { +sealed class TagsFeedItem( + open val tag: ReaderTag, +) { data class Success( - val tag: ReaderTag, - val posts: List - ) + override val tag: ReaderTag, + val posts: List, + ) : TagsFeedItem(tag) data class Error( - val tag: ReaderTag, - ) + override val tag: ReaderTag, + ) : TagsFeedItem(tag) +} + +data class TagsFeedPostItem( + val siteName: String, + val postDateLine: String, + val postTitle: String, + val postExcerpt: String, + val postImageUrl: String, + val postNumberOfLikesText: String, + val postNumberOfCommentsText: String, + val isPostLiked: Boolean, + val onPostImageClick: () -> Unit, + val onPostLikeClick: () -> Unit, + val onPostMoreMenuClick: () -> Unit, +) + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun ReaderTagsFeedLoaded() { + AppTheme { + ReaderTagsFeed( + uiState = UiState.Loaded( + items = listOf( + + ) + ) + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun ReaderTagsFeedLoading() { + AppTheme { + ReaderTagsFeed( + uiState = UiState.Loading + ) + } } 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 fd847dffffe5..7aef61f6653b 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 @@ -43,7 +43,7 @@ import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin @Composable -fun HorizontalPostListItem( +fun ReaderTagsFeedPostListItem( siteName: String, postDateLine: String, postTitle: String, @@ -234,7 +234,7 @@ fun PostImage( @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun HorizontalPostListItemPreview() { +fun ReaderTagsFeedPostListItemPreview() { AppTheme { Box( modifier = Modifier @@ -248,7 +248,7 @@ fun HorizontalPostListItemPreview() { contentPadding = PaddingValues(horizontal = 24.dp), ) { item { - HorizontalPostListItem( + ReaderTagsFeedPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + " pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", @@ -276,7 +276,7 @@ fun HorizontalPostListItemPreview() { onPostMoreMenuClick = {}, ) Spacer(Modifier.width(24.dp)) - HorizontalPostListItem( + ReaderTagsFeedPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", @@ -305,7 +305,7 @@ fun HorizontalPostListItemPreview() { onPostMoreMenuClick = {}, ) Spacer(Modifier.width(24.dp)) - HorizontalPostListItem( + ReaderTagsFeedPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", @@ -320,7 +320,7 @@ fun HorizontalPostListItemPreview() { onPostMoreMenuClick = {}, ) Spacer(Modifier.width(24.dp)) - HorizontalPostListItem( + ReaderTagsFeedPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", @@ -335,7 +335,7 @@ fun HorizontalPostListItemPreview() { onPostMoreMenuClick = {}, ) Spacer(Modifier.width(24.dp)) - HorizontalPostListItem( + ReaderTagsFeedPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", @@ -351,7 +351,7 @@ fun HorizontalPostListItemPreview() { onPostMoreMenuClick = {}, ) Spacer(Modifier.width(24.dp)) - HorizontalPostListItem( + ReaderTagsFeedPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", @@ -367,7 +367,7 @@ fun HorizontalPostListItemPreview() { onPostMoreMenuClick = {}, ) Spacer(Modifier.width(24.dp)) - HorizontalPostListItem( + ReaderTagsFeedPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", @@ -394,7 +394,7 @@ fun HorizontalPostListItemPreview() { onPostMoreMenuClick = {}, ) Spacer(Modifier.width(24.dp)) - HorizontalPostListItem( + ReaderTagsFeedPostListItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt index 64adaadd7b22..6ead0bec1f9f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt @@ -24,7 +24,7 @@ import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin @Composable -fun HorizontalPostListItemLoading() { +fun ReaderTagsFeedPostListItemLoading() { Column( modifier = Modifier .width(240.dp) @@ -94,7 +94,7 @@ fun HorizontalPostListItemLoading() { @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun HorizontalPostListItemLoadingPreview() { +fun ReaderTagsFeedPostListItemLoadingPreview() { AppTheme { Box( modifier = Modifier @@ -108,13 +108,13 @@ fun HorizontalPostListItemLoadingPreview() { contentPadding = PaddingValues(horizontal = 12.dp), ) { item { - HorizontalPostListItemLoading() + ReaderTagsFeedPostListItemLoading() Spacer(Modifier.width(12.dp)) - HorizontalPostListItemLoading() + ReaderTagsFeedPostListItemLoading() Spacer(Modifier.width(12.dp)) - HorizontalPostListItemLoading() + ReaderTagsFeedPostListItemLoading() Spacer(Modifier.width(12.dp)) - HorizontalPostListItemLoading() + ReaderTagsFeedPostListItemLoading() } } } From 4f9c2f5498fd5494ca2d045a4e42ac33e158164e Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:27:38 -0300 Subject: [PATCH 046/237] Implement ReaderTagsFeed preview --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 250 +++++++++++++++++- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 2 + 2 files changed, 251 insertions(+), 1 deletion(-) 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 ccac5c6de576..cfcba55435c5 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 @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.models.ReaderTag +import org.wordpress.android.models.ReaderTagType import org.wordpress.android.ui.compose.theme.AppTheme @Composable @@ -141,7 +142,243 @@ fun ReaderTagsFeedLoaded() { ReaderTagsFeed( uiState = UiState.Loaded( items = listOf( - + TagsFeedItem.Success( + tag = ReaderTag( + "Tag 1", + "Tag 1", + "Tag 1", + "Tag 1", + ReaderTagType.TAGS, + ), + posts = listOf( + TagsFeedPostItem( + siteName = "siteName1", + postDateLine = "postDateLine1", + postTitle = "postTitle1", + postExcerpt = "postExcerpt1", + postImageUrl = "postImageUrl1", + postNumberOfLikesText = "postNumberOfLikesText1", + postNumberOfCommentsText = "postNumberOfCommentsText1", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName2", + postDateLine = "postDateLine2", + postTitle = "postTitle2", + postExcerpt = "postExcerpt2", + postImageUrl = "postImageUrl2", + postNumberOfLikesText = "postNumberOfLikesText2", + postNumberOfCommentsText = "postNumberOfCommentsText2", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName2", + postDateLine = "postDateLine2", + postTitle = "postTitle2", + postExcerpt = "postExcerpt2", + postImageUrl = "postImageUrl2", + postNumberOfLikesText = "postNumberOfLikesText2", + postNumberOfCommentsText = "postNumberOfCommentsText2", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName3", + postDateLine = "postDateLine3", + postTitle = "postTitle3", + postExcerpt = "postExcerpt3", + postImageUrl = "postImageUrl3", + postNumberOfLikesText = "postNumberOfLikesText3", + postNumberOfCommentsText = "postNumberOfCommentsText3", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName4", + postDateLine = "postDateLine4", + postTitle = "postTitle4", + postExcerpt = "postExcerpt4", + postImageUrl = "postImageUrl4", + postNumberOfLikesText = "postNumberOfLikesText4", + postNumberOfCommentsText = "postNumberOfCommentsText4", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + ), + ), + TagsFeedItem.Success( + tag = ReaderTag( + "Tag 2", + "Tag 2", + "Tag 2", + "Tag 2", + ReaderTagType.TAGS, + ), + posts = listOf( + TagsFeedPostItem( + siteName = "siteName1", + postDateLine = "postDateLine1", + postTitle = "postTitle1", + postExcerpt = "postExcerpt1", + postImageUrl = "postImageUrl1", + postNumberOfLikesText = "postNumberOfLikesText1", + postNumberOfCommentsText = "postNumberOfCommentsText1", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName2", + postDateLine = "postDateLine2", + postTitle = "postTitle2", + postExcerpt = "postExcerpt2", + postImageUrl = "postImageUrl2", + postNumberOfLikesText = "postNumberOfLikesText2", + postNumberOfCommentsText = "postNumberOfCommentsText2", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName2", + postDateLine = "postDateLine2", + postTitle = "postTitle2", + postExcerpt = "postExcerpt2", + postImageUrl = "postImageUrl2", + postNumberOfLikesText = "postNumberOfLikesText2", + postNumberOfCommentsText = "postNumberOfCommentsText2", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName3", + postDateLine = "postDateLine3", + postTitle = "postTitle3", + postExcerpt = "postExcerpt3", + postImageUrl = "postImageUrl3", + postNumberOfLikesText = "postNumberOfLikesText3", + postNumberOfCommentsText = "postNumberOfCommentsText3", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName4", + postDateLine = "postDateLine4", + postTitle = "postTitle4", + postExcerpt = "postExcerpt4", + postImageUrl = "postImageUrl4", + postNumberOfLikesText = "postNumberOfLikesText4", + postNumberOfCommentsText = "postNumberOfCommentsText4", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + ), + ), + TagsFeedItem.Error( + tag = ReaderTag( + "Tag 3", + "Tag 3", + "Tag 3", + "Tag 3", + ReaderTagType.TAGS, + ), + ), + TagsFeedItem.Success( + tag = ReaderTag( + "Tag 4", + "Tag 4", + "Tag 4", + "Tag 4", + ReaderTagType.TAGS, + ), + posts = listOf( + TagsFeedPostItem( + siteName = "siteName1", + postDateLine = "postDateLine1", + postTitle = "postTitle1", + postExcerpt = "postExcerpt1", + postImageUrl = "postImageUrl1", + postNumberOfLikesText = "postNumberOfLikesText1", + postNumberOfCommentsText = "postNumberOfCommentsText1", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName2", + postDateLine = "postDateLine2", + postTitle = "postTitle2", + postExcerpt = "postExcerpt2", + postImageUrl = "postImageUrl2", + postNumberOfLikesText = "postNumberOfLikesText2", + postNumberOfCommentsText = "postNumberOfCommentsText2", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName2", + postDateLine = "postDateLine2", + postTitle = "postTitle2", + postExcerpt = "postExcerpt2", + postImageUrl = "postImageUrl2", + postNumberOfLikesText = "postNumberOfLikesText2", + postNumberOfCommentsText = "postNumberOfCommentsText2", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName3", + postDateLine = "postDateLine3", + postTitle = "postTitle3", + postExcerpt = "postExcerpt3", + postImageUrl = "postImageUrl3", + postNumberOfLikesText = "postNumberOfLikesText3", + postNumberOfCommentsText = "postNumberOfCommentsText3", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName4", + postDateLine = "postDateLine4", + postTitle = "postTitle4", + postExcerpt = "postExcerpt4", + postImageUrl = "postImageUrl4", + postNumberOfLikesText = "postNumberOfLikesText4", + postNumberOfCommentsText = "postNumberOfCommentsText4", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + ), + ), ) ) ) @@ -158,3 +395,14 @@ fun ReaderTagsFeedLoading() { ) } } + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun ReaderTagsFeedEmpty() { + AppTheme { + ReaderTagsFeed( + uiState = UiState.Empty + ) + } +} 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 7aef61f6653b..f105b74bec42 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 @@ -139,6 +139,7 @@ fun ReaderTagsFeedPostListItem( text = postNumberOfLikesText, style = MaterialTheme.typography.bodyMedium, color = secondaryElementColor, + maxLines = 1, ) Spacer(Modifier.height(Margin.Medium.value)) // "•" separator @@ -155,6 +156,7 @@ fun ReaderTagsFeedPostListItem( text = postNumberOfCommentsText, style = MaterialTheme.typography.bodyMedium, color = secondaryElementColor, + maxLines = 1, ) } Spacer(Modifier.height(Margin.Medium.value)) From 4a8a19a14fa661d76a2e637c28e38442bf39ae5f Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:52:13 -0300 Subject: [PATCH 047/237] Change shimmer implementation --- .../ui/compose/components/shimmer/Shimmer.kt | 96 ++++++------------- .../compose/components/shimmer/ShimmerBox.kt | 13 +-- .../HorizontalPostListItemLoading.kt | 6 -- 3 files changed, 32 insertions(+), 83 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt index 7782a19ede72..d0ab8e9ef3b2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt @@ -7,11 +7,17 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.unit.IntSize import org.wordpress.android.ui.compose.theme.AppColor object Shimmer { @@ -20,72 +26,32 @@ object Shimmer { ) } -/** - * Taken from https://github.com/canerkaseler/jetpack-compose-shimmer-loading-animation - */ -internal fun Modifier.shimmerLoadingAnimation( - isLoadingCompleted: Boolean = true, - isLightModeActive: Boolean = true, - widthOfShadowBrush: Int = 500, - angleOfAxisY: Float = 270f, - durationMillis: Int = 1000, -): Modifier { - if (isLoadingCompleted) { - return this - } else { - return composed { - val shimmerColors = ShimmerAnimationData(isLightMode = isLightModeActive).getColours() - - val transition = rememberInfiniteTransition(label = "") - - val translateAnimation = transition.animateFloat( - initialValue = 0f, - targetValue = (durationMillis + widthOfShadowBrush).toFloat(), - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = durationMillis, - easing = LinearEasing, - ), - repeatMode = RepeatMode.Restart, - ), - label = "Shimmer loading animation", - ) - - this.background( - brush = Brush.linearGradient( - colors = shimmerColors, - start = Offset(x = translateAnimation.value - widthOfShadowBrush, y = 0.0f), - end = Offset(x = translateAnimation.value, y = angleOfAxisY), - ), - ) - } +fun Modifier.shimmerLoadingAnimation(): Modifier = composed { + var size by remember { + mutableStateOf(IntSize.Zero) } -} - -internal data class ShimmerAnimationData( - private val isLightMode: Boolean -) { - fun getColours(): List { - return if (isLightMode) { - val color = AppColor.White - - listOf( - color.copy(alpha = 0.3f), - color.copy(alpha = 0.5f), - color.copy(alpha = 1.0f), - color.copy(alpha = 0.5f), - color.copy(alpha = 0.3f), - ) - } else { - val color = AppColor.Black + val transition = rememberInfiniteTransition() + val startOffsetX by transition.animateFloat( + initialValue = -2 * size.width.toFloat(), + targetValue = 2 * size.width.toFloat(), + animationSpec = infiniteRepeatable( + animation = tween(1000) + ), + label = "Shimmer animation", + ) - listOf( - color.copy(alpha = 0.0f), - color.copy(alpha = 0.3f), - color.copy(alpha = 0.5f), - color.copy(alpha = 0.3f), - color.copy(alpha = 0.0f), - ) + background( + brush = Brush.linearGradient( + colors = listOf( + Color(0xFFB8B5B5), + Color(0xFF8F8B8B), + Color(0xFFB8B5B5), + ), + start = Offset(startOffsetX, 0f), + end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat()) + ) + ) + .onGloballyPositioned { + size = it.size } - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt index 20f9c174331e..713ed3a92498 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt @@ -8,21 +8,10 @@ import androidx.compose.ui.Modifier @Composable fun ShimmerBox( modifier: Modifier = Modifier, - isLoadingCompleted: Boolean = true, - isLightModeActive: Boolean = true, - widthOfShadowBrush: Int = 500, - angleOfAxisY: Float = 270f, - durationMillis: Int = 1000, ) { Box( modifier = modifier .background(Shimmer.color) - .shimmerLoadingAnimation( - isLoadingCompleted = isLoadingCompleted, - isLightModeActive = isLightModeActive, - widthOfShadowBrush = widthOfShadowBrush, - angleOfAxisY = angleOfAxisY, - durationMillis = durationMillis, - ) + .shimmerLoadingAnimation() ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt index c236b8ead01a..b2903eb9b866 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt @@ -41,7 +41,6 @@ fun HorizontalPostListItemLoading() { .width(99.dp) .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)), - isLoadingCompleted = false, ) } ShimmerBox( @@ -50,7 +49,6 @@ fun HorizontalPostListItemLoading() { .width(204.dp) .height(18.dp) .clip(shape = RoundedCornerShape(16.dp)), - isLoadingCompleted = false, ) ShimmerBox( modifier = Modifier @@ -58,7 +56,6 @@ fun HorizontalPostListItemLoading() { .width(140.dp) .height(18.dp) .clip(shape = RoundedCornerShape(16.dp)), - isLoadingCompleted = false, ) ShimmerBox( modifier = Modifier @@ -66,7 +63,6 @@ fun HorizontalPostListItemLoading() { .fillMaxWidth() .height(150.dp) .clip(shape = RoundedCornerShape(8.dp)), - isLoadingCompleted = false, ) ShimmerBox( modifier = Modifier @@ -77,7 +73,6 @@ fun HorizontalPostListItemLoading() { .width(170.dp) .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)), - isLoadingCompleted = false, ) ShimmerBox( modifier = Modifier @@ -88,7 +83,6 @@ fun HorizontalPostListItemLoading() { .width(170.dp) .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)), - isLoadingCompleted = false, ) } } From 429fcbfba496591bd47130b5ee5e3a969a7108e3 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:58:25 -0300 Subject: [PATCH 048/237] Fix detekt --- .../wordpress/android/ui/compose/components/shimmer/Shimmer.kt | 3 +-- .../horizontalpostlist/HorizontalPostListItemLoading.kt | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt index d0ab8e9ef3b2..9bb0065be7ec 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt @@ -1,7 +1,5 @@ package org.wordpress.android.ui.compose.components.shimmer -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition @@ -26,6 +24,7 @@ object Shimmer { ) } +@Suppress("MagicNumber") fun Modifier.shimmerLoadingAnimation(): Modifier = composed { var size by remember { mutableStateOf(IntSize.Zero) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt index b2903eb9b866..4f33ed8568a7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt @@ -1,7 +1,6 @@ package org.wordpress.android.ui.reader.views.compose.horizontalpostlist import android.content.res.Configuration -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -21,7 +20,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.ui.compose.components.shimmer.ShimmerBox -import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin From e596f1cf9a73daf7a7c7c2e9633fad26f30a049c Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 17 Apr 2024 18:12:18 -0300 Subject: [PATCH 049/237] Create function to fetch newer posts for tag --- .../reader/repository/ReaderPostRepository.kt | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt index 448ca5860b1e..7e4a98959fee 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt @@ -2,6 +2,7 @@ package org.wordpress.android.ui.reader.repository import com.android.volley.VolleyError import com.wordpress.rest.RestRequest +import kotlinx.coroutines.suspendCancellableCoroutine import org.json.JSONObject import org.wordpress.android.WordPress.Companion.getRestClientUtilsV1_2 import org.wordpress.android.datasets.ReaderPostTable @@ -21,10 +22,30 @@ import org.wordpress.android.util.LocaleManagerWrapper import org.wordpress.android.util.UrlUtils import java.util.Locale import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException class ReaderPostRepository @Inject constructor( private val localeManagerWrapper: LocaleManagerWrapper ) { + /** + * Fetches and returns the most recent posts for the passed tag, respecting the maxPosts limit. + * It always fetches the most recent posts, saves them to the local DB and returns the latest from that cache. + */ + suspend fun fetchNewerPostsForTag(tag: ReaderTag, maxPosts: Int = 7): ReaderPostList { + return suspendCancellableCoroutine { cont -> + val resultListener = UpdateResultListener { result -> + if (result == ReaderActions.UpdateResult.FAILED) { + cont.resumeWithException(Exception("Failed to fetch newer posts for tag")) + } else { + val posts = ReaderPostTable.getPostsWithTag(tag, maxPosts, false) + cont.resume(posts) + } + } + requestPostsWithTag(tag, ReaderPostServiceStarter.UpdateAction.REQUEST_NEWER, resultListener) + } + } + fun requestPostsWithTag( tag: ReaderTag, updateAction: ReaderPostServiceStarter.UpdateAction, @@ -140,7 +161,7 @@ class ReaderPostRepository @Inject constructor( getRestClientUtilsV1_2().getWithLocale(path, null, null, listener, errorListener) } - /* + /** * called after requesting posts with a specific tag or in a specific blog/feed */ private fun handleUpdatePostsResponse( @@ -229,7 +250,7 @@ class ReaderPostRepository @Inject constructor( }.start() } - /* + /** * returns the endpoint to use when requesting posts with the passed tag */ private fun getRelativeEndpointForTag(tag: ReaderTag): String? { @@ -250,7 +271,7 @@ class ReaderPostRepository @Inject constructor( return String.format(Locale.US, "read/tags/%s/posts", ReaderUtils.sanitizeWithDashes(tagSlug)) } - /* + /** * returns the passed endpoint without the unnecessary path - this is * needed because as of 20-Feb-2015 the /read/menu/ call returns the * full path but we don't want to use the full path since it may change From 23a0771a49b31d69e00fcf1fe96698cb0854f4d3 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 17 Apr 2024 18:12:40 -0300 Subject: [PATCH 050/237] Create ViewModel methods to fetch posts for Tags Feed --- .../viewmodels/ReaderTagsFeedViewModel.kt | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt index bc38dbbb331b..0ec39b37a6fd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -1,7 +1,67 @@ package org.wordpress.android.ui.reader.viewmodels -import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import org.wordpress.android.models.ReaderPostList +import org.wordpress.android.models.ReaderTag +import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.reader.repository.ReaderPostRepository +import org.wordpress.android.viewmodel.ScopedViewModel +import javax.inject.Inject +import javax.inject.Named -class ReaderTagsFeedViewModel : ViewModel() { - // TODO implement ViewModel +@HiltViewModel +class ReaderTagsFeedViewModel @Inject constructor( + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, + private val readerPostRepository: ReaderPostRepository, +) : ScopedViewModel(bgDispatcher) { + private val _uiStateFlow = MutableStateFlow(UiState(emptyMap())) + val uiStateFlow: StateFlow = _uiStateFlow + + fun fetchAll() { + FAKE_TAGS.forEach { + fetchTag(it) + } + } + + fun fetchTag(tag: ReaderTag) { + launch { + _uiStateFlow.update { + it.copy(tagStates = it.tagStates + (tag to FetchState.Loading)) + } + + try { + val posts = readerPostRepository.fetchNewerPostsForTag(tag) + _uiStateFlow.update { + it.copy(tagStates = it.tagStates + (tag to FetchState.Success(posts))) + } + } catch (e: Exception) { + _uiStateFlow.update { + it.copy(tagStates = it.tagStates + (tag to FetchState.Error)) + } + } + } + } + + data class UiState( + val tagStates: Map, + ) + + sealed class FetchState { + data object Loading : FetchState() + data object Error : FetchState() + data class Success(val posts: ReaderPostList) : FetchState() + } + + companion object { + private val FAKE_TAGS = listOf( + ReaderTag("science", "Science", "Science", null, ReaderTagType.FOLLOWED), + ReaderTag("fiction", "Fiction", "Fiction", null, ReaderTagType.FOLLOWED), + ReaderTag("rpg", "RPG", "RPG", null, ReaderTagType.FOLLOWED), + ) + } } From d95ab6ff33364dcebf95db1190118db1cfcb75c3 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 17 Apr 2024 18:13:02 -0300 Subject: [PATCH 051/237] Create temporary UI for testing tag fetching --- .../ui/reader/ReaderTagsFeedFragment.kt | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) 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 c4ee6df6bb14..82b814782c21 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 @@ -2,11 +2,31 @@ package org.wordpress.android.ui.reader import android.os.Bundle import android.view.View +import androidx.compose.foundation.background +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.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.TextOverflow +import androidx.compose.ui.unit.dp import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.databinding.ReaderTagFeedFragmentLayoutBinding 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.viewmodels.ReaderTagsFeedViewModel @@ -29,6 +49,15 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme 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) + } + } + + viewModel.fetchAll() } override fun getScrollableViewForUniqueIdProvision(): View { @@ -39,3 +68,82 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme // TODO scroll current content to top } } + +/** + * Throwaway UI code just for testing the initial Tags Feed fetching code. + */ +@Composable +private fun ReaderTagsFeedScreen( + uiState: ReaderTagsFeedViewModel.UiState, +) { + AppThemeWithoutBackground { + Column( + modifier = Modifier + .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 -> { + Text( + text = "Error loading posts", + style = MaterialTheme.typography.body1, + ) + } + + 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, + ) + } + } + } + } + } + } + } + } + } +} From 527a689ed30705716895b973cbfc21513b5112bc Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 17 Apr 2024 19:20:28 -0300 Subject: [PATCH 052/237] Add onTagClicked parameter to TagsFeedItem --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) 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 cfcba55435c5..8659978e752e 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 @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items @@ -16,6 +17,10 @@ import androidx.compose.ui.unit.dp import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.unit.Margin +import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterChip +import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterType +import org.wordpress.android.ui.utils.UiString @Composable fun ReaderTagsFeed(uiState: UiState) { @@ -26,8 +31,8 @@ fun ReaderTagsFeed(uiState: UiState) { ) { when (uiState) { is UiState.Loaded -> Loaded(uiState) - is UiState.Loading -> Loading() - is UiState.Empty -> Empty() + UiState.Loading -> Loading() + UiState.Empty -> Empty() } } } @@ -37,18 +42,26 @@ private fun Loaded(uiState: UiState.Loaded) { LazyColumn( modifier = Modifier .fillMaxSize() + .padding( + start = Margin.Large.value, + end = Margin.Large.value, + ) ) { items( items = uiState.items, key = { it.tag.tagDisplayName }, ) { tagsFeedItem -> + ReaderFilterChip( + text = UiString.UiStringText(tagsFeedItem.tag.tagTitle), + onClick = tagsFeedItem.onTagClicked, + height = 36.dp, + ) when (tagsFeedItem) { // If item is Success, show posts list is TagsFeedItem.Success -> { LazyRow( modifier = Modifier .fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 24.dp), ) { items( items = tagsFeedItem.posts, @@ -109,15 +122,18 @@ sealed class UiState { sealed class TagsFeedItem( open val tag: ReaderTag, + open val onTagClicked: () -> Unit, ) { data class Success( override val tag: ReaderTag, + override val onTagClicked: () -> Unit, val posts: List, - ) : TagsFeedItem(tag) + ) : TagsFeedItem(tag, onTagClicked) data class Error( override val tag: ReaderTag, - ) : TagsFeedItem(tag) + override val onTagClicked: () -> Unit, + ) : TagsFeedItem(tag, onTagClicked) } data class TagsFeedPostItem( @@ -217,6 +233,7 @@ fun ReaderTagsFeedLoaded() { onPostMoreMenuClick = {}, ), ), + onTagClicked = {}, ), TagsFeedItem.Success( tag = ReaderTag( @@ -293,6 +310,7 @@ fun ReaderTagsFeedLoaded() { onPostMoreMenuClick = {}, ), ), + onTagClicked = {}, ), TagsFeedItem.Error( tag = ReaderTag( @@ -302,6 +320,7 @@ fun ReaderTagsFeedLoaded() { "Tag 3", ReaderTagType.TAGS, ), + onTagClicked = {}, ), TagsFeedItem.Success( tag = ReaderTag( @@ -378,6 +397,7 @@ fun ReaderTagsFeedLoaded() { onPostMoreMenuClick = {}, ), ), + onTagClicked = {}, ), ) ) From 5c592c683f00286bb1f6a4d4b3aaf21392be1692 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 17 Apr 2024 21:39:59 -0300 Subject: [PATCH 053/237] Fix horizontal list item spacing --- .../ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 8659978e752e..d7af3c066431 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 @@ -1,11 +1,14 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow @@ -51,17 +54,20 @@ private fun Loaded(uiState: UiState.Loaded) { items = uiState.items, key = { it.tag.tagDisplayName }, ) { tagsFeedItem -> + Spacer(modifier = Modifier.height(Margin.Large.value)) ReaderFilterChip( text = UiString.UiStringText(tagsFeedItem.tag.tagTitle), onClick = tagsFeedItem.onTagClicked, height = 36.dp, ) + Spacer(modifier = Modifier.height(Margin.Large.value)) when (tagsFeedItem) { // If item is Success, show posts list is TagsFeedItem.Success -> { LazyRow( modifier = Modifier .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), ) { items( items = tagsFeedItem.posts, From 831dbdf4eefa2bdf57fde08bb830e73a478eb31f Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:08:25 -0300 Subject: [PATCH 054/237] Implement ReaderTagsFeed loading state --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 180 +++--------------- 1 file changed, 22 insertions(+), 158 deletions(-) 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 d7af3c066431..2da041a5f893 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 @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items @@ -100,11 +101,28 @@ private fun Loaded(uiState: UiState.Loaded) { @Composable private fun Loading() { - LazyRow( - modifier = Modifier - .fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 24.dp), + LazyColumn( + modifier = Modifier.fillMaxSize(), + userScrollEnabled = false, ) { + val numberOfLoadingRows = 3 + repeat(numberOfLoadingRows) { + item { + LazyRow( + modifier = Modifier + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 12.dp), + userScrollEnabled = false, + ) { + item { + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(12.dp)) + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(12.dp)) + } + } + } + } } } @@ -241,83 +259,6 @@ fun ReaderTagsFeedLoaded() { ), onTagClicked = {}, ), - TagsFeedItem.Success( - tag = ReaderTag( - "Tag 2", - "Tag 2", - "Tag 2", - "Tag 2", - ReaderTagType.TAGS, - ), - posts = listOf( - TagsFeedPostItem( - siteName = "siteName1", - postDateLine = "postDateLine1", - postTitle = "postTitle1", - postExcerpt = "postExcerpt1", - postImageUrl = "postImageUrl1", - postNumberOfLikesText = "postNumberOfLikesText1", - postNumberOfCommentsText = "postNumberOfCommentsText1", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName2", - postDateLine = "postDateLine2", - postTitle = "postTitle2", - postExcerpt = "postExcerpt2", - postImageUrl = "postImageUrl2", - postNumberOfLikesText = "postNumberOfLikesText2", - postNumberOfCommentsText = "postNumberOfCommentsText2", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName2", - postDateLine = "postDateLine2", - postTitle = "postTitle2", - postExcerpt = "postExcerpt2", - postImageUrl = "postImageUrl2", - postNumberOfLikesText = "postNumberOfLikesText2", - postNumberOfCommentsText = "postNumberOfCommentsText2", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName3", - postDateLine = "postDateLine3", - postTitle = "postTitle3", - postExcerpt = "postExcerpt3", - postImageUrl = "postImageUrl3", - postNumberOfLikesText = "postNumberOfLikesText3", - postNumberOfCommentsText = "postNumberOfCommentsText3", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName4", - postDateLine = "postDateLine4", - postTitle = "postTitle4", - postExcerpt = "postExcerpt4", - postImageUrl = "postImageUrl4", - postNumberOfLikesText = "postNumberOfLikesText4", - postNumberOfCommentsText = "postNumberOfCommentsText4", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - ), - onTagClicked = {}, - ), TagsFeedItem.Error( tag = ReaderTag( "Tag 3", @@ -328,83 +269,6 @@ fun ReaderTagsFeedLoaded() { ), onTagClicked = {}, ), - TagsFeedItem.Success( - tag = ReaderTag( - "Tag 4", - "Tag 4", - "Tag 4", - "Tag 4", - ReaderTagType.TAGS, - ), - posts = listOf( - TagsFeedPostItem( - siteName = "siteName1", - postDateLine = "postDateLine1", - postTitle = "postTitle1", - postExcerpt = "postExcerpt1", - postImageUrl = "postImageUrl1", - postNumberOfLikesText = "postNumberOfLikesText1", - postNumberOfCommentsText = "postNumberOfCommentsText1", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName2", - postDateLine = "postDateLine2", - postTitle = "postTitle2", - postExcerpt = "postExcerpt2", - postImageUrl = "postImageUrl2", - postNumberOfLikesText = "postNumberOfLikesText2", - postNumberOfCommentsText = "postNumberOfCommentsText2", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName2", - postDateLine = "postDateLine2", - postTitle = "postTitle2", - postExcerpt = "postExcerpt2", - postImageUrl = "postImageUrl2", - postNumberOfLikesText = "postNumberOfLikesText2", - postNumberOfCommentsText = "postNumberOfCommentsText2", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName3", - postDateLine = "postDateLine3", - postTitle = "postTitle3", - postExcerpt = "postExcerpt3", - postImageUrl = "postImageUrl3", - postNumberOfLikesText = "postNumberOfLikesText3", - postNumberOfCommentsText = "postNumberOfCommentsText3", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName4", - postDateLine = "postDateLine4", - postTitle = "postTitle4", - postExcerpt = "postExcerpt4", - postImageUrl = "postImageUrl4", - postNumberOfLikesText = "postNumberOfLikesText4", - postNumberOfCommentsText = "postNumberOfCommentsText4", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - ), - onTagClicked = {}, - ), ) ) ) From aa2b46fdb98fd6acac7aea40eec5ec93bb810d2b Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 18 Apr 2024 01:23:57 -0300 Subject: [PATCH 055/237] Implement loading posts and tags loaded state for ReaderTagsFeed --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 206 +++++++++++++++--- .../ReaderTagsFeedPostListItemLoading.kt | 4 +- 2 files changed, 171 insertions(+), 39 deletions(-) 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 2da041a5f893..5b896fe36dbc 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 @@ -14,16 +14,18 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.ui.compose.components.shimmer.ShimmerBox import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterChip -import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterType import org.wordpress.android.ui.utils.UiString @Composable @@ -34,15 +36,90 @@ fun ReaderTagsFeed(uiState: UiState) { .fillMaxHeight(), ) { when (uiState) { - is UiState.Loaded -> Loaded(uiState) - UiState.Loading -> Loading() - UiState.Empty -> Empty() + is UiState.LoadingTagsAndPosts -> LoadingTagsAndPosts() + is UiState.LoadedTagsLoadingPosts -> LoadedTagsLoadingPosts(uiState) + is UiState.LoadedTagsAndPosts -> LoadedTagsAndPosts(uiState) + is UiState.Empty -> Empty() } } } @Composable -private fun Loaded(uiState: UiState.Loaded) { +private fun LoadingTagsAndPosts() { + LazyColumn( + modifier = Modifier.fillMaxSize(), + userScrollEnabled = false, + ) { + val numberOfLoadingRows = 3 + repeat(numberOfLoadingRows) { + item { + Spacer(modifier = Modifier.height(Margin.Large.value)) + ShimmerBox( + modifier = Modifier + .padding(start = Margin.Large.value) + .width(75.dp) + .height(36.dp) + .clip(shape = RoundedCornerShape(16.dp)), + ) + + Spacer(modifier = Modifier.height(Margin.Large.value)) + LazyRow( + modifier = Modifier + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 12.dp), + userScrollEnabled = false, + ) { + item { + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(12.dp)) + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(12.dp)) + } + } + } + } + } +} + +@Composable +private fun LoadedTagsLoadingPosts(uiState: UiState.LoadedTagsLoadingPosts) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding( + start = Margin.Large.value, + end = Margin.Large.value, + ), + userScrollEnabled = false, + ) { + uiState.items.forEach { + item { + Spacer(modifier = Modifier.height(Margin.Large.value)) + ReaderFilterChip( + text = UiString.UiStringText(it.tag.tagTitle), + onClick = it.onTagClicked, + height = 36.dp, + ) + Spacer(modifier = Modifier.height(Margin.Large.value)) + LazyRow( + modifier = Modifier + .fillMaxWidth(), + userScrollEnabled = false, + ) { + item { + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(12.dp)) + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(12.dp)) + } + } + } + } + } +} + +@Composable +private fun LoadedTagsAndPosts(uiState: UiState.LoadedTagsAndPosts) { LazyColumn( modifier = Modifier .fillMaxSize() @@ -64,7 +141,7 @@ private fun Loaded(uiState: UiState.Loaded) { Spacer(modifier = Modifier.height(Margin.Large.value)) when (tagsFeedItem) { // If item is Success, show posts list - is TagsFeedItem.Success -> { + is TagsFeedItem.Loaded.Success -> { LazyRow( modifier = Modifier .fillMaxWidth(), @@ -92,7 +169,7 @@ private fun Loaded(uiState: UiState.Loaded) { } } // If item is Error, show error UI and retry button - is TagsFeedItem.Error -> { + is TagsFeedItem.Loaded.Error -> { } } } @@ -133,33 +210,46 @@ private fun Empty() { // TODO move to VM -sealed class UiState { - // TODO review Loaded parameters - data class Loaded( - val items: List, - ) : UiState() - - object Loading : UiState() - - object Empty : UiState() -} - sealed class TagsFeedItem( open val tag: ReaderTag, open val onTagClicked: () -> Unit, ) { - data class Success( + sealed class Loaded( override val tag: ReaderTag, override val onTagClicked: () -> Unit, - val posts: List, - ) : TagsFeedItem(tag, onTagClicked) + ) : TagsFeedItem(tag, onTagClicked) { + data class Success( + override val tag: ReaderTag, + override val onTagClicked: () -> Unit, + val posts: List, + ) : Loaded(tag, onTagClicked) + + data class Error( + override val tag: ReaderTag, + override val onTagClicked: () -> Unit, + ) : Loaded(tag, onTagClicked) + } - data class Error( + data class Loading( override val tag: ReaderTag, override val onTagClicked: () -> Unit, ) : TagsFeedItem(tag, onTagClicked) } +sealed class UiState { + object LoadingTagsAndPosts : UiState() + + data class LoadedTagsLoadingPosts( + val items: List, + ) : UiState() + + data class LoadedTagsAndPosts( + val items: List, + ) : UiState() + + object Empty : UiState() +} + data class TagsFeedPostItem( val siteName: String, val postDateLine: String, @@ -177,17 +267,17 @@ data class TagsFeedPostItem( @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun ReaderTagsFeedLoaded() { +fun ReaderTagsFeedLoadedTagsAndPosts() { AppTheme { ReaderTagsFeed( - uiState = UiState.Loaded( + uiState = UiState.LoadedTagsAndPosts( items = listOf( - TagsFeedItem.Success( + TagsFeedItem.Loaded.Success( tag = ReaderTag( - "Tag 1", - "Tag 1", - "Tag 1", - "Tag 1", + "Tag Loaded Success", + "Tag Loaded Success", + "Tag Loaded Success", + "Tag Loaded Success", ReaderTagType.TAGS, ), posts = listOf( @@ -259,12 +349,12 @@ fun ReaderTagsFeedLoaded() { ), onTagClicked = {}, ), - TagsFeedItem.Error( + TagsFeedItem.Loaded.Error( tag = ReaderTag( - "Tag 3", - "Tag 3", - "Tag 3", - "Tag 3", + "Tag Loaded Error", + "Tag Loaded Error", + "Tag Loaded Error", + "Tag Loaded Error", ReaderTagType.TAGS, ), onTagClicked = {}, @@ -278,10 +368,54 @@ fun ReaderTagsFeedLoaded() { @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun ReaderTagsFeedLoading() { +fun ReaderTagsFeedLoadingTagsAndPosts() { AppTheme { ReaderTagsFeed( - uiState = UiState.Loading + uiState = UiState.LoadingTagsAndPosts + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun ReaderTagsFeedLoadedTagsLoadingPosts() { + AppTheme { + ReaderTagsFeed( + uiState = UiState.LoadedTagsLoadingPosts( + items = listOf( + TagsFeedItem.Loading( + tag = ReaderTag( + "Tag 1", + "Tag 1", + "Tag 1", + "Tag 1", + ReaderTagType.TAGS, + ), + onTagClicked = {}, + ), + TagsFeedItem.Loading( + tag = ReaderTag( + "Tag 2", + "Tag 2", + "Tag 2", + "Tag 2", + ReaderTagType.TAGS, + ), + onTagClicked = {}, + ), + TagsFeedItem.Loading( + tag = ReaderTag( + "Tag 3", + "Tag 3", + "Tag 3", + "Tag 3", + ReaderTagType.TAGS, + ), + onTagClicked = {}, + ) + ) + ) ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt index 5db459a98768..9dae94e7bfb5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt @@ -97,9 +97,7 @@ fun ReaderTagsFeedPostListItemLoadingPreview() { ) { LazyRow( modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, bottom = 16.dp), - contentPadding = PaddingValues(horizontal = 12.dp), + .fillMaxWidth(), ) { item { ReaderTagsFeedPostListItemLoading() From 3ae8b583b1ef2195c57412322e50b82012b5c76a Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:55:37 -0300 Subject: [PATCH 056/237] Remove shimmer effect --- .../ui/compose/components/shimmer/Shimmer.kt | 56 ------------------- .../compose/components/shimmer/ShimmerBox.kt | 17 ------ .../HorizontalPostListItemLoading.kt | 36 +++++++----- 3 files changed, 23 insertions(+), 86 deletions(-) delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt deleted file mode 100644 index 9bb0065be7ec..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/Shimmer.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.wordpress.android.ui.compose.components.shimmer - -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.unit.IntSize -import org.wordpress.android.ui.compose.theme.AppColor - -object Shimmer { - val color = AppColor.Black.copy( - alpha = 0.08F - ) -} - -@Suppress("MagicNumber") -fun Modifier.shimmerLoadingAnimation(): Modifier = composed { - var size by remember { - mutableStateOf(IntSize.Zero) - } - val transition = rememberInfiniteTransition() - val startOffsetX by transition.animateFloat( - initialValue = -2 * size.width.toFloat(), - targetValue = 2 * size.width.toFloat(), - animationSpec = infiniteRepeatable( - animation = tween(1000) - ), - label = "Shimmer animation", - ) - - background( - brush = Brush.linearGradient( - colors = listOf( - Color(0xFFB8B5B5), - Color(0xFF8F8B8B), - Color(0xFFB8B5B5), - ), - start = Offset(startOffsetX, 0f), - end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat()) - ) - ) - .onGloballyPositioned { - size = it.size - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt deleted file mode 100644 index 713ed3a92498..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/shimmer/ShimmerBox.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.wordpress.android.ui.compose.components.shimmer - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun ShimmerBox( - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .background(Shimmer.color) - .shimmerLoadingAnimation() - ) -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt index 4f33ed8568a7..08ab7386d2d7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt @@ -1,6 +1,7 @@ package org.wordpress.android.ui.reader.views.compose.horizontalpostlist import android.content.res.Configuration +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -17,14 +18,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.colorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.wordpress.android.ui.compose.components.shimmer.ShimmerBox +import org.wordpress.android.R +import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin @Composable fun HorizontalPostListItemLoading() { + val backgroundColor = AppColor.Black.copy(alpha = 0.08F) Column( modifier = Modifier .width(240.dp) @@ -34,35 +38,39 @@ fun HorizontalPostListItemLoading() { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - ShimmerBox( + Box( modifier = Modifier .width(99.dp) .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)), + .clip(shape = RoundedCornerShape(16.dp)) + .background(backgroundColor), ) } - ShimmerBox( + Box( modifier = Modifier .padding(top = Margin.Large.value) .width(204.dp) .height(18.dp) - .clip(shape = RoundedCornerShape(16.dp)), + .clip(shape = RoundedCornerShape(16.dp)) + .background(backgroundColor), ) - ShimmerBox( + Box( modifier = Modifier .padding(top = Margin.Large.value) .width(140.dp) .height(18.dp) - .clip(shape = RoundedCornerShape(16.dp)), + .clip(shape = RoundedCornerShape(16.dp)) + .background(backgroundColor), ) - ShimmerBox( + Box( modifier = Modifier .padding(top = Margin.Large.value) .fillMaxWidth() .height(150.dp) - .clip(shape = RoundedCornerShape(8.dp)), + .clip(shape = RoundedCornerShape(8.dp)) + .background(backgroundColor), ) - ShimmerBox( + Box( modifier = Modifier .padding( start = Margin.Small.value, @@ -70,9 +78,10 @@ fun HorizontalPostListItemLoading() { ) .width(170.dp) .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)), + .clip(shape = RoundedCornerShape(16.dp)) + .background(backgroundColor), ) - ShimmerBox( + Box( modifier = Modifier .padding( start = Margin.Small.value, @@ -80,7 +89,8 @@ fun HorizontalPostListItemLoading() { ) .width(170.dp) .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)), + .clip(shape = RoundedCornerShape(16.dp)) + .background(backgroundColor), ) } } From 3c1a7c71a5afe37ebd8ab84859015d99bfd8e66c Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:02:13 -0300 Subject: [PATCH 057/237] Replace ShimmerBox with Box in ReaderTagsFeed --- .../ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 5b896fe36dbc..64dc47c8798d 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 @@ -1,6 +1,7 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -22,7 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType -import org.wordpress.android.ui.compose.components.shimmer.ShimmerBox +import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterChip @@ -54,12 +55,13 @@ private fun LoadingTagsAndPosts() { repeat(numberOfLoadingRows) { item { Spacer(modifier = Modifier.height(Margin.Large.value)) - ShimmerBox( + Box( modifier = Modifier .padding(start = Margin.Large.value) .width(75.dp) .height(36.dp) - .clip(shape = RoundedCornerShape(16.dp)), + .clip(shape = RoundedCornerShape(16.dp)) + .background(AppColor.Black.copy(alpha = 0.08F)), ) Spacer(modifier = Modifier.height(Margin.Large.value)) From c54757cf50efd3319a4e7fdff8bb436dbe283e77 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:00:55 -0300 Subject: [PATCH 058/237] Fix detekt --- .../compose/horizontalpostlist/HorizontalPostListItemLoading.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt index 08ab7386d2d7..9318c577756a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/horizontalpostlist/HorizontalPostListItemLoading.kt @@ -18,10 +18,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.colorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin From 18d6b51f93ddd08239eff490acd1806200e44b0a Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 18 Apr 2024 20:01:57 -0300 Subject: [PATCH 059/237] Fix reader tags feed loading color for dark theme --- .../ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 2 +- .../compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) 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 64dc47c8798d..faf41cbcccdd 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 @@ -205,9 +205,9 @@ private fun Loading() { } } -// TODO empty state (https://github.com/wordpress-mobile/WordPress-Android/issues/20584) @Composable private fun Empty() { +// TODO empty state (https://github.com/wordpress-mobile/WordPress-Android/issues/20584) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt index 9782721054d8..5f58c3164ff9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt @@ -2,6 +2,7 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -25,7 +26,11 @@ import org.wordpress.android.ui.compose.unit.Margin @Composable fun ReaderTagsFeedPostListItemLoading() { - val backgroundColor = AppColor.Black.copy(alpha = 0.08F) + val backgroundColor = if (isSystemInDarkTheme()) { + AppColor.White.copy(alpha = 0.12F) + } else { + AppColor.Black.copy(alpha = 0.08F) + } Column( modifier = Modifier .width(240.dp) From a123b3221edf8cccd7cb9cb449d58c8cb5b633f0 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 18 Apr 2024 20:19:40 -0300 Subject: [PATCH 060/237] Fix reader tag button loading background color --- .../ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 faf41cbcccdd..ced8e5636d03 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 @@ -2,6 +2,7 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -54,6 +55,11 @@ private fun LoadingTagsAndPosts() { val numberOfLoadingRows = 3 repeat(numberOfLoadingRows) { item { + val backgroundColor = if (isSystemInDarkTheme()) { + AppColor.White.copy(alpha = 0.12F) + } else { + AppColor.Black.copy(alpha = 0.08F) + } Spacer(modifier = Modifier.height(Margin.Large.value)) Box( modifier = Modifier @@ -61,7 +67,7 @@ private fun LoadingTagsAndPosts() { .width(75.dp) .height(36.dp) .clip(shape = RoundedCornerShape(16.dp)) - .background(AppColor.Black.copy(alpha = 0.08F)), + .background(backgroundColor), ) Spacer(modifier = Modifier.height(Margin.Large.value)) From 5e0eeae90e493e95c52ff64ee3a5cf1a45ddb3a8 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 18 Apr 2024 23:00:15 -0300 Subject: [PATCH 061/237] Refactor ReaderTagsFeed UI state --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 486 +++++++----------- 1 file changed, 196 insertions(+), 290 deletions(-) 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 ced8e5636d03..53d39dcb2e67 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 @@ -38,96 +38,15 @@ fun ReaderTagsFeed(uiState: UiState) { .fillMaxHeight(), ) { when (uiState) { - is UiState.LoadingTagsAndPosts -> LoadingTagsAndPosts() - is UiState.LoadedTagsLoadingPosts -> LoadedTagsLoadingPosts(uiState) - is UiState.LoadedTagsAndPosts -> LoadedTagsAndPosts(uiState) + is UiState.Loading -> LoadingTagsAndPosts() + is UiState.Loaded -> Loaded(uiState) is UiState.Empty -> Empty() } } } @Composable -private fun LoadingTagsAndPosts() { - LazyColumn( - modifier = Modifier.fillMaxSize(), - userScrollEnabled = false, - ) { - val numberOfLoadingRows = 3 - repeat(numberOfLoadingRows) { - item { - val backgroundColor = if (isSystemInDarkTheme()) { - AppColor.White.copy(alpha = 0.12F) - } else { - AppColor.Black.copy(alpha = 0.08F) - } - Spacer(modifier = Modifier.height(Margin.Large.value)) - Box( - modifier = Modifier - .padding(start = Margin.Large.value) - .width(75.dp) - .height(36.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(backgroundColor), - ) - - Spacer(modifier = Modifier.height(Margin.Large.value)) - LazyRow( - modifier = Modifier - .fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 12.dp), - userScrollEnabled = false, - ) { - item { - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) - } - } - } - } - } -} - -@Composable -private fun LoadedTagsLoadingPosts(uiState: UiState.LoadedTagsLoadingPosts) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding( - start = Margin.Large.value, - end = Margin.Large.value, - ), - userScrollEnabled = false, - ) { - uiState.items.forEach { - item { - Spacer(modifier = Modifier.height(Margin.Large.value)) - ReaderFilterChip( - text = UiString.UiStringText(it.tag.tagTitle), - onClick = it.onTagClicked, - height = 36.dp, - ) - Spacer(modifier = Modifier.height(Margin.Large.value)) - LazyRow( - modifier = Modifier - .fillMaxWidth(), - userScrollEnabled = false, - ) { - item { - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) - } - } - } - } - } -} - -@Composable -private fun LoadedTagsAndPosts(uiState: UiState.LoadedTagsAndPosts) { +private fun Loaded(uiState: UiState.Loaded) { LazyColumn( modifier = Modifier .fillMaxSize() @@ -137,47 +56,85 @@ private fun LoadedTagsAndPosts(uiState: UiState.LoadedTagsAndPosts) { ) ) { items( - items = uiState.items, - key = { it.tag.tagDisplayName }, - ) { tagsFeedItem -> - Spacer(modifier = Modifier.height(Margin.Large.value)) - ReaderFilterChip( - text = UiString.UiStringText(tagsFeedItem.tag.tagTitle), - onClick = tagsFeedItem.onTagClicked, - height = 36.dp, - ) + items = uiState.data, + ) { (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)) - when (tagsFeedItem) { - // If item is Success, show posts list - is TagsFeedItem.Loaded.Success -> { - LazyRow( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), - ) { - items( - items = tagsFeedItem.posts, - ) { postItem -> - with(postItem) { - ReaderTagsFeedPostListItem( - siteName = siteName, - postDateLine = postDateLine, - postTitle = postTitle, - postExcerpt = postExcerpt, - postImageUrl = postImageUrl, - postNumberOfLikesText = postNumberOfLikesText, - postNumberOfCommentsText = postNumberOfCommentsText, - isPostLiked = isPostLiked, - onPostImageClick = onPostImageClick, - onPostLikeClick = onPostLikeClick, - onPostMoreMenuClick = onPostMoreMenuClick, - ) + with(uiState) { + // Tag chip UI + when (tagChip) { + is TagChip.Loading -> { + Box( + modifier = Modifier + .padding(start = Margin.Large.value) + .width(75.dp) + .height(36.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(backgroundColor), + ) + } + + is TagChip.Loaded -> { + ReaderFilterChip( + text = UiString.UiStringText(tagChip.tag.tagTitle), + onClick = tagChip.onTagClicked, + height = 36.dp, + ) + } + } + Spacer(modifier = Modifier.height(Margin.Large.value)) + // Posts list UI + when (postList) { + is PostList.Loading -> { + LazyRow( + modifier = Modifier + .fillMaxWidth(), + userScrollEnabled = false, + ) { + item { + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(12.dp)) + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(12.dp)) } } } - } - // If item is Error, show error UI and retry button - is TagsFeedItem.Loaded.Error -> { + + is PostList.Loaded -> { + LazyRow( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), + ) { + items( + items = postList.items, + ) { postItem -> + with(postItem) { + ReaderTagsFeedPostListItem( + siteName = siteName, + postDateLine = postDateLine, + postTitle = postTitle, + postExcerpt = postExcerpt, + postImageUrl = postImageUrl, + postNumberOfLikesText = postNumberOfLikesText, + postNumberOfCommentsText = postNumberOfCommentsText, + isPostLiked = isPostLiked, + onPostImageClick = onPostImageClick, + onPostLikeClick = onPostLikeClick, + onPostMoreMenuClick = onPostMoreMenuClick, + ) + } + } + } + } + + is PostList.Error -> { +// TODO() + } } } } @@ -185,7 +142,7 @@ private fun LoadedTagsAndPosts(uiState: UiState.LoadedTagsAndPosts) { } @Composable -private fun Loading() { +private fun LoadingTagsAndPosts() { LazyColumn( modifier = Modifier.fillMaxSize(), userScrollEnabled = false, @@ -193,6 +150,22 @@ private fun Loading() { val numberOfLoadingRows = 3 repeat(numberOfLoadingRows) { item { + val backgroundColor = if (isSystemInDarkTheme()) { + AppColor.White.copy(alpha = 0.12F) + } else { + AppColor.Black.copy(alpha = 0.08F) + } + Spacer(modifier = Modifier.height(Margin.Large.value)) + Box( + modifier = Modifier + .padding(start = Margin.Large.value) + .width(75.dp) + .height(36.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(backgroundColor), + ) + + Spacer(modifier = Modifier.height(Margin.Large.value)) LazyRow( modifier = Modifier .fillMaxWidth(), @@ -216,46 +189,30 @@ private fun Empty() { // TODO empty state (https://github.com/wordpress-mobile/WordPress-Android/issues/20584) } - // TODO move to VM -sealed class TagsFeedItem( - open val tag: ReaderTag, - open val onTagClicked: () -> Unit, -) { - sealed class Loaded( - override val tag: ReaderTag, - override val onTagClicked: () -> Unit, - ) : TagsFeedItem(tag, onTagClicked) { - data class Success( - override val tag: ReaderTag, - override val onTagClicked: () -> Unit, - val posts: List, - ) : Loaded(tag, onTagClicked) +sealed class UiState { + data class Loaded(val data: List>) : UiState() - data class Error( - override val tag: ReaderTag, - override val onTagClicked: () -> Unit, - ) : Loaded(tag, onTagClicked) - } + object Loading : UiState() - data class Loading( - override val tag: ReaderTag, - override val onTagClicked: () -> Unit, - ) : TagsFeedItem(tag, onTagClicked) + object Empty : UiState() } -sealed class UiState { - object LoadingTagsAndPosts : UiState() +sealed class TagChip { + data class Loaded( + val tag: ReaderTag, + val onTagClicked: () -> Unit, + ) : TagChip() - data class LoadedTagsLoadingPosts( - val items: List, - ) : UiState() + object Loading : TagChip() +} - data class LoadedTagsAndPosts( - val items: List, - ) : UiState() +sealed class PostList { + data class Loaded(val items: List) : PostList() - object Empty : UiState() + object Loading : PostList() + + object Error : PostList() } data class TagsFeedPostItem( @@ -275,111 +232,93 @@ data class TagsFeedPostItem( @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun ReaderTagsFeedLoadedTagsAndPosts() { +fun ReaderTagsFeedLoaded() { AppTheme { - ReaderTagsFeed( - uiState = UiState.LoadedTagsAndPosts( - items = listOf( - TagsFeedItem.Loaded.Success( - tag = ReaderTag( - "Tag Loaded Success", - "Tag Loaded Success", - "Tag Loaded Success", - "Tag Loaded Success", - ReaderTagType.TAGS, - ), - posts = listOf( - TagsFeedPostItem( - siteName = "siteName1", - postDateLine = "postDateLine1", - postTitle = "postTitle1", - postExcerpt = "postExcerpt1", - postImageUrl = "postImageUrl1", - postNumberOfLikesText = "postNumberOfLikesText1", - postNumberOfCommentsText = "postNumberOfCommentsText1", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName2", - postDateLine = "postDateLine2", - postTitle = "postTitle2", - postExcerpt = "postExcerpt2", - postImageUrl = "postImageUrl2", - postNumberOfLikesText = "postNumberOfLikesText2", - postNumberOfCommentsText = "postNumberOfCommentsText2", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName2", - postDateLine = "postDateLine2", - postTitle = "postTitle2", - postExcerpt = "postExcerpt2", - postImageUrl = "postImageUrl2", - postNumberOfLikesText = "postNumberOfLikesText2", - postNumberOfCommentsText = "postNumberOfCommentsText2", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName3", - postDateLine = "postDateLine3", - postTitle = "postTitle3", - postExcerpt = "postExcerpt3", - postImageUrl = "postImageUrl3", - postNumberOfLikesText = "postNumberOfLikesText3", - postNumberOfCommentsText = "postNumberOfCommentsText3", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - TagsFeedPostItem( - siteName = "siteName4", - postDateLine = "postDateLine4", - postTitle = "postTitle4", - postExcerpt = "postExcerpt4", - postImageUrl = "postImageUrl4", - postNumberOfLikesText = "postNumberOfLikesText4", - postNumberOfCommentsText = "postNumberOfCommentsText4", - isPostLiked = true, - onPostImageClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, - ), - ), - onTagClicked = {}, - ), - TagsFeedItem.Loaded.Error( - tag = ReaderTag( - "Tag Loaded Error", - "Tag Loaded Error", - "Tag Loaded Error", - "Tag Loaded Error", - ReaderTagType.TAGS, - ), - onTagClicked = {}, - ), - ) + val postListLoaded = PostList.Loaded( + listOf( + TagsFeedPostItem( + siteName = "siteName1", + postDateLine = "postDateLine1", + postTitle = "postTitle1", + postExcerpt = "postExcerpt1", + postImageUrl = "postImageUrl1", + postNumberOfLikesText = "postNumberOfLikesText1", + postNumberOfCommentsText = "postNumberOfCommentsText1", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName2", + postDateLine = "postDateLine2", + postTitle = "postTitle2", + postExcerpt = "postExcerpt2", + postImageUrl = "postImageUrl2", + postNumberOfLikesText = "postNumberOfLikesText2", + postNumberOfCommentsText = "postNumberOfCommentsText2", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName2", + postDateLine = "postDateLine2", + postTitle = "postTitle2", + postExcerpt = "postExcerpt2", + postImageUrl = "postImageUrl2", + postNumberOfLikesText = "postNumberOfLikesText2", + postNumberOfCommentsText = "postNumberOfCommentsText2", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName3", + postDateLine = "postDateLine3", + postTitle = "postTitle3", + postExcerpt = "postExcerpt3", + postImageUrl = "postImageUrl3", + postNumberOfLikesText = "postNumberOfLikesText3", + postNumberOfCommentsText = "postNumberOfCommentsText3", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), + TagsFeedPostItem( + siteName = "siteName4", + postDateLine = "postDateLine4", + postTitle = "postTitle4", + postExcerpt = "postExcerpt4", + postImageUrl = "postImageUrl4", + postNumberOfLikesText = "postNumberOfLikesText4", + postNumberOfCommentsText = "postNumberOfCommentsText4", + isPostLiked = true, + onPostImageClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ), ) ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun ReaderTagsFeedLoadingTagsAndPosts() { - AppTheme { + val readerTag = ReaderTag( + "Tag 1", + "Tag 1", + "Tag 1", + "Tag 1", + ReaderTagType.TAGS, + ) ReaderTagsFeed( - uiState = UiState.LoadingTagsAndPosts + uiState = UiState.Loaded( + data = listOf( + (TagChip.Loaded(readerTag, {}) to postListLoaded), + (TagChip.Loaded(readerTag, {}) to PostList.Loading), + (TagChip.Loaded(readerTag, {}) to PostList.Error), + (TagChip.Loading to PostList.Loading), + ) + ) ) } } @@ -387,43 +326,10 @@ fun ReaderTagsFeedLoadingTagsAndPosts() { @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun ReaderTagsFeedLoadedTagsLoadingPosts() { +fun ReaderTagsFeedLoading() { AppTheme { ReaderTagsFeed( - uiState = UiState.LoadedTagsLoadingPosts( - items = listOf( - TagsFeedItem.Loading( - tag = ReaderTag( - "Tag 1", - "Tag 1", - "Tag 1", - "Tag 1", - ReaderTagType.TAGS, - ), - onTagClicked = {}, - ), - TagsFeedItem.Loading( - tag = ReaderTag( - "Tag 2", - "Tag 2", - "Tag 2", - "Tag 2", - ReaderTagType.TAGS, - ), - onTagClicked = {}, - ), - TagsFeedItem.Loading( - tag = ReaderTag( - "Tag 3", - "Tag 3", - "Tag 3", - "Tag 3", - ReaderTagType.TAGS, - ), - onTagClicked = {}, - ) - ) - ) + uiState = UiState.Loading ) } } From c52106a089aec7b2d91c3554f4d35f1ffe32857b Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 19 Apr 2024 02:29:00 -0300 Subject: [PATCH 062/237] WIP ReaderTagsFeed error state --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 202 ++++++++++++------ .../main/res/drawable/ic_wifi_off_24px.xml | 14 ++ WordPress/src/main/res/values/strings.xml | 5 + 3 files changed, 157 insertions(+), 64 deletions(-) create mode 100644 WordPress/src/main/res/drawable/ic_wifi_off_24px.xml 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 53d39dcb2e67..a8b0efcf726f 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 @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -13,15 +14,28 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import org.wordpress.android.R import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType import org.wordpress.android.ui.compose.theme.AppColor @@ -38,7 +52,7 @@ fun ReaderTagsFeed(uiState: UiState) { .fillMaxHeight(), ) { when (uiState) { - is UiState.Loading -> LoadingTagsAndPosts() + is UiState.Loading -> Loading() is UiState.Loaded -> Loaded(uiState) is UiState.Empty -> Empty() } @@ -64,77 +78,137 @@ private fun Loaded(uiState: UiState.Loaded) { AppColor.Black.copy(alpha = 0.08F) } Spacer(modifier = Modifier.height(Margin.Large.value)) - with(uiState) { - // Tag chip UI - when (tagChip) { - is TagChip.Loading -> { - Box( - modifier = Modifier - .padding(start = Margin.Large.value) - .width(75.dp) - .height(36.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(backgroundColor), - ) - } + // Tag chip UI + when (tagChip) { + is TagChip.Loading -> { + Box( + modifier = Modifier + .padding(start = Margin.Large.value) + .width(75.dp) + .height(36.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(backgroundColor), + ) + } - is TagChip.Loaded -> { - ReaderFilterChip( - text = UiString.UiStringText(tagChip.tag.tagTitle), - onClick = tagChip.onTagClicked, - height = 36.dp, - ) + is TagChip.Loaded -> { + ReaderFilterChip( + text = UiString.UiStringText(tagChip.tag.tagTitle), + onClick = tagChip.onTagClicked, + height = 36.dp, + ) + } + } + Spacer(modifier = Modifier.height(Margin.Large.value)) + // Posts list UI + when (postList) { + is PostList.Loading -> { + LazyRow( + modifier = Modifier + .fillMaxWidth(), + userScrollEnabled = false, + ) { + item { + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(12.dp)) + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(12.dp)) + } } } - Spacer(modifier = Modifier.height(Margin.Large.value)) - // Posts list UI - when (postList) { - is PostList.Loading -> { - LazyRow( - modifier = Modifier - .fillMaxWidth(), - userScrollEnabled = false, - ) { - item { - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) + + is PostList.Loaded -> { + LazyRow( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), + ) { + items( + items = postList.items, + ) { postItem -> + with(postItem) { + ReaderTagsFeedPostListItem( + siteName = siteName, + postDateLine = postDateLine, + postTitle = postTitle, + postExcerpt = postExcerpt, + postImageUrl = postImageUrl, + postNumberOfLikesText = postNumberOfLikesText, + postNumberOfCommentsText = postNumberOfCommentsText, + isPostLiked = isPostLiked, + onPostImageClick = onPostImageClick, + onPostLikeClick = onPostLikeClick, + onPostMoreMenuClick = onPostMoreMenuClick, + ) } } } + } - is PostList.Loaded -> { - LazyRow( + is PostList.Error -> { + Column( + modifier = Modifier + .height(280.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), - ) { - items( - items = postList.items, - ) { postItem -> - with(postItem) { - ReaderTagsFeedPostListItem( - siteName = siteName, - postDateLine = postDateLine, - postTitle = postTitle, - postExcerpt = postExcerpt, - postImageUrl = postImageUrl, - postNumberOfLikesText = postNumberOfLikesText, - postNumberOfCommentsText = postNumberOfCommentsText, - isPostLiked = isPostLiked, - onPostImageClick = onPostImageClick, - onPostLikeClick = onPostLikeClick, - onPostMoreMenuClick = onPostMoreMenuClick, + .drawBehind { + drawCircle( + color = backgroundColor, + radius = this.size.maxDimension ) - } - } + }, + painter = painterResource(R.drawable.ic_wifi_off_24px), + tint = MaterialTheme.colors.onPrimary, + contentDescription = null + ) + Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value)) + val tagName = if (tagChip is TagChip.Loaded) tagChip.tag.tagDisplayName else "" + Text( + text = stringResource(id = R.string.reader_tags_feed_error_title, tagName), + style = androidx.compose.material3.MaterialTheme.typography.labelLarge, + color = MaterialTheme.colors.onSurface, + ) + Spacer(modifier = Modifier.height(Margin.Medium.value)) + Text( + text = stringResource(id = R.string.reader_tags_feed_error_description, tagName), + style = androidx.compose.material3.MaterialTheme.typography.bodySmall, + color = AppColor.Black.copy(alpha = 0.4F), + ) + Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) + Button( + onClick = { postList.onRetryClick() }, + modifier = Modifier.height(36.dp), + elevation = ButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + ), + colors = ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colors.onPrimary, + backgroundColor = MaterialTheme.colors.onSurface, + ), + shape = RoundedCornerShape(50), + contentPadding = PaddingValues( + start = Margin.MediumLarge.value, + end = Margin.MediumLarge.value, + top = 0.dp, + bottom = 0.dp + ) + ) { + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .widthIn(max = 280.dp), + style = androidx.compose.material3.MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + text = stringResource(R.string.reader_tags_feed_error_retry), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) } } - - is PostList.Error -> { -// TODO() - } } } } @@ -142,7 +216,7 @@ private fun Loaded(uiState: UiState.Loaded) { } @Composable -private fun LoadingTagsAndPosts() { +private fun Loading() { LazyColumn( modifier = Modifier.fillMaxSize(), userScrollEnabled = false, @@ -212,7 +286,7 @@ sealed class PostList { object Loading : PostList() - object Error : PostList() + data class Error(val onRetryClick: () -> Unit) : PostList() } data class TagsFeedPostItem( @@ -315,7 +389,7 @@ fun ReaderTagsFeedLoaded() { data = listOf( (TagChip.Loaded(readerTag, {}) to postListLoaded), (TagChip.Loaded(readerTag, {}) to PostList.Loading), - (TagChip.Loaded(readerTag, {}) to PostList.Error), + (TagChip.Loaded(readerTag, {}) to PostList.Error {}), (TagChip.Loading to PostList.Loading), ) ) diff --git a/WordPress/src/main/res/drawable/ic_wifi_off_24px.xml b/WordPress/src/main/res/drawable/ic_wifi_off_24px.xml new file mode 100644 index 000000000000..463a7f1120f8 --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_wifi_off_24px.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 6969e14b0513..55eb9e3b5bce 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2303,6 +2303,11 @@ Save Posts for Later Save this post, and come back to read it whenever you\'d like. It will only be available on this device — saved posts don\'t sync to your other devices. + + No posts found for %s + This tag might not have any posts, or there was no internet connection. + Retry + No connection From 61a82996efe54b9576f63f9881de0c27d9061522 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 19 Apr 2024 02:33:15 -0300 Subject: [PATCH 063/237] WIP Update ReaderTagsFeed error state height --- .../android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 a8b0efcf726f..eeb56455589e 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 @@ -152,6 +152,7 @@ private fun Loaded(uiState: UiState.Loaded) { .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { + Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) Icon( modifier = Modifier .drawBehind { @@ -200,7 +201,7 @@ private fun Loaded(uiState: UiState.Loaded) { Text( modifier = Modifier .align(Alignment.CenterVertically) - .widthIn(max = 280.dp), + .widthIn(max = 250.dp), style = androidx.compose.material3.MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium, text = stringResource(R.string.reader_tags_feed_error_retry), From 3a8bbfd62cc4f89e6f0b34acd1a2705c1cc3886a Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 19 Apr 2024 13:57:56 -0300 Subject: [PATCH 064/237] Fetch content for the actual followed tags --- .../ui/reader/ReaderTagsFeedFragment.kt | 174 ++++++++++++------ .../viewmodels/ReaderTagsFeedViewModel.kt | 13 +- 2 files changed, 117 insertions(+), 70 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 82b814782c21..d4c81cde82b6 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 @@ -7,6 +7,7 @@ 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 @@ -22,13 +23,23 @@ import androidx.compose.ui.Modifier 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.models.ReaderTagType 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 javax.inject.Inject /** * Initial implementation of ReaderTagFeedFragment with the idea of it containing both a ComposeView, which will host @@ -41,7 +52,25 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel @AndroidEntryPoint class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragment_layout), WPMainActivity.OnScrollToTopListener { + // TODO thomashortadev get this via Fragment arguments + private val tagsFeedTag by lazy { + ReaderTag( + "", + getString(R.string.reader_tags_display_name), + getString(R.string.reader_tags_display_name), + "", + ReaderTagType.TAGS + ) + } + + @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() } + ) // binding private lateinit var binding: ReaderTagFeedFragmentLayoutBinding @@ -57,7 +86,34 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } - viewModel.fetchAll() + 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().map { it.tag } + viewModel.fetchAll(tags) + } + subFilterViewModel.updateTagsAndSites() } override fun getScrollableViewForUniqueIdProvision(): View { @@ -71,73 +127,73 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme /** * 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, ) { - AppThemeWithoutBackground { - Column( - modifier = Modifier - .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, - ) - } + 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, + ) - is ReaderTagsFeedViewModel.FetchState.Error -> { - Text( - text = "Error loading posts", - style = MaterialTheme.typography.body1, - ) - } + when (fetchState) { + is ReaderTagsFeedViewModel.FetchState.Loading -> { + Text( + text = "Loading...", + style = MaterialTheme.typography.body1, + ) + } - 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, - ) + is ReaderTagsFeedViewModel.FetchState.Error -> { + Text( + text = "Error loading posts", + style = MaterialTheme.typography.body1, + ) + } - Text( - text = post.excerpt, - style = MaterialTheme.typography.body1, - maxLines = 4, - overflow = TextOverflow.Ellipsis, + 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, + ) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt index 0ec39b37a6fd..a3a7e089008f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag -import org.wordpress.android.models.ReaderTagType import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.viewmodel.ScopedViewModel @@ -22,8 +21,8 @@ class ReaderTagsFeedViewModel @Inject constructor( private val _uiStateFlow = MutableStateFlow(UiState(emptyMap())) val uiStateFlow: StateFlow = _uiStateFlow - fun fetchAll() { - FAKE_TAGS.forEach { + fun fetchAll(tags: List) { + tags.forEach { fetchTag(it) } } @@ -56,12 +55,4 @@ class ReaderTagsFeedViewModel @Inject constructor( data object Error : FetchState() data class Success(val posts: ReaderPostList) : FetchState() } - - companion object { - private val FAKE_TAGS = listOf( - ReaderTag("science", "Science", "Science", null, ReaderTagType.FOLLOWED), - ReaderTag("fiction", "Fiction", "Fiction", null, ReaderTagType.FOLLOWED), - ReaderTag("rpg", "RPG", "RPG", null, ReaderTagType.FOLLOWED), - ) - } } From a82d1e8b5113b88944985e5cac2cc071641d49e1 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 19 Apr 2024 14:26:27 -0300 Subject: [PATCH 065/237] Get the Tags Feed tag from Fragment's newInstance --- .../android/ui/reader/ReaderFragment.kt | 2 +- .../ui/reader/ReaderTagsFeedFragment.kt | 27 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 50f5c0071217..eb8a55d29038 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -229,7 +229,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView val selectedTag = uiState.selectedReaderTag val fragment = when { selectedTag.isDiscover -> ReaderDiscoverFragment() - selectedTag.isTags -> ReaderTagsFeedFragment() + selectedTag.isTags -> ReaderTagsFeedFragment.newInstance(selectedTag) else -> ReaderPostListFragment.newInstanceForTag( selectedTag, ReaderTypes.ReaderPostListType.TAG_FOLLOWED, 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 d4c81cde82b6..d5e586deaa7d 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 @@ -28,7 +28,6 @@ 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.models.ReaderTagType import org.wordpress.android.ui.ViewPagerFragment import org.wordpress.android.ui.compose.theme.AppThemeWithoutBackground import org.wordpress.android.ui.main.WPMainActivity @@ -39,6 +38,7 @@ 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 /** @@ -52,15 +52,10 @@ import javax.inject.Inject @AndroidEntryPoint class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragment_layout), WPMainActivity.OnScrollToTopListener { - // TODO thomashortadev get this via Fragment arguments private val tagsFeedTag by lazy { - ReaderTag( - "", - getString(R.string.reader_tags_display_name), - getString(R.string.reader_tags_display_name), - "", - ReaderTagType.TAGS - ) + // TODO maybe we can just create a static function somewhere that returns the Tags Feed ReaderTag, since it's + // used in multiple places, client-side only, and always the same. + requireArguments().getSerializableCompat(ARG_TAGS_FEED_TAG)!! } @Inject @@ -90,7 +85,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } private fun initViewModels(savedInstanceState: Bundle?) { - subFilterViewModel = ViewModelProvider(this, viewModelFactory).get( + subFilterViewModel = ViewModelProvider(this, viewModelFactory).get( getViewModelKeyForTag(tagsFeedTag), SubFilterViewModel::class.java ) @@ -123,6 +118,18 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme 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) + } + } + } } /** From e29762fc988b9683ce0481dd016c9e5b19ef17f4 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 19 Apr 2024 16:30:27 -0300 Subject: [PATCH 066/237] Add some doc comments in the Tags Feed ViewModel --- .../ui/reader/viewmodels/ReaderTagsFeedViewModel.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt index a3a7e089008f..a148dc0b10ec 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -21,12 +21,24 @@ class ReaderTagsFeedViewModel @Inject constructor( private val _uiStateFlow = MutableStateFlow(UiState(emptyMap())) val uiStateFlow: StateFlow = _uiStateFlow + /** + * Fetch multiple tag posts in parallel. Each tag load causes a new state to be emitted, so multiple emissions of + * [uiStateFlow] are expected when calling this method for each tag, since each can go through the following + * [FetchState]s: [FetchState.Loading], [FetchState.Success], [FetchState.Error]. + */ fun fetchAll(tags: List) { tags.forEach { fetchTag(it) } } + /** + * Fetch posts for a single tag. This method will emit a new state to [uiStateFlow] for different [FetchState]s: + * [FetchState.Loading], [FetchState.Success], [FetchState.Error], but only for the tag being fetched. + * + * Can be used for retrying a failed fetch, for instance. + */ + @Suppress("MemberVisibilityCanBePrivate") fun fetchTag(tag: ReaderTag) { launch { _uiStateFlow.update { From 10d17ac1df4e4a160cd17eced38fe3f602165016 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:01:55 -0300 Subject: [PATCH 067/237] Implement error state for reader tags feed --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 80 +++++++------------ 1 file changed, 30 insertions(+), 50 deletions(-) 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 eeb56455589e..8ed90c0d5bbb 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 @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items @@ -32,7 +31,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.R @@ -79,26 +78,11 @@ private fun Loaded(uiState: UiState.Loaded) { } Spacer(modifier = Modifier.height(Margin.Large.value)) // Tag chip UI - when (tagChip) { - is TagChip.Loading -> { - Box( - modifier = Modifier - .padding(start = Margin.Large.value) - .width(75.dp) - .height(36.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(backgroundColor), - ) - } - - is TagChip.Loaded -> { - ReaderFilterChip( - text = UiString.UiStringText(tagChip.tag.tagTitle), - onClick = tagChip.onTagClicked, - height = 36.dp, - ) - } - } + ReaderFilterChip( + text = UiString.UiStringText(tagChip.tag.tagTitle), + onClick = tagChip.onTagClicked, + height = 36.dp, + ) Spacer(modifier = Modifier.height(Margin.Large.value)) // Posts list UI when (postList) { @@ -148,8 +132,9 @@ private fun Loaded(uiState: UiState.Loaded) { is PostList.Error -> { Column( modifier = Modifier - .height(280.dp) - .fillMaxWidth(), + .height(250.dp) + .fillMaxWidth() + .padding(start = 60.dp, end = 60.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) @@ -166,22 +151,30 @@ private fun Loaded(uiState: UiState.Loaded) { contentDescription = null ) Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value)) - val tagName = if (tagChip is TagChip.Loaded) tagChip.tag.tagDisplayName else "" + val tagName = tagChip.tag.tagDisplayName Text( text = stringResource(id = R.string.reader_tags_feed_error_title, tagName), style = androidx.compose.material3.MaterialTheme.typography.labelLarge, color = MaterialTheme.colors.onSurface, + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(Margin.Medium.value)) Text( text = stringResource(id = R.string.reader_tags_feed_error_description, tagName), style = androidx.compose.material3.MaterialTheme.typography.bodySmall, - color = AppColor.Black.copy(alpha = 0.4F), + color = if (isSystemInDarkTheme()) { + AppColor.White.copy(alpha = 0.4F) + } else { + AppColor.Black.copy(alpha = 0.4F) + }, + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) Button( onClick = { postList.onRetryClick() }, - modifier = Modifier.height(36.dp), + modifier = Modifier + .height(36.dp) + .width(114.dp), elevation = ButtonDefaults.elevation( defaultElevation = 0.dp, pressedElevation = 0.dp, @@ -191,22 +184,14 @@ private fun Loaded(uiState: UiState.Loaded) { backgroundColor = MaterialTheme.colors.onSurface, ), shape = RoundedCornerShape(50), - contentPadding = PaddingValues( - start = Margin.MediumLarge.value, - end = Margin.MediumLarge.value, - top = 0.dp, - bottom = 0.dp - ) ) { Text( modifier = Modifier - .align(Alignment.CenterVertically) - .widthIn(max = 250.dp), - style = androidx.compose.material3.MaterialTheme.typography.titleMedium, + .align(Alignment.CenterVertically), + style = androidx.compose.material3.MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.surface, text = stringResource(R.string.reader_tags_feed_error_retry), - overflow = TextOverflow.Ellipsis, - maxLines = 1, ) } } @@ -273,14 +258,10 @@ sealed class UiState { object Empty : UiState() } -sealed class TagChip { - data class Loaded( - val tag: ReaderTag, - val onTagClicked: () -> Unit, - ) : TagChip() - - object Loading : TagChip() -} +data class TagChip( + val tag: ReaderTag, + val onTagClicked: () -> Unit, +) sealed class PostList { data class Loaded(val items: List) : PostList() @@ -388,10 +369,9 @@ fun ReaderTagsFeedLoaded() { ReaderTagsFeed( uiState = UiState.Loaded( data = listOf( - (TagChip.Loaded(readerTag, {}) to postListLoaded), - (TagChip.Loaded(readerTag, {}) to PostList.Loading), - (TagChip.Loaded(readerTag, {}) to PostList.Error {}), - (TagChip.Loading to PostList.Loading), + (TagChip(readerTag, {}) to postListLoaded), + (TagChip(readerTag, {}) to PostList.Loading), + (TagChip(readerTag, {}) to PostList.Error {}), ) ) ) From ca30b4715b5fdeefa5263215a04d9705abed6000 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 19 Apr 2024 17:32:59 -0300 Subject: [PATCH 068/237] Create a more specific exception for post fetching --- .../ui/reader/exception/ReaderPostFetchException.kt | 5 +++++ .../android/ui/reader/repository/ReaderPostRepository.kt | 5 ++++- .../ui/reader/viewmodels/ReaderTagsFeedViewModel.kt | 7 ++++--- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/exception/ReaderPostFetchException.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/exception/ReaderPostFetchException.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/exception/ReaderPostFetchException.kt new file mode 100644 index 000000000000..b1918ef2f372 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/exception/ReaderPostFetchException.kt @@ -0,0 +1,5 @@ +package org.wordpress.android.ui.reader.exception + +class ReaderPostFetchException( + message: String = "Failed to fetch post(s).", +) : RuntimeException(message) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt index 7e4a98959fee..e33951ac82eb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt @@ -15,6 +15,7 @@ import org.wordpress.android.ui.prefs.AppPrefs import org.wordpress.android.ui.reader.ReaderConstants import org.wordpress.android.ui.reader.actions.ReaderActions import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResultListener +import org.wordpress.android.ui.reader.exception.ReaderPostFetchException import org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter import org.wordpress.android.ui.reader.utils.ReaderUtils import org.wordpress.android.util.AppLog @@ -36,7 +37,9 @@ class ReaderPostRepository @Inject constructor( return suspendCancellableCoroutine { cont -> val resultListener = UpdateResultListener { result -> if (result == ReaderActions.UpdateResult.FAILED) { - cont.resumeWithException(Exception("Failed to fetch newer posts for tag")) + cont.resumeWithException( + ReaderPostFetchException("Failed to fetch newer posts for tag: ${tag.tagSlug}") + ) } else { val posts = ReaderPostTable.getPostsWithTag(tag, maxPosts, false) cont.resume(posts) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt index a148dc0b10ec..fba36feca81c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.update import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.reader.exception.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject @@ -50,9 +51,9 @@ class ReaderTagsFeedViewModel @Inject constructor( _uiStateFlow.update { it.copy(tagStates = it.tagStates + (tag to FetchState.Success(posts))) } - } catch (e: Exception) { + } catch (e: ReaderPostFetchException) { _uiStateFlow.update { - it.copy(tagStates = it.tagStates + (tag to FetchState.Error)) + it.copy(tagStates = it.tagStates + (tag to FetchState.Error(e))) } } } @@ -64,7 +65,7 @@ class ReaderTagsFeedViewModel @Inject constructor( sealed class FetchState { data object Loading : FetchState() - data object Error : FetchState() data class Success(val posts: ReaderPostList) : FetchState() + data class Error(val exception: Exception) : FetchState() } } From ad521d9fc9de057ec6ba75e71d4527c9af35a532 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 19 Apr 2024 18:09:07 -0300 Subject: [PATCH 069/237] Create ReaderPostLocalSource for extracting local DB logic --- .../ReaderPostFetchException.kt | 2 +- .../reader/repository/ReaderPostRepository.kt | 82 ++---------- .../reader/sources/ReaderPostLocalSource.kt | 121 ++++++++++++++++++ .../viewmodels/ReaderTagsFeedViewModel.kt | 2 +- 4 files changed, 133 insertions(+), 74 deletions(-) rename WordPress/src/main/java/org/wordpress/android/ui/reader/{exception => exceptions}/ReaderPostFetchException.kt (68%) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/exception/ReaderPostFetchException.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/exceptions/ReaderPostFetchException.kt similarity index 68% rename from WordPress/src/main/java/org/wordpress/android/ui/reader/exception/ReaderPostFetchException.kt rename to WordPress/src/main/java/org/wordpress/android/ui/reader/exceptions/ReaderPostFetchException.kt index b1918ef2f372..de4cc47ae7df 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/exception/ReaderPostFetchException.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/exceptions/ReaderPostFetchException.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.reader.exception +package org.wordpress.android.ui.reader.exceptions class ReaderPostFetchException( message: String = "Failed to fetch post(s).", diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt index e33951ac82eb..9c24b917023f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt @@ -2,21 +2,21 @@ package org.wordpress.android.ui.reader.repository import com.android.volley.VolleyError import com.wordpress.rest.RestRequest +import dagger.Reusable import kotlinx.coroutines.suspendCancellableCoroutine import org.json.JSONObject import org.wordpress.android.WordPress.Companion.getRestClientUtilsV1_2 import org.wordpress.android.datasets.ReaderPostTable import org.wordpress.android.datasets.ReaderTagTable -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.prefs.AppPrefs import org.wordpress.android.ui.reader.ReaderConstants import org.wordpress.android.ui.reader.actions.ReaderActions import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResultListener -import org.wordpress.android.ui.reader.exception.ReaderPostFetchException +import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter +import org.wordpress.android.ui.reader.sources.ReaderPostLocalSource import org.wordpress.android.ui.reader.utils.ReaderUtils import org.wordpress.android.util.AppLog import org.wordpress.android.util.LocaleManagerWrapper @@ -26,8 +26,10 @@ import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException +@Reusable class ReaderPostRepository @Inject constructor( - private val localeManagerWrapper: LocaleManagerWrapper + private val localeManagerWrapper: LocaleManagerWrapper, + private val localSource: ReaderPostLocalSource, ) { /** * Fetches and returns the most recent posts for the passed tag, respecting the maxPosts limit. @@ -177,77 +179,13 @@ class ReaderPostRepository @Inject constructor( resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED) return } + + // this should ideally be done using coroutines, but this class is currently being used from Java, which makes + // it difficult to use coroutines. This should be refactored to use coroutines when possible. object : Thread() { override fun run() { val serverPosts = ReaderPostList.fromJson(jsonObject) - val updateResult = ReaderPostTable.comparePosts(serverPosts) - if (updateResult.isNewOrChanged) { - // gap detection - only applies to posts with a specific tag - var postWithGap: ReaderPost? = null - if (tag != null) { - when (updateAction) { - ReaderPostServiceStarter.UpdateAction.REQUEST_NEWER -> { - // if there's no overlap between server and local (ie: all server - // posts are new), assume there's a gap between server and local - // provided that local posts exist - val numServerPosts = serverPosts.size - if (numServerPosts >= 2 && ReaderPostTable.getNumPostsWithTag(tag) > 0 && - !ReaderPostTable.hasOverlap( - serverPosts, - tag - ) - ) { - // treat the second to last server post as having a gap - postWithGap = serverPosts[numServerPosts - 2] - // remove the last server post to deal with the edge case of - // there actually not being a gap between local & server - serverPosts.removeAt(numServerPosts - 1) - val gapMarker = ReaderPostTable.getGapMarkerIdsForTag(tag) - if (gapMarker != null) { - // We mustn't have two gapMarkers at the same time. Therefor we need to - // delete all posts before the current gapMarker and clear the gapMarker flag. - ReaderPostTable.deletePostsBeforeGapMarkerForTag(tag) - ReaderPostTable.removeGapMarkerForTag(tag) - } - } - } - - ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP -> { - // if service was started as a request to fill a gap, delete existing posts - // before the one with the gap marker, then remove the existing gap marker - ReaderPostTable.deletePostsBeforeGapMarkerForTag(tag) - ReaderPostTable.removeGapMarkerForTag(tag) - } - - ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH -> ReaderPostTable.deletePostsWithTag( - tag - ) - - ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER -> {} - } - } - ReaderPostTable.addOrUpdatePosts(tag, serverPosts) - if (AppPrefs.shouldUpdateBookmarkPostsPseudoIds(tag)) { - ReaderPostTable.updateBookmarkedPostPseudoId(serverPosts) - AppPrefs.setBookmarkPostsPseudoIdsUpdated() - } - - // gap marker must be set after saving server posts - if (postWithGap != null) { - ReaderPostTable.setGapMarkerForTag(postWithGap.blogId, postWithGap.postId, tag) - AppLog.d(AppLog.T.READER, "added gap marker to tag " + tag!!.tagNameForLog) - } - } else if (updateResult == ReaderActions.UpdateResult.UNCHANGED - && updateAction == ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP - ) { - // edge case - request to fill gap returned nothing new, so remove the gap marker - ReaderPostTable.removeGapMarkerForTag(tag) - AppLog.w(AppLog.T.READER, "attempt to fill gap returned nothing new") - } - AppLog.d( - AppLog.T.READER, - "requested posts response = $updateResult" - ) + val updateResult = localSource.saveUpdatedPosts(serverPosts, updateAction, tag) resultListener.onUpdateResult(updateResult) } }.start() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt new file mode 100644 index 000000000000..d327effc5015 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt @@ -0,0 +1,121 @@ +package org.wordpress.android.ui.reader.sources + +import dagger.Reusable +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.prefs.AppPrefs +import org.wordpress.android.ui.reader.actions.ReaderActions +import org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter +import org.wordpress.android.util.AppLog +import javax.inject.Inject + +/** + * Manage the saving of posts to the local database table. + */ +@Reusable +class ReaderPostLocalSource @Inject constructor() { + /** + * Save the list of posts to the local database, and handle any gaps between local and server posts. + * + * Ideally this should be a suspend function but since it's being ultimately used by Java in some scenarios we + * are keeping it blocking for now and it's up to the caller to run it in a coroutine or different thread. + */ + fun saveUpdatedPosts( + serverPosts: ReaderPostList, + updateAction: ReaderPostServiceStarter.UpdateAction, + requestedTag: ReaderTag?, + ): ReaderActions.UpdateResult { + val updateResult = ReaderPostTable.comparePosts(serverPosts) + if (updateResult.isNewOrChanged) { + // gap detection - only applies to posts with a specific tag + var postWithGap: ReaderPost? = null + if (requestedTag != null) { + when (updateAction) { + ReaderPostServiceStarter.UpdateAction.REQUEST_NEWER -> { + postWithGap = handleRequestNewerResult(serverPosts, requestedTag) + } + + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP -> { + handleRequestOlderThanGapResult(requestedTag) + } + + ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH -> ReaderPostTable.deletePostsWithTag( + requestedTag + ) + + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER -> { /* noop */ + } + } + } + + // save posts to local db + ReaderPostTable.addOrUpdatePosts(requestedTag, serverPosts) + if (AppPrefs.shouldUpdateBookmarkPostsPseudoIds(requestedTag)) { + ReaderPostTable.updateBookmarkedPostPseudoId(serverPosts) + AppPrefs.setBookmarkPostsPseudoIdsUpdated() + } + + // gap marker must be set after saving server posts + if (postWithGap != null) { + ReaderPostTable.setGapMarkerForTag(postWithGap.blogId, postWithGap.postId, requestedTag) + AppLog.d(AppLog.T.READER, "added gap marker to tag " + requestedTag!!.tagNameForLog) + } + } else if (updateResult == ReaderActions.UpdateResult.UNCHANGED + && updateAction == ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP + ) { + // edge case - request to fill gap returned nothing new, so remove the gap marker + ReaderPostTable.removeGapMarkerForTag(requestedTag) + AppLog.w(AppLog.T.READER, "attempt to fill gap returned nothing new") + } + AppLog.d( + AppLog.T.READER, + "requested posts response = $updateResult" + ) + return updateResult + } + + private fun handleRequestOlderThanGapResult(requestedTag: ReaderTag?) { + // if service was started as a request to fill a gap, delete existing posts + // before the one with the gap marker, then remove the existing gap marker + ReaderPostTable.deletePostsBeforeGapMarkerForTag(requestedTag) + ReaderPostTable.removeGapMarkerForTag(requestedTag) + } + + /** + * Handle the result of a request for newer posts, which may include a gap between local and server posts. + * + * @return the post that has a gap, or null if there's no gap + */ + private fun handleRequestNewerResult( + serverPosts: ReaderPostList, + requestedTag: ReaderTag?, + ): ReaderPost? { + // if there's no overlap between server and local (ie: all server + // posts are new), assume there's a gap between server and local + // provided that local posts exist + var postWithGap: ReaderPost? = null + val numServerPosts = serverPosts.size + if (numServerPosts >= 2 && ReaderPostTable.getNumPostsWithTag(requestedTag) > 0 && + !ReaderPostTable.hasOverlap( + serverPosts, + requestedTag + ) + ) { + // treat the second to last server post as having a gap + postWithGap = serverPosts[numServerPosts - 2] + // remove the last server post to deal with the edge case of + // there actually not being a gap between local & server + serverPosts.removeAt(numServerPosts - 1) + val gapMarker = ReaderPostTable.getGapMarkerIdsForTag(requestedTag) + if (gapMarker != null) { + // We mustn't have two gapMarkers at the same time. Therefor we need to + // delete all posts before the current gapMarker and clear the gapMarker flag. + ReaderPostTable.deletePostsBeforeGapMarkerForTag(requestedTag) + ReaderPostTable.removeGapMarkerForTag(requestedTag) + } + } + return postWithGap + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt index fba36feca81c..7d25a4bfe557 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.update import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag import org.wordpress.android.modules.BG_THREAD -import org.wordpress.android.ui.reader.exception.ReaderPostFetchException +import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject From b2602034fbbd274103d4448c55028596b0653e27 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 19 Apr 2024 20:04:01 -0300 Subject: [PATCH 070/237] Implement more from tag button, fix clip to padding --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 132 ++++++++++++------ .../tagsfeed/ReaderTagsFeedPostListItem.kt | 20 +-- 2 files changed, 101 insertions(+), 51 deletions(-) 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 8ed90c0d5bbb..d058e28212cf 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 @@ -32,6 +32,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.R @@ -62,11 +63,7 @@ fun ReaderTagsFeed(uiState: UiState) { private fun Loaded(uiState: UiState.Loaded) { LazyColumn( modifier = Modifier - .fillMaxSize() - .padding( - start = Margin.Large.value, - end = Margin.Large.value, - ) + .fillMaxSize(), ) { items( items = uiState.data, @@ -79,6 +76,9 @@ private fun Loaded(uiState: UiState.Loaded) { 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.onTagClicked, height = 36.dp, @@ -91,12 +91,16 @@ private fun Loaded(uiState: UiState.Loaded) { modifier = Modifier .fillMaxWidth(), userScrollEnabled = false, + contentPadding = PaddingValues( + start = Margin.Large.value, + end = Margin.Large.value + ), ) { item { ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(Margin.ExtraMediumLarge.value)) ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(Margin.ExtraMediumLarge.value)) } } } @@ -106,6 +110,10 @@ private fun Loaded(uiState: UiState.Loaded) { modifier = Modifier .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), + contentPadding = PaddingValues( + start = Margin.Large.value, + end = Margin.Large.value + ), ) { items( items = postList.items, @@ -126,6 +134,45 @@ private fun Loaded(uiState: UiState.Loaded) { ) } } + item { + val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black + val primaryElementColor = baseColor.copy( + alpha = 0.87F + ) + Column( + modifier = Modifier + .height(340.dp) + .padding( + start = Margin.ExtraLarge.value, + end = Margin.ExtraLarge.value, + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .drawBehind { + drawCircle( + color = backgroundColor, + radius = this.size.maxDimension + ) + }, + painter = painterResource(R.drawable.ic_arrow_right_white_24dp), + tint = MaterialTheme.colors.onSurface, + contentDescription = null, + ) + Spacer(modifier = Modifier.height(Margin.ExtraMediumLarge.value)) + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = "More from Nature", + style = androidx.compose.material3.MaterialTheme.typography.labelLarge, + color = primaryElementColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } } } @@ -135,7 +182,7 @@ private fun Loaded(uiState: UiState.Loaded) { .height(250.dp) .fillMaxWidth() .padding(start = 60.dp, end = 60.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) Icon( @@ -147,7 +194,7 @@ private fun Loaded(uiState: UiState.Loaded) { ) }, painter = painterResource(R.drawable.ic_wifi_off_24px), - tint = MaterialTheme.colors.onPrimary, + tint = MaterialTheme.colors.onSurface, contentDescription = null ) Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value)) @@ -197,6 +244,7 @@ private fun Loaded(uiState: UiState.Loaded) { } } } + Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value)) } } } @@ -293,65 +341,65 @@ fun ReaderTagsFeedLoaded() { val postListLoaded = PostList.Loaded( listOf( TagsFeedPostItem( - siteName = "siteName1", - postDateLine = "postDateLine1", - postTitle = "postTitle1", - postExcerpt = "postExcerpt1", + siteName = "Site Name 1", + postDateLine = "1h", + postTitle = "Post Title 1", + postExcerpt = "Post excerpt 1", postImageUrl = "postImageUrl1", - postNumberOfLikesText = "postNumberOfLikesText1", - postNumberOfCommentsText = "postNumberOfCommentsText1", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "", isPostLiked = true, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ), TagsFeedPostItem( - siteName = "siteName2", - postDateLine = "postDateLine2", - postTitle = "postTitle2", - postExcerpt = "postExcerpt2", + siteName = "Site Name 2", + postDateLine = "2h", + postTitle = "Post Title 2", + postExcerpt = "Post excerpt 2", postImageUrl = "postImageUrl2", - postNumberOfLikesText = "postNumberOfLikesText2", - postNumberOfCommentsText = "postNumberOfCommentsText2", + postNumberOfLikesText = "", + postNumberOfCommentsText = "3 comments", isPostLiked = true, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ), TagsFeedPostItem( - siteName = "siteName2", - postDateLine = "postDateLine2", - postTitle = "postTitle2", - postExcerpt = "postExcerpt2", - postImageUrl = "postImageUrl2", - postNumberOfLikesText = "postNumberOfLikesText2", - postNumberOfCommentsText = "postNumberOfCommentsText2", + siteName = "Site Name 3", + postDateLine = "3h", + postTitle = "Post Title 3", + postExcerpt = "Post excerpt 3", + postImageUrl = "postImageUrl3", + postNumberOfLikesText = "123 likes", + postNumberOfCommentsText = "9 comments", isPostLiked = true, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ), TagsFeedPostItem( - siteName = "siteName3", - postDateLine = "postDateLine3", - postTitle = "postTitle3", - postExcerpt = "postExcerpt3", - postImageUrl = "postImageUrl3", - postNumberOfLikesText = "postNumberOfLikesText3", - postNumberOfCommentsText = "postNumberOfCommentsText3", + siteName = "Site Name 4", + postDateLine = "4h", + postTitle = "Post Title 4", + postExcerpt = "Post excerpt 4", + postImageUrl = "postImageUrl4", + postNumberOfLikesText = "1234 likes", + postNumberOfCommentsText = "91 comments", isPostLiked = true, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ), TagsFeedPostItem( - siteName = "siteName4", - postDateLine = "postDateLine4", - postTitle = "postTitle4", - postExcerpt = "postExcerpt4", - postImageUrl = "postImageUrl4", - postNumberOfLikesText = "postNumberOfLikesText4", - postNumberOfCommentsText = "postNumberOfCommentsText4", + siteName = "Site Name 5", + postDateLine = "5h", + postTitle = "Post Title 5", + postExcerpt = "Post excerpt 5", + postImageUrl = "postImageUrl5", + postNumberOfLikesText = "12 likes", + postNumberOfCommentsText = "34 comments", isPostLiked = true, onPostImageClick = {}, onPostLikeClick = {}, 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 f105b74bec42..99715e1d8a02 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 @@ -142,15 +142,17 @@ fun ReaderTagsFeedPostListItem( maxLines = 1, ) Spacer(Modifier.height(Margin.Medium.value)) - // "•" separator - Text( - modifier = Modifier.padding( - horizontal = Margin.Small.value - ), - text = "•", - style = MaterialTheme.typography.bodyMedium, - color = secondaryElementColor, - ) + // "•" separator. We should only show it if likes *and* comments text is not empty. + if (postNumberOfLikesText.isNotBlank() && postNumberOfCommentsText.isNotBlank()) { + Text( + modifier = Modifier.padding( + horizontal = Margin.Small.value + ), + text = "•", + style = MaterialTheme.typography.bodyMedium, + color = secondaryElementColor, + ) + } // Number of comments Text( text = postNumberOfCommentsText, From f293c315789d48f822ddcbadb806988fce591491 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 19 Apr 2024 20:09:18 -0300 Subject: [PATCH 071/237] Implement reader tags feed see more label --- .../ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 5 ++++- WordPress/src/main/res/values/strings.xml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) 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 d058e28212cf..49179b23bcee 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 @@ -165,7 +165,10 @@ private fun Loaded(uiState: UiState.Loaded) { Text( modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, - text = "More from Nature", + text = stringResource( + id = R.string.reader_tags_feed_see_more_from_tag, + tagChip.tag.tagDisplayName + ), style = androidx.compose.material3.MaterialTheme.typography.labelLarge, color = primaryElementColor, maxLines = 1, diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 55eb9e3b5bce..ffd83f70568f 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2304,6 +2304,7 @@ Save this post, and come back to read it whenever you\'d like. It will only be available on this device — saved posts don\'t sync to your other devices. + More from %s No posts found for %s This tag might not have any posts, or there was no internet connection. Retry From 97926a770a2dfc792a885d12e9d4c52a229483fc Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 19 Apr 2024 20:43:39 -0300 Subject: [PATCH 072/237] Fix ripple effect on more from tag button --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 74 ++++++++++++------- 1 file changed, 46 insertions(+), 28 deletions(-) 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 49179b23bcee..c264a17bf231 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 @@ -2,6 +2,8 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -21,9 +23,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -43,6 +47,7 @@ import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterChip import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.util.AppLog @Composable fun ReaderTagsFeed(uiState: UiState) { @@ -139,41 +144,54 @@ private fun Loaded(uiState: UiState.Loaded) { val primaryElementColor = baseColor.copy( alpha = 0.87F ) - Column( + Box( modifier = Modifier .height(340.dp) .padding( start = Margin.ExtraLarge.value, end = Margin.ExtraLarge.value, - ), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + ) ) { - Icon( + Column( modifier = Modifier - .drawBehind { - drawCircle( - color = backgroundColor, - radius = this.size.maxDimension - ) - }, - painter = painterResource(R.drawable.ic_arrow_right_white_24dp), - tint = MaterialTheme.colors.onSurface, - contentDescription = null, - ) - Spacer(modifier = Modifier.height(Margin.ExtraMediumLarge.value)) - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = stringResource( - id = R.string.reader_tags_feed_see_more_from_tag, - tagChip.tag.tagDisplayName - ), - style = androidx.compose.material3.MaterialTheme.typography.labelLarge, - color = primaryElementColor, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + .align(Alignment.Center) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onClick = { + tagChip.onTagClicked() + AppLog.e(AppLog.T.READER, "RL-> Tag clicked") + } + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .drawBehind { + drawCircle( + color = backgroundColor, + radius = this.size.maxDimension + ) + }, + painter = painterResource(R.drawable.ic_arrow_right_white_24dp), + tint = MaterialTheme.colors.onSurface, + contentDescription = null, + ) + Spacer(modifier = Modifier.height(Margin.ExtraMediumLarge.value)) + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = stringResource( + id = R.string.reader_tags_feed_see_more_from_tag, + tagChip.tag.tagDisplayName + ), + style = androidx.compose.material3.MaterialTheme.typography.labelLarge, + color = primaryElementColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } } From df77510c0243eb671a556ff06118d07a22ca7976 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 19 Apr 2024 21:02:49 -0300 Subject: [PATCH 073/237] Refactor ReaderTagsFeed states --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 362 +++++++++--------- 1 file changed, 189 insertions(+), 173 deletions(-) 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 c264a17bf231..959d5014e023 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 @@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -91,179 +92,9 @@ private fun Loaded(uiState: UiState.Loaded) { Spacer(modifier = Modifier.height(Margin.Large.value)) // Posts list UI when (postList) { - is PostList.Loading -> { - LazyRow( - modifier = Modifier - .fillMaxWidth(), - userScrollEnabled = false, - contentPadding = PaddingValues( - start = Margin.Large.value, - end = Margin.Large.value - ), - ) { - item { - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(Margin.ExtraMediumLarge.value)) - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(Margin.ExtraMediumLarge.value)) - } - } - } - - is PostList.Loaded -> { - LazyRow( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), - contentPadding = PaddingValues( - start = Margin.Large.value, - end = Margin.Large.value - ), - ) { - items( - items = postList.items, - ) { postItem -> - with(postItem) { - ReaderTagsFeedPostListItem( - siteName = siteName, - postDateLine = postDateLine, - postTitle = postTitle, - postExcerpt = postExcerpt, - postImageUrl = postImageUrl, - postNumberOfLikesText = postNumberOfLikesText, - postNumberOfCommentsText = postNumberOfCommentsText, - isPostLiked = isPostLiked, - onPostImageClick = onPostImageClick, - onPostLikeClick = onPostLikeClick, - onPostMoreMenuClick = onPostMoreMenuClick, - ) - } - } - item { - val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black - val primaryElementColor = baseColor.copy( - alpha = 0.87F - ) - Box( - modifier = Modifier - .height(340.dp) - .padding( - start = Margin.ExtraLarge.value, - end = Margin.ExtraLarge.value, - ) - ) { - Column( - modifier = Modifier - .align(Alignment.Center) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), - onClick = { - tagChip.onTagClicked() - AppLog.e(AppLog.T.READER, "RL-> Tag clicked") - } - ), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - modifier = Modifier - .drawBehind { - drawCircle( - color = backgroundColor, - radius = this.size.maxDimension - ) - }, - painter = painterResource(R.drawable.ic_arrow_right_white_24dp), - tint = MaterialTheme.colors.onSurface, - contentDescription = null, - ) - Spacer(modifier = Modifier.height(Margin.ExtraMediumLarge.value)) - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = stringResource( - id = R.string.reader_tags_feed_see_more_from_tag, - tagChip.tag.tagDisplayName - ), - style = androidx.compose.material3.MaterialTheme.typography.labelLarge, - color = primaryElementColor, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - } - } - } - - is PostList.Error -> { - Column( - modifier = Modifier - .height(250.dp) - .fillMaxWidth() - .padding(start = 60.dp, end = 60.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) - Icon( - modifier = Modifier - .drawBehind { - drawCircle( - color = backgroundColor, - radius = this.size.maxDimension - ) - }, - painter = painterResource(R.drawable.ic_wifi_off_24px), - tint = MaterialTheme.colors.onSurface, - contentDescription = null - ) - Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value)) - val tagName = tagChip.tag.tagDisplayName - Text( - text = stringResource(id = R.string.reader_tags_feed_error_title, tagName), - style = androidx.compose.material3.MaterialTheme.typography.labelLarge, - color = MaterialTheme.colors.onSurface, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(Margin.Medium.value)) - Text( - text = stringResource(id = R.string.reader_tags_feed_error_description, tagName), - style = androidx.compose.material3.MaterialTheme.typography.bodySmall, - color = if (isSystemInDarkTheme()) { - AppColor.White.copy(alpha = 0.4F) - } else { - AppColor.Black.copy(alpha = 0.4F) - }, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) - Button( - onClick = { postList.onRetryClick() }, - modifier = Modifier - .height(36.dp) - .width(114.dp), - elevation = ButtonDefaults.elevation( - defaultElevation = 0.dp, - pressedElevation = 0.dp, - ), - colors = ButtonDefaults.buttonColors( - contentColor = MaterialTheme.colors.onPrimary, - backgroundColor = MaterialTheme.colors.onSurface, - ), - shape = RoundedCornerShape(50), - ) { - Text( - modifier = Modifier - .align(Alignment.CenterVertically), - style = androidx.compose.material3.MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colors.surface, - text = stringResource(R.string.reader_tags_feed_error_retry), - ) - } - } - } + 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)) } @@ -318,6 +149,191 @@ private fun Empty() { // TODO empty state (https://github.com/wordpress-mobile/WordPress-Android/issues/20584) } +@Composable +private fun PostListLoading() { + LazyRow( + modifier = Modifier + .fillMaxWidth(), + userScrollEnabled = false, + contentPadding = PaddingValues( + start = Margin.Large.value, + end = Margin.Large.value + ), + ) { + item { + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(Margin.ExtraMediumLarge.value)) + ReaderTagsFeedPostListItemLoading() + Spacer(Modifier.width(Margin.ExtraMediumLarge.value)) + } + } +} + +@Composable +private fun PostListLoaded( + postList: PostList.Loaded, + tagChip: TagChip, + backgroundColor: Color +) { + LazyRow( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), + contentPadding = PaddingValues( + start = Margin.Large.value, + end = Margin.Large.value + ), + ) { + items( + items = postList.items, + ) { postItem -> + with(postItem) { + ReaderTagsFeedPostListItem( + siteName = siteName, + postDateLine = postDateLine, + postTitle = postTitle, + postExcerpt = postExcerpt, + postImageUrl = postImageUrl, + postNumberOfLikesText = postNumberOfLikesText, + postNumberOfCommentsText = postNumberOfCommentsText, + isPostLiked = isPostLiked, + onPostImageClick = onPostImageClick, + onPostLikeClick = onPostLikeClick, + onPostMoreMenuClick = onPostMoreMenuClick, + ) + } + } + item { + val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black + val primaryElementColor = baseColor.copy( + alpha = 0.87F + ) + Box( + modifier = Modifier + .height(340.dp) + .padding( + start = Margin.ExtraLarge.value, + end = Margin.ExtraLarge.value, + ) + ) { + Column( + modifier = Modifier + .align(Alignment.Center) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onClick = { + tagChip.onTagClicked() + AppLog.e(AppLog.T.READER, "RL-> Tag clicked") + } + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .drawBehind { + drawCircle( + color = backgroundColor, + radius = this.size.maxDimension + ) + }, + painter = painterResource(R.drawable.ic_arrow_right_white_24dp), + tint = MaterialTheme.colors.onSurface, + contentDescription = null, + ) + Spacer(modifier = Modifier.height(Margin.ExtraMediumLarge.value)) + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = stringResource( + id = R.string.reader_tags_feed_see_more_from_tag, + tagChip.tag.tagDisplayName + ), + style = androidx.compose.material3.MaterialTheme.typography.labelLarge, + color = primaryElementColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } +} + +@Composable +private fun PostListError( + backgroundColor: Color, + tagChip: TagChip, + postList: PostList.Error, +) { + Column( + modifier = Modifier + .height(250.dp) + .fillMaxWidth() + .padding(start = 60.dp, end = 60.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) + Icon( + modifier = Modifier + .drawBehind { + drawCircle( + color = backgroundColor, + radius = this.size.maxDimension + ) + }, + painter = painterResource(R.drawable.ic_wifi_off_24px), + tint = MaterialTheme.colors.onSurface, + contentDescription = null + ) + Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value)) + val tagName = tagChip.tag.tagDisplayName + Text( + text = stringResource(id = R.string.reader_tags_feed_error_title, tagName), + style = androidx.compose.material3.MaterialTheme.typography.labelLarge, + color = MaterialTheme.colors.onSurface, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(Margin.Medium.value)) + Text( + text = stringResource(id = R.string.reader_tags_feed_error_description, tagName), + style = androidx.compose.material3.MaterialTheme.typography.bodySmall, + color = if (isSystemInDarkTheme()) { + AppColor.White.copy(alpha = 0.4F) + } else { + AppColor.Black.copy(alpha = 0.4F) + }, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) + Button( + onClick = { postList.onRetryClick() }, + modifier = Modifier + .height(36.dp) + .width(114.dp), + elevation = ButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + ), + colors = ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colors.onPrimary, + backgroundColor = MaterialTheme.colors.onSurface, + ), + shape = RoundedCornerShape(50), + ) { + Text( + modifier = Modifier + .align(Alignment.CenterVertically), + style = androidx.compose.material3.MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.surface, + text = stringResource(R.string.reader_tags_feed_error_retry), + ) + } + } +} + // TODO move to VM sealed class UiState { data class Loaded(val data: List>) : UiState() From d01a27e192dde40c2170e26162c87157ac69f8a1 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 19 Apr 2024 21:03:49 -0300 Subject: [PATCH 074/237] Add click to post title, title excerpt and site name --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 9 ++- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 60 ++++++++++++++----- 2 files changed, 53 insertions(+), 16 deletions(-) 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 959d5014e023..99cf5d474c5c 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 @@ -197,7 +197,8 @@ private fun PostListLoaded( postNumberOfLikesText = postNumberOfLikesText, postNumberOfCommentsText = postNumberOfCommentsText, isPostLiked = isPostLiked, - onPostImageClick = onPostImageClick, + onSiteClick = onSiteClick, + onPostClick = onPostImageClick, onPostLikeClick = onPostLikeClick, onPostMoreMenuClick = onPostMoreMenuClick, ) @@ -365,6 +366,7 @@ data class TagsFeedPostItem( val postNumberOfLikesText: String, val postNumberOfCommentsText: String, val isPostLiked: Boolean, + val onSiteClick: () -> Unit, val onPostImageClick: () -> Unit, val onPostLikeClick: () -> Unit, val onPostMoreMenuClick: () -> Unit, @@ -386,6 +388,7 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "", isPostLiked = true, + onSiteClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -399,6 +402,7 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "", postNumberOfCommentsText = "3 comments", isPostLiked = true, + onSiteClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -412,6 +416,7 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "123 likes", postNumberOfCommentsText = "9 comments", isPostLiked = true, + onSiteClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -425,6 +430,7 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "1234 likes", postNumberOfCommentsText = "91 comments", isPostLiked = true, + onSiteClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, @@ -438,6 +444,7 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "12 likes", postNumberOfCommentsText = "34 comments", isPostLiked = true, + onSiteClick = {}, onPostImageClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, 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 99715e1d8a02..977ec18646f1 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 @@ -2,6 +2,7 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -24,6 +25,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -52,7 +54,8 @@ fun ReaderTagsFeedPostListItem( postNumberOfLikesText: String, postNumberOfCommentsText: String, isPostLiked: Boolean, - onPostImageClick: () -> Unit, + onSiteClick: () -> Unit, + onPostClick: () -> Unit, onPostLikeClick: () -> Unit, onPostMoreMenuClick: () -> Unit, ) { @@ -63,16 +66,24 @@ fun ReaderTagsFeedPostListItem( val secondaryElementColor = baseColor.copy( alpha = 0.6F ) - Column(modifier = Modifier - .width(240.dp) - .height(340.dp)) { + Column( + modifier = Modifier + .width(240.dp) + .height(340.dp) + ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { // Site name Text( - modifier = Modifier.weight(1F), + modifier = Modifier + .weight(1F) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onSiteClick() }, + ), text = siteName, style = MaterialTheme.typography.labelLarge, color = primaryElementColor, @@ -97,7 +108,13 @@ fun ReaderTagsFeedPostListItem( } // Post title Text( - modifier = Modifier.padding(top = Margin.Medium.value), + modifier = Modifier + .padding(top = Margin.Medium.value) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onPostClick() }, + ), text = postTitle, style = MaterialTheme.typography.titleMedium, color = baseColor, @@ -114,6 +131,11 @@ fun ReaderTagsFeedPostListItem( .conditionalThen( predicate = postImageUrl == null, other = Modifier.height(180.dp) + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onPostClick() }, ), text = postExcerpt, style = MaterialTheme.typography.bodySmall, @@ -125,7 +147,7 @@ fun ReaderTagsFeedPostListItem( if (!postImageUrl.isNullOrBlank()) { PostImage( imageUrl = postImageUrl, - onClick = onPostImageClick, + onClick = onPostClick, ) } Spacer(Modifier.weight(1f)) @@ -275,7 +297,8 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onPostImageClick = {}, + onSiteClick = {}, + onPostClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ) @@ -304,7 +327,8 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onPostImageClick = {}, + onSiteClick = {}, + onPostClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ) @@ -319,7 +343,8 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onPostImageClick = {}, + onSiteClick = {}, + onPostClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ) @@ -334,7 +359,8 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onPostImageClick = {}, + onSiteClick = {}, + onPostClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ) @@ -350,7 +376,8 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onPostImageClick = {}, + onSiteClick = {}, + onPostClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ) @@ -366,7 +393,8 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onPostImageClick = {}, + onSiteClick = {}, + onPostClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ) @@ -393,7 +421,8 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onPostImageClick = {}, + onSiteClick = {}, + onPostClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ) @@ -419,7 +448,8 @@ fun ReaderTagsFeedPostListItemPreview() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", isPostLiked = true, - onPostImageClick = {}, + onSiteClick = {}, + onPostClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ) From 83bec26ce4bd3cbc5a223589cec2ba97ef497e04 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 22 Apr 2024 12:16:14 -0300 Subject: [PATCH 075/237] Add unit test for ReaderPostLogicFactory --- .../post/ReaderPostLogicFactoryTest.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactoryTest.kt diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactoryTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactoryTest.kt new file mode 100644 index 000000000000..981220e7d45f --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactoryTest.kt @@ -0,0 +1,32 @@ +package org.wordpress.android.ui.reader.services.post + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.wordpress.android.ui.reader.repository.ReaderPostRepository +import org.wordpress.android.ui.reader.services.ServiceCompletionListener + +@RunWith(MockitoJUnitRunner::class) +class ReaderPostLogicFactoryTest { + @Mock + lateinit var readerPostRepository: ReaderPostRepository + + private lateinit var factory: ReaderPostLogicFactory + + @Before + fun setUp() { + factory = ReaderPostLogicFactory(readerPostRepository) + } + + @Test + fun `create should return a PostLogic instance`() { + val listener = ServiceCompletionListener { + // no-op + } + val logic = factory.create(listener) + assertThat(logic).isNotNull + } +} From c08eb4c11336f52dc06c48e1bf4a46276c60893b Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:59:16 -0300 Subject: [PATCH 076/237] Implement ReaderTagsFeed empty state --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 83 +++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) 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 99cf5d474c5c..89e1a41db54b 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 @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -22,6 +23,7 @@ import androidx.compose.foundation.lazy.items 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.MaterialTheme import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon @@ -40,6 +42,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.wordpress.android.R import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType @@ -60,7 +63,7 @@ fun ReaderTagsFeed(uiState: UiState) { when (uiState) { is UiState.Loading -> Loading() is UiState.Loaded -> Loaded(uiState) - is UiState.Empty -> Empty() + is UiState.Empty -> Empty(uiState) } } } @@ -145,8 +148,76 @@ private fun Loading() { } @Composable -private fun Empty() { -// TODO empty state (https://github.com/wordpress-mobile/WordPress-Android/issues/20584) +private fun Empty(uiState: UiState.Empty) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // Title + Text( + modifier = Modifier + .padding( + start = Margin.ExtraExtraMediumLarge.value, + end = Margin.ExtraExtraMediumLarge.value, + bottom = Margin.Medium.value, + ), + text = stringResource(id = R.string.reader_discover_empty_title), + textAlign = TextAlign.Center, + fontSize = 20.sp, + style = MaterialTheme.typography.subtitle1, + color = MaterialTheme.colors.onSurface.copy( + alpha = ContentAlpha.medium, + ), + ) + // Subtitle + Text( + modifier = Modifier + .padding( + start = Margin.ExtraExtraMediumLarge.value, + end = Margin.ExtraExtraMediumLarge.value, + bottom = Margin.Large.value, + ), + text = stringResource(id = R.string.reader_discover_empty_subtitle_follow), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.subtitle1, + color = MaterialTheme.colors.onSurface.copy( + alpha = ContentAlpha.medium, + ), + ) + // Button + Button( + onClick = uiState.onOpenTagsListClick, + modifier = Modifier.padding( + start = Margin.ExtraMediumLarge.value, + end = Margin.ExtraMediumLarge.value, + bottom = Margin.ExtraLarge.value, + ), + contentPadding = PaddingValues( + horizontal = 32.dp, + vertical = 8.dp, + ), + elevation = ButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + ), + colors = ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colors.onPrimary, + backgroundColor = MaterialTheme.colors.onSurface, + ), + ) { + Row() { + androidx.compose.material.Text( + modifier = Modifier + .align(Alignment.CenterVertically), + style = androidx.compose.material3.MaterialTheme.typography.titleMedium, + text = stringResource(id = R.string.reader_discover_empty_button_text), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + } + } } @Composable @@ -341,7 +412,7 @@ sealed class UiState { object Loading : UiState() - object Empty : UiState() + data class Empty(val onOpenTagsListClick: () -> Unit) : UiState() } data class TagChip( @@ -487,7 +558,9 @@ fun ReaderTagsFeedLoading() { fun ReaderTagsFeedEmpty() { AppTheme { ReaderTagsFeed( - uiState = UiState.Empty + uiState = UiState.Empty( + onOpenTagsListClick = {}, + ) ) } } From d06808eb5f919df1749e2feaea8a8db77662ad57 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 22 Apr 2024 17:47:56 -0300 Subject: [PATCH 077/237] Use ReaderPostTableWrapper in ReaderPostLocalSource --- .../wrappers/ReaderPostTableWrapper.kt | 21 +++++++++- .../reader/sources/ReaderPostLocalSource.kt | 42 ++++++++++--------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/ReaderPostTableWrapper.kt b/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/ReaderPostTableWrapper.kt index eeabbd6a0e0f..8b532688a530 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/ReaderPostTableWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/datasets/wrappers/ReaderPostTableWrapper.kt @@ -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 @@ -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) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt index d327effc5015..dfd03752dcc1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt @@ -1,7 +1,7 @@ package org.wordpress.android.ui.reader.sources import dagger.Reusable -import org.wordpress.android.datasets.ReaderPostTable +import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag @@ -15,7 +15,9 @@ import javax.inject.Inject * Manage the saving of posts to the local database table. */ @Reusable -class ReaderPostLocalSource @Inject constructor() { +class ReaderPostLocalSource @Inject constructor( + private val readerPostTable: ReaderPostTableWrapper, +) { /** * Save the list of posts to the local database, and handle any gaps between local and server posts. * @@ -27,7 +29,7 @@ class ReaderPostLocalSource @Inject constructor() { updateAction: ReaderPostServiceStarter.UpdateAction, requestedTag: ReaderTag?, ): ReaderActions.UpdateResult { - val updateResult = ReaderPostTable.comparePosts(serverPosts) + val updateResult = readerPostTable.comparePosts(serverPosts) if (updateResult.isNewOrChanged) { // gap detection - only applies to posts with a specific tag var postWithGap: ReaderPost? = null @@ -41,7 +43,7 @@ class ReaderPostLocalSource @Inject constructor() { handleRequestOlderThanGapResult(requestedTag) } - ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH -> ReaderPostTable.deletePostsWithTag( + ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH -> readerPostTable.deletePostsWithTag( requestedTag ) @@ -51,22 +53,24 @@ class ReaderPostLocalSource @Inject constructor() { } // save posts to local db - ReaderPostTable.addOrUpdatePosts(requestedTag, serverPosts) + readerPostTable.addOrUpdatePosts(requestedTag, serverPosts) + if (AppPrefs.shouldUpdateBookmarkPostsPseudoIds(requestedTag)) { - ReaderPostTable.updateBookmarkedPostPseudoId(serverPosts) + readerPostTable.updateBookmarkedPostPseudoId(serverPosts) AppPrefs.setBookmarkPostsPseudoIdsUpdated() } // gap marker must be set after saving server posts - if (postWithGap != null) { - ReaderPostTable.setGapMarkerForTag(postWithGap.blogId, postWithGap.postId, requestedTag) - AppLog.d(AppLog.T.READER, "added gap marker to tag " + requestedTag!!.tagNameForLog) + if (postWithGap != null && requestedTag != null) { + readerPostTable.setGapMarkerForTag(postWithGap.blogId, postWithGap.postId, requestedTag) + AppLog.d(AppLog.T.READER, "added gap marker to tag " + requestedTag.tagNameForLog) } } else if (updateResult == ReaderActions.UpdateResult.UNCHANGED && updateAction == ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP + && requestedTag != null ) { // edge case - request to fill gap returned nothing new, so remove the gap marker - ReaderPostTable.removeGapMarkerForTag(requestedTag) + readerPostTable.removeGapMarkerForTag(requestedTag) AppLog.w(AppLog.T.READER, "attempt to fill gap returned nothing new") } AppLog.d( @@ -76,11 +80,11 @@ class ReaderPostLocalSource @Inject constructor() { return updateResult } - private fun handleRequestOlderThanGapResult(requestedTag: ReaderTag?) { + private fun handleRequestOlderThanGapResult(requestedTag: ReaderTag) { // if service was started as a request to fill a gap, delete existing posts // before the one with the gap marker, then remove the existing gap marker - ReaderPostTable.deletePostsBeforeGapMarkerForTag(requestedTag) - ReaderPostTable.removeGapMarkerForTag(requestedTag) + readerPostTable.deletePostsBeforeGapMarkerForTag(requestedTag) + readerPostTable.removeGapMarkerForTag(requestedTag) } /** @@ -90,15 +94,15 @@ class ReaderPostLocalSource @Inject constructor() { */ private fun handleRequestNewerResult( serverPosts: ReaderPostList, - requestedTag: ReaderTag?, + requestedTag: ReaderTag, ): ReaderPost? { // if there's no overlap between server and local (ie: all server // posts are new), assume there's a gap between server and local // provided that local posts exist var postWithGap: ReaderPost? = null val numServerPosts = serverPosts.size - if (numServerPosts >= 2 && ReaderPostTable.getNumPostsWithTag(requestedTag) > 0 && - !ReaderPostTable.hasOverlap( + if (numServerPosts >= 2 && readerPostTable.getNumPostsWithTag(requestedTag) > 0 && + !readerPostTable.hasOverlap( serverPosts, requestedTag ) @@ -108,12 +112,12 @@ class ReaderPostLocalSource @Inject constructor() { // remove the last server post to deal with the edge case of // there actually not being a gap between local & server serverPosts.removeAt(numServerPosts - 1) - val gapMarker = ReaderPostTable.getGapMarkerIdsForTag(requestedTag) + val gapMarker = readerPostTable.getGapMarkerIdsForTag(requestedTag) if (gapMarker != null) { // We mustn't have two gapMarkers at the same time. Therefor we need to // delete all posts before the current gapMarker and clear the gapMarker flag. - ReaderPostTable.deletePostsBeforeGapMarkerForTag(requestedTag) - ReaderPostTable.removeGapMarkerForTag(requestedTag) + readerPostTable.deletePostsBeforeGapMarkerForTag(requestedTag) + readerPostTable.removeGapMarkerForTag(requestedTag) } } return postWithGap From 50e28c43aee830457a4a64b5481375adefafd8fc Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 22 Apr 2024 19:12:37 -0300 Subject: [PATCH 078/237] Use AppPrefsWrapper in ReaderPostLocalSource --- .../android/ui/prefs/AppPrefsWrapper.kt | 4 +++ .../reader/sources/ReaderPostLocalSource.kt | 35 ++++++++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt index ac2df764c1f4..d42aa5fa4797 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt @@ -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 = AppPrefs.getAllPrefs() fun setString(prefKey: PrefKey, value: String) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt index dfd03752dcc1..f1be3b98bae3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt @@ -5,7 +5,7 @@ import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag -import org.wordpress.android.ui.prefs.AppPrefs +import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.reader.actions.ReaderActions import org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter import org.wordpress.android.util.AppLog @@ -16,7 +16,8 @@ import javax.inject.Inject */ @Reusable class ReaderPostLocalSource @Inject constructor( - private val readerPostTable: ReaderPostTableWrapper, + private val readerPostTableWrapper: ReaderPostTableWrapper, + private val appPrefsWrapper: AppPrefsWrapper, ) { /** * Save the list of posts to the local database, and handle any gaps between local and server posts. @@ -29,7 +30,7 @@ class ReaderPostLocalSource @Inject constructor( updateAction: ReaderPostServiceStarter.UpdateAction, requestedTag: ReaderTag?, ): ReaderActions.UpdateResult { - val updateResult = readerPostTable.comparePosts(serverPosts) + val updateResult = readerPostTableWrapper.comparePosts(serverPosts) if (updateResult.isNewOrChanged) { // gap detection - only applies to posts with a specific tag var postWithGap: ReaderPost? = null @@ -43,7 +44,7 @@ class ReaderPostLocalSource @Inject constructor( handleRequestOlderThanGapResult(requestedTag) } - ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH -> readerPostTable.deletePostsWithTag( + ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH -> readerPostTableWrapper.deletePostsWithTag( requestedTag ) @@ -53,16 +54,16 @@ class ReaderPostLocalSource @Inject constructor( } // save posts to local db - readerPostTable.addOrUpdatePosts(requestedTag, serverPosts) + readerPostTableWrapper.addOrUpdatePosts(requestedTag, serverPosts) - if (AppPrefs.shouldUpdateBookmarkPostsPseudoIds(requestedTag)) { - readerPostTable.updateBookmarkedPostPseudoId(serverPosts) - AppPrefs.setBookmarkPostsPseudoIdsUpdated() + if (appPrefsWrapper.shouldUpdateBookmarkPostsPseudoIds(requestedTag)) { + readerPostTableWrapper.updateBookmarkedPostPseudoId(serverPosts) + appPrefsWrapper.setBookmarkPostsPseudoIdsUpdated() } // gap marker must be set after saving server posts if (postWithGap != null && requestedTag != null) { - readerPostTable.setGapMarkerForTag(postWithGap.blogId, postWithGap.postId, requestedTag) + readerPostTableWrapper.setGapMarkerForTag(postWithGap.blogId, postWithGap.postId, requestedTag) AppLog.d(AppLog.T.READER, "added gap marker to tag " + requestedTag.tagNameForLog) } } else if (updateResult == ReaderActions.UpdateResult.UNCHANGED @@ -70,7 +71,7 @@ class ReaderPostLocalSource @Inject constructor( && requestedTag != null ) { // edge case - request to fill gap returned nothing new, so remove the gap marker - readerPostTable.removeGapMarkerForTag(requestedTag) + readerPostTableWrapper.removeGapMarkerForTag(requestedTag) AppLog.w(AppLog.T.READER, "attempt to fill gap returned nothing new") } AppLog.d( @@ -83,8 +84,8 @@ class ReaderPostLocalSource @Inject constructor( private fun handleRequestOlderThanGapResult(requestedTag: ReaderTag) { // if service was started as a request to fill a gap, delete existing posts // before the one with the gap marker, then remove the existing gap marker - readerPostTable.deletePostsBeforeGapMarkerForTag(requestedTag) - readerPostTable.removeGapMarkerForTag(requestedTag) + readerPostTableWrapper.deletePostsBeforeGapMarkerForTag(requestedTag) + readerPostTableWrapper.removeGapMarkerForTag(requestedTag) } /** @@ -101,8 +102,8 @@ class ReaderPostLocalSource @Inject constructor( // provided that local posts exist var postWithGap: ReaderPost? = null val numServerPosts = serverPosts.size - if (numServerPosts >= 2 && readerPostTable.getNumPostsWithTag(requestedTag) > 0 && - !readerPostTable.hasOverlap( + if (numServerPosts >= 2 && readerPostTableWrapper.getNumPostsWithTag(requestedTag) > 0 && + !readerPostTableWrapper.hasOverlap( serverPosts, requestedTag ) @@ -112,12 +113,12 @@ class ReaderPostLocalSource @Inject constructor( // remove the last server post to deal with the edge case of // there actually not being a gap between local & server serverPosts.removeAt(numServerPosts - 1) - val gapMarker = readerPostTable.getGapMarkerIdsForTag(requestedTag) + val gapMarker = readerPostTableWrapper.getGapMarkerIdsForTag(requestedTag) if (gapMarker != null) { // We mustn't have two gapMarkers at the same time. Therefor we need to // delete all posts before the current gapMarker and clear the gapMarker flag. - readerPostTable.deletePostsBeforeGapMarkerForTag(requestedTag) - readerPostTable.removeGapMarkerForTag(requestedTag) + readerPostTableWrapper.deletePostsBeforeGapMarkerForTag(requestedTag) + readerPostTableWrapper.removeGapMarkerForTag(requestedTag) } } return postWithGap From 404d22b8e4c159893576367eda390362501c0b5a Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 22 Apr 2024 19:12:52 -0300 Subject: [PATCH 079/237] Add unit tests for ReaderPostLocalSource --- .../sources/ReaderPostLocalSourceTest.kt | 327 ++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSourceTest.kt diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSourceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSourceTest.kt new file mode 100644 index 000000000000..34e555cf219e --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSourceTest.kt @@ -0,0 +1,327 @@ +package org.wordpress.android.ui.reader.sources + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.only +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper +import org.wordpress.android.models.ReaderPostList +import org.wordpress.android.models.ReaderTag +import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResult +import org.wordpress.android.ui.reader.services.post.ReaderPostServiceStarter + +@OptIn(ExperimentalCoroutinesApi::class) +class ReaderPostLocalSourceTest : BaseUnitTest() { + @Mock + lateinit var readerPostTableWrapper: ReaderPostTableWrapper + + @Mock + lateinit var appPrefsWrapper: AppPrefsWrapper + + private lateinit var localSource: ReaderPostLocalSource + + @Before + fun setUp() { + localSource = ReaderPostLocalSource(readerPostTableWrapper, appPrefsWrapper) + } + + @Test + fun `given no changes and no tag provided, when saveUpdatedPosts, then do nothing`() { + // Given + val serverPosts = ReaderPostList() + val requestedTag = null + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(UpdateResult.UNCHANGED) + + // it doesn't matter which update action was used, so let's test all of them + ReaderPostServiceStarter.UpdateAction.values().forEach { updateAction -> + clearInvocations(readerPostTableWrapper) + + // When + val result = localSource.saveUpdatedPosts(serverPosts, updateAction, requestedTag) + + // Then + verify(readerPostTableWrapper, only()).comparePosts(serverPosts) // only comparePosts should be + + assertThat(result).isEqualTo(UpdateResult.UNCHANGED) + } + } + + @Test + fun `given no changes and tag provided, when saveUpdatedPosts, then do nothing`() { + // Given + val serverPosts = ReaderPostList() + val requestedTag = ReaderTag("tag", "tag", "tag", "endpoint", ReaderTagType.FOLLOWED) + + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(UpdateResult.UNCHANGED) + + // if the action is any but REQUEST_OLDER_THAN_GAP we should not do anything + ReaderPostServiceStarter.UpdateAction.values() + .filterNot { it == ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP } + .forEach { updateAction -> + clearInvocations(readerPostTableWrapper) + + // When + val result = localSource.saveUpdatedPosts(serverPosts, updateAction, requestedTag) + + // Then + verify(readerPostTableWrapper, only()).comparePosts(serverPosts) // only comparePosts should be + + assertThat(result).isEqualTo(UpdateResult.UNCHANGED) + } + } + + @Test + fun `given no changes, tag provided and OLDER_THAN_GAP, when saveUpdatedPosts, then remove gap marker`() { + // Given + val serverPosts = ReaderPostList() + val requestedTag = ReaderTag("tag", "tag", "tag", "endpoint", ReaderTagType.FOLLOWED) + + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(UpdateResult.UNCHANGED) + + // When + val result = localSource.saveUpdatedPosts( + serverPosts, + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP, + requestedTag, + ) + + // Then + verify(readerPostTableWrapper).removeGapMarkerForTag(requestedTag) + + assertThat(result).isEqualTo(UpdateResult.UNCHANGED) + } + + @Test + fun `given new posts and no tag provided, when saveUpdatedPosts, then save posts`() { + // Given + val serverPosts = ReaderPostList() + val requestedTag = null + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(UpdateResult.HAS_NEW) + + // it doesn't matter which update action was used, so let's test all of them + ReaderPostServiceStarter.UpdateAction.values().forEach { updateAction -> + clearInvocations(readerPostTableWrapper) + + // When + val result = localSource.saveUpdatedPosts(serverPosts, updateAction, requestedTag) + + // Then + verify(readerPostTableWrapper).addOrUpdatePosts(requestedTag, serverPosts) + + assertThat(result).isEqualTo(UpdateResult.HAS_NEW) + } + } + + @Test + fun `given posts changed, tag provided and OLDER_THAN_GAP, when saveUpdatedPosts, then remove gap marker`() { + // Given + val serverPosts = ReaderPostList() + val requestedTag = ReaderTag("tag", "tag", "tag", "endpoint", ReaderTagType.FOLLOWED) + + listOf(UpdateResult.CHANGED, UpdateResult.HAS_NEW).forEach { updateResult -> + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(updateResult) + clearInvocations(readerPostTableWrapper) + + // When + val result = localSource.saveUpdatedPosts( + serverPosts, + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER_THAN_GAP, + requestedTag, + ) + + // Then + verify(readerPostTableWrapper).deletePostsBeforeGapMarkerForTag(requestedTag) + verify(readerPostTableWrapper).removeGapMarkerForTag(requestedTag) + verify(readerPostTableWrapper).addOrUpdatePosts(requestedTag, serverPosts) + + assertThat(result).isEqualTo(updateResult) + } + } + + @Test + fun `given posts changed, tag provided and REFRESH, when saveUpdatedPosts, then delete posts and save`() { + // Given + val serverPosts = ReaderPostList() + val requestedTag = ReaderTag("tag", "tag", "tag", "endpoint", ReaderTagType.FOLLOWED) + + listOf(UpdateResult.CHANGED, UpdateResult.HAS_NEW).forEach { updateResult -> + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(updateResult) + clearInvocations(readerPostTableWrapper) + + // When + val result = localSource.saveUpdatedPosts( + serverPosts, + ReaderPostServiceStarter.UpdateAction.REQUEST_REFRESH, + requestedTag, + ) + + // Then + verify(readerPostTableWrapper).deletePostsWithTag(requestedTag) + verify(readerPostTableWrapper).addOrUpdatePosts(requestedTag, serverPosts) + + assertThat(result).isEqualTo(updateResult) + } + } + + @Test + fun `given posts changed, tag provided and OLDER, when saveUpdatedPosts, then save`() { + // Given + val serverPosts = ReaderPostList() + val requestedTag = ReaderTag("tag", "tag", "tag", "endpoint", ReaderTagType.FOLLOWED) + + listOf(UpdateResult.CHANGED, UpdateResult.HAS_NEW).forEach { updateResult -> + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(updateResult) + clearInvocations(readerPostTableWrapper) + + + // When + val result = localSource.saveUpdatedPosts( + serverPosts, + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER, + requestedTag, + ) + + // Then + verify(readerPostTableWrapper).addOrUpdatePosts(requestedTag, serverPosts) + + assertThat(result).isEqualTo(updateResult) + } + } + + @Test + fun `given posts changed, tag provided and NEWER with no gap, when saveUpdatedPosts, then save`() { + // Given + val serverPosts = ReaderPostList().apply { + repeat(4) { add(mock()) } + } + val requestedTag = ReaderTag("tag", "tag", "tag", "endpoint", ReaderTagType.FOLLOWED) + + whenever(readerPostTableWrapper.getNumPostsWithTag(requestedTag)).thenReturn(4) + whenever(readerPostTableWrapper.hasOverlap(serverPosts, requestedTag)).thenReturn(true) + + listOf(UpdateResult.CHANGED, UpdateResult.HAS_NEW).forEach { updateResult -> + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(updateResult) + clearInvocations(readerPostTableWrapper) + + // When + val result = localSource.saveUpdatedPosts( + serverPosts, + ReaderPostServiceStarter.UpdateAction.REQUEST_NEWER, + requestedTag, + ) + + // Then + verify(readerPostTableWrapper).addOrUpdatePosts(requestedTag, serverPosts) + + assertThat(result).isEqualTo(updateResult) + } + } + + @Test + fun `given posts changed, tag provided and NEWER with gap, when saveUpdatedPosts, then save and set gap`() { + // Given + val serverPosts = ReaderPostList().apply { + repeat(4) { add(mock()) } + } + val requestedTag = ReaderTag("tag", "tag", "tag", "endpoint", ReaderTagType.FOLLOWED) + + whenever(readerPostTableWrapper.getNumPostsWithTag(requestedTag)).thenReturn(4) + whenever(readerPostTableWrapper.hasOverlap(serverPosts, requestedTag)).thenReturn(false) + + listOf(UpdateResult.CHANGED, UpdateResult.HAS_NEW).forEach { updateResult -> + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(updateResult) + clearInvocations(readerPostTableWrapper) + + // When + val result = localSource.saveUpdatedPosts( + serverPosts, + ReaderPostServiceStarter.UpdateAction.REQUEST_NEWER, + requestedTag, + ) + + // Then + verify(readerPostTableWrapper, never()).deletePostsBeforeGapMarkerForTag(requestedTag) + verify(readerPostTableWrapper, never()).removeGapMarkerForTag(requestedTag) + verify(readerPostTableWrapper).addOrUpdatePosts(requestedTag, serverPosts) + verify(readerPostTableWrapper).setGapMarkerForTag(any(), any(), eq(requestedTag)) + + assertThat(result).isEqualTo(updateResult) + } + } + + @Test + fun `given posts changed, tag provided and NEWER with gap, when saveUpdatedPosts, then keep 1 gap only and save`() { + // Given + val serverPosts = ReaderPostList().apply { + repeat(4) { add(mock()) } + } + val requestedTag = ReaderTag("tag", "tag", "tag", "endpoint", ReaderTagType.FOLLOWED) + + whenever(readerPostTableWrapper.getNumPostsWithTag(requestedTag)).thenReturn(5) + whenever(readerPostTableWrapper.hasOverlap(serverPosts, requestedTag)).thenReturn(false) + whenever(readerPostTableWrapper.getGapMarkerIdsForTag(requestedTag)).thenReturn(mock()) + + listOf(UpdateResult.CHANGED, UpdateResult.HAS_NEW).forEach { updateResult -> + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(updateResult) + clearInvocations(readerPostTableWrapper) + + // When + val result = localSource.saveUpdatedPosts( + serverPosts, + ReaderPostServiceStarter.UpdateAction.REQUEST_NEWER, + requestedTag, + ) + + // Then + verify(readerPostTableWrapper).deletePostsBeforeGapMarkerForTag(requestedTag) + verify(readerPostTableWrapper).removeGapMarkerForTag(requestedTag) + verify(readerPostTableWrapper).addOrUpdatePosts(requestedTag, serverPosts) + verify(readerPostTableWrapper).setGapMarkerForTag(any(), any(), eq(requestedTag)) + + assertThat(result).isEqualTo(updateResult) + } + } + + @Test + fun `given posts changed, tag provided and update bookmark, when saveUpdatedPosts, then update bookmark`() { + // Given + val serverPosts = ReaderPostList() + val requestedTag = ReaderTag("tag", "tag", "tag", "endpoint", ReaderTagType.FOLLOWED) + + listOf(UpdateResult.CHANGED, UpdateResult.HAS_NEW).forEach { updateResult -> + whenever(readerPostTableWrapper.comparePosts(serverPosts)).thenReturn(updateResult) + whenever(appPrefsWrapper.shouldUpdateBookmarkPostsPseudoIds(requestedTag)).thenReturn(true) + + // it doesn't matter which update action was used, so let's test all of them + ReaderPostServiceStarter.UpdateAction.values().forEach { updateAction -> + clearInvocations(readerPostTableWrapper, appPrefsWrapper) + + // When + val result = localSource.saveUpdatedPosts( + serverPosts, + updateAction, + requestedTag, + ) + + // Then + verify(readerPostTableWrapper).addOrUpdatePosts(requestedTag, serverPosts) + verify(readerPostTableWrapper).updateBookmarkedPostPseudoId(serverPosts) + verify(appPrefsWrapper).setBookmarkPostsPseudoIdsUpdated() + + assertThat(result).isEqualTo(updateResult) + } + } + } +} From 088e9c54312924b2c10c32f560b93a68e5161052 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:42:01 -0300 Subject: [PATCH 080/237] Update error state messages for ReaderTagsFeed --- .../ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 6 +++++- WordPress/src/main/res/values/strings.xml | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) 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 99cf5d474c5c..54c0a80dcab0 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 @@ -297,8 +297,12 @@ private fun PostListError( textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(Margin.Medium.value)) + val errorMessageResId = when (postList.type) { + is ErrorType.Loading -> R.string.reader_tags_feed_loading_error_description + is ErrorType.NoContent -> R.string.reader_tags_feed_no_content_error_description + } Text( - text = stringResource(id = R.string.reader_tags_feed_error_description, tagName), + text = stringResource(errorMessageResId, tagName), style = androidx.compose.material3.MaterialTheme.typography.bodySmall, color = if (isSystemInDarkTheme()) { AppColor.White.copy(alpha = 0.4F) diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index ffd83f70568f..a218ce0fdc9d 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2306,7 +2306,8 @@ More from %s No posts found for %s - This tag might not have any posts, or there was no internet connection. + We couldn\'t load posts from this tag right now + We couldn\'t find any posts tagged %s right now Retry From 1bab9d10db2e2cf09a4a32784bdc3c8e07292560 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:54:22 -0300 Subject: [PATCH 081/237] Refactor tags feed item to use a specific object instead of a pair --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) 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 54c0a80dcab0..ac9c574d3770 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 @@ -297,12 +297,12 @@ private fun PostListError( textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(Margin.Medium.value)) - val errorMessageResId = when (postList.type) { - is ErrorType.Loading -> R.string.reader_tags_feed_loading_error_description - is ErrorType.NoContent -> R.string.reader_tags_feed_no_content_error_description + val errorMessage = when (postList.type) { + is ErrorType.Loading -> stringResource(R.string.reader_tags_feed_loading_error_description) + is ErrorType.NoContent -> stringResource(R.string.reader_tags_feed_no_content_error_description, tagName) } Text( - text = stringResource(errorMessageResId, tagName), + text = errorMessage, style = androidx.compose.material3.MaterialTheme.typography.bodySmall, color = if (isSystemInDarkTheme()) { AppColor.White.copy(alpha = 0.4F) @@ -341,13 +341,18 @@ private fun PostListError( // TODO move to VM sealed class UiState { - data class Loaded(val data: List>) : UiState() + data class Loaded(val data: List) : UiState() object Loading : UiState() object Empty : UiState() } +data class TagFeedItem( + val tagChip: TagChip, + val postList: PostList, +) + data class TagChip( val tag: ReaderTag, val onTagClicked: () -> Unit, @@ -358,7 +363,16 @@ sealed class PostList { object Loading : PostList() - data class Error(val onRetryClick: () -> Unit) : PostList() + data class Error( + val type: ErrorType, + val onRetryClick: () -> Unit + ) : PostList() +} + +sealed interface ErrorType { + data object Loading : ErrorType + + data object NoContent : ErrorType } data class TagsFeedPostItem( @@ -465,9 +479,22 @@ fun ReaderTagsFeedLoaded() { ReaderTagsFeed( uiState = UiState.Loaded( data = listOf( - (TagChip(readerTag, {}) to postListLoaded), - (TagChip(readerTag, {}) to PostList.Loading), - (TagChip(readerTag, {}) to PostList.Error {}), + TagFeedItem( + tagChip = TagChip(readerTag, {}), + postList = postListLoaded + ), + TagFeedItem( + tagChip = TagChip(readerTag, {}), + postList = PostList.Loading, + ), + TagFeedItem( + tagChip = TagChip(readerTag, {}), + postList = PostList.Error(ErrorType.Loading, {}), + ), + TagFeedItem( + tagChip = TagChip(readerTag, {}), + postList = PostList.Error(ErrorType.NoContent, {}), + ), ) ) ) From 101e25ed3929e349599ae19799cf80eb7cba3074 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:40:55 -0300 Subject: [PATCH 082/237] Apply PR suggestion: remove useless Row element --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) 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 9e043da7b81d..172e55c19197 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 @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -206,16 +205,14 @@ private fun Empty(uiState: UiState.Empty) { backgroundColor = MaterialTheme.colors.onSurface, ), ) { - Row() { - androidx.compose.material.Text( - modifier = Modifier - .align(Alignment.CenterVertically), - style = androidx.compose.material3.MaterialTheme.typography.titleMedium, - text = stringResource(id = R.string.reader_discover_empty_button_text), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - } + androidx.compose.material.Text( + modifier = Modifier + .align(Alignment.CenterVertically), + style = androidx.compose.material3.MaterialTheme.typography.titleMedium, + text = stringResource(id = R.string.reader_discover_empty_button_text), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) } } } From b414849b36f439f05a32356b25f79723b00a8f4f Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 23 Apr 2024 14:56:22 -0300 Subject: [PATCH 083/237] Add retry to temporary UI to remove suppress annotation --- .../ui/reader/ReaderTagsFeedFragment.kt | 30 +++++++++++++++---- .../viewmodels/ReaderTagsFeedViewModel.kt | 1 - 2 files changed, 25 insertions(+), 6 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 d5e586deaa7d..25560fcff18a 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 @@ -3,6 +3,7 @@ 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 @@ -20,6 +21,7 @@ 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 @@ -77,7 +79,10 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme binding.composeView.setContent { AppThemeWithoutBackground { val uiState by viewModel.uiStateFlow.collectAsState() - ReaderTagsFeedScreen(uiState) + ReaderTagsFeedScreen( + uiState = uiState, + onRetryClicked = viewModel::fetchTag, + ) } } @@ -139,6 +144,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme @Composable private fun ReaderTagsFeedScreen( uiState: ReaderTagsFeedViewModel.UiState, + onRetryClicked: (ReaderTag) -> Unit, ) { Column( modifier = Modifier @@ -165,10 +171,24 @@ private fun ReaderTagsFeedScreen( } is ReaderTagsFeedViewModel.FetchState.Error -> { - Text( - text = "Error loading posts", - style = MaterialTheme.typography.body1, - ) + 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 -> { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt index 7d25a4bfe557..6016df2cc87a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -39,7 +39,6 @@ class ReaderTagsFeedViewModel @Inject constructor( * * Can be used for retrying a failed fetch, for instance. */ - @Suppress("MemberVisibilityCanBePrivate") fun fetchTag(tag: ReaderTag) { launch { _uiStateFlow.update { From d7f3946076f9e1de748aaaea7793fa4cdff38858 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 25 Apr 2024 15:02:54 -0300 Subject: [PATCH 084/237] Fix comment typo --- .../org/wordpress/android/ui/reader/ReaderTagsFeedFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 25560fcff18a..c4ce39ad344c 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 @@ -44,7 +44,7 @@ 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 + * Initial implementation of ReaderTagsFeedFragment with the idea of it containing both a ComposeView, which will host * 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. * From 05e043348d72972c968961995c6546b98c684fdd Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 25 Apr 2024 15:03:04 -0300 Subject: [PATCH 085/237] Add return type to public function --- .../android/ui/reader/services/post/ReaderPostLogicFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactory.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactory.kt index 6d64d4b5117d..952ef6b31db8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactory.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactory.kt @@ -7,7 +7,7 @@ import javax.inject.Inject class ReaderPostLogicFactory @Inject constructor( private val readerPostRepository: ReaderPostRepository, ) { - fun create(listener: ServiceCompletionListener) = ReaderPostLogic( + fun create(listener: ServiceCompletionListener): ReaderPostLogic = ReaderPostLogic( listener, readerPostRepository, ) From 6a0d1395d71ea94607623af5762ae30dad56e53c Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 25 Apr 2024 15:05:11 -0300 Subject: [PATCH 086/237] Check for returned instance type instead of null in test --- .../ui/reader/services/post/ReaderPostLogicFactoryTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactoryTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactoryTest.kt index 981220e7d45f..dea73350f024 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactoryTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogicFactoryTest.kt @@ -27,6 +27,6 @@ class ReaderPostLogicFactoryTest { // no-op } val logic = factory.create(listener) - assertThat(logic).isNotNull + assertThat(logic).isInstanceOf(ReaderPostLogic::class.java) } } From 4b99a1f313dc7b82b867f65a5755068deff2f7fb Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 25 Apr 2024 18:39:14 -0300 Subject: [PATCH 087/237] Add unit tests for ReaderTagsFeedViewModel --- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt 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 new file mode 100644 index 000000000000..d4008189759b --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt @@ -0,0 +1,234 @@ +package org.wordpress.android.ui.reader.viewmodels + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.kotlin.doSuspendableAnswer +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.models.ReaderPostList +import org.wordpress.android.models.ReaderTag +import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException +import org.wordpress.android.ui.reader.repository.ReaderPostRepository +import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.FetchState + +@OptIn(ExperimentalCoroutinesApi::class) +class ReaderTagsFeedViewModelTest : BaseUnitTest() { + @Mock + lateinit var readerPostRepository: ReaderPostRepository + + private lateinit var viewModel: ReaderTagsFeedViewModel + + private val collectedUiStates: MutableList = mutableListOf() + + @Before + fun setUp() { + viewModel = ReaderTagsFeedViewModel(testDispatcher(), readerPostRepository) + } + + @Test + fun `given valid tag, when fetchTag, then UI state should update properly`() = testCollectingUiStates { + // Given + val tag = ReaderTag( + "tag", + "tag", + "tag", + "endpoint", + ReaderTagType.FOLLOWED, + ) + val posts = ReaderPostList() + whenever(readerPostRepository.fetchNewerPostsForTag(tag)).doSuspendableAnswer { + delay(100) + posts + } + + // When + viewModel.fetchTag(tag) + advanceUntilIdle() + + // Then + assertThat(collectedUiStates).contains( + ReaderTagsFeedViewModel.UiState( + mapOf( + tag to FetchState.Loading, + ) + ), + ReaderTagsFeedViewModel.UiState( + mapOf( + tag to FetchState.Success(posts), + ) + ), + ) + } + + @Test + fun `given invalid tag, when fetchTag, then UI state should update properly`() = testCollectingUiStates { + // Given + val tag = ReaderTag( + "tag", + "tag", + "tag", + "endpoint", + ReaderTagType.FOLLOWED, + ) + val error = ReaderPostFetchException("error") + whenever(readerPostRepository.fetchNewerPostsForTag(tag)).doSuspendableAnswer { + delay(100) + throw error + } + + // When + viewModel.fetchTag(tag) + advanceUntilIdle() + + // Then + assertThat(collectedUiStates).contains( + ReaderTagsFeedViewModel.UiState( + mapOf( + tag to FetchState.Loading, + ) + ), + ReaderTagsFeedViewModel.UiState( + mapOf( + tag to FetchState.Error(error), + ) + ), + ) + } + + @Test + fun `given valid tags, when fetchAll, then UI state should update properly`() = testCollectingUiStates { + // Given + val tag1 = ReaderTag( + "tag1", + "tag1", + "tag1", + "endpoint1", + ReaderTagType.FOLLOWED, + ) + val tag2 = ReaderTag( + "tag2", + "tag2", + "tag2", + "endpoint2", + ReaderTagType.FOLLOWED, + ) + val posts1 = ReaderPostList() + val posts2 = ReaderPostList() + whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { + delay(100) + posts1 + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag2)).doSuspendableAnswer { + delay(200) + posts2 + } + + // When + viewModel.fetchAll(listOf(tag1, tag2)) + advanceUntilIdle() + + // Then + + // tag 1 + assertThat(collectedUiStates).anyMatch { + it.tagStates[tag1] == FetchState.Loading + } + assertThat(collectedUiStates).anyMatch { + it.tagStates[tag1] == FetchState.Success(posts1) + } + + // tag 2 + assertThat(collectedUiStates).anyMatch { + it.tagStates[tag2] == FetchState.Loading + } + assertThat(collectedUiStates).anyMatch { + it.tagStates[tag2] == FetchState.Success(posts1) + } + + assertThat(collectedUiStates.last()).isEqualTo( + ReaderTagsFeedViewModel.UiState( + mapOf( + tag1 to FetchState.Success(posts1), + tag2 to FetchState.Success(posts2), + ) + ) + ) + } + + @Test + fun `given valid and invalid tags, when fetchAll, then UI state should update properly`() = testCollectingUiStates { + // Given + val tag1 = ReaderTag( + "tag1", + "tag1", + "tag1", + "endpoint1", + ReaderTagType.FOLLOWED, + ) + val tag2 = ReaderTag( + "tag2", + "tag2", + "tag2", + "endpoint2", + ReaderTagType.FOLLOWED, + ) + val posts1 = ReaderPostList() + val error2 = ReaderPostFetchException("error") + whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { + delay(100) + posts1 + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag2)).doSuspendableAnswer { + delay(200) + throw error2 + } + + // When + viewModel.fetchAll(listOf(tag1, tag2)) + advanceUntilIdle() + + // Then + + // tag 1 + assertThat(collectedUiStates).anyMatch { + it.tagStates[tag1] == FetchState.Loading + } + assertThat(collectedUiStates).anyMatch { + it.tagStates[tag1] == FetchState.Success(posts1) + } + + // tag 2 + assertThat(collectedUiStates).anyMatch { + it.tagStates[tag2] == FetchState.Loading + } + assertThat(collectedUiStates).anyMatch { + it.tagStates[tag2] == FetchState.Error(error2) + } + + assertThat(collectedUiStates.last()).isEqualTo( + ReaderTagsFeedViewModel.UiState( + mapOf( + tag1 to FetchState.Success(posts1), + tag2 to FetchState.Error(error2), + ) + ) + ) + } + + private fun testCollectingUiStates(block: suspend TestScope.() -> Unit) = test { + val collectedUiStatesJob = launch { + collectedUiStates.clear() + viewModel.uiStateFlow.toList(collectedUiStates) + } + this.block() + collectedUiStatesJob.cancel() + } +} From 59ba75c6f3ace25198d20d2c7c56d33c8edb2e37 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 25 Apr 2024 18:39:31 -0300 Subject: [PATCH 088/237] Fix linebreak format --- .../android/ui/reader/sources/ReaderPostLocalSource.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt index f1be3b98bae3..b649d5fb55ec 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/sources/ReaderPostLocalSource.kt @@ -48,7 +48,8 @@ class ReaderPostLocalSource @Inject constructor( requestedTag ) - ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER -> { /* noop */ + ReaderPostServiceStarter.UpdateAction.REQUEST_OLDER -> { + /* noop */ } } } From e7f9b57e421492dcd6be47956aef65e06e0cb0f4 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 26 Apr 2024 17:41:27 -0300 Subject: [PATCH 089/237] [WIP] Extract SubFilterViewModel init logic to ReaderFragment --- .../android/ui/reader/ReaderFragment.kt | 185 ++++++++++++++++-- .../ui/reader/ReaderPostListFragment.java | 85 +------- .../ui/reader/ReaderTagsFeedFragment.kt | 24 +-- .../ui/reader/SubfilterBottomSheetFragment.kt | 7 +- .../ui/reader/subfilter/SubFilterViewModel.kt | 3 +- .../subfilter/SubFilterViewModelOwner.kt | 66 +++++++ .../reader/subfilter/SubfilterPageFragment.kt | 5 +- 7 files changed, 245 insertions(+), 130 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelOwner.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index eb8a55d29038..e692f05a4288 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -22,23 +22,34 @@ import org.greenrobot.eventbus.ThreadMode.MAIN import org.wordpress.android.R import org.wordpress.android.databinding.ReaderFragmentLayoutBinding import org.wordpress.android.models.JetpackPoweredScreen +import org.wordpress.android.models.ReaderTag import org.wordpress.android.ui.ScrollableViewInitializedListener import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayFragment import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureOverlayScreenType -import org.wordpress.android.ui.main.WPMainActivity import org.wordpress.android.ui.main.WPMainActivity.OnScrollToTopListener import org.wordpress.android.ui.main.WPMainNavigationView.PageType.READER import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredBottomSheetFragment import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.quickstart.QuickStartEvent +import org.wordpress.android.ui.reader.SubfilterBottomSheetFragment.Companion.newInstance import org.wordpress.android.ui.reader.discover.ReaderDiscoverFragment import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsFragment +import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask.FOLLOWED_BLOGS import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask.TAGS import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter +import org.wordpress.android.ui.reader.subfilter.ActionType +import org.wordpress.android.ui.reader.subfilter.ActionType.OpenLoginPage +import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSearchPage +import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSubsAtPage +import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSuggestedTagsPage +import org.wordpress.android.ui.reader.subfilter.BottomSheetUiState +import org.wordpress.android.ui.reader.subfilter.BottomSheetUiState.BottomSheetVisible import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel +import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelOwner import org.wordpress.android.ui.reader.subfilter.SubfilterCategory +import org.wordpress.android.ui.reader.subfilter.SubfilterListItem import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState import org.wordpress.android.ui.reader.views.compose.ReaderTopAppBar @@ -46,18 +57,21 @@ import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterType import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.util.JetpackBrandingUtils +import org.wordpress.android.util.NetworkUtils import org.wordpress.android.util.QuickStartUtilsWrapper import org.wordpress.android.util.SnackbarItem import org.wordpress.android.util.SnackbarItem.Action import org.wordpress.android.util.SnackbarItem.Info import org.wordpress.android.util.SnackbarSequencer +import org.wordpress.android.viewmodel.Event +import org.wordpress.android.viewmodel.main.WPMainActivityViewModel import org.wordpress.android.viewmodel.observeEvent import java.util.EnumSet import javax.inject.Inject @AndroidEntryPoint class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableViewInitializedListener, - WPMainActivity.OnScrollToTopListener { + OnScrollToTopListener, SubFilterViewModelOwner { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -72,12 +86,15 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView @Inject lateinit var snackbarSequencer: SnackbarSequencer + private lateinit var viewModel: ReaderViewModel private var binding: ReaderFragmentLayoutBinding? = null private var readerSearchResultLauncher: ActivityResultLauncher? = null + private var readerSubsActivityResultLauncher: ActivityResultLauncher? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding = ReaderFragmentLayoutBinding.bind(view).apply { initTopAppBar() @@ -103,6 +120,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView override fun onAttach(context: Context) { super.onAttach(context) initReaderSearchActivityResultLauncher() + initReaderSubsActivityResultLauncher() } private fun initReaderSearchActivityResultLauncher() { @@ -122,6 +140,25 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView } } + private fun initReaderSubsActivityResultLauncher() { + readerSubsActivityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + val data = result.data + if (data != null) { + val shouldRefreshSubscriptions = data.getBooleanExtra( + ReaderSubsActivity.RESULT_SHOULD_REFRESH_SUBSCRIPTIONS, + false + ) + if (shouldRefreshSubscriptions) { + getSubFilterViewModel()?.loadSubFilters() + } + } + } + } + } + private fun ReaderFragmentLayoutBinding.initTopAppBar() { readerTopBarComposeView.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) @@ -144,7 +181,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView } private fun ReaderFragmentLayoutBinding.initViewModel(savedInstanceState: Bundle?) { - viewModel = ViewModelProvider(this@ReaderFragment, viewModelFactory).get(ReaderViewModel::class.java) + viewModel = ViewModelProvider(this@ReaderFragment, viewModelFactory)[ReaderViewModel::class.java] startReaderViewModel(savedInstanceState) } @@ -353,19 +390,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView return childFragmentManager.findFragmentById(R.id.container) } - // The view model is started by the ReaderPostListFragment for feeds that support filtering - private fun getSubFilterViewModel(): SubFilterViewModel? { - val currentFragment = getCurrentFeedFragment() - val selectedTag = (viewModel.uiState.value as? ContentUiState)?.selectedReaderTag - - if (currentFragment == null || selectedTag == null) return null - - return ViewModelProvider(currentFragment, viewModelFactory).get( - SubFilterViewModel.getViewModelKeyForTag(selectedTag), - SubFilterViewModel::class.java - ) - } - private fun tryOpenFilterList(type: ReaderFilterType) { val viewModel = getSubFilterViewModel() ?: return @@ -390,4 +414,133 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView currentFragment.onScrollToTop() } } + + private fun getSubFilterViewModel(): SubFilterViewModel? { + val selectedTag = (viewModel.uiState.value as? ContentUiState)?.selectedReaderTag ?: return null + return getSubFilterViewModelForTag(selectedTag) + } + + /** + * Get the SubFilterViewModel for the given key. It doesn't initialize the ViewModel if it's not already started, so + * should only be used for getting a ViewModel that's already been started. + */ + override fun getSubFilterViewModelForKey(key: String): SubFilterViewModel { + return ViewModelProvider(this, viewModelFactory)[key, SubFilterViewModel::class.java] + } + + override fun getSubFilterViewModelForTag(tag: ReaderTag, savedInstanceState: Bundle?): SubFilterViewModel { + // TODO thomashortadev now that this Fragment owns the SubFilterViewModel, it lives longer than the context + // that uses it (selected feed), so we need to make sure it's properly cleared when the feed is changed. + // OR maybe we can set the owner to be the current feed Fragment but have this ReaderFragment manage the + // retrieval and initialization of the SubFilterViewModel. 🤔 + + return ViewModelProvider(this, viewModelFactory)[ + SubFilterViewModel.getViewModelKeyForTag(tag), + SubFilterViewModel::class.java + ].also { + it.initSubFilterViewModel(tag, savedInstanceState) + + if (tag.isFilterable) { + it.updateTagsAndSites() + } else { + viewModel.hideTopBarFilterGroup(tag) + } + } + } + + private fun SubFilterViewModel.initSubFilterViewModel(startedTag: ReaderTag, savedInstanceState: Bundle?) { + if (isStarted) return + + val wpMainActivityViewModel = ViewModelProvider( + requireActivity(), + viewModelFactory + )[WPMainActivityViewModel::class.java] + + val viewModelKey = SubFilterViewModel.getViewModelKeyForTag(startedTag) + + currentSubFilter.observe( + viewLifecycleOwner + ) { subfilterListItem: SubfilterListItem -> + onSubfilterSelected(subfilterListItem) + viewModel.onSubFilterItemSelected(subfilterListItem) + } + + bottomSheetUiState.observe( + viewLifecycleOwner + ) { event: Event -> + event.applyIfNotHandled { + val fm = childFragmentManager + var bottomSheet = fm.findFragmentByTag(SUBFILTER_BOTTOM_SHEET_TAG) as SubfilterBottomSheetFragment? + if (isVisible && bottomSheet == null) { + loadSubFilters() + val (title, category) = this as BottomSheetVisible + bottomSheet = newInstance( + viewModelKey, + category, + uiHelpers.getTextOfUiString(requireContext(), title) + ) + bottomSheet.show(childFragmentManager, SUBFILTER_BOTTOM_SHEET_TAG) + } else if (!isVisible && bottomSheet != null) { + bottomSheet.dismiss() + } + } + } + + bottomSheetAction.observe( + viewLifecycleOwner + ) { event: Event -> + event.applyIfNotHandled { + when (this) { + is OpenSubsAtPage -> { + readerSubsActivityResultLauncher?.launch( + ReaderActivityLauncher.createIntentShowReaderSubs( + requireActivity(), + tabIndex + ) + ) + } + + is OpenLoginPage -> { + wpMainActivityViewModel.onOpenLoginPage() + } + + is OpenSearchPage -> { + ReaderActivityLauncher.showReaderSearch(requireActivity()) + } + + is OpenSuggestedTagsPage -> { + ReaderActivityLauncher.showReaderInterests(requireActivity()) + } + } + } + } + + updateTagsAndSites.observe( + viewLifecycleOwner + ) { event: Event> -> + event.applyIfNotHandled { + if (NetworkUtils.isNetworkAvailable(activity)) { + ReaderUpdateServiceStarter.startService(activity, this) + } + } + } + + if (startedTag.isFilterable) { + subFilters.observe( + viewLifecycleOwner + ) { subFilters: List -> + viewModel.showTopBarFilterGroup( + startedTag, + subFilters + ) + } + } + + // TODO thomashortadev not sure if always passing the same tags can cause problems + start(startedTag, startedTag, savedInstanceState) + } + + companion object { + private const val SUBFILTER_BOTTOM_SHEET_TAG = "SUBFILTER_BOTTOM_SHEET_TAG" + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index 6b2e3aa4ba6c..cf8c0d4f0de1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -29,7 +29,6 @@ import androidx.core.content.ContextCompat; import androidx.core.text.HtmlCompat; import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; @@ -110,12 +109,8 @@ import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask; import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter; import org.wordpress.android.ui.reader.services.update.TagUpdateClientUtilsProvider; -import org.wordpress.android.ui.reader.subfilter.ActionType.OpenLoginPage; -import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSearchPage; -import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSubsAtPage; -import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSuggestedTagsPage; -import org.wordpress.android.ui.reader.subfilter.BottomSheetUiState.BottomSheetVisible; import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel; +import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelOwner; import org.wordpress.android.ui.reader.subfilter.SubfilterListItem.Site; import org.wordpress.android.ui.reader.subfilter.SubfilterListItem.SiteAll; import org.wordpress.android.ui.reader.tracker.ReaderTracker; @@ -145,7 +140,6 @@ import org.wordpress.android.util.config.ReaderImprovementsFeatureConfig; import org.wordpress.android.util.config.SeenUnseenWithCounterFeatureConfig; import org.wordpress.android.util.image.ImageManager; -import org.wordpress.android.viewmodel.main.WPMainActivityViewModel; import org.wordpress.android.widgets.AppRatingDialog; import org.wordpress.android.widgets.RecyclerItemDecoration; import org.wordpress.android.widgets.WPSnackbar; @@ -607,24 +601,15 @@ private void addWebViewCachingFragment(Long blogId, Long postId) { } private void initSubFilterViewModel(@Nullable Bundle savedInstanceState) { - WPMainActivityViewModel wpMainActivityViewModel = new ViewModelProvider(requireActivity(), mViewModelFactory) - .get(WPMainActivityViewModel.class); - - mSubFilterViewModel = new ViewModelProvider(this, mViewModelFactory).get( - SubFilterViewModel.getViewModelKeyForTag(mTagFragmentStartedWith), - SubFilterViewModel.class - ); + mSubFilterViewModel = SubFilterViewModelOwner. + getSubFilterViewModelForTag(this, mTagFragmentStartedWith, savedInstanceState); mSubFilterViewModel.getCurrentSubFilter().observe(getViewLifecycleOwner(), subfilterListItem -> { if (getPostListType() != ReaderPostListType.SEARCH_RESULTS) { - mSubFilterViewModel.onSubfilterSelected(subfilterListItem); - if (shouldShowEmptyViewForSelfHostedCta()) { setEmptyTitleDescriptionAndButton(false); showEmptyView(); } - - if (mReaderViewModel != null) mReaderViewModel.onSubFilterItemSelected(subfilterListItem); } }); @@ -633,70 +618,6 @@ private void initSubFilterViewModel(@Nullable Bundle savedInstanceState) { changeReaderMode(readerModeInfo, true); } }); - - mSubFilterViewModel.getBottomSheetUiState().observe(getViewLifecycleOwner(), event -> { - event.applyIfNotHandled(uiState -> { - FragmentManager fm = getChildFragmentManager(); - if (fm != null) { - SubfilterBottomSheetFragment bottomSheet = - (SubfilterBottomSheetFragment) fm.findFragmentByTag(SUBFILTER_BOTTOM_SHEET_TAG); - if (uiState.isVisible() && bottomSheet == null) { - mSubFilterViewModel.loadSubFilters(); - BottomSheetVisible visibleState = (BottomSheetVisible) uiState; - bottomSheet = SubfilterBottomSheetFragment.newInstance( - SubFilterViewModel.getViewModelKeyForTag(mTagFragmentStartedWith), - visibleState.getCategory(), - mUiHelpers.getTextOfUiString(requireContext(), visibleState.getTitle()) - ); - bottomSheet.show(getChildFragmentManager(), SUBFILTER_BOTTOM_SHEET_TAG); - } else if (!uiState.isVisible() && bottomSheet != null) { - bottomSheet.dismiss(); - } - } - return null; - }); - }); - - mSubFilterViewModel.getBottomSheetAction().observe(getViewLifecycleOwner(), event -> { - event.applyIfNotHandled(action -> { - if (action instanceof OpenSubsAtPage) { - mReaderSubsActivityResultLauncher.launch( - ReaderActivityLauncher.createIntentShowReaderSubs( - requireActivity(), - ((OpenSubsAtPage) action).getTabIndex() - ) - ); - } else if (action instanceof OpenLoginPage) { - wpMainActivityViewModel.onOpenLoginPage(); - } else if (action instanceof OpenSearchPage) { - ReaderActivityLauncher.showReaderSearch(requireActivity()); - } else if (action instanceof OpenSuggestedTagsPage) { - ReaderActivityLauncher.showReaderInterests(requireActivity()); - } - - return null; - }); - }); - - mSubFilterViewModel.getUpdateTagsAndSites().observe(getViewLifecycleOwner(), event -> { - event.applyIfNotHandled(tasks -> { - if (NetworkUtils.isNetworkAvailable(getActivity())) { - ReaderUpdateServiceStarter.startService(getActivity(), tasks); - } - return null; - }); - }); - - if (mIsFilterableScreen) { - mSubFilterViewModel.getSubFilters().observe(getViewLifecycleOwner(), subFilters -> { - mReaderViewModel.showTopBarFilterGroup(mTagFragmentStartedWith, subFilters); - }); - mSubFilterViewModel.updateTagsAndSites(); - } else { - mReaderViewModel.hideTopBarFilterGroup(mTagFragmentStartedWith); - } - - mSubFilterViewModel.start(mTagFragmentStartedWith, mCurrentTag, savedInstanceState); } private void changeReaderMode(ReaderModeInfo readerModeInfo, boolean onlyOnChanges) { 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 c4ce39ad344c..3e23b650e730 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 @@ -33,13 +33,11 @@ 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.SubFilterViewModelOwner 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 @@ -90,30 +88,12 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } 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 = SubFilterViewModelOwner.getSubFilterViewModelForTag(this, tagsFeedTag, savedInstanceState) subFilterViewModel.subFilters.observe(viewLifecycleOwner) { subFilters -> - readerViewModel.showTopBarFilterGroup( - tagsFeedTag, - subFilters - ) - val tags = subFilters.filterIsInstance().map { it.tag } viewModel.fetchAll(tags) } - subFilterViewModel.updateTagsAndSites() } override fun getScrollableViewForUniqueIdProvision(): View { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt index 0aa90d286fbd..d783046708de 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt @@ -10,7 +10,6 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelStoreOwner import androidx.viewpager.widget.ViewPager import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog @@ -19,6 +18,7 @@ import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.ui.reader.subfilter.ActionType import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel +import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelOwner import org.wordpress.android.ui.reader.subfilter.SubfilterCategory import org.wordpress.android.ui.reader.subfilter.SubfilterCategory.SITES import org.wordpress.android.ui.reader.subfilter.SubfilterCategory.TAGS @@ -75,10 +75,7 @@ class SubfilterBottomSheetFragment : BottomSheetDialogFragment() { return } - viewModel = ViewModelProvider( - parentFragment as ViewModelStoreOwner, - viewModelFactory - )[subfilterVmKey, SubFilterViewModel::class.java] + viewModel = SubFilterViewModelOwner.getSubFilterViewModelForKey(this, subfilterVmKey) // TODO remove the pager and support only one category val pager = view.findViewById(R.id.view_pager) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index 5f806bb6ad8b..c86a8c6313b3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -76,9 +76,10 @@ class SubFilterViewModel @Inject constructor( private var lastKnownUserId: Long? = null private var lastTokenAvailableStatus: Boolean? = null - private var isStarted = false private var isFirstLoad = true private var mTagFragmentStartedWith: ReaderTag? = null + var isStarted = false + private set /** * Tag may be null for Blog previews for instance. diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelOwner.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelOwner.kt new file mode 100644 index 000000000000..1745275b8085 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelOwner.kt @@ -0,0 +1,66 @@ +package org.wordpress.android.ui.reader.subfilter + +import android.os.Bundle +import androidx.fragment.app.Fragment +import org.wordpress.android.models.ReaderTag + +interface SubFilterViewModelOwner { + fun getSubFilterViewModelForKey(key: String): SubFilterViewModel + fun getSubFilterViewModelForTag(tag: ReaderTag, savedInstanceState: Bundle? = null): SubFilterViewModel + + companion object { + /** + * Helper function to get the [SubFilterViewModel] for a given [ReaderTag] from a [Fragment]. Note that the + * [Fragment] must be a child or descendant of a Fragment that implements [SubFilterViewModelOwner], otherwise + * this function will throw an [IllegalStateException]. + * + * @param fragment the [Fragment] to get the [SubFilterViewModel] from + * @param tag the [ReaderTag] to get the [SubFilterViewModel] for + * @param savedInstanceState the [Bundle] to pass to the [SubFilterViewModel] when it is created + * @return the [SubFilterViewModel] for the given [ReaderTag] + */ + @JvmStatic + @JvmOverloads + fun getSubFilterViewModelForTag( + fragment: Fragment, + tag: ReaderTag, + savedInstanceState: Bundle? = null + ): SubFilterViewModel { + // traverse the parent fragment hierarchy to find the SubFilterViewModelOwner + var possibleOwner: Fragment? = fragment + while (possibleOwner != null) { + if (possibleOwner is SubFilterViewModelOwner) { + return possibleOwner.getSubFilterViewModelForTag(tag, savedInstanceState) + } + possibleOwner = possibleOwner.parentFragment + } + error("Fragment must be a child or descendant of a Fragment that implements SubFilterViewModelOwner") + } + + /** + * Helper function to get the [SubFilterViewModel] for a given key from a [Fragment]. Note that the [Fragment] + * must be a child or descendant of a Fragment that implements [SubFilterViewModelOwner], otherwise this + * function will throw an [IllegalStateException]. + * + * @param fragment the [Fragment] to get the [SubFilterViewModel] from + * @param key the key to get the [SubFilterViewModel] for + * @return the [SubFilterViewModel] for the given key, or null if the [Fragment] is not a child or descendant + * of a Fragment that implements [SubFilterViewModelOwner] + */ + @JvmStatic + fun getSubFilterViewModelForKey( + fragment: Fragment, + key: String, + ): SubFilterViewModel { + // traverse the parent fragment hierarchy to find the SubFilterViewModelOwner + var possibleOwner: Fragment? = fragment + while (possibleOwner != null) { + if (possibleOwner is SubFilterViewModelOwner) { + return possibleOwner.getSubFilterViewModelForKey(key) + } + possibleOwner = possibleOwner.parentFragment + } + error("Fragment must be a child or descendant of a Fragment that implements SubFilterViewModelOwner") + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt index 4820df8c4599..c739f3253ef3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt @@ -101,10 +101,7 @@ class SubfilterPageFragment : Fragment() { primaryButton = emptyStateContainer.findViewById(R.id.action_button_primary) secondaryButton = emptyStateContainer.findViewById(R.id.action_button_secondary) - subFilterViewModel = ViewModelProvider( - requireParentFragment().parentFragment as ViewModelStoreOwner, - viewModelFactory - )[subfilterVmKey, SubFilterViewModel::class.java] + subFilterViewModel = SubFilterViewModelOwner.getSubFilterViewModelForKey(this, subfilterVmKey) subFilterViewModel.subFilters.observe(viewLifecycleOwner) { (recyclerView.adapter as? SubfilterListAdapter)?.let { adapter -> From c763bead46ef3ffc8f60adaa37c26edbc67bc1ea Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 26 Apr 2024 18:13:58 -0300 Subject: [PATCH 090/237] Change SubFilterViewModelOwner to ...Provider The idea is that the Provider is the component responsible for fetching the correct SubFilterViewModel, but it doesn't necessarily own the ViewModel. This was done so we can use the current feed as the Owner of the SubFilter ViewModel without it actually declaring it, so the outermost layer of the Reader (ReaderFragment) is still the source of truth for getting the SubFilterViewModel but the actual lifecycle of the ViewModel is tied to the current feed fragment. --- .../android/ui/reader/ReaderFragment.kt | 105 ++++++++++-------- .../ui/reader/ReaderPostListFragment.java | 4 +- .../ui/reader/ReaderTagsFeedFragment.kt | 4 +- .../ui/reader/SubfilterBottomSheetFragment.kt | 4 +- ...Owner.kt => SubFilterViewModelProvider.kt} | 30 ++--- .../reader/subfilter/SubfilterPageFragment.kt | 3 +- 6 files changed, 80 insertions(+), 70 deletions(-) rename WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/{SubFilterViewModelOwner.kt => SubFilterViewModelProvider.kt} (71%) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index e692f05a4288..474ef518d722 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import org.greenrobot.eventbus.EventBus @@ -47,7 +48,7 @@ import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSuggestedTagsPag import org.wordpress.android.ui.reader.subfilter.BottomSheetUiState import org.wordpress.android.ui.reader.subfilter.BottomSheetUiState.BottomSheetVisible import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel -import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelOwner +import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider import org.wordpress.android.ui.reader.subfilter.SubfilterCategory import org.wordpress.android.ui.reader.subfilter.SubfilterListItem import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel @@ -71,7 +72,7 @@ import javax.inject.Inject @AndroidEntryPoint class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableViewInitializedListener, - OnScrollToTopListener, SubFilterViewModelOwner { + OnScrollToTopListener, SubFilterViewModelProvider { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -415,6 +416,20 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView } } + /** + * The owner of the SubFilterViewModel should be the current feed Fragment, so it can be properly cleared when the + * feed is changed, since it will be properly tied to the expected feed Fragment lifecycle instead of the + * [ReaderFragment] lifecycle. + * + * This method exists mainly for readability purposes and to avoid passing the Fragment as a parameter. + * + * Note: it can cause a crash if the current feed Fragment is not available for any reason, which should never + * happen since the calling methods are always called by the feed Fragment or their children. + */ + private fun getSubFilterViewModelOwner(): ViewModelStoreOwner { + return getCurrentFeedFragment() as ViewModelStoreOwner + } + private fun getSubFilterViewModel(): SubFilterViewModel? { val selectedTag = (viewModel.uiState.value as? ContentUiState)?.selectedReaderTag ?: return null return getSubFilterViewModelForTag(selectedTag) @@ -425,38 +440,20 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView * should only be used for getting a ViewModel that's already been started. */ override fun getSubFilterViewModelForKey(key: String): SubFilterViewModel { - return ViewModelProvider(this, viewModelFactory)[key, SubFilterViewModel::class.java] + return ViewModelProvider(getSubFilterViewModelOwner(), viewModelFactory)[key, SubFilterViewModel::class.java] } override fun getSubFilterViewModelForTag(tag: ReaderTag, savedInstanceState: Bundle?): SubFilterViewModel { - // TODO thomashortadev now that this Fragment owns the SubFilterViewModel, it lives longer than the context - // that uses it (selected feed), so we need to make sure it's properly cleared when the feed is changed. - // OR maybe we can set the owner to be the current feed Fragment but have this ReaderFragment manage the - // retrieval and initialization of the SubFilterViewModel. 🤔 - - return ViewModelProvider(this, viewModelFactory)[ + return ViewModelProvider(getSubFilterViewModelOwner(), viewModelFactory)[ SubFilterViewModel.getViewModelKeyForTag(tag), SubFilterViewModel::class.java ].also { it.initSubFilterViewModel(tag, savedInstanceState) - - if (tag.isFilterable) { - it.updateTagsAndSites() - } else { - viewModel.hideTopBarFilterGroup(tag) - } } } private fun SubFilterViewModel.initSubFilterViewModel(startedTag: ReaderTag, savedInstanceState: Bundle?) { - if (isStarted) return - - val wpMainActivityViewModel = ViewModelProvider( - requireActivity(), - viewModelFactory - )[WPMainActivityViewModel::class.java] - - val viewModelKey = SubFilterViewModel.getViewModelKeyForTag(startedTag) + setupSubFilterBottomSheetObservers(startedTag) currentSubFilter.observe( viewLifecycleOwner @@ -465,6 +462,44 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView viewModel.onSubFilterItemSelected(subfilterListItem) } + + updateTagsAndSites.observe( + viewLifecycleOwner + ) { event: Event> -> + event.applyIfNotHandled { + if (NetworkUtils.isNetworkAvailable(activity)) { + ReaderUpdateServiceStarter.startService(activity, this) + } + } + } + + if (startedTag.isFilterable) { + subFilters.observe( + viewLifecycleOwner + ) { subFilters: List -> + viewModel.showTopBarFilterGroup( + startedTag, + subFilters + ) + } + + updateTagsAndSites() + } else { + viewModel.hideTopBarFilterGroup(startedTag) + } + + // thomashortadev: not sure if always passing the same tags can cause problems + start(startedTag, startedTag, savedInstanceState) + } + + private fun SubFilterViewModel.setupSubFilterBottomSheetObservers(startedTag: ReaderTag) { + val wpMainActivityViewModel = ViewModelProvider( + requireActivity(), + viewModelFactory + )[WPMainActivityViewModel::class.java] + + val viewModelKey = SubFilterViewModel.getViewModelKeyForTag(startedTag) + bottomSheetUiState.observe( viewLifecycleOwner ) { event: Event -> @@ -514,30 +549,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView } } } - - updateTagsAndSites.observe( - viewLifecycleOwner - ) { event: Event> -> - event.applyIfNotHandled { - if (NetworkUtils.isNetworkAvailable(activity)) { - ReaderUpdateServiceStarter.startService(activity, this) - } - } - } - - if (startedTag.isFilterable) { - subFilters.observe( - viewLifecycleOwner - ) { subFilters: List -> - viewModel.showTopBarFilterGroup( - startedTag, - subFilters - ) - } - } - - // TODO thomashortadev not sure if always passing the same tags can cause problems - start(startedTag, startedTag, savedInstanceState) } companion object { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index cf8c0d4f0de1..822a2aa2f2be 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -110,7 +110,7 @@ import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter; import org.wordpress.android.ui.reader.services.update.TagUpdateClientUtilsProvider; import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel; -import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelOwner; +import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider; import org.wordpress.android.ui.reader.subfilter.SubfilterListItem.Site; import org.wordpress.android.ui.reader.subfilter.SubfilterListItem.SiteAll; import org.wordpress.android.ui.reader.tracker.ReaderTracker; @@ -601,7 +601,7 @@ private void addWebViewCachingFragment(Long blogId, Long postId) { } private void initSubFilterViewModel(@Nullable Bundle savedInstanceState) { - mSubFilterViewModel = SubFilterViewModelOwner. + mSubFilterViewModel = SubFilterViewModelProvider. getSubFilterViewModelForTag(this, mTagFragmentStartedWith, savedInstanceState); mSubFilterViewModel.getCurrentSubFilter().observe(getViewLifecycleOwner(), subfilterListItem -> { 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 3e23b650e730..80897531c895 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 @@ -34,7 +34,7 @@ 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.subfilter.SubFilterViewModel -import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelOwner +import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider import org.wordpress.android.ui.reader.subfilter.SubfilterListItem import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel @@ -88,7 +88,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } private fun initViewModels(savedInstanceState: Bundle?) { - subFilterViewModel = SubFilterViewModelOwner.getSubFilterViewModelForTag(this, tagsFeedTag, savedInstanceState) + subFilterViewModel = SubFilterViewModelProvider.getSubFilterViewModelForTag(this, tagsFeedTag, savedInstanceState) subFilterViewModel.subFilters.observe(viewLifecycleOwner) { subFilters -> val tags = subFilters.filterIsInstance().map { it.tag } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt index d783046708de..262c6f2d119d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt @@ -18,7 +18,7 @@ import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.ui.reader.subfilter.ActionType import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel -import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelOwner +import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider import org.wordpress.android.ui.reader.subfilter.SubfilterCategory import org.wordpress.android.ui.reader.subfilter.SubfilterCategory.SITES import org.wordpress.android.ui.reader.subfilter.SubfilterCategory.TAGS @@ -75,7 +75,7 @@ class SubfilterBottomSheetFragment : BottomSheetDialogFragment() { return } - viewModel = SubFilterViewModelOwner.getSubFilterViewModelForKey(this, subfilterVmKey) + viewModel = SubFilterViewModelProvider.getSubFilterViewModelForKey(this, subfilterVmKey) // TODO remove the pager and support only one category val pager = view.findViewById(R.id.view_pager) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelOwner.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProvider.kt similarity index 71% rename from WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelOwner.kt rename to WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProvider.kt index 1745275b8085..33057daf33cd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelOwner.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProvider.kt @@ -4,15 +4,15 @@ import android.os.Bundle import androidx.fragment.app.Fragment import org.wordpress.android.models.ReaderTag -interface SubFilterViewModelOwner { +interface SubFilterViewModelProvider { fun getSubFilterViewModelForKey(key: String): SubFilterViewModel fun getSubFilterViewModelForTag(tag: ReaderTag, savedInstanceState: Bundle? = null): SubFilterViewModel companion object { /** * Helper function to get the [SubFilterViewModel] for a given [ReaderTag] from a [Fragment]. Note that the - * [Fragment] must be a child or descendant of a Fragment that implements [SubFilterViewModelOwner], otherwise - * this function will throw an [IllegalStateException]. + * [Fragment] must be a child or descendant of a Fragment that implements [SubFilterViewModelProvider], + * otherwise this function will throw an [IllegalStateException]. * * @param fragment the [Fragment] to get the [SubFilterViewModel] from * @param tag the [ReaderTag] to get the [SubFilterViewModel] for @@ -27,25 +27,25 @@ interface SubFilterViewModelOwner { savedInstanceState: Bundle? = null ): SubFilterViewModel { // traverse the parent fragment hierarchy to find the SubFilterViewModelOwner - var possibleOwner: Fragment? = fragment - while (possibleOwner != null) { - if (possibleOwner is SubFilterViewModelOwner) { - return possibleOwner.getSubFilterViewModelForTag(tag, savedInstanceState) + var possibleProvider: Fragment? = fragment + while (possibleProvider != null) { + if (possibleProvider is SubFilterViewModelProvider) { + return possibleProvider.getSubFilterViewModelForTag(tag, savedInstanceState) } - possibleOwner = possibleOwner.parentFragment + possibleProvider = possibleProvider.parentFragment } error("Fragment must be a child or descendant of a Fragment that implements SubFilterViewModelOwner") } /** * Helper function to get the [SubFilterViewModel] for a given key from a [Fragment]. Note that the [Fragment] - * must be a child or descendant of a Fragment that implements [SubFilterViewModelOwner], otherwise this + * must be a child or descendant of a Fragment that implements [SubFilterViewModelProvider], otherwise this * function will throw an [IllegalStateException]. * * @param fragment the [Fragment] to get the [SubFilterViewModel] from * @param key the key to get the [SubFilterViewModel] for * @return the [SubFilterViewModel] for the given key, or null if the [Fragment] is not a child or descendant - * of a Fragment that implements [SubFilterViewModelOwner] + * of a Fragment that implements [SubFilterViewModelProvider] */ @JvmStatic fun getSubFilterViewModelForKey( @@ -53,12 +53,12 @@ interface SubFilterViewModelOwner { key: String, ): SubFilterViewModel { // traverse the parent fragment hierarchy to find the SubFilterViewModelOwner - var possibleOwner: Fragment? = fragment - while (possibleOwner != null) { - if (possibleOwner is SubFilterViewModelOwner) { - return possibleOwner.getSubFilterViewModelForKey(key) + var possibleProvider: Fragment? = fragment + while (possibleProvider != null) { + if (possibleProvider is SubFilterViewModelProvider) { + return possibleProvider.getSubFilterViewModelForKey(key) } - possibleOwner = possibleOwner.parentFragment + possibleProvider = possibleProvider.parentFragment } error("Fragment must be a child or descendant of a Fragment that implements SubFilterViewModelOwner") } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt index c739f3253ef3..120886d85947 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt @@ -14,7 +14,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelStoreOwner import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint @@ -101,7 +100,7 @@ class SubfilterPageFragment : Fragment() { primaryButton = emptyStateContainer.findViewById(R.id.action_button_primary) secondaryButton = emptyStateContainer.findViewById(R.id.action_button_secondary) - subFilterViewModel = SubFilterViewModelOwner.getSubFilterViewModelForKey(this, subfilterVmKey) + subFilterViewModel = SubFilterViewModelProvider.getSubFilterViewModelForKey(this, subfilterVmKey) subFilterViewModel.subFilters.observe(viewLifecycleOwner) { (recyclerView.adapter as? SubfilterListAdapter)?.let { adapter -> From b90c924629f0f2c41d2555581d78d079d18c77ea Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 25 Apr 2024 18:49:43 -0300 Subject: [PATCH 091/237] WIP replace test UI with real UI in tags feed screen --- .../ui/reader/ReaderTagsFeedFragment.kt | 120 +----------------- .../viewmodels/ReaderTagsFeedViewModel.kt | 69 +++++++--- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 44 ++----- 3 files changed, 58 insertions(+), 175 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 c4ce39ad344c..e393d8fa5b56 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 @@ -2,28 +2,8 @@ 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 @@ -39,6 +19,7 @@ import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel.Companion.ge 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.ui.reader.views.compose.tagsfeed.ReaderTagsFeed import org.wordpress.android.util.NetworkUtils import org.wordpress.android.util.extensions.getSerializableCompat import javax.inject.Inject @@ -79,10 +60,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme binding.composeView.setContent { AppThemeWithoutBackground { val uiState by viewModel.uiStateFlow.collectAsState() - ReaderTagsFeedScreen( - uiState = uiState, - onRetryClicked = viewModel::fetchTag, - ) + ReaderTagsFeed(uiState) } } @@ -136,97 +114,3 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } } - -/** - * 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, - ) - } - } - } - } - } - } - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt index 6016df2cc87a..21a02e10dc2b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -10,6 +10,7 @@ import org.wordpress.android.models.ReaderTag import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository +import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject import javax.inject.Named @@ -19,7 +20,7 @@ class ReaderTagsFeedViewModel @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val readerPostRepository: ReaderPostRepository, ) : ScopedViewModel(bgDispatcher) { - private val _uiStateFlow = MutableStateFlow(UiState(emptyMap())) + private val _uiStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow /** @@ -41,30 +42,56 @@ class ReaderTagsFeedViewModel @Inject constructor( */ fun fetchTag(tag: ReaderTag) { launch { - _uiStateFlow.update { - it.copy(tagStates = it.tagStates + (tag to FetchState.Loading)) - } - - try { - val posts = readerPostRepository.fetchNewerPostsForTag(tag) - _uiStateFlow.update { - it.copy(tagStates = it.tagStates + (tag to FetchState.Success(posts))) - } - } catch (e: ReaderPostFetchException) { - _uiStateFlow.update { - it.copy(tagStates = it.tagStates + (tag to FetchState.Error(e))) - } - } +// _uiStateFlow.update { +// it.copy(tagStates = it.tagStates + (tag to FetchState.Loading)) +// } +// +// try { +// val posts = readerPostRepository.fetchNewerPostsForTag(tag) +// _uiStateFlow.update { +// it.copy(tagStates = it.tagStates + (tag to FetchState.Success(posts))) +// } +// } catch (e: ReaderPostFetchException) { +// _uiStateFlow.update { +// it.copy(tagStates = it.tagStates + (tag to FetchState.Error(e))) +// } +// } } } - data class UiState( - val tagStates: Map, + sealed class UiState { + object Initial : UiState() + data class Loaded(val data: List) : UiState() + + object Loading : UiState() + + data class Empty(val onOpenTagsListClick: () -> Unit) : UiState() + } + + data class TagFeedItem( + val tagChip: TagChip, + val postList: PostList, + ) + + data class TagChip( + val tag: ReaderTag, + val onTagClicked: () -> Unit, ) - sealed class FetchState { - data object Loading : FetchState() - data class Success(val posts: ReaderPostList) : FetchState() - data class Error(val exception: Exception) : FetchState() + sealed class PostList { + data class Loaded(val items: List) : PostList() + + object Loading : PostList() + + data class Error( + val type: ErrorType, + val onRetryClick: () -> Unit + ) : PostList() + } + + sealed interface ErrorType { + data object Loading : ErrorType + + data object NoContent : ErrorType } } 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 172e55c19197..c9a0396f2f2a 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 @@ -48,6 +48,11 @@ import org.wordpress.android.models.ReaderTagType import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin +import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.ErrorType +import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.PostList +import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.TagChip +import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.TagFeedItem +import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.UiState import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterChip import org.wordpress.android.ui.utils.UiString import org.wordpress.android.util.AppLog @@ -63,6 +68,9 @@ fun ReaderTagsFeed(uiState: UiState) { is UiState.Loading -> Loading() is UiState.Loaded -> Loaded(uiState) is UiState.Empty -> Empty(uiState) + is UiState.Initial -> { + // no-op + } } } } @@ -407,42 +415,6 @@ private fun PostListError( } } -// TODO move to VM -sealed class UiState { - data class Loaded(val data: List) : UiState() - - object Loading : UiState() - - data class Empty(val onOpenTagsListClick: () -> Unit) : UiState() -} - -data class TagFeedItem( - val tagChip: TagChip, - val postList: PostList, -) - -data class TagChip( - val tag: ReaderTag, - val onTagClicked: () -> Unit, -) - -sealed class PostList { - data class Loaded(val items: List) : PostList() - - object Loading : PostList() - - data class Error( - val type: ErrorType, - val onRetryClick: () -> Unit - ) : PostList() -} - -sealed interface ErrorType { - data object Loading : ErrorType - - data object NoContent : ErrorType -} - data class TagsFeedPostItem( val siteName: String, val postDateLine: String, From eec3ef8d1673cf87853b61bcc25be70f48b707e9 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 25 Apr 2024 21:33:30 -0300 Subject: [PATCH 092/237] WIP Start using UI state from ReaderTagsFeed in ReaderTagsFeedViewModel --- .../ui/reader/utils/ReaderUtilsWrapper.kt | 6 + .../viewmodels/ReaderTagsFeedViewModel.kt | 127 +++++++++++++++--- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 8 +- 3 files changed, 119 insertions(+), 22 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtilsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtilsWrapper.kt index 68d27dacaeb9..aacaf2d52eae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtilsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtilsWrapper.kt @@ -63,4 +63,10 @@ class ReaderUtilsWrapper @Inject constructor( fun isSelfHosted(authorBlogId: Long) = ReaderUtils.isSelfHosted(authorBlogId) fun getTagFromTagUrl(url: String): String = ReaderUtils.getTagFromTagUrl(url) + + fun getShortLikeLabelText(numLikes: Int): String = + ReaderUtils.getShortLikeLabelText(contextProvider.getContext(), numLikes) + + fun getShortCommentLabelText(numComments: Int): String = + ReaderUtils.getShortCommentLabelText(contextProvider.getContext(), numComments) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt index 21a02e10dc2b..c4c8abd9c11f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -5,11 +5,11 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository +import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject @@ -19,8 +19,9 @@ import javax.inject.Named class ReaderTagsFeedViewModel @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val readerPostRepository: ReaderPostRepository, + private val readerUtilsWrapper: ReaderUtilsWrapper, ) : ScopedViewModel(bgDispatcher) { - private val _uiStateFlow = MutableStateFlow(UiState.Initial) + private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow /** @@ -29,6 +30,10 @@ class ReaderTagsFeedViewModel @Inject constructor( * [FetchState]s: [FetchState.Loading], [FetchState.Success], [FetchState.Error]. */ fun fetchAll(tags: List) { + if (tags.isEmpty()) { + _uiStateFlow.value = UiState.Empty(::onOpenTagsListClick) + return + } tags.forEach { fetchTag(it) } @@ -42,23 +47,109 @@ class ReaderTagsFeedViewModel @Inject constructor( */ fun fetchTag(tag: ReaderTag) { launch { -// _uiStateFlow.update { -// it.copy(tagStates = it.tagStates + (tag to FetchState.Loading)) -// } -// -// try { -// val posts = readerPostRepository.fetchNewerPostsForTag(tag) -// _uiStateFlow.update { -// it.copy(tagStates = it.tagStates + (tag to FetchState.Success(posts))) -// } -// } catch (e: ReaderPostFetchException) { -// _uiStateFlow.update { -// it.copy(tagStates = it.tagStates + (tag to FetchState.Error(e))) -// } -// } + _uiStateFlow.update { UiState.Loading } + + val loadedData = mutableListOf() + val currentValue = _uiStateFlow.value + if (currentValue is UiState.Loaded) { + loadedData.addAll(currentValue.data) + } + try { + val posts = readerPostRepository.fetchNewerPostsForTag(tag) + if (posts.isNotEmpty()) { + loadedData.add( + TagFeedItem( + tagChip = TagChip( + tag = tag, + onTagClick = ::onTagClick, + ), + postList = PostList.Loaded( + posts.map { + TagsFeedPostItem( + siteName = it.blogName, + postDateLine = "1H", + postTitle = it.title, + postExcerpt = it.excerpt, + postImageUrl = it.blogImageUrl, + postNumberOfLikesText = readerUtilsWrapper.getShortLikeLabelText( + numLikes = it.numLikes + ), + postNumberOfCommentsText = readerUtilsWrapper.getShortCommentLabelText( + numComments = it.numReplies + ), + isPostLiked = it.isLikedByCurrentUser, + onSiteClick = ::onSiteClick, + onPostImageClick = ::onPostImageClick, + onPostLikeClick = ::onPostLikeClick, + onPostMoreMenuClick = ::onPostMoreMenuClick, + ) + } + ), + ) + ) + } else { + loadedData.add( + errorTagFeedItem( + tag = tag, + errorType = ErrorType.NoContent, + ) + ) + } + } catch (e: ReaderPostFetchException) { + loadedData.add( + errorTagFeedItem( + tag = tag, + errorType = ErrorType.Default, + ) + ) + } + _uiStateFlow.update { UiState.Loaded(loadedData) } } } + private fun errorTagFeedItem( + tag: ReaderTag, + errorType: ErrorType, + ): TagFeedItem = + TagFeedItem( + tagChip = TagChip( + tag = tag, + onTagClick = ::onTagClick + ), + postList = PostList.Error( + type = errorType, + onRetryClick = ::onRetryClick + ), + ) + + private fun onOpenTagsListClick() { + // TODO + } + + private fun onTagClick() { + // TODO + } + + private fun onRetryClick() { + // TODO + } + + private fun onSiteClick() { + // TODO + } + + private fun onPostImageClick() { + // TODO + } + + private fun onPostLikeClick() { + // TODO + } + + private fun onPostMoreMenuClick() { + // TODO + } + sealed class UiState { object Initial : UiState() data class Loaded(val data: List) : UiState() @@ -75,7 +166,7 @@ class ReaderTagsFeedViewModel @Inject constructor( data class TagChip( val tag: ReaderTag, - val onTagClicked: () -> Unit, + val onTagClick: () -> Unit, ) sealed class PostList { @@ -90,7 +181,7 @@ class ReaderTagsFeedViewModel @Inject constructor( } sealed interface ErrorType { - data object Loading : ErrorType + data object Default : ErrorType data object NoContent : ErrorType } 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 c9a0396f2f2a..41fc36ab1790 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 @@ -96,7 +96,7 @@ private fun Loaded(uiState: UiState.Loaded) { start = Margin.Large.value, ), text = UiString.UiStringText(tagChip.tag.tagTitle), - onClick = tagChip.onTagClicked, + onClick = tagChip.onTagClick, height = 36.dp, ) Spacer(modifier = Modifier.height(Margin.Large.value)) @@ -300,7 +300,7 @@ private fun PostListLoaded( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), onClick = { - tagChip.onTagClicked() + tagChip.onTagClick() AppLog.e(AppLog.T.READER, "RL-> Tag clicked") } ), @@ -374,7 +374,7 @@ private fun PostListError( ) Spacer(modifier = Modifier.height(Margin.Medium.value)) val errorMessage = when (postList.type) { - is ErrorType.Loading -> stringResource(R.string.reader_tags_feed_loading_error_description) + is ErrorType.Default -> stringResource(R.string.reader_tags_feed_loading_error_description) is ErrorType.NoContent -> stringResource(R.string.reader_tags_feed_no_content_error_description, tagName) } Text( @@ -529,7 +529,7 @@ fun ReaderTagsFeedLoaded() { ), TagFeedItem( tagChip = TagChip(readerTag, {}), - postList = PostList.Error(ErrorType.Loading, {}), + postList = PostList.Error(ErrorType.Default, {}), ), TagFeedItem( tagChip = TagChip(readerTag, {}), From fd946b9647bcc2230621906dcc5575f4185aca7e Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:34:38 -0300 Subject: [PATCH 093/237] Implement post dateline in tags feed --- .../wordpress/android/ui/reader/ReaderTagsFeedFragment.kt | 1 + .../android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) 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 e393d8fa5b56..228692c583e7 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 @@ -82,6 +82,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } + // TODO not triggered when there's no internet, so the error/no connection UI is not shown. subFilterViewModel.subFilters.observe(viewLifecycleOwner) { subFilters -> readerViewModel.showTopBarFilterGroup( tagsFeedTag, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt index c4c8abd9c11f..3c7bfff55d56 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -11,6 +11,7 @@ import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem +import org.wordpress.android.util.DateTimeUtilsWrapper import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject import javax.inject.Named @@ -20,6 +21,7 @@ class ReaderTagsFeedViewModel @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val readerPostRepository: ReaderPostRepository, private val readerUtilsWrapper: ReaderUtilsWrapper, + private val dateTimeUtilsWrapper: DateTimeUtilsWrapper, ) : ScopedViewModel(bgDispatcher) { private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow @@ -67,7 +69,9 @@ class ReaderTagsFeedViewModel @Inject constructor( posts.map { TagsFeedPostItem( siteName = it.blogName, - postDateLine = "1H", + postDateLine = dateTimeUtilsWrapper.javaDateToTimeSpan( + it.getDisplayDate(dateTimeUtilsWrapper) + ), postTitle = it.title, postExcerpt = it.excerpt, postImageUrl = it.blogImageUrl, From 8c42298ab8d265b2733272b0d1a83a655ebcd968 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 26 Apr 2024 12:29:29 -0300 Subject: [PATCH 094/237] Fix: Add max lines value to post excerpt that has no image --- .../reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 977ec18646f1..f3c724e30b98 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 @@ -140,7 +140,7 @@ fun ReaderTagsFeedPostListItem( text = postExcerpt, style = MaterialTheme.typography.bodySmall, color = primaryElementColor, - maxLines = if (!postImageUrl.isNullOrBlank()) 2 else Int.MAX_VALUE, + maxLines = if (!postImageUrl.isNullOrBlank()) 2 else 10, overflow = TextOverflow.Ellipsis, ) // Post image From d5ac0307b7e0cbb10bed2bedb4d5c11b1c597b93 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:14:35 -0300 Subject: [PATCH 095/237] Change ReaderPostRepository fetch default limit to 10 instead of 7 --- .../android/ui/reader/repository/ReaderPostRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt index 9c24b917023f..7fbb8e88556e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt @@ -35,7 +35,7 @@ class ReaderPostRepository @Inject constructor( * Fetches and returns the most recent posts for the passed tag, respecting the maxPosts limit. * It always fetches the most recent posts, saves them to the local DB and returns the latest from that cache. */ - suspend fun fetchNewerPostsForTag(tag: ReaderTag, maxPosts: Int = 7): ReaderPostList { + suspend fun fetchNewerPostsForTag(tag: ReaderTag, maxPosts: Int = 10): ReaderPostList { return suspendCancellableCoroutine { cont -> val resultListener = UpdateResultListener { result -> if (result == ReaderActions.UpdateResult.FAILED) { From 9e0fc794025ca70801053ca0a3adfed66d7338de Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 26 Apr 2024 22:38:11 -0300 Subject: [PATCH 096/237] Finish integrating ReaderTagsFeed with ReaderTagsFeedViewModel --- .../viewmodels/ReaderTagsFeedViewModel.kt | 158 +++++++++++------- 1 file changed, 97 insertions(+), 61 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt index 3c7bfff55d56..895528a831f5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException @@ -29,88 +30,123 @@ class ReaderTagsFeedViewModel @Inject constructor( /** * Fetch multiple tag posts in parallel. Each tag load causes a new state to be emitted, so multiple emissions of * [uiStateFlow] are expected when calling this method for each tag, since each can go through the following - * [FetchState]s: [FetchState.Loading], [FetchState.Success], [FetchState.Error]. + * [UiState]s: [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty]. */ fun fetchAll(tags: List) { if (tags.isEmpty()) { _uiStateFlow.value = UiState.Empty(::onOpenTagsListClick) return } - tags.forEach { - fetchTag(it) + // Initially add all tags to the list with the posts loading UI + loadingTagsUiState(tags) + // Fetch all posts and update the posts loading UI to either loaded or error when the request finishes + launch { + tags.forEach { + fetchTag(it) + } } } /** - * Fetch posts for a single tag. This method will emit a new state to [uiStateFlow] for different [FetchState]s: - * [FetchState.Loading], [FetchState.Success], [FetchState.Error], but only for the tag being fetched. + * 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. * * Can be used for retrying a failed fetch, for instance. */ - fun fetchTag(tag: ReaderTag) { - launch { - _uiStateFlow.update { UiState.Loading } - - val loadedData = mutableListOf() - val currentValue = _uiStateFlow.value - if (currentValue is UiState.Loaded) { - loadedData.addAll(currentValue.data) - } - try { - val posts = readerPostRepository.fetchNewerPostsForTag(tag) - if (posts.isNotEmpty()) { - loadedData.add( - TagFeedItem( - tagChip = TagChip( - tag = tag, - onTagClick = ::onTagClick, - ), - postList = PostList.Loaded( - posts.map { - TagsFeedPostItem( - siteName = it.blogName, - postDateLine = dateTimeUtilsWrapper.javaDateToTimeSpan( - it.getDisplayDate(dateTimeUtilsWrapper) - ), - postTitle = it.title, - postExcerpt = it.excerpt, - postImageUrl = it.blogImageUrl, - postNumberOfLikesText = readerUtilsWrapper.getShortLikeLabelText( - numLikes = it.numLikes - ), - postNumberOfCommentsText = readerUtilsWrapper.getShortCommentLabelText( - numComments = it.numReplies - ), - isPostLiked = it.isLikedByCurrentUser, - onSiteClick = ::onSiteClick, - onPostImageClick = ::onPostImageClick, - onPostLikeClick = ::onPostLikeClick, - onPostMoreMenuClick = ::onPostMoreMenuClick, - ) - } - ), - ) - ) - } else { - loadedData.add( - errorTagFeedItem( - tag = tag, - errorType = ErrorType.NoContent, - ) - ) - } - } catch (e: ReaderPostFetchException) { - loadedData.add( + private suspend fun fetchTag(tag: ReaderTag) { + val updatedLoadedData = getUpdatedLoadedData() + // At this point, all tag feed items already exist in the UI with the loading status. + // We need it's index to update it to either Loaded or Error when the request is finished. + val existingIndex = updatedLoadedData.indexOfFirst { it.tagChip.tag == tag } + // Remove the current row of this tag (which is loading). This will be used to later add an updated item with + // either Loaded or Error status, depending on the result of the request. + updatedLoadedData.removeAll { it.tagChip.tag == tag } + try { + // Fetch posts for tag + val posts = readerPostRepository.fetchNewerPostsForTag(tag) + if (posts.isNotEmpty()) { + updatedLoadedData.add(existingIndex, loadedTagFeedItem(tag, posts)) + } else { + updatedLoadedData.add( + existingIndex, errorTagFeedItem( tag = tag, - errorType = ErrorType.Default, + errorType = ErrorType.NoContent, ) ) } - _uiStateFlow.update { UiState.Loaded(loadedData) } + } catch (e: ReaderPostFetchException) { + updatedLoadedData.add( + existingIndex, + errorTagFeedItem( + tag = tag, + errorType = ErrorType.Default, + ) + ) + } + _uiStateFlow.update { UiState.Loaded(updatedLoadedData) } + } + + private fun getUpdatedLoadedData(): MutableList { + val updatedLoadedData = mutableListOf() + val currentUiState = _uiStateFlow.value + if (currentUiState is UiState.Loaded) { + val currentLoadedData = currentUiState.data + updatedLoadedData.addAll(currentLoadedData) + } + return updatedLoadedData + } + + private fun loadingTagsUiState(tags: List) { + _uiStateFlow.update { + UiState.Loaded( + tags.map { tag -> + TagFeedItem( + tagChip = TagChip( + tag = tag, + onTagClick = ::onTagClick, + ), + postList = PostList.Loading, + ) + } + ) } } + private fun loadedTagFeedItem( + tag: ReaderTag, + posts: ReaderPostList + ) = TagFeedItem( + tagChip = TagChip( + tag = tag, + onTagClick = ::onTagClick, + ), + postList = PostList.Loaded( + posts.map { + TagsFeedPostItem( + siteName = it.blogName, + postDateLine = dateTimeUtilsWrapper.javaDateToTimeSpan( + it.getDisplayDate(dateTimeUtilsWrapper) + ), + postTitle = it.title, + postExcerpt = it.excerpt, + postImageUrl = it.blogImageUrl, + postNumberOfLikesText = readerUtilsWrapper.getShortLikeLabelText( + numLikes = it.numLikes + ), + postNumberOfCommentsText = readerUtilsWrapper.getShortCommentLabelText( + numComments = it.numReplies + ), + isPostLiked = it.isLikedByCurrentUser, + onSiteClick = ::onSiteClick, + onPostImageClick = ::onPostImageClick, + onPostLikeClick = ::onPostLikeClick, + onPostMoreMenuClick = ::onPostMoreMenuClick, + ) + } + ), + ) + private fun errorTagFeedItem( tag: ReaderTag, errorType: ErrorType, From 987168a256d78d334b79c4d7fd1061c8ae965b40 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:57:52 -0300 Subject: [PATCH 097/237] Extract loaded TagFeedItem mapping to ReaderTagsFeedUiStateMapper --- .../ui/reader/ReaderTagsFeedFragment.kt | 2 +- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 52 ++++++++++++++++++ .../{ => tagsfeed}/ReaderTagsFeedViewModel.kt | 54 +++++-------------- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 10 ++-- 4 files changed, 72 insertions(+), 46 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt rename WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/{ => tagsfeed}/ReaderTagsFeedViewModel.kt (78%) 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 228692c583e7..cd43e7d53a66 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 @@ -17,7 +17,7 @@ import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarte 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.tagsfeed.ReaderTagsFeedViewModel import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel import org.wordpress.android.ui.reader.views.compose.tagsfeed.ReaderTagsFeed import org.wordpress.android.util.NetworkUtils 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 new file mode 100644 index 000000000000..846b338143e8 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt @@ -0,0 +1,52 @@ +package org.wordpress.android.ui.reader.viewmodels.tagsfeed + +import org.wordpress.android.models.ReaderPostList +import org.wordpress.android.models.ReaderTag +import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper +import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem +import org.wordpress.android.util.DateTimeUtilsWrapper +import javax.inject.Inject + +class ReaderTagsFeedUiStateMapper @Inject constructor( + private val dateTimeUtilsWrapper: DateTimeUtilsWrapper, + private val readerUtilsWrapper: ReaderUtilsWrapper, +) { + fun mapLoadedTagFeedItem( + tag: ReaderTag, + posts: ReaderPostList, + onTagClick: () -> Unit, + onSiteClick: () -> Unit, + onPostImageClick: () -> Unit, + onPostLikeClick: () -> Unit, + onPostMoreMenuClick: () -> Unit, + ) = ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag, + onTagClick = onTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Loaded( + posts.map { + TagsFeedPostItem( + siteName = it.blogName, + postDateLine = dateTimeUtilsWrapper.javaDateToTimeSpan( + it.getDisplayDate(dateTimeUtilsWrapper) + ), + postTitle = it.title, + postExcerpt = it.excerpt, + postImageUrl = it.blogImageUrl, + postNumberOfLikesText = readerUtilsWrapper.getShortLikeLabelText( + numLikes = it.numLikes + ), + postNumberOfCommentsText = readerUtilsWrapper.getShortCommentLabelText( + numComments = it.numReplies + ), + isPostLiked = it.isLikedByCurrentUser, + onSiteClick = onSiteClick, + onPostImageClick = onPostImageClick, + onPostLikeClick = onPostLikeClick, + onPostMoreMenuClick = onPostMoreMenuClick, + ) + } + ), + ) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt similarity index 78% rename from WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt index 895528a831f5..fc1a5275d025 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderTagsFeedViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.reader.viewmodels +package org.wordpress.android.ui.reader.viewmodels.tagsfeed import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher @@ -10,9 +10,7 @@ import org.wordpress.android.models.ReaderTag import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository -import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem -import org.wordpress.android.util.DateTimeUtilsWrapper import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject import javax.inject.Named @@ -21,8 +19,7 @@ import javax.inject.Named class ReaderTagsFeedViewModel @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val readerPostRepository: ReaderPostRepository, - private val readerUtilsWrapper: ReaderUtilsWrapper, - private val dateTimeUtilsWrapper: DateTimeUtilsWrapper, + private val readerTagsFeedUiStateMapper: ReaderTagsFeedUiStateMapper, ) : ScopedViewModel(bgDispatcher) { private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow @@ -65,7 +62,18 @@ class ReaderTagsFeedViewModel @Inject constructor( // Fetch posts for tag val posts = readerPostRepository.fetchNewerPostsForTag(tag) if (posts.isNotEmpty()) { - updatedLoadedData.add(existingIndex, loadedTagFeedItem(tag, posts)) + updatedLoadedData.add( + existingIndex, + readerTagsFeedUiStateMapper.mapLoadedTagFeedItem( + tag = tag, + posts = posts, + onTagClick = ::onTagClick, + onSiteClick = ::onSiteClick, + onPostImageClick = ::onPostImageClick, + onPostLikeClick = ::onPostLikeClick, + onPostMoreMenuClick = ::onPostMoreMenuClick, + ) + ) } else { updatedLoadedData.add( existingIndex, @@ -113,40 +121,6 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun loadedTagFeedItem( - tag: ReaderTag, - posts: ReaderPostList - ) = TagFeedItem( - tagChip = TagChip( - tag = tag, - onTagClick = ::onTagClick, - ), - postList = PostList.Loaded( - posts.map { - TagsFeedPostItem( - siteName = it.blogName, - postDateLine = dateTimeUtilsWrapper.javaDateToTimeSpan( - it.getDisplayDate(dateTimeUtilsWrapper) - ), - postTitle = it.title, - postExcerpt = it.excerpt, - postImageUrl = it.blogImageUrl, - postNumberOfLikesText = readerUtilsWrapper.getShortLikeLabelText( - numLikes = it.numLikes - ), - postNumberOfCommentsText = readerUtilsWrapper.getShortCommentLabelText( - numComments = it.numReplies - ), - isPostLiked = it.isLikedByCurrentUser, - onSiteClick = ::onSiteClick, - onPostImageClick = ::onPostImageClick, - onPostLikeClick = ::onPostLikeClick, - onPostMoreMenuClick = ::onPostMoreMenuClick, - ) - } - ), - ) - private fun errorTagFeedItem( tag: ReaderTag, errorType: ErrorType, 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 41fc36ab1790..471e00631133 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 @@ -48,11 +48,11 @@ import org.wordpress.android.models.ReaderTagType import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin -import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.ErrorType -import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.PostList -import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.TagChip -import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.TagFeedItem -import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.UiState +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ErrorType +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.PostList +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.TagChip +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.TagFeedItem +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.UiState import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterChip import org.wordpress.android.ui.utils.UiString import org.wordpress.android.util.AppLog From f99984ac347e2fe486b3748349cbfebaa6414116 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 29 Apr 2024 18:04:45 -0300 Subject: [PATCH 098/237] Extract loading and error UI states to UI state mapper --- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 33 ++++++++++++++ .../tagsfeed/ReaderTagsFeedViewModel.kt | 44 ++++--------------- 2 files changed, 42 insertions(+), 35 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 846b338143e8..93af86c807a2 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 @@ -49,4 +49,37 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( } ), ) + + fun mapErrorTagFeedItem( + tag: ReaderTag, + errorType: ReaderTagsFeedViewModel.ErrorType, + onTagClick: () -> Unit, + onRetryClick: () -> Unit, + ): ReaderTagsFeedViewModel.TagFeedItem = + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag, + onTagClick = onTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Error( + type = errorType, + onRetryClick = onRetryClick, + ), + ) + + fun mapLoadingTagsUiState( + tags: List, + onTagClick: () -> Unit, + ): ReaderTagsFeedViewModel.UiState.Loaded = + ReaderTagsFeedViewModel.UiState.Loaded( + tags.map { tag -> + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag, + onTagClick = onTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + ) + } + ) } 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 fc1a5275d025..1e6afe9cb542 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 @@ -5,7 +5,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException @@ -35,7 +34,9 @@ class ReaderTagsFeedViewModel @Inject constructor( return } // Initially add all tags to the list with the posts loading UI - loadingTagsUiState(tags) + _uiStateFlow.update { + readerTagsFeedUiStateMapper.mapLoadingTagsUiState(tags, ::onTagClick) + } // Fetch all posts and update the posts loading UI to either loaded or error when the request finishes launch { tags.forEach { @@ -77,18 +78,22 @@ class ReaderTagsFeedViewModel @Inject constructor( } else { updatedLoadedData.add( existingIndex, - errorTagFeedItem( + readerTagsFeedUiStateMapper.mapErrorTagFeedItem( tag = tag, errorType = ErrorType.NoContent, + onTagClick = ::onTagClick, + onRetryClick = ::onRetryClick, ) ) } } catch (e: ReaderPostFetchException) { updatedLoadedData.add( existingIndex, - errorTagFeedItem( + readerTagsFeedUiStateMapper.mapErrorTagFeedItem( tag = tag, errorType = ErrorType.Default, + onTagClick = ::onTagClick, + onRetryClick = ::onRetryClick, ) ) } @@ -105,37 +110,6 @@ class ReaderTagsFeedViewModel @Inject constructor( return updatedLoadedData } - private fun loadingTagsUiState(tags: List) { - _uiStateFlow.update { - UiState.Loaded( - tags.map { tag -> - TagFeedItem( - tagChip = TagChip( - tag = tag, - onTagClick = ::onTagClick, - ), - postList = PostList.Loading, - ) - } - ) - } - } - - private fun errorTagFeedItem( - tag: ReaderTag, - errorType: ErrorType, - ): TagFeedItem = - TagFeedItem( - tagChip = TagChip( - tag = tag, - onTagClick = ::onTagClick - ), - postList = PostList.Error( - type = errorType, - onRetryClick = ::onRetryClick - ), - ) - private fun onOpenTagsListClick() { // TODO } From fe34af730fa9cb2dc06ea0bde3a6f2cd97b39d93 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 29 Apr 2024 21:47:17 -0300 Subject: [PATCH 099/237] Fix ReaderTagsFeedViewModelTest tests --- .../ui/reader/ReaderTagsFeedFragment.kt | 2 +- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 2 +- .../tagsfeed/ReaderTagsFeedViewModel.kt | 4 +- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 231 +++++++++++------- 4 files changed, 148 insertions(+), 91 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 cd43e7d53a66..b54fdb83f60f 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 @@ -90,7 +90,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme ) val tags = subFilters.filterIsInstance().map { it.tag } - viewModel.fetchAll(tags) + viewModel.start(tags) } subFilterViewModel.updateTagsAndSites() } 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 93af86c807a2..d2b664c233b5 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 @@ -67,7 +67,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( ), ) - fun mapLoadingTagsUiState( + fun mapLoadingPostsUiState( tags: List, onTagClick: () -> Unit, ): ReaderTagsFeedViewModel.UiState.Loaded = 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 1e6afe9cb542..85779f675c6d 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 @@ -28,14 +28,14 @@ class ReaderTagsFeedViewModel @Inject constructor( * [uiStateFlow] are expected when calling this method for each tag, since each can go through the following * [UiState]s: [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty]. */ - fun fetchAll(tags: List) { + fun start(tags: List) { if (tags.isEmpty()) { _uiStateFlow.value = UiState.Empty(::onOpenTagsListClick) return } // Initially add all tags to the list with the posts loading UI _uiStateFlow.update { - readerTagsFeedUiStateMapper.mapLoadingTagsUiState(tags, ::onTagClick) + readerTagsFeedUiStateMapper.mapLoadingPostsUiState(tags, ::onTagClick) } // Fetch all posts and update the posts loading UI to either loaded or error when the request finishes launch { 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 d4008189759b..3a3a9f91174a 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 @@ -9,97 +9,120 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.mockito.Mock +import org.mockito.kotlin.any import org.mockito.kotlin.doSuspendableAnswer import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest +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.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository -import org.wordpress.android.ui.reader.viewmodels.ReaderTagsFeedViewModel.FetchState +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedUiStateMapper +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel @OptIn(ExperimentalCoroutinesApi::class) class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Mock lateinit var readerPostRepository: ReaderPostRepository + @Mock + lateinit var readerTagsFeedUiStateMapper: ReaderTagsFeedUiStateMapper + private lateinit var viewModel: ReaderTagsFeedViewModel private val collectedUiStates: MutableList = mutableListOf() + val tag = ReaderTag( + "tag", + "tag", + "tag", + "endpoint", + ReaderTagType.FOLLOWED, + ) + + private val postListLoadingItem = ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag, + onTagClick = {}, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + ) + @Before fun setUp() { - viewModel = ReaderTagsFeedViewModel(testDispatcher(), readerPostRepository) + viewModel = ReaderTagsFeedViewModel(testDispatcher(), readerPostRepository, readerTagsFeedUiStateMapper) } @Test fun `given valid tag, when fetchTag, then UI state should update properly`() = testCollectingUiStates { // Given - val tag = ReaderTag( - "tag", - "tag", - "tag", - "endpoint", - ReaderTagType.FOLLOWED, + val tagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( + ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.PostList.Loaded(listOf()) ) - val posts = ReaderPostList() + val posts = ReaderPostList().apply { + add(ReaderPost()) + } whenever(readerPostRepository.fetchNewerPostsForTag(tag)).doSuspendableAnswer { delay(100) posts } + whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) + .thenReturn( + ReaderTagsFeedViewModel.UiState.Loaded( + listOf(postListLoadingItem, postListLoadingItem) + ) + ) + whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any())) + .thenReturn(tagFeedItem) // When - viewModel.fetchTag(tag) + viewModel.start(listOf(tag)) advanceUntilIdle() // Then assertThat(collectedUiStates).contains( - ReaderTagsFeedViewModel.UiState( - mapOf( - tag to FetchState.Loading, - ) - ), - ReaderTagsFeedViewModel.UiState( - mapOf( - tag to FetchState.Success(posts), - ) - ), + ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf(tagFeedItem) + ) ) } @Test fun `given invalid tag, when fetchTag, then UI state should update properly`() = testCollectingUiStates { // Given - val tag = ReaderTag( - "tag", - "tag", - "tag", - "endpoint", - ReaderTagType.FOLLOWED, - ) val error = ReaderPostFetchException("error") + val tagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( + ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.PostList.Error( + ReaderTagsFeedViewModel.ErrorType.Default, {} + ), + ) whenever(readerPostRepository.fetchNewerPostsForTag(tag)).doSuspendableAnswer { delay(100) throw error } + whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) + .thenReturn( + ReaderTagsFeedViewModel.UiState.Loaded( + listOf(postListLoadingItem, postListLoadingItem) + ) + ) + whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any())) + .thenReturn(tagFeedItem) // When - viewModel.fetchTag(tag) + viewModel.start(listOf(tag)) +// viewModel.fetchTag(tag) advanceUntilIdle() // Then assertThat(collectedUiStates).contains( - ReaderTagsFeedViewModel.UiState( - mapOf( - tag to FetchState.Loading, - ) - ), - ReaderTagsFeedViewModel.UiState( - mapOf( - tag to FetchState.Error(error), - ) - ), + ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf(tagFeedItem) + ) ) } @@ -120,8 +143,12 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { "endpoint2", ReaderTagType.FOLLOWED, ) - val posts1 = ReaderPostList() - val posts2 = ReaderPostList() + val posts1 = ReaderPostList().apply { + add(ReaderPost()) + } + val posts2 = ReaderPostList().apply { + add(ReaderPost()) + } whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { delay(100) posts1 @@ -130,34 +157,44 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(200) posts2 } + whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) + .thenReturn( + ReaderTagsFeedViewModel.UiState.Loaded( + listOf( + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag1, + onTagClick = {}, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + ), + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag2, + onTagClick = {}, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + ), + ) + ) + ) + val tagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( + ReaderTagsFeedViewModel.TagChip(tag1, {}), + ReaderTagsFeedViewModel.PostList.Loaded(listOf()), + ) + whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any())) + .thenReturn(tagFeedItem) // When - viewModel.fetchAll(listOf(tag1, tag2)) + viewModel.start(listOf(tag1, tag2)) advanceUntilIdle() // Then - - // tag 1 - assertThat(collectedUiStates).anyMatch { - it.tagStates[tag1] == FetchState.Loading - } - assertThat(collectedUiStates).anyMatch { - it.tagStates[tag1] == FetchState.Success(posts1) - } - - // tag 2 - assertThat(collectedUiStates).anyMatch { - it.tagStates[tag2] == FetchState.Loading - } - assertThat(collectedUiStates).anyMatch { - it.tagStates[tag2] == FetchState.Success(posts1) - } - - assertThat(collectedUiStates.last()).isEqualTo( - ReaderTagsFeedViewModel.UiState( - mapOf( - tag1 to FetchState.Success(posts1), - tag2 to FetchState.Success(posts2), + assertThat(collectedUiStates).contains( + ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf( + tagFeedItem, + tagFeedItem, ) ) ) @@ -180,7 +217,9 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { "endpoint2", ReaderTagType.FOLLOWED, ) - val posts1 = ReaderPostList() + val posts1 = ReaderPostList().apply { + add(ReaderPost()) + } val error2 = ReaderPostFetchException("error") whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { delay(100) @@ -190,34 +229,52 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(200) throw error2 } + whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) + .thenReturn( + ReaderTagsFeedViewModel.UiState.Loaded( + listOf( + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag1, + onTagClick = {}, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + ), + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag2, + onTagClick = {}, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + ) + ) + ) + ) + val tagFeedItemLoaded = ReaderTagsFeedViewModel.TagFeedItem( + ReaderTagsFeedViewModel.TagChip(tag1, {}), + ReaderTagsFeedViewModel.PostList.Loaded(listOf()) + ) + val tagFeedItemError = ReaderTagsFeedViewModel.TagFeedItem( + ReaderTagsFeedViewModel.TagChip(tag2, {}), + ReaderTagsFeedViewModel.PostList.Error( + ReaderTagsFeedViewModel.ErrorType.Default, {} + ) + ) + whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any())) + .thenReturn(tagFeedItemLoaded) + whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any())) + .thenReturn(tagFeedItemError) // When - viewModel.fetchAll(listOf(tag1, tag2)) + viewModel.start(listOf(tag1, tag2)) advanceUntilIdle() // Then - - // tag 1 - assertThat(collectedUiStates).anyMatch { - it.tagStates[tag1] == FetchState.Loading - } - assertThat(collectedUiStates).anyMatch { - it.tagStates[tag1] == FetchState.Success(posts1) - } - - // tag 2 - assertThat(collectedUiStates).anyMatch { - it.tagStates[tag2] == FetchState.Loading - } - assertThat(collectedUiStates).anyMatch { - it.tagStates[tag2] == FetchState.Error(error2) - } - - assertThat(collectedUiStates.last()).isEqualTo( - ReaderTagsFeedViewModel.UiState( - mapOf( - tag1 to FetchState.Success(posts1), - tag2 to FetchState.Error(error2), + assertThat(collectedUiStates).contains( + ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf( + tagFeedItemLoaded, + tagFeedItemError, ) ) ) From 436eb8f94c685894063785f10fb3844a7635a24e Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 29 Apr 2024 23:32:48 -0300 Subject: [PATCH 100/237] Implement ReaderTagsFeedUiStateMapper tests --- .../ReaderTagsFeedUiStateMapperTest.kt | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt new file mode 100644 index 000000000000..9556b6ddbe0f --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -0,0 +1,192 @@ +package org.wordpress.android.ui.reader.viewmodels.tagsfeed + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +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.utils.ReaderUtilsWrapper +import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem +import org.wordpress.android.util.DateTimeUtilsWrapper +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { + private val dateTimeUtilsWrapper = mock() + + private val readerUtilsWrapper = mock() + + private val classToTest: ReaderTagsFeedUiStateMapper = ReaderTagsFeedUiStateMapper( + dateTimeUtilsWrapper = dateTimeUtilsWrapper, + readerUtilsWrapper = readerUtilsWrapper, + ) + + @Test + fun `Should map loaded TagFeedItem correctly`() { + // Given + val readerPost = ReaderPost().apply { + blogName = "Name" + title = "Title" + excerpt = "Excerpt" + blogImageUrl = "url" + numLikes = 5 + numReplies = 10 + isLikedByCurrentUser = true + datePublished = "" + } + val postList = ReaderPostList().apply { + add(readerPost) + } + val readerTag = ReaderTag( + "tag", + "tag", + "tag", + "endpoint", + ReaderTagType.FOLLOWED, + ) + val onTagClick = {} + val onSiteClick = {} + val onPostImageClick = {} + val onPostLikeClick = {} + val onPostMoreMenuClick = {} + + val dateLine = "dateLine" + val numberLikesText = "numberLikesText" + val numberCommentsText = "numberCommentsText" + + // When + whenever(dateTimeUtilsWrapper.dateFromIso8601(any())) + .thenReturn(Date(0)) + whenever(dateTimeUtilsWrapper.javaDateToTimeSpan(any())) + .thenReturn(dateLine) + whenever(readerUtilsWrapper.getShortLikeLabelText(readerPost.numLikes)) + .thenReturn(numberLikesText) + whenever(readerUtilsWrapper.getShortCommentLabelText(readerPost.numReplies)) + .thenReturn(numberCommentsText) + + val actual = classToTest.mapLoadedTagFeedItem( + tag = readerTag, + posts = postList, + onTagClick = onTagClick, + onSiteClick = onSiteClick, + onPostImageClick = onPostImageClick, + onPostLikeClick = onPostLikeClick, + onPostMoreMenuClick = onPostMoreMenuClick, + ) + // Then + val expected = ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = readerTag, + onTagClick = onTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Loaded( + listOf( + TagsFeedPostItem( + siteName = readerPost.blogName, + postDateLine = dateLine, + postTitle = readerPost.title, + postExcerpt = readerPost.excerpt, + postImageUrl = readerPost.blogImageUrl, + postNumberOfLikesText = numberLikesText, + postNumberOfCommentsText = numberCommentsText, + isPostLiked = readerPost.isLikedByCurrentUser, + onSiteClick = onSiteClick, + onPostLikeClick = onPostLikeClick, + onPostImageClick = onPostImageClick, + onPostMoreMenuClick = onPostMoreMenuClick, + ) + ) + ), + ) + assertEquals(expected, actual) + } + + @Test + fun `Should map error TagFeedItem correctly`() { + // Given + val readerTag = ReaderTag( + "tag", + "tag", + "tag", + "endpoint", + ReaderTagType.FOLLOWED, + ) + val errorType = ReaderTagsFeedViewModel.ErrorType.Default + val onTagClick = {} + val onRetryClick = {} + // When + val actual = classToTest.mapErrorTagFeedItem( + tag = readerTag, + errorType = errorType, + onTagClick = onTagClick, + onRetryClick = onRetryClick, + ) + + // Then + val expected = ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = readerTag, + onTagClick = onTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Error( + type = errorType, + onRetryClick = onRetryClick, + ) + ) + assertEquals(expected, actual) + } + + @Test + fun `Should map loading posts UI state correctly`() { + // Given + val onTagClick = {} + val tag1 = ReaderTag( + "tag", + "tag", + "tag", + "endpoint", + ReaderTagType.FOLLOWED, + ) + val tag2 = ReaderTag( + "tag2", + "tag2", + "tag2", + "endpoint2", + ReaderTagType.FOLLOWED, + ) + val tags = listOf(tag1, tag2) + + // When + val actual = classToTest.mapLoadingPostsUiState( + tags = tags, + onTagClick = onTagClick, + ) + + // Then + val expected = ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf( + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag1, + onTagClick = onTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + ), + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag2, + onTagClick = onTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + ) + ) + ) + assertEquals(expected, actual) + } +} From f6abbed0ab02c5bb1565a5b224c81137ac1230a8 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 29 Apr 2024 23:43:40 -0300 Subject: [PATCH 101/237] Fix detekt --- .../reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt | 1 + .../ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt | 1 + .../android/ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt | 2 ++ .../viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt | 1 + 4 files changed, 5 insertions(+) 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 d2b664c233b5..b591258043f1 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 @@ -11,6 +11,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( private val dateTimeUtilsWrapper: DateTimeUtilsWrapper, private val readerUtilsWrapper: ReaderUtilsWrapper, ) { + @Suppress("LongParameterList") fun mapLoadedTagFeedItem( tag: ReaderTag, posts: ReaderPostList, 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 85779f675c6d..de71d090c6c1 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 @@ -51,6 +51,7 @@ class ReaderTagsFeedViewModel @Inject constructor( * * Can be used for retrying a failed fetch, for instance. */ + @Suppress("SwallowedException") private suspend fun fetchTag(tag: ReaderTag) { val updatedLoadedData = getUpdatedLoadedData() // At this point, all tag feed items already exist in the UI with the loading status. 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 3a3a9f91174a..3e55571fc74a 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 @@ -126,6 +126,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { ) } + @Suppress("LongMethod") @Test fun `given valid tags, when fetchAll, then UI state should update properly`() = testCollectingUiStates { // Given @@ -200,6 +201,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { ) } + @Suppress("LongMethod") @Test fun `given valid and invalid tags, when fetchAll, then UI state should update properly`() = testCollectingUiStates { // Given diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index 9556b6ddbe0f..c88e8ac499b0 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -27,6 +27,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { readerUtilsWrapper = readerUtilsWrapper, ) + @Suppress("LongMethod") @Test fun `Should map loaded TagFeedItem correctly`() { // Given From 4f077a7abce2cc925df6b40045e8e3a7ef5b7e41 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 1 May 2024 16:49:10 -0300 Subject: [PATCH 102/237] Fix lint issue --- .../wordpress/android/ui/reader/ReaderTagsFeedFragment.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 80897531c895..d9a41cf57e7b 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 @@ -88,7 +88,11 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } private fun initViewModels(savedInstanceState: Bundle?) { - subFilterViewModel = SubFilterViewModelProvider.getSubFilterViewModelForTag(this, tagsFeedTag, savedInstanceState) + subFilterViewModel = SubFilterViewModelProvider.getSubFilterViewModelForTag( + this, + tagsFeedTag, + savedInstanceState + ) subFilterViewModel.subFilters.observe(viewLifecycleOwner) { subFilters -> val tags = subFilters.filterIsInstance().map { it.tag } From f01998bcbdae988bf29b5e18fd6525009db35855 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 1 May 2024 17:59:38 -0300 Subject: [PATCH 103/237] Stop starting the Tags Feed VM multiple times --- .../reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 de71d090c6c1..61e311a03088 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 @@ -29,6 +29,13 @@ class ReaderTagsFeedViewModel @Inject constructor( * [UiState]s: [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty]. */ fun start(tags: List) { + // 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 + } + if (tags.isEmpty()) { _uiStateFlow.value = UiState.Empty(::onOpenTagsListClick) return From d85126b350d6889cb8b641785682f45f0e47f711 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 1 May 2024 18:02:40 -0300 Subject: [PATCH 104/237] Fix number of likes and comments when the count is 0 --- .../viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 b591258043f1..aeeffa95ab64 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 @@ -35,12 +35,12 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( postTitle = it.title, postExcerpt = it.excerpt, postImageUrl = it.blogImageUrl, - postNumberOfLikesText = readerUtilsWrapper.getShortLikeLabelText( + postNumberOfLikesText = if (it.numLikes > 0) readerUtilsWrapper.getShortLikeLabelText( numLikes = it.numLikes - ), - postNumberOfCommentsText = readerUtilsWrapper.getShortCommentLabelText( + ) else "", + postNumberOfCommentsText = if (it.numReplies > 0) readerUtilsWrapper.getShortCommentLabelText( numComments = it.numReplies - ), + ) else "", isPostLiked = it.isLikedByCurrentUser, onSiteClick = onSiteClick, onPostImageClick = onPostImageClick, From 338560d4b6f451ef31a871d30c7b6e2b9eafb4da Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:56:51 -0300 Subject: [PATCH 105/237] Implement show tag posts list when tag button is tapped in Tags feed --- .../ui/reader/ReaderTagsFeedFragment.kt | 23 ++++++++++++++++++- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 6 ++--- .../tagsfeed/ReaderTagsFeedViewModel.kt | 15 +++++++++--- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 4 ++-- 4 files changed, 39 insertions(+), 9 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 2d081f663749..d9cb53856007 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 @@ -16,10 +16,12 @@ import org.wordpress.android.ui.main.WPMainActivity import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider import org.wordpress.android.ui.reader.subfilter.SubfilterListItem -import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent import org.wordpress.android.ui.reader.views.compose.tagsfeed.ReaderTagsFeed import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.setVisible import javax.inject.Inject /** @@ -63,6 +65,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } initViewModels(savedInstanceState) + observeActionEvents() } private fun initViewModels(savedInstanceState: Bundle?) { @@ -79,6 +82,24 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } + private fun observeActionEvents() { + viewModel.actionEvents.observe(viewLifecycleOwner) { + when (it) { + is ActionEvent.OpenTagPostsFeed -> { + binding.composeView.setVisible(false) + binding.postListContainer.setVisible(true) + val tagPostsFeedFragment = ReaderPostListFragment.newInstanceForTag( + // TODO double-check TAG_FOLLOWED type + it.readerTag, ReaderTypes.ReaderPostListType.TAG_FOLLOWED + ) + childFragmentManager.beginTransaction() + .replace(R.id.post_list_container, tagPostsFeedFragment) + .commitNow() + } + } + } + } + override fun getScrollableViewForUniqueIdProvision(): View { return binding.composeView } 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 b591258043f1..5f010ac199fd 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 @@ -15,7 +15,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapLoadedTagFeedItem( tag: ReaderTag, posts: ReaderPostList, - onTagClick: () -> Unit, + onTagClick: (ReaderTag) -> Unit, onSiteClick: () -> Unit, onPostImageClick: () -> Unit, onPostLikeClick: () -> Unit, @@ -54,7 +54,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapErrorTagFeedItem( tag: ReaderTag, errorType: ReaderTagsFeedViewModel.ErrorType, - onTagClick: () -> Unit, + onTagClick: (ReaderTag) -> Unit, onRetryClick: () -> Unit, ): ReaderTagsFeedViewModel.TagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( @@ -70,7 +70,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapLoadingPostsUiState( tags: List, - onTagClick: () -> Unit, + onTagClick: (ReaderTag) -> Unit, ): ReaderTagsFeedViewModel.UiState.Loaded = ReaderTagsFeedViewModel.UiState.Loaded( tags.map { tag -> 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 61e311a03088..cdd636a933e7 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 @@ -1,5 +1,6 @@ package org.wordpress.android.ui.reader.viewmodels.tagsfeed +import androidx.lifecycle.LiveData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow @@ -11,6 +12,7 @@ import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.viewmodel.ScopedViewModel +import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject import javax.inject.Named @@ -23,6 +25,9 @@ class ReaderTagsFeedViewModel @Inject constructor( private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow + private val _actionEvents = SingleLiveEvent() + val actionEvents: LiveData = _actionEvents + /** * Fetch multiple tag posts in parallel. Each tag load causes a new state to be emitted, so multiple emissions of * [uiStateFlow] are expected when calling this method for each tag, since each can go through the following @@ -122,8 +127,8 @@ class ReaderTagsFeedViewModel @Inject constructor( // TODO } - private fun onTagClick() { - // TODO + private fun onTagClick(readerTag: ReaderTag) { + _actionEvents.value = ActionEvent.OpenTagPostsFeed(readerTag) } private fun onRetryClick() { @@ -146,6 +151,10 @@ class ReaderTagsFeedViewModel @Inject constructor( // TODO } + sealed class ActionEvent { + data class OpenTagPostsFeed(val readerTag: ReaderTag) : ActionEvent() + } + sealed class UiState { object Initial : UiState() data class Loaded(val data: List) : UiState() @@ -162,7 +171,7 @@ class ReaderTagsFeedViewModel @Inject constructor( data class TagChip( val tag: ReaderTag, - val onTagClick: () -> Unit, + val onTagClick: (ReaderTag) -> Unit, ) sealed class PostList { 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 471e00631133..cfe442922533 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 @@ -96,7 +96,7 @@ private fun Loaded(uiState: UiState.Loaded) { start = Margin.Large.value, ), text = UiString.UiStringText(tagChip.tag.tagTitle), - onClick = tagChip.onTagClick, + onClick = { tagChip.onTagClick(tagChip.tag) }, height = 36.dp, ) Spacer(modifier = Modifier.height(Margin.Large.value)) @@ -300,7 +300,7 @@ private fun PostListLoaded( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), onClick = { - tagChip.onTagClick() + tagChip.onTagClick(tagChip.tag) AppLog.e(AppLog.T.READER, "RL-> Tag clicked") } ), From d1cd86c1644e80201af3a17800472371834ba000 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 1 May 2024 18:31:32 -0300 Subject: [PATCH 106/237] Fix bottom navigation view overlapping with compose view --- .../android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 471e00631133..b528bfbf5ba6 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 @@ -62,7 +62,8 @@ fun ReaderTagsFeed(uiState: UiState) { Box( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(), + .fillMaxHeight() + .padding(bottom = 48.dp), ) { when (uiState) { is UiState.Loading -> Loading() From 88ebf84ee322cdac03aa145f9a3fb0703e8ba463 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 1 May 2024 19:14:36 -0300 Subject: [PATCH 107/237] Integrate SubFilters with Tag Feed --- .../ui/reader/ReaderTagsFeedFragment.kt | 94 +++++++++++++++---- 1 file changed, 78 insertions(+), 16 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 d9cb53856007..6f597948169e 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 @@ -4,6 +4,9 @@ import android.os.Bundle import android.view.View import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.core.view.ViewCompat.animate +import androidx.core.view.isVisible +import androidx.fragment.app.commitNow import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import dagger.hilt.android.AndroidEntryPoint @@ -16,12 +19,10 @@ import org.wordpress.android.ui.main.WPMainActivity import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider import org.wordpress.android.ui.reader.subfilter.SubfilterListItem -import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent import org.wordpress.android.ui.reader.views.compose.tagsfeed.ReaderTagsFeed import org.wordpress.android.util.extensions.getSerializableCompat -import org.wordpress.android.util.extensions.setVisible import javax.inject.Inject /** @@ -46,9 +47,6 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme private lateinit var subFilterViewModel: SubFilterViewModel private val viewModel: ReaderTagsFeedViewModel by viewModels() - private val readerViewModel: ReaderViewModel by viewModels( - ownerProducer = { requireParentFragment() } - ) // binding private lateinit var binding: ReaderTagFeedFragmentLayoutBinding @@ -64,11 +62,11 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } - initViewModels(savedInstanceState) + observeSubFilterViewModel(savedInstanceState) observeActionEvents() } - private fun initViewModels(savedInstanceState: Bundle?) { + private fun observeSubFilterViewModel(savedInstanceState: Bundle?) { subFilterViewModel = SubFilterViewModelProvider.getSubFilterViewModelForTag( this, tagsFeedTag, @@ -80,26 +78,88 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme val tags = subFilters.filterIsInstance().map { it.tag } viewModel.start(tags) } + + subFilterViewModel.currentSubFilter.observe(viewLifecycleOwner) { subFilter -> + if (subFilter is SubfilterListItem.Tag) { + showTagPostList(subFilter.tag) + } else { + hideTagPostList() + } + } } private fun observeActionEvents() { viewModel.actionEvents.observe(viewLifecycleOwner) { when (it) { is ActionEvent.OpenTagPostsFeed -> { - binding.composeView.setVisible(false) - binding.postListContainer.setVisible(true) - val tagPostsFeedFragment = ReaderPostListFragment.newInstanceForTag( - // TODO double-check TAG_FOLLOWED type - it.readerTag, ReaderTypes.ReaderPostListType.TAG_FOLLOWED - ) - childFragmentManager.beginTransaction() - .replace(R.id.post_list_container, tagPostsFeedFragment) - .commitNow() + subFilterViewModel.setSubfilterFromTag(it.readerTag) } } } } + private fun showTagPostList(tag: ReaderTag) { + startPostListFragment(tag) + binding.postListContainer.fadeIn( + withEndAction = { binding.composeView.isVisible = false }, + ) + } + + private fun hideTagPostList() { + binding.composeView.isVisible = true + binding.postListContainer.fadeOut( + withEndAction = { removeCurrentPostListFragment() }, + ) + } + + private fun startPostListFragment(tag: ReaderTag) { + val tagPostListFragment = ReaderPostListFragment.newInstanceForTag( + tag, + ReaderTypes.ReaderPostListType.TAG_FOLLOWED + ) + + childFragmentManager.commitNow { + replace(R.id.post_list_container, tagPostListFragment) + } + } + + private fun removeCurrentPostListFragment() { + childFragmentManager.run { + findFragmentById(R.id.post_list_container)?.let { + commitNow { + remove(it) + } + } + } + } + + private fun View.fadeIn( + withEndAction: (() -> Unit)? = null + ) { + alpha = 0f + isVisible = true + + animate(this) + // add quick delay to give time for the fragment to be added and load some content + .setStartDelay(POST_LIST_FADE_IN_DELAY) + .setDuration(POST_LIST_FADE_DURATION) + .withEndAction { withEndAction?.invoke() } + .alpha(1f) + } + + private fun View.fadeOut( + withEndAction: (() -> Unit)? = null, + ) { + animate(this) + .withEndAction { + isVisible = false + alpha = 1f + withEndAction?.invoke() + } + .setDuration(POST_LIST_FADE_DURATION) + .alpha(0f) + } + override fun getScrollableViewForUniqueIdProvision(): View { return binding.composeView } @@ -110,6 +170,8 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme companion object { private const val ARG_TAGS_FEED_TAG = "tags_feed_tag" + private const val POST_LIST_FADE_DURATION = 250L + private const val POST_LIST_FADE_IN_DELAY = 300L fun newInstance( feedTag: ReaderTag From b5cc8baebca0378d7841d40866818646b6021945 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 1 May 2024 21:35:43 -0300 Subject: [PATCH 108/237] WIP open post details action on tags feed --- .../ui/reader/tracker/ReaderTracker.kt | 1 + .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 6 +- .../tagsfeed/ReaderTagsFeedViewModel.kt | 27 +- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 39 +- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 371 +++++++++--------- .../ReaderTagsFeedUiStateMapperTest.kt | 6 +- 6 files changed, 247 insertions(+), 203 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt index 1788e4148ef2..c6fc4e1006b9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt @@ -467,6 +467,7 @@ class ReaderTracker @Inject constructor( const val SOURCE_SEARCH = "search" const val SOURCE_SITE_PREVIEW = "site_preview" const val SOURCE_TAG_PREVIEW = "tag_preview" + const val SOURCE_TAGS_FEED = "tags_feed" const val SOURCE_POST_DETAIL = "post_detail" const val SOURCE_POST_DETAIL_TOOLBAR = "post_detail_toolbar" const val SOURCE_POST_DETAIL_COMMENT_SNIPPET = "post_detail_comment_snippet" 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 aeeffa95ab64..01581c99032b 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 @@ -17,7 +17,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( posts: ReaderPostList, onTagClick: () -> Unit, onSiteClick: () -> Unit, - onPostImageClick: () -> Unit, + onPostCardClick: (TagsFeedPostItem) -> Unit, onPostLikeClick: () -> Unit, onPostMoreMenuClick: () -> Unit, ) = ReaderTagsFeedViewModel.TagFeedItem( @@ -42,8 +42,10 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( numComments = it.numReplies ) else "", isPostLiked = it.isLikedByCurrentUser, + postId = it.postId, + blogId = it.blogId, onSiteClick = onSiteClick, - onPostImageClick = onPostImageClick, + onPostCardClick = onPostCardClick, onPostLikeClick = onPostLikeClick, onPostMoreMenuClick = onPostMoreMenuClick, ) 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 de71d090c6c1..9e42b40604e9 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 @@ -5,10 +5,14 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +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.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.tracker.ReaderTracker import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject @@ -19,6 +23,8 @@ class ReaderTagsFeedViewModel @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val readerPostRepository: ReaderPostRepository, private val readerTagsFeedUiStateMapper: ReaderTagsFeedUiStateMapper, + private val readerPostCardActionsHandler: ReaderPostCardActionsHandler, + private val readerPostTableWrapper: ReaderPostTableWrapper, ) : ScopedViewModel(bgDispatcher) { private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow @@ -71,7 +77,7 @@ class ReaderTagsFeedViewModel @Inject constructor( posts = posts, onTagClick = ::onTagClick, onSiteClick = ::onSiteClick, - onPostImageClick = ::onPostImageClick, + onPostCardClick = ::onPostCardClick, onPostLikeClick = ::onPostLikeClick, onPostMoreMenuClick = ::onPostMoreMenuClick, ) @@ -127,8 +133,15 @@ class ReaderTagsFeedViewModel @Inject constructor( // TODO } - private fun onPostImageClick() { - // TODO + private fun onPostCardClick(postItem: TagsFeedPostItem) { + launch { + findPost(postItem.postId, postItem.blogId)?.let { + readerPostCardActionsHandler.handleOnItemClicked( + it, + ReaderTracker.SOURCE_TAGS_FEED + ) + } + } } private fun onPostLikeClick() { @@ -139,6 +152,14 @@ class ReaderTagsFeedViewModel @Inject constructor( // TODO } + private fun findPost(postId: Long, blogId: Long): ReaderPost? { + return readerPostTableWrapper.getBlogPost( + blogId = blogId, + postId = postId, + excludeTextColumn = true, + ) + } + sealed class UiState { object Initial : UiState() data class Loaded(val data: List) : UiState() 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 b528bfbf5ba6..b97220751d3f 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 @@ -264,22 +264,9 @@ private fun PostListLoaded( items( items = postList.items, ) { postItem -> - with(postItem) { ReaderTagsFeedPostListItem( - siteName = siteName, - postDateLine = postDateLine, - postTitle = postTitle, - postExcerpt = postExcerpt, - postImageUrl = postImageUrl, - postNumberOfLikesText = postNumberOfLikesText, - postNumberOfCommentsText = postNumberOfCommentsText, - isPostLiked = isPostLiked, - onSiteClick = onSiteClick, - onPostClick = onPostImageClick, - onPostLikeClick = onPostLikeClick, - onPostMoreMenuClick = onPostMoreMenuClick, + item = postItem ) - } } item { val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black @@ -425,8 +412,10 @@ data class TagsFeedPostItem( val postNumberOfLikesText: String, val postNumberOfCommentsText: String, val isPostLiked: Boolean, + val postId: Long, + val blogId: Long, val onSiteClick: () -> Unit, - val onPostImageClick: () -> Unit, + val onPostCardClick: (TagsFeedPostItem) -> Unit, val onPostLikeClick: () -> Unit, val onPostMoreMenuClick: () -> Unit, ) @@ -447,8 +436,10 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "", isPostLiked = true, + postId = 123L, + blogId = 123L, onSiteClick = {}, - onPostImageClick = {}, + onPostCardClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ), @@ -461,8 +452,10 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "", postNumberOfCommentsText = "3 comments", isPostLiked = true, + postId = 456L, + blogId = 456L, onSiteClick = {}, - onPostImageClick = {}, + onPostCardClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ), @@ -475,8 +468,10 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "123 likes", postNumberOfCommentsText = "9 comments", isPostLiked = true, + postId = 789L, + blogId = 789L, onSiteClick = {}, - onPostImageClick = {}, + onPostCardClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ), @@ -489,8 +484,10 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "1234 likes", postNumberOfCommentsText = "91 comments", isPostLiked = true, + postId = 1234L, + blogId = 1234L, onSiteClick = {}, - onPostImageClick = {}, + onPostCardClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ), @@ -503,8 +500,10 @@ fun ReaderTagsFeedLoaded() { postNumberOfLikesText = "12 likes", postNumberOfCommentsText = "34 comments", isPostLiked = true, + postId = 5678L, + blogId = 5678L, onSiteClick = {}, - onPostImageClick = {}, + onPostCardClick = {}, onPostLikeClick = {}, onPostMoreMenuClick = {}, ), 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 f3c724e30b98..3f9374bc7e53 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 @@ -46,19 +46,8 @@ import org.wordpress.android.ui.compose.unit.Margin @Composable fun ReaderTagsFeedPostListItem( - siteName: String, - postDateLine: String, - postTitle: String, - postExcerpt: String, - postImageUrl: String?, - postNumberOfLikesText: String, - postNumberOfCommentsText: String, - isPostLiked: Boolean, - onSiteClick: () -> Unit, - onPostClick: () -> Unit, - onPostLikeClick: () -> Unit, - onPostMoreMenuClick: () -> Unit, -) { + item: TagsFeedPostItem, +) = with(item) { val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black val primaryElementColor = baseColor.copy( alpha = 0.87F @@ -113,7 +102,7 @@ fun ReaderTagsFeedPostListItem( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - onClick = { onPostClick() }, + onClick = { onPostCardClick(item) }, ), text = postTitle, style = MaterialTheme.typography.titleMedium, @@ -129,25 +118,25 @@ fun ReaderTagsFeedPostListItem( bottom = Margin.Medium.value, ) .conditionalThen( - predicate = postImageUrl == null, + predicate = postImageUrl.isBlank(), other = Modifier.height(180.dp) ) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - onClick = { onPostClick() }, + onClick = { onPostCardClick(item) }, ), text = postExcerpt, style = MaterialTheme.typography.bodySmall, color = primaryElementColor, - maxLines = if (!postImageUrl.isNullOrBlank()) 2 else 10, + maxLines = if (!postImageUrl.isBlank()) 2 else 10, overflow = TextOverflow.Ellipsis, ) // Post image - if (!postImageUrl.isNullOrBlank()) { + if (!postImageUrl.isBlank()) { PostImage( imageUrl = postImageUrl, - onClick = onPostClick, + onClick = { onPostCardClick(item) }, ) } Spacer(Modifier.weight(1f)) @@ -275,183 +264,215 @@ fun ReaderTagsFeedPostListItemPreview() { ) { item { ReaderTagsFeedPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + - " pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + - "Lorem ipsum dolor sit amet consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + - " Vivamus in pretium nisl. Lorem ipsum dolor " + - "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + - " adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in" + - "pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem" + - "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - "urna fermentum posuere. Vivamus in pretium nisl.", - postImageUrl = "https://picsum.photos/200/300", - postNumberOfLikesText = "15 likes", - postNumberOfCommentsText = "4 comments", - isPostLiked = true, - onSiteClick = {}, - onPostClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, + TagsFeedPostItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + " pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + + "Lorem ipsum dolor sit amet consectetur adipiscing elit. Integer pellentesque sapien " + + "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + + " Vivamus in pretium nisl. Lorem ipsum dolor " + + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + " adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in" + + "pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem" + + "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + "urna fermentum posuere. Vivamus in pretium nisl.", + postImageUrl = "https://picsum.photos/200/300", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + blogId = 123L, + postId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) ) Spacer(Modifier.width(24.dp)) ReaderTagsFeedPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem" + - "ipsum dolor sit amet, " + - "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + - "Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - "Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + - "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - "urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + - " consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + - "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + - "pretium nisl.", - postImageUrl = null, - postNumberOfLikesText = "15 likes", - postNumberOfCommentsText = "4 comments", - isPostLiked = true, - onSiteClick = {}, - onPostClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, + item = TagsFeedPostItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem" + + "ipsum dolor sit amet, " + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + + "Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + "Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + + "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + "urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + + " consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + + "pretium nisl.", + postImageUrl = "", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + blogId = 123L, + postId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) ) Spacer(Modifier.width(24.dp)) ReaderTagsFeedPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet.", - postExcerpt = "Lorem ipsum dolor sit amet.", - postImageUrl = "https://picsum.photos/200/300", - postNumberOfLikesText = "15 likes", - postNumberOfCommentsText = "4 comments", - isPostLiked = true, - onSiteClick = {}, - onPostClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, + item = TagsFeedPostItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet.", + postExcerpt = "Lorem ipsum dolor sit amet.", + postImageUrl = "https://picsum.photos/200/300", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + blogId = 123L, + postId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) ) Spacer(Modifier.width(24.dp)) ReaderTagsFeedPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet.", - postExcerpt = "Lorem ipsum dolor sit amet.", - postImageUrl = null, - postNumberOfLikesText = "15 likes", - postNumberOfCommentsText = "4 comments", - isPostLiked = true, - onSiteClick = {}, - onPostClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, + item = TagsFeedPostItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet.", + postExcerpt = "Lorem ipsum dolor sit amet.", + postImageUrl = "", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + blogId = 123L, + postId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) ) Spacer(Modifier.width(24.dp)) ReaderTagsFeedPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postExcerpt = "Lorem ipsum dolor sit amet.", - postImageUrl = "https://picsum.photos/200/300", - postNumberOfLikesText = "15 likes", - postNumberOfCommentsText = "4 comments", - isPostLiked = true, - onSiteClick = {}, - onPostClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, + item = TagsFeedPostItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet.", + postImageUrl = "https://picsum.photos/200/300", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + blogId = 123L, + postId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) ) Spacer(Modifier.width(24.dp)) ReaderTagsFeedPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postExcerpt = "Lorem ipsum dolor sit amet.", - postImageUrl = null, - postNumberOfLikesText = "15 likes", - postNumberOfCommentsText = "4 comments", - isPostLiked = true, - onSiteClick = {}, - onPostClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, + item = TagsFeedPostItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postExcerpt = "Lorem ipsum dolor sit amet.", + postImageUrl = "", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + blogId = 123L, + postId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) ) Spacer(Modifier.width(24.dp)) ReaderTagsFeedPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit" + - "amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + - "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + - "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - "urna fermentum posuere. Vivamus in pretium nisl. Lorem " + - "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - "urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + - "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + - "posuere. Vivamus in pretium nisl.", - postImageUrl = "https://picsum.photos/200/300", - postNumberOfLikesText = "15 likes", - postNumberOfCommentsText = "4 comments", - isPostLiked = true, - onSiteClick = {}, - onPostClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, + item = TagsFeedPostItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit" + + "amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + + "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + "urna fermentum posuere. Vivamus in pretium nisl. Lorem " + + "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + + "urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + + "posuere. Vivamus in pretium nisl.", + postImageUrl = "https://picsum.photos/200/300", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + blogId = 123L, + postId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) ) Spacer(Modifier.width(24.dp)) ReaderTagsFeedPostListItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor" + - "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + - "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", - postImageUrl = null, - postNumberOfLikesText = "15 likes", - postNumberOfCommentsText = "4 comments", - isPostLiked = true, - onSiteClick = {}, - onPostClick = {}, - onPostLikeClick = {}, - onPostMoreMenuClick = {}, + item = TagsFeedPostItem( + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postDateLine = "1h", + postTitle = "Lorem ipsum dolor sit amet.", + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor" + + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + + "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + + "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postImageUrl = "", + postNumberOfLikesText = "15 likes", + postNumberOfCommentsText = "4 comments", + isPostLiked = true, + blogId = 123L, + postId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {}, + ) ) } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index c88e8ac499b0..c199d09bb395 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -53,7 +53,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { ) val onTagClick = {} val onSiteClick = {} - val onPostImageClick = {} + val onPostCardClick = {} val onPostLikeClick = {} val onPostMoreMenuClick = {} @@ -76,7 +76,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { posts = postList, onTagClick = onTagClick, onSiteClick = onSiteClick, - onPostImageClick = onPostImageClick, + onPostCardClick = onPostCardClick, onPostLikeClick = onPostLikeClick, onPostMoreMenuClick = onPostMoreMenuClick, ) @@ -99,7 +99,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { isPostLiked = readerPost.isLikedByCurrentUser, onSiteClick = onSiteClick, onPostLikeClick = onPostLikeClick, - onPostImageClick = onPostImageClick, + onPostCardClick = onPostCardClick, onPostMoreMenuClick = onPostMoreMenuClick, ) ) From 156e75854e3513ea6b2e84d43fe3cdb85b20d1b8 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:56:51 -0300 Subject: [PATCH 109/237] Implement show tag posts list when tag button is tapped in Tags feed --- .../ui/reader/ReaderTagsFeedFragment.kt | 23 ++++++++++++++++++- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 6 ++--- .../tagsfeed/ReaderTagsFeedViewModel.kt | 15 +++++++++--- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 4 ++-- 4 files changed, 39 insertions(+), 9 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 b54fdb83f60f..ee41aaee27ca 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 @@ -17,11 +17,13 @@ import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarte 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.tagsfeed.ReaderTagsFeedViewModel import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent import org.wordpress.android.ui.reader.views.compose.tagsfeed.ReaderTagsFeed import org.wordpress.android.util.NetworkUtils import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.setVisible import javax.inject.Inject /** @@ -65,6 +67,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } initViewModels(savedInstanceState) + observeActionEvents() } private fun initViewModels(savedInstanceState: Bundle?) { @@ -95,6 +98,24 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme subFilterViewModel.updateTagsAndSites() } + private fun observeActionEvents() { + viewModel.actionEvents.observe(viewLifecycleOwner) { + when (it) { + is ActionEvent.OpenTagPostsFeed -> { + binding.composeView.setVisible(false) + binding.postListContainer.setVisible(true) + val tagPostsFeedFragment = ReaderPostListFragment.newInstanceForTag( + // TODO double-check TAG_FOLLOWED type + it.readerTag, ReaderTypes.ReaderPostListType.TAG_FOLLOWED + ) + childFragmentManager.beginTransaction() + .replace(R.id.post_list_container, tagPostsFeedFragment) + .commitNow() + } + } + } + } + override fun getScrollableViewForUniqueIdProvision(): View { return binding.composeView } 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 01581c99032b..eb6aa353fe99 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 @@ -15,7 +15,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapLoadedTagFeedItem( tag: ReaderTag, posts: ReaderPostList, - onTagClick: () -> Unit, + onTagClick: (ReaderTag) -> Unit, onSiteClick: () -> Unit, onPostCardClick: (TagsFeedPostItem) -> Unit, onPostLikeClick: () -> Unit, @@ -56,7 +56,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapErrorTagFeedItem( tag: ReaderTag, errorType: ReaderTagsFeedViewModel.ErrorType, - onTagClick: () -> Unit, + onTagClick: (ReaderTag) -> Unit, onRetryClick: () -> Unit, ): ReaderTagsFeedViewModel.TagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( @@ -72,7 +72,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapLoadingPostsUiState( tags: List, - onTagClick: () -> Unit, + onTagClick: (ReaderTag) -> Unit, ): ReaderTagsFeedViewModel.UiState.Loaded = ReaderTagsFeedViewModel.UiState.Loaded( tags.map { tag -> 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 9e42b40604e9..d0feb259b336 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 @@ -1,5 +1,6 @@ package org.wordpress.android.ui.reader.viewmodels.tagsfeed +import androidx.lifecycle.LiveData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow @@ -15,6 +16,7 @@ import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.ui.reader.tracker.ReaderTracker import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.viewmodel.ScopedViewModel +import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject import javax.inject.Named @@ -29,6 +31,9 @@ class ReaderTagsFeedViewModel @Inject constructor( private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow + private val _actionEvents = SingleLiveEvent() + val actionEvents: LiveData = _actionEvents + /** * Fetch multiple tag posts in parallel. Each tag load causes a new state to be emitted, so multiple emissions of * [uiStateFlow] are expected when calling this method for each tag, since each can go through the following @@ -121,8 +126,8 @@ class ReaderTagsFeedViewModel @Inject constructor( // TODO } - private fun onTagClick() { - // TODO + private fun onTagClick(readerTag: ReaderTag) { + _actionEvents.value = ActionEvent.OpenTagPostsFeed(readerTag) } private fun onRetryClick() { @@ -160,6 +165,10 @@ class ReaderTagsFeedViewModel @Inject constructor( ) } + sealed class ActionEvent { + data class OpenTagPostsFeed(val readerTag: ReaderTag) : ActionEvent() + } + sealed class UiState { object Initial : UiState() data class Loaded(val data: List) : UiState() @@ -176,7 +185,7 @@ class ReaderTagsFeedViewModel @Inject constructor( data class TagChip( val tag: ReaderTag, - val onTagClick: () -> Unit, + val onTagClick: (ReaderTag) -> Unit, ) sealed class PostList { 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 b97220751d3f..821a32c7f075 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 @@ -97,7 +97,7 @@ private fun Loaded(uiState: UiState.Loaded) { start = Margin.Large.value, ), text = UiString.UiStringText(tagChip.tag.tagTitle), - onClick = tagChip.onTagClick, + onClick = { tagChip.onTagClick(tagChip.tag) }, height = 36.dp, ) Spacer(modifier = Modifier.height(Margin.Large.value)) @@ -288,7 +288,7 @@ private fun PostListLoaded( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), onClick = { - tagChip.onTagClick() + tagChip.onTagClick(tagChip.tag) AppLog.e(AppLog.T.READER, "RL-> Tag clicked") } ), From 84e3b45c72774170f27dbe2e08ea0153e092037f Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 2 May 2024 16:03:42 -0300 Subject: [PATCH 110/237] Implement open post details action in tags feed --- .../ui/reader/ReaderTagsFeedFragment.kt | 112 ++++++++++++++++++ .../tagsfeed/ReaderTagsFeedViewModel.kt | 27 +++++ 2 files changed, 139 insertions(+) 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 ee41aaee27ca..3384a01c270e 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 @@ -6,17 +6,23 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.ActivityLauncher 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.comments.ThreadedCommentsActionSource +import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents 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.tracker.ReaderTracker +import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent @@ -24,6 +30,7 @@ import org.wordpress.android.ui.reader.views.compose.tagsfeed.ReaderTagsFeed import org.wordpress.android.util.NetworkUtils import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.extensions.setVisible +import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject /** @@ -52,6 +59,12 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme ownerProducer = { requireParentFragment() } ) + @Inject + lateinit var readerUtilsWrapper: ReaderUtilsWrapper + + @Inject + lateinit var readerTracker: ReaderTracker + // binding private lateinit var binding: ReaderTagFeedFragmentLayoutBinding @@ -68,6 +81,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme initViewModels(savedInstanceState) observeActionEvents() + observeNavigationEvents() } private fun initViewModels(savedInstanceState: Bundle?) { @@ -116,6 +130,104 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } + private fun observeNavigationEvents() { + viewModel.navigationEvents.observeEvent(viewLifecycleOwner) { event -> + when (event) { + is ReaderNavigationEvents.ShowPostDetail -> ReaderActivityLauncher.showReaderPostDetail( + context, + event.post.blogId, + event.post.postId + ) + + is ReaderNavigationEvents.SharePost -> ReaderActivityLauncher.sharePost(context, event.post) + is ReaderNavigationEvents.OpenPost -> ReaderActivityLauncher.openPost(context, event.post) + is ReaderNavigationEvents.ShowReaderComments -> ReaderActivityLauncher.showReaderComments( + context, + event.blogId, + event.postId, + ThreadedCommentsActionSource.READER_POST_CARD.sourceDescription + ) + + is ReaderNavigationEvents.ShowNoSitesToReblog -> ReaderActivityLauncher.showNoSiteToReblog(activity) + is ReaderNavigationEvents.ShowSitePickerForResult -> ActivityLauncher.showSitePickerForResult( + this@ReaderTagsFeedFragment, + event.preselectedSite, + event.mode + ) + + is ReaderNavigationEvents.OpenEditorForReblog -> ActivityLauncher.openEditorForReblog( + activity, + event.site, + event.post, + event.source + ) + + is ReaderNavigationEvents.ShowBookmarkedTab -> ActivityLauncher.viewSavedPostsListInReader(activity) + is ReaderNavigationEvents.ShowBookmarkedSavedOnlyLocallyDialog -> { + showBookmarkSavedLocallyDialog(event) + } + is ReaderNavigationEvents.ShowPostsByTag -> ReaderActivityLauncher.showReaderTagPreview( + context, + event.tag, + ReaderTracker.SOURCE_DISCOVER, + readerTracker + ) + + is ReaderNavigationEvents.ShowVideoViewer -> ReaderActivityLauncher.showReaderVideoViewer( + context, + event.videoUrl + ) + + is ReaderNavigationEvents.ShowBlogPreview -> ReaderActivityLauncher.showReaderBlogOrFeedPreview( + context, + event.siteId, + event.feedId, + event.isFollowed, + ReaderTracker.SOURCE_DISCOVER, + readerTracker + ) + + is ReaderNavigationEvents.ShowReportPost -> ReaderActivityLauncher.openUrl( + context, + readerUtilsWrapper.getReportPostUrl(event.url), + ReaderActivityLauncher.OpenUrlType.INTERNAL + ) + + is ReaderNavigationEvents.ShowReportUser -> ReaderActivityLauncher.openUrl( + context, + readerUtilsWrapper.getReportUserUrl(event.url, event.authorId), + ReaderActivityLauncher.OpenUrlType.INTERNAL + ) + + is ReaderNavigationEvents.ShowReaderSubs -> ReaderActivityLauncher.showReaderSubs(context) + else -> Unit // Do Nothing + } + } + } + + private fun showBookmarkSavedLocallyDialog( + bookmarkDialog: ReaderNavigationEvents.ShowBookmarkedSavedOnlyLocallyDialog + ) { + bookmarkDialog.buttonLabel +// if (bookmarksSavedLocallyDialog == null) { +// MaterialAlertDialogBuilder(requireActivity()) +// .setTitle(getString(bookmarkDialog.title)) +// .setMessage(getString(bookmarkDialog.message)) +// .setPositiveButton(getString(bookmarkDialog.buttonLabel)) { _, _ -> +// bookmarkDialog.okButtonAction.invoke() +// } +// .setOnDismissListener { +// bookmarksSavedLocallyDialog = null +// } +// .setCancelable(false) +// .create() +// .let { +// bookmarksSavedLocallyDialog = it +// it.show() +// } +// } + } + override fun getScrollableViewForUniqueIdProvision(): View { return binding.composeView } 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 d0feb259b336..ddb5fe6e2b6b 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 @@ -1,6 +1,7 @@ package org.wordpress.android.ui.reader.viewmodels.tagsfeed import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow @@ -10,11 +11,13 @@ 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.reader.discover.ReaderNavigationEvents 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.tracker.ReaderTracker import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem +import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject @@ -34,12 +37,25 @@ class ReaderTagsFeedViewModel @Inject constructor( private val _actionEvents = SingleLiveEvent() val actionEvents: LiveData = _actionEvents + private val _navigationEvents = MediatorLiveData>() + val navigationEvents: LiveData> = _navigationEvents + + private var hasInitialized = false + /** * Fetch multiple tag posts in parallel. Each tag load causes a new state to be emitted, so multiple emissions of * [uiStateFlow] are expected when calling this method for each tag, since each can go through the following * [UiState]s: [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty]. */ fun start(tags: List) { + startUiState(tags) + if (!hasInitialized) { + hasInitialized = true + initNavigationEvents() + } + } + + private fun startUiState(tags: List) { if (tags.isEmpty()) { _uiStateFlow.value = UiState.Empty(::onOpenTagsListClick) return @@ -56,6 +72,17 @@ class ReaderTagsFeedViewModel @Inject constructor( } } + private fun initNavigationEvents() { + _navigationEvents.addSource(readerPostCardActionsHandler.navigationEvents) { event -> + // TODO reblog supported in this screen? See ReaderPostDetailViewModel and ReaderDiscoverViewModel +// val target = event.peekContent() +// if (target is ReaderNavigationEvents.ShowSitePickerForResult) { +// pendingReblogPost = target.post +// } + _navigationEvents.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. From 9f83126bea638278d2f2bc1d7f248524cf823853 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 2 May 2024 17:25:42 -0300 Subject: [PATCH 111/237] Fix broken unit test compilation --- .../viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index c88e8ac499b0..c5251634c31c 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -51,7 +51,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { "endpoint", ReaderTagType.FOLLOWED, ) - val onTagClick = {} + val onTagClick = { _: ReaderTag -> } val onSiteClick = {} val onPostImageClick = {} val onPostLikeClick = {} @@ -119,7 +119,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { ReaderTagType.FOLLOWED, ) val errorType = ReaderTagsFeedViewModel.ErrorType.Default - val onTagClick = {} + val onTagClick = { _: ReaderTag -> } val onRetryClick = {} // When val actual = classToTest.mapErrorTagFeedItem( @@ -146,7 +146,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { @Test fun `Should map loading posts UI state correctly`() { // Given - val onTagClick = {} + val onTagClick = { _: ReaderTag -> } val tag1 = ReaderTag( "tag", "tag", From 619b3f659a8ffb0d03571d4cb07bef096500cd34 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 2 May 2024 17:57:33 -0300 Subject: [PATCH 112/237] Implement open blog details action in tags feed --- .../viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt | 2 +- .../viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt | 10 ++++++++-- .../ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 2 +- .../compose/tagsfeed/ReaderTagsFeedPostListItem.kt | 2 +- 4 files changed, 11 insertions(+), 5 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 eb6aa353fe99..341584d3e700 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 @@ -16,7 +16,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( tag: ReaderTag, posts: ReaderPostList, onTagClick: (ReaderTag) -> Unit, - onSiteClick: () -> Unit, + onSiteClick: (TagsFeedPostItem) -> Unit, onPostCardClick: (TagsFeedPostItem) -> Unit, onPostLikeClick: () -> Unit, onPostMoreMenuClick: () -> Unit, 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 ddb5fe6e2b6b..7b546a71d029 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 @@ -161,8 +161,14 @@ class ReaderTagsFeedViewModel @Inject constructor( // TODO } - private fun onSiteClick() { - // TODO + private fun onSiteClick(postItem: TagsFeedPostItem) { + launch { + findPost(postItem.postId, postItem.blogId)?.let { + _navigationEvents.postValue( + Event(ReaderNavigationEvents.ShowBlogPreview(it.blogId, it.feedId, it.isFollowedByCurrentUser)) + ) + } + } } private fun onPostCardClick(postItem: TagsFeedPostItem) { 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 821a32c7f075..eba913b2840e 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 @@ -414,7 +414,7 @@ data class TagsFeedPostItem( val isPostLiked: Boolean, val postId: Long, val blogId: Long, - val onSiteClick: () -> Unit, + val onSiteClick: (TagsFeedPostItem) -> Unit, val onPostCardClick: (TagsFeedPostItem) -> Unit, val onPostLikeClick: () -> 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 3f9374bc7e53..5415144ce4a5 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 @@ -71,7 +71,7 @@ fun ReaderTagsFeedPostListItem( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - onClick = { onSiteClick() }, + onClick = { onSiteClick(item) }, ), text = siteName, style = MaterialTheme.typography.labelLarge, From a89c9214c5263556b4cc3b0103b89ce01f477484 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 3 May 2024 14:29:16 -0300 Subject: [PATCH 113/237] Fix detekt --- .../ui/reader/ReaderTagsFeedFragment.kt | 2 +- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 103 +++++++++--------- 2 files changed, 54 insertions(+), 51 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 3384a01c270e..e9683331cb3a 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 @@ -6,7 +6,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.databinding.ReaderTagFeedFragmentLayoutBinding @@ -130,6 +129,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } + @Suppress("LongMethod") private fun observeNavigationEvents() { viewModel.navigationEvents.observeEvent(viewLifecycleOwner) { event -> when (event) { 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 5415144ce4a5..76fc7c2b4507 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 @@ -272,17 +272,18 @@ fun ReaderTagsFeedPostListItemPreview() { "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + - "Lorem ipsum dolor sit amet consectetur adipiscing elit. Integer pellentesque sapien " + - "sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + - " Vivamus in pretium nisl. Lorem ipsum dolor " + + "Lorem ipsum dolor sit amet consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor " + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + - " adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in" + - "pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer " + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem" + - "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - "urna fermentum posuere. Vivamus in pretium nisl.", + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit" + + "amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + + "Vivamus in pretium nisl.", postImageUrl = "https://picsum.photos/200/300", postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", @@ -301,22 +302,22 @@ fun ReaderTagsFeedPostListItemPreview() { siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem" + "ipsum dolor sit amet, " + - "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + - "Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - "Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + - "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - "urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + - " consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum " + - "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur " + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in " + - "pretium nisl.", + "consectetur adipiscing elit. Integer pellentesque sapien sed urna" + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + + "Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing" + + "elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in" + + "pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem" + + "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien" + + "sed urna fermentum posuere. Vivamus in pretium nisl.", postImageUrl = "", postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", @@ -375,8 +376,8 @@ fun ReaderTagsFeedPostListItemPreview() { siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postExcerpt = "Lorem ipsum dolor sit amet.", postImageUrl = "https://picsum.photos/200/300", postNumberOfLikesText = "15 likes", @@ -393,11 +394,11 @@ fun ReaderTagsFeedPostListItemPreview() { Spacer(Modifier.width(24.dp)) ReaderTagsFeedPostListItem( item = TagsFeedPostItem( - siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", - postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + postTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + "Integer pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postExcerpt = "Lorem ipsum dolor sit amet.", postImageUrl = "", postNumberOfLikesText = "15 likes", @@ -418,18 +419,19 @@ fun ReaderTagsFeedPostListItemPreview() { "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + - "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit" + - "amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + - "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + - "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - "urna fermentum posuere. Vivamus in pretium nisl. Lorem " + - "ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed" + - "urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor" + + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + + "Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing" + + "elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in" + + "pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + "Integer pellentesque sapien sed urna fermentum" + "posuere. Vivamus in pretium nisl.", postImageUrl = "https://picsum.photos/200/300", postNumberOfLikesText = "15 likes", @@ -450,18 +452,19 @@ fun ReaderTagsFeedPostListItemPreview() { "sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postDateLine = "1h", postTitle = "Lorem ipsum dolor sit amet.", - postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + + postExcerpt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl." + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + "sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor" + "sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl. Lorem ipsum" + - "dolor sit amet, consectetur adipiscing elit. Integer pellentesque sapien sed urna" + - "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + - "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus" + - "in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + - "pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", + "fermentum posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet," + + "consectetur adipiscing elit. Integer pellentesque sapien sed urna fermentum" + + "posuere. Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur" + + "adipiscing elit. Integer pellentesque sapien sed urna fermentum posuere." + + "Vivamus in pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing" + + "elit. Integer pellentesque sapien sed urna fermentum posuere. Vivamus in" + + "pretium nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + + " pellentesque sapien sed urna fermentum posuere. Vivamus in pretium nisl.", postImageUrl = "", postNumberOfLikesText = "15 likes", postNumberOfCommentsText = "4 comments", From 93cbb168c67afeb65a94a8262b5185214d8eeb0f Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 3 May 2024 14:49:56 -0300 Subject: [PATCH 114/237] Fix ReaderTagsFeedViewModelTest unit tests --- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 25 ++++++++++++++++++- .../ReaderTagsFeedUiStateMapperTest.kt | 12 +++++---- 2 files changed, 31 insertions(+), 6 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 3e55571fc74a..9e798a3ac85a 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 @@ -1,5 +1,6 @@ package org.wordpress.android.ui.reader.viewmodels +import androidx.lifecycle.MediatorLiveData import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.toList @@ -13,14 +14,18 @@ import org.mockito.kotlin.any import org.mockito.kotlin.doSuspendableAnswer import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest +import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper 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.discover.ReaderNavigationEvents +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.viewmodels.tagsfeed.ReaderTagsFeedUiStateMapper import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel +import org.wordpress.android.viewmodel.Event @OptIn(ExperimentalCoroutinesApi::class) class ReaderTagsFeedViewModelTest : BaseUnitTest() { @@ -30,6 +35,15 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Mock lateinit var readerTagsFeedUiStateMapper: ReaderTagsFeedUiStateMapper + @Mock + lateinit var readerPostCardActionsHandler: ReaderPostCardActionsHandler + + @Mock + lateinit var readerPostTableWrapper: ReaderPostTableWrapper + + @Mock + lateinit var navigationEvents: MediatorLiveData> + private lateinit var viewModel: ReaderTagsFeedViewModel private val collectedUiStates: MutableList = mutableListOf() @@ -52,7 +66,16 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Before fun setUp() { - viewModel = ReaderTagsFeedViewModel(testDispatcher(), readerPostRepository, readerTagsFeedUiStateMapper) + viewModel = ReaderTagsFeedViewModel( + bgDispatcher = testDispatcher(), + readerPostRepository = readerPostRepository, + readerTagsFeedUiStateMapper = readerTagsFeedUiStateMapper, + readerPostCardActionsHandler = readerPostCardActionsHandler, + readerPostTableWrapper = readerPostTableWrapper, + ) + + whenever(readerPostCardActionsHandler.navigationEvents) + .thenReturn(navigationEvents) } @Test diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index c199d09bb395..faf46defd579 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -51,9 +51,9 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { "endpoint", ReaderTagType.FOLLOWED, ) - val onTagClick = {} - val onSiteClick = {} - val onPostCardClick = {} + val onTagClick: (ReaderTag) -> Unit = {} + val onSiteClick: (TagsFeedPostItem) -> Unit = {} + val onPostCardClick: (TagsFeedPostItem) -> Unit = {} val onPostLikeClick = {} val onPostMoreMenuClick = {} @@ -97,6 +97,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { postNumberOfLikesText = numberLikesText, postNumberOfCommentsText = numberCommentsText, isPostLiked = readerPost.isLikedByCurrentUser, + postId = 123L, + blogId = 123L, onSiteClick = onSiteClick, onPostLikeClick = onPostLikeClick, onPostCardClick = onPostCardClick, @@ -119,7 +121,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { ReaderTagType.FOLLOWED, ) val errorType = ReaderTagsFeedViewModel.ErrorType.Default - val onTagClick = {} + val onTagClick: (ReaderTag) -> Unit = {} val onRetryClick = {} // When val actual = classToTest.mapErrorTagFeedItem( @@ -146,7 +148,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { @Test fun `Should map loading posts UI state correctly`() { // Given - val onTagClick = {} + val onTagClick: (ReaderTag) -> Unit = {} val tag1 = ReaderTag( "tag", "tag", From 15a57e19791d619e62c80675e88bdf4788f20c4a Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 3 May 2024 15:05:28 -0300 Subject: [PATCH 115/237] Fix ReaderTagsFeedUiStateMapperTest unit tests --- .../viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index faf46defd579..f4730e50c09f 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -97,8 +97,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { postNumberOfLikesText = numberLikesText, postNumberOfCommentsText = numberCommentsText, isPostLiked = readerPost.isLikedByCurrentUser, - postId = 123L, - blogId = 123L, + postId = 0L, + blogId = 0L, onSiteClick = onSiteClick, onPostLikeClick = onPostLikeClick, onPostCardClick = onPostCardClick, From 3f388ce14b45de28e9449bec1a19922803100658 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 3 May 2024 17:01:45 -0300 Subject: [PATCH 116/237] Make isStarted private again --- .../android/ui/reader/subfilter/SubFilterViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index c86a8c6313b3..5f806bb6ad8b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -76,10 +76,9 @@ class SubFilterViewModel @Inject constructor( private var lastKnownUserId: Long? = null private var lastTokenAvailableStatus: Boolean? = null + private var isStarted = false private var isFirstLoad = true private var mTagFragmentStartedWith: ReaderTag? = null - var isStarted = false - private set /** * Tag may be null for Blog previews for instance. From af1a021817c5c14c2c50c68e7eeccabb7f37014d Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 3 May 2024 17:19:52 -0300 Subject: [PATCH 117/237] Fix tags feed item site text width --- .../reader/views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f3c724e30b98..1d34cd3c7233 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 @@ -78,7 +78,7 @@ fun ReaderTagsFeedPostListItem( // Site name Text( modifier = Modifier - .weight(1F) + .weight(1f, fill = false) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, From 50114ee70140b86b8a4d9291c88c316bdf27988d Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 3 May 2024 17:56:12 -0300 Subject: [PATCH 118/237] Update ReaderTagsFeedViewModelTest tests --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 7 ++- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 45 ++++++++++++++++++- 2 files changed, 49 insertions(+), 3 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 7b546a71d029..328a4f5dcfe7 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 @@ -1,5 +1,6 @@ package org.wordpress.android.ui.reader.viewmodels.tagsfeed +import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import dagger.hilt.android.lifecycle.HiltViewModel @@ -153,7 +154,8 @@ class ReaderTagsFeedViewModel @Inject constructor( // TODO } - private fun onTagClick(readerTag: ReaderTag) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun onTagClick(readerTag: ReaderTag) { _actionEvents.value = ActionEvent.OpenTagPostsFeed(readerTag) } @@ -161,7 +163,8 @@ class ReaderTagsFeedViewModel @Inject constructor( // TODO } - private fun onSiteClick(postItem: TagsFeedPostItem) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun onSiteClick(postItem: TagsFeedPostItem) { launch { findPost(postItem.postId, postItem.blogId)?.let { _navigationEvents.postValue( 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 9e798a3ac85a..a2b0a79a8888 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 @@ -25,7 +25,10 @@ import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedUiStateMapper import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel +import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent +import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.viewmodel.Event +import kotlin.test.assertIs @OptIn(ExperimentalCoroutinesApi::class) class ReaderTagsFeedViewModelTest : BaseUnitTest() { @@ -48,6 +51,9 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { private val collectedUiStates: MutableList = mutableListOf() + private val actionEvents = mutableListOf() + private val readerNavigationEvents = mutableListOf>() + val tag = ReaderTag( "tag", "tag", @@ -73,9 +79,10 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { readerPostCardActionsHandler = readerPostCardActionsHandler, readerPostTableWrapper = readerPostTableWrapper, ) - whenever(readerPostCardActionsHandler.navigationEvents) .thenReturn(navigationEvents) + observeActionEvents() + observeNavigationEvents() } @Test @@ -305,6 +312,30 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { ) } + @Test + fun `Should emit OpenTagPostsFeed when onTagClick is called`() { + // When + viewModel.onTagClick(tag) + + // Then + assertIs(actionEvents.first()) + } + + @Test + fun `Should emit ShowBlogPreview when onSiteClick is called`() = test { + // Given + whenever(readerPostTableWrapper.getBlogPost(any(), any(), any())) + .thenReturn(ReaderPost()) + + // When + viewModel.onSiteClick(TagsFeedPostItem( + "", "", "", "", "", "", "", true, 123L, 123L, {}, {}, {}, {} + )) + + // Then + assertIs>(readerNavigationEvents.first()) + } + private fun testCollectingUiStates(block: suspend TestScope.() -> Unit) = test { val collectedUiStatesJob = launch { collectedUiStates.clear() @@ -313,4 +344,16 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { this.block() collectedUiStatesJob.cancel() } + + private fun observeActionEvents() { + viewModel.actionEvents.observeForever { + it?.let { actionEvents.add(it) } + } + } + + private fun observeNavigationEvents() { + viewModel.navigationEvents.observeForever { + it?.let { readerNavigationEvents.add(it) } + } + } } From c6d43955645faa94eafdc3ea47c86e7290cb8b7b Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 6 May 2024 15:52:48 -0300 Subject: [PATCH 119/237] Add unit tests to SubFilterViewModelProvider --- .../SubFilterViewModelProviderTest.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt new file mode 100644 index 000000000000..30f7f844c05f --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt @@ -0,0 +1,74 @@ +package org.wordpress.android.ui.reader.subfilter + +import android.os.Bundle +import androidx.fragment.app.Fragment +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.kotlin.mock +import org.wordpress.android.models.ReaderTag +import org.wordpress.android.models.ReaderTagType + +// fragment implementing SubFilterViewModelProvider for testing purposes only +@Suppress("MemberVisibilityCanBePrivate") +private open class SubFilterViewModelProviderFakeFragment( + val viewModelKeyMap: Map = emptyMap(), + val viewModelTagMap: Map = emptyMap(), +) : Fragment(), SubFilterViewModelProvider { + override fun getSubFilterViewModelForKey(key: String): SubFilterViewModel { + return viewModelKeyMap[key] ?: error("No SubFilterViewModel found for key: $key") + } + + override fun getSubFilterViewModelForTag(tag: ReaderTag, savedInstanceState: Bundle?): SubFilterViewModel { + return viewModelTagMap[tag] ?: error("No SubFilterViewModel found for tag: $tag") + } +} + +private fun createTag(tagName: String): ReaderTag { + return ReaderTag(tagName, tagName, tagName, "endpoint", ReaderTagType.FOLLOWED) +} + +class SubFilterViewModelProviderTest { + @Test + fun `getSubFilterViewModelForTag should use given tag for retrieving the appropriate ViewModel`() { + // Given + val tag1 = createTag("tag1") + val viewModel1: SubFilterViewModel = mock() + + val tag2 = createTag("tag2") + val viewModel2: SubFilterViewModel = mock() + + val fragment = SubFilterViewModelProviderFakeFragment( + viewModelTagMap = mapOf(tag1 to viewModel1, tag2 to viewModel2) + ) + + // When + val result1 = SubFilterViewModelProvider.getSubFilterViewModelForTag(fragment, tag1) + val result2 = SubFilterViewModelProvider.getSubFilterViewModelForTag(fragment, tag2) + + // Then + assertThat(result1).isEqualTo(viewModel1) + assertThat(result2).isEqualTo(viewModel2) + } + + @Test + fun `getSubFilterViewModelForKey should use given key for retrieving the appropriate ViewModel`() { + // Given + val key1 = "key1" + val viewModel1: SubFilterViewModel = mock() + + val key2 = "key2" + val viewModel2: SubFilterViewModel = mock() + + val fragment = SubFilterViewModelProviderFakeFragment( + viewModelKeyMap = mapOf(key1 to viewModel1, key2 to viewModel2) + ) + + // When + val result1 = SubFilterViewModelProvider.getSubFilterViewModelForKey(fragment, key1) + val result2 = SubFilterViewModelProvider.getSubFilterViewModelForKey(fragment, key2) + + // Then + assertThat(result1).isEqualTo(viewModel1) + assertThat(result2).isEqualTo(viewModel2) + } +} 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 120/237] 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 e9683331cb3a..29694cebaf84 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 341584d3e700..576ed66f92c0 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 328a4f5dcfe7..965723ded0ff 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 eba913b2840e..ad8a98ec758c 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 76fc7c2b4507..e362df3c85ab 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 0eba1785053a689e0072ac64b05d17124a601ada Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 6 May 2024 17:17:24 -0300 Subject: [PATCH 121/237] Improve and add unit tests for ReaderTagsFeedViewModel --- .../android/ui/reader/ReaderTestUtils.kt | 17 ++ .../SubFilterViewModelProviderTest.kt | 9 +- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 264 ++++++++---------- 3 files changed, 142 insertions(+), 148 deletions(-) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTestUtils.kt diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTestUtils.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTestUtils.kt new file mode 100644 index 000000000000..99c7fe6e3ac5 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTestUtils.kt @@ -0,0 +1,17 @@ +package org.wordpress.android.ui.reader + +import org.wordpress.android.models.ReaderTag +import org.wordpress.android.models.ReaderTagType + +object ReaderTestUtils { + fun createTag( + slug: String, + type: ReaderTagType = ReaderTagType.FOLLOWED, + ): ReaderTag = ReaderTag( + slug, + slug, + slug, + "endpoint/$slug", + type, + ) +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt index 30f7f844c05f..9d935913257a 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt @@ -7,6 +7,7 @@ import org.junit.Test import org.mockito.kotlin.mock import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.ui.reader.ReaderTestUtils // fragment implementing SubFilterViewModelProvider for testing purposes only @Suppress("MemberVisibilityCanBePrivate") @@ -23,18 +24,14 @@ private open class SubFilterViewModelProviderFakeFragment( } } -private fun createTag(tagName: String): ReaderTag { - return ReaderTag(tagName, tagName, tagName, "endpoint", ReaderTagType.FOLLOWED) -} - class SubFilterViewModelProviderTest { @Test fun `getSubFilterViewModelForTag should use given tag for retrieving the appropriate ViewModel`() { // Given - val tag1 = createTag("tag1") + val tag1 = ReaderTestUtils.createTag("tag1") val viewModel1: SubFilterViewModel = mock() - val tag2 = createTag("tag2") + val tag2 = ReaderTestUtils.createTag("tag2") val viewModel2: SubFilterViewModel = mock() val fragment = SubFilterViewModelProviderFakeFragment( 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 3e55571fc74a..aa13618744ce 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 @@ -5,18 +5,21 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.mockito.Mock +import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.doSuspendableAnswer +import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest 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.exceptions.ReaderPostFetchException import org.wordpress.android.ui.reader.repository.ReaderPostRepository import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedUiStateMapper @@ -34,22 +37,6 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { private val collectedUiStates: MutableList = mutableListOf() - val tag = ReaderTag( - "tag", - "tag", - "tag", - "endpoint", - ReaderTagType.FOLLOWED, - ) - - private val postListLoadingItem = ReaderTagsFeedViewModel.TagFeedItem( - tagChip = ReaderTagsFeedViewModel.TagChip( - tag = tag, - onTagClick = {}, - ), - postList = ReaderTagsFeedViewModel.PostList.Loading, - ) - @Before fun setUp() { viewModel = ReaderTagsFeedViewModel(testDispatcher(), readerPostRepository, readerTagsFeedUiStateMapper) @@ -58,10 +45,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Test fun `given valid tag, when fetchTag, then UI state should update properly`() = testCollectingUiStates { // Given - val tagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( - ReaderTagsFeedViewModel.TagChip(tag, {}), - ReaderTagsFeedViewModel.PostList.Loaded(listOf()) - ) + val tag = ReaderTestUtils.createTag("tag") val posts = ReaderPostList().apply { add(ReaderPost()) } @@ -69,14 +53,8 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(100) posts } - whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) - .thenReturn( - ReaderTagsFeedViewModel.UiState.Loaded( - listOf(postListLoadingItem, postListLoadingItem) - ) - ) - whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any())) - .thenReturn(tagFeedItem) + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems() // When viewModel.start(listOf(tag)) @@ -85,7 +63,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { // Then assertThat(collectedUiStates).contains( ReaderTagsFeedViewModel.UiState.Loaded( - data = listOf(tagFeedItem) + data = listOf(getLoadedTagFeedItem(tag)) ) ) } @@ -93,57 +71,33 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Test fun `given invalid tag, when fetchTag, then UI state should update properly`() = testCollectingUiStates { // Given + val tag = ReaderTestUtils.createTag("tag") val error = ReaderPostFetchException("error") - val tagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( - ReaderTagsFeedViewModel.TagChip(tag, {}), - ReaderTagsFeedViewModel.PostList.Error( - ReaderTagsFeedViewModel.ErrorType.Default, {} - ), - ) whenever(readerPostRepository.fetchNewerPostsForTag(tag)).doSuspendableAnswer { delay(100) throw error } - whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) - .thenReturn( - ReaderTagsFeedViewModel.UiState.Loaded( - listOf(postListLoadingItem, postListLoadingItem) - ) - ) - whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any())) - .thenReturn(tagFeedItem) + mockMapLoadingTagFeedItems() + mockMapErrorTagFeedItems() // When viewModel.start(listOf(tag)) -// viewModel.fetchTag(tag) advanceUntilIdle() // Then assertThat(collectedUiStates).contains( ReaderTagsFeedViewModel.UiState.Loaded( - data = listOf(tagFeedItem) + data = listOf(getErrorTagFeedItem(tag)) ) ) } @Suppress("LongMethod") @Test - fun `given valid tags, when fetchAll, then UI state should update properly`() = testCollectingUiStates { + fun `given valid tags, when start, then UI state should update properly`() = testCollectingUiStates { // Given - val tag1 = ReaderTag( - "tag1", - "tag1", - "tag1", - "endpoint1", - ReaderTagType.FOLLOWED, - ) - val tag2 = ReaderTag( - "tag2", - "tag2", - "tag2", - "endpoint2", - ReaderTagType.FOLLOWED, - ) + val tag1 = ReaderTestUtils.createTag("tag1") + val tag2 = ReaderTestUtils.createTag("tag2") val posts1 = ReaderPostList().apply { add(ReaderPost()) } @@ -158,33 +112,8 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(200) posts2 } - whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) - .thenReturn( - ReaderTagsFeedViewModel.UiState.Loaded( - listOf( - ReaderTagsFeedViewModel.TagFeedItem( - tagChip = ReaderTagsFeedViewModel.TagChip( - tag = tag1, - onTagClick = {}, - ), - postList = ReaderTagsFeedViewModel.PostList.Loading, - ), - ReaderTagsFeedViewModel.TagFeedItem( - tagChip = ReaderTagsFeedViewModel.TagChip( - tag = tag2, - onTagClick = {}, - ), - postList = ReaderTagsFeedViewModel.PostList.Loading, - ), - ) - ) - ) - val tagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( - ReaderTagsFeedViewModel.TagChip(tag1, {}), - ReaderTagsFeedViewModel.PostList.Loaded(listOf()), - ) - whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any())) - .thenReturn(tagFeedItem) + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems() // When viewModel.start(listOf(tag1, tag2)) @@ -194,9 +123,9 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertThat(collectedUiStates).contains( ReaderTagsFeedViewModel.UiState.Loaded( data = listOf( - tagFeedItem, - tagFeedItem, - ) + getLoadedTagFeedItem(tag1), + getLoadedTagFeedItem(tag2) + ), ) ) } @@ -205,20 +134,8 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Test fun `given valid and invalid tags, when fetchAll, then UI state should update properly`() = testCollectingUiStates { // Given - val tag1 = ReaderTag( - "tag1", - "tag1", - "tag1", - "endpoint1", - ReaderTagType.FOLLOWED, - ) - val tag2 = ReaderTag( - "tag2", - "tag2", - "tag2", - "endpoint2", - ReaderTagType.FOLLOWED, - ) + val tag1 = ReaderTestUtils.createTag("tag1") + val tag2 = ReaderTestUtils.createTag("tag2") val posts1 = ReaderPostList().apply { add(ReaderPost()) } @@ -231,41 +148,9 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(200) throw error2 } - whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) - .thenReturn( - ReaderTagsFeedViewModel.UiState.Loaded( - listOf( - ReaderTagsFeedViewModel.TagFeedItem( - tagChip = ReaderTagsFeedViewModel.TagChip( - tag = tag1, - onTagClick = {}, - ), - postList = ReaderTagsFeedViewModel.PostList.Loading, - ), - ReaderTagsFeedViewModel.TagFeedItem( - tagChip = ReaderTagsFeedViewModel.TagChip( - tag = tag2, - onTagClick = {}, - ), - postList = ReaderTagsFeedViewModel.PostList.Loading, - ) - ) - ) - ) - val tagFeedItemLoaded = ReaderTagsFeedViewModel.TagFeedItem( - ReaderTagsFeedViewModel.TagChip(tag1, {}), - ReaderTagsFeedViewModel.PostList.Loaded(listOf()) - ) - val tagFeedItemError = ReaderTagsFeedViewModel.TagFeedItem( - ReaderTagsFeedViewModel.TagChip(tag2, {}), - ReaderTagsFeedViewModel.PostList.Error( - ReaderTagsFeedViewModel.ErrorType.Default, {} - ) - ) - whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any())) - .thenReturn(tagFeedItemLoaded) - whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any())) - .thenReturn(tagFeedItemError) + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems() + mockMapErrorTagFeedItems() // When viewModel.start(listOf(tag1, tag2)) @@ -275,13 +160,108 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertThat(collectedUiStates).contains( ReaderTagsFeedViewModel.UiState.Loaded( data = listOf( - tagFeedItemLoaded, - tagFeedItemError, + getLoadedTagFeedItem(tag1), + getErrorTagFeedItem(tag2), ) ) ) } + @Suppress("LongMethod") + @Test + fun `given tags fetched, when start again, then nothing happens`() = testCollectingUiStates { + // Given + val tag1 = ReaderTestUtils.createTag("tag1") + val tag2 = ReaderTestUtils.createTag("tag2") + val posts1 = ReaderPostList().apply { + add(ReaderPost()) + } + val posts2 = ReaderPostList().apply { + add(ReaderPost()) + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { + delay(100) + posts1 + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag2)).doSuspendableAnswer { + delay(200) + posts2 + } + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems() + + // When + viewModel.start(listOf(tag1, tag2)) + advanceUntilIdle() + val firstCollectedStates = collectedUiStates.toList() + Mockito.clearInvocations(readerPostRepository) + + // Then + viewModel.start(listOf(tag1, tag2)) + advanceUntilIdle() + + assertThat(collectedUiStates).isEqualTo(firstCollectedStates) // still same states, nothing new emitted + verifyNoInteractions(readerPostRepository) + } + + @Suppress("LongMethod") + @Test + fun `given no tags requested, when start, then UI state should update properly`() = testCollectingUiStates { + // Given + val tags = emptyList() + + // When + viewModel.start(tags) + advanceUntilIdle() + + // Then + assertThat(collectedUiStates).last().isInstanceOf(ReaderTagsFeedViewModel.UiState.Empty::class.java) + } + + private fun mockMapLoadingTagFeedItems() { + whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) + .thenAnswer { + val tags = it.getArgument>(0) + ReaderTagsFeedViewModel.UiState.Loaded( + tags.map { tag -> + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag, + onTagClick = {}, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + ) + } + ) + } + } + + private fun mockMapLoadedTagFeedItems() { + whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any())) + .thenAnswer { + getLoadedTagFeedItem(it.getArgument(0)) + } + } + + private fun mockMapErrorTagFeedItems() { + whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any())) + .thenAnswer { + getErrorTagFeedItem(it.getArgument(0)) + } + } + + private fun getLoadedTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem( + ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.PostList.Loaded(listOf()) + ) + + private fun getErrorTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem( + ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.PostList.Error( + ReaderTagsFeedViewModel.ErrorType.Default, {} + ), + ) + private fun testCollectingUiStates(block: suspend TestScope.() -> Unit) = test { val collectedUiStatesJob = launch { collectedUiStates.clear() From 306dc7c755baf60d8f3ada6605b43ca7fa8ccf74 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 6 May 2024 17:21:07 -0300 Subject: [PATCH 122/237] Remove unused import --- .../ui/reader/subfilter/SubFilterViewModelProviderTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt index 9d935913257a..ab0827aaaf68 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelProviderTest.kt @@ -6,7 +6,6 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.mockito.kotlin.mock import org.wordpress.android.models.ReaderTag -import org.wordpress.android.models.ReaderTagType import org.wordpress.android.ui.reader.ReaderTestUtils // fragment implementing SubFilterViewModelProvider for testing purposes only 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 123/237] 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 29694cebaf84..3c89c03e000b 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 965723ded0ff..342354fbbdda 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 ad8a98ec758c..54a9bebd2743 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 124/237] 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 576ed66f92c0..8f6cb3f34e2b 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 342354fbbdda..95eacaf2236d 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 54a9bebd2743..0b84fb09ed4c 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 e362df3c85ab..65fddc0e987a 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 125/237] 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 95eacaf2236d..779e21d60498 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 126/237] 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 779e21d60498..6bcf25efe71d 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 768cb0df5f1c54de09ff6f15ce4ccc3a9613189a Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 7 May 2024 17:32:00 -0300 Subject: [PATCH 127/237] Implement Lazy Loading in Tags Feed --- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 24 +++- .../tagsfeed/ReaderTagsFeedViewModel.kt | 129 +++++++++++------- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 14 +- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 8 +- 4 files changed, 116 insertions(+), 59 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 341584d3e700..51829c1bd3b6 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 @@ -20,6 +20,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( onPostCardClick: (TagsFeedPostItem) -> Unit, onPostLikeClick: () -> Unit, onPostMoreMenuClick: () -> Unit, + onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit, ) = ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = tag, @@ -51,6 +52,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( ) } ), + onItemEnteredView = onItemEnteredView, ) fun mapErrorTagFeedItem( @@ -58,6 +60,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( errorType: ReaderTagsFeedViewModel.ErrorType, onTagClick: (ReaderTag) -> Unit, onRetryClick: () -> Unit, + onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit, ): ReaderTagsFeedViewModel.TagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( @@ -68,11 +71,13 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( type = errorType, onRetryClick = onRetryClick, ), + onItemEnteredView = onItemEnteredView, ) - fun mapLoadingPostsUiState( + fun mapInitialPostsUiState( tags: List, onTagClick: (ReaderTag) -> Unit, + onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit, ): ReaderTagsFeedViewModel.UiState.Loaded = ReaderTagsFeedViewModel.UiState.Loaded( tags.map { tag -> @@ -81,8 +86,23 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( tag = tag, onTagClick = onTagClick, ), - postList = ReaderTagsFeedViewModel.PostList.Loading, + postList = ReaderTagsFeedViewModel.PostList.Initial, + onItemEnteredView = onItemEnteredView, ) } ) + + fun mapLoadingTagFeedItem( + tag: ReaderTag, + onTagClick: (ReaderTag) -> Unit, + onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit, + ): ReaderTagsFeedViewModel.TagFeedItem = + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag, + onTagClick = onTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + onItemEnteredView = onItemEnteredView, + ) } 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 0558624aa9d9..8f6b0bb9c716 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 @@ -1,10 +1,12 @@ package org.wordpress.android.ui.reader.viewmodels.tagsfeed +import android.util.Log import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -24,6 +26,7 @@ import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject import javax.inject.Named +@OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class ReaderTagsFeedViewModel @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, @@ -68,13 +71,7 @@ class ReaderTagsFeedViewModel @Inject constructor( // Initially add all tags to the list with the posts loading UI _uiStateFlow.update { - readerTagsFeedUiStateMapper.mapLoadingPostsUiState(tags, ::onTagClick) - } - // Fetch all posts and update the posts loading UI to either loaded or error when the request finishes - launch { - tags.forEach { - fetchTag(it) - } + readerTagsFeedUiStateMapper.mapInitialPostsUiState(tags, ::onTagClick, ::onItemEnteredView) } } @@ -97,64 +94,89 @@ class ReaderTagsFeedViewModel @Inject constructor( */ @Suppress("SwallowedException") private suspend fun fetchTag(tag: ReaderTag) { - val updatedLoadedData = getUpdatedLoadedData() - // At this point, all tag feed items already exist in the UI with the loading status. - // We need it's index to update it to either Loaded or Error when the request is finished. - val existingIndex = updatedLoadedData.indexOfFirst { it.tagChip.tag == tag } - // Remove the current row of this tag (which is loading). This will be used to later add an updated item with - // either Loaded or Error status, depending on the result of the request. - updatedLoadedData.removeAll { it.tagChip.tag == tag } - try { + // Set the tag to loading state + updateTagFeedItem( + readerTagsFeedUiStateMapper.mapLoadingTagFeedItem( + tag = tag, + onTagClick = ::onTagClick, + onItemEnteredView = ::onItemEnteredView, + ) + ) + + val updatedItem: TagFeedItem = try { // Fetch posts for tag val posts = readerPostRepository.fetchNewerPostsForTag(tag) if (posts.isNotEmpty()) { - updatedLoadedData.add( - existingIndex, - readerTagsFeedUiStateMapper.mapLoadedTagFeedItem( - tag = tag, - posts = posts, - onTagClick = ::onTagClick, - onSiteClick = ::onSiteClick, - onPostCardClick = ::onPostCardClick, - onPostLikeClick = ::onPostLikeClick, - onPostMoreMenuClick = ::onPostMoreMenuClick, - ) + readerTagsFeedUiStateMapper.mapLoadedTagFeedItem( + tag = tag, + posts = posts, + onTagClick = ::onTagClick, + onSiteClick = ::onSiteClick, + onPostCardClick = ::onPostCardClick, + onPostLikeClick = ::onPostLikeClick, + onPostMoreMenuClick = ::onPostMoreMenuClick, + onItemEnteredView = ::onItemEnteredView, ) } else { - updatedLoadedData.add( - existingIndex, - readerTagsFeedUiStateMapper.mapErrorTagFeedItem( - tag = tag, - errorType = ErrorType.NoContent, - onTagClick = ::onTagClick, - onRetryClick = ::onRetryClick, - ) - ) - } - } catch (e: ReaderPostFetchException) { - updatedLoadedData.add( - existingIndex, readerTagsFeedUiStateMapper.mapErrorTagFeedItem( tag = tag, - errorType = ErrorType.Default, + errorType = ErrorType.NoContent, onTagClick = ::onTagClick, onRetryClick = ::onRetryClick, + onItemEnteredView = ::onItemEnteredView, ) + } + } catch (e: ReaderPostFetchException) { + readerTagsFeedUiStateMapper.mapErrorTagFeedItem( + tag = tag, + errorType = ErrorType.Default, + onTagClick = ::onTagClick, + onRetryClick = ::onRetryClick, + onItemEnteredView = ::onItemEnteredView, ) } - _uiStateFlow.update { UiState.Loaded(updatedLoadedData) } + + updateTagFeedItem(updatedItem) } - private fun getUpdatedLoadedData(): MutableList { + private fun getLoadedData(uiState: UiState): MutableList { val updatedLoadedData = mutableListOf() - val currentUiState = _uiStateFlow.value - if (currentUiState is UiState.Loaded) { - val currentLoadedData = currentUiState.data - updatedLoadedData.addAll(currentLoadedData) + if (uiState is UiState.Loaded) { + updatedLoadedData.addAll(uiState.data) } return updatedLoadedData } + // Update the UI state for a single feed item, making sure to do it atomically so we don't lose any updates. + private fun updateTagFeedItem(updatedItem: TagFeedItem) { + _uiStateFlow.update { uiState -> + val updatedLoadedData = getLoadedData(uiState) + + // At this point, all tag feed items already exist in the UI. + // We need it's index to update it and keep it in place. + val existingIndex = updatedLoadedData.indexOfFirst { it.tagChip.tag == updatedItem.tagChip.tag } + + // Remove the current row(s) of this tag, so we don't have duplicates. + updatedLoadedData.removeAll { it.tagChip.tag == updatedItem.tagChip.tag } + + // Add the updated item in the correct position. + updatedLoadedData.add(existingIndex, updatedItem) + + UiState.Loaded(updatedLoadedData) + } + } + + private fun onItemEnteredView(item: TagFeedItem) { + if (item.postList != PostList.Initial) { + // do nothing as it's still loading or already loaded + return + } + + launch { + fetchTag(item.tagChip.tag) + } + } + private fun onOpenTagsListClick() { // TODO } @@ -211,10 +233,10 @@ class ReaderTagsFeedViewModel @Inject constructor( } sealed class UiState { - object Initial : UiState() + data object Initial : UiState() data class Loaded(val data: List) : UiState() - object Loading : UiState() + data object Loading : UiState() data class Empty(val onOpenTagsListClick: () -> Unit) : UiState() } @@ -222,7 +244,12 @@ class ReaderTagsFeedViewModel @Inject constructor( data class TagFeedItem( val tagChip: TagChip, val postList: PostList, - ) + private val onItemEnteredView: (TagFeedItem) -> Unit = {}, + ) { + fun onEnteredView() { + onItemEnteredView(this) + } + } data class TagChip( val tag: ReaderTag, @@ -230,9 +257,11 @@ class ReaderTagsFeedViewModel @Inject constructor( ) sealed class PostList { + data object Initial : PostList() + data class Loaded(val items: List) : PostList() - object Loading : PostList() + data object Loading : PostList() data class Error( val type: ErrorType, 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 eba913b2840e..9aa3186797f4 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 @@ -28,6 +28,7 @@ import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -84,7 +85,14 @@ private fun Loaded(uiState: UiState.Loaded) { ) { items( items = uiState.data, - ) { (tagChip, postList) -> + ) { item -> + val tagChip = item.tagChip + val postList = item.postList + + LaunchedEffect(Unit) { + item.onEnteredView() + } + val backgroundColor = if (isSystemInDarkTheme()) { AppColor.White.copy(alpha = 0.12F) } else { @@ -103,7 +111,7 @@ private fun Loaded(uiState: UiState.Loaded) { Spacer(modifier = Modifier.height(Margin.Large.value)) // Posts list UI when (postList) { - is PostList.Loading -> PostListLoading() + is PostList.Initial, is PostList.Loading -> PostListLoading() is PostList.Loaded -> PostListLoaded(postList, tagChip, backgroundColor) is PostList.Error -> PostListError(backgroundColor, tagChip, postList) } @@ -525,7 +533,7 @@ fun ReaderTagsFeedLoaded() { ), TagFeedItem( tagChip = TagChip(readerTag, {}), - postList = PostList.Loading, + postList = PostList.Initial, ), TagFeedItem( tagChip = TagChip(readerTag, {}), 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 b9bda3144e48..125453190f92 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 @@ -282,7 +282,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } private fun mockMapLoadingTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapLoadingPostsUiState(any(), any())) + whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any())) .thenAnswer { val tags = it.getArgument>(0) ReaderTagsFeedViewModel.UiState.Loaded( @@ -292,7 +292,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { tag = tag, onTagClick = {}, ), - postList = ReaderTagsFeedViewModel.PostList.Loading, + postList = ReaderTagsFeedViewModel.PostList.Initial, ) } ) @@ -300,14 +300,14 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } private fun mockMapLoadedTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any())) + whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any(),)) .thenAnswer { getLoadedTagFeedItem(it.getArgument(0)) } } private fun mockMapErrorTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any())) + whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any(),)) .thenAnswer { getErrorTagFeedItem(it.getArgument(0)) } 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 128/237] 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 1f055fd9d4d2..a47ed4f91ec6 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 129/237] 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 cfb749da5b19..5dea6134f1a9 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 a47ed4f91ec6..ee51c0e699f1 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 130/237] 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 ee51c0e699f1..810760d6fe6e 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 79e683ad292b..d054c2692ff3 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 131/237] 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 d054c2692ff3..f81950465a4e 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 132/237] 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 810760d6fe6e..a6b19ec24e34 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 133/237] 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 a6b19ec24e34..cec69fbe913c 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 { From e5603d4ebd9ca6957814196d56a17f83f187be46 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 8 May 2024 23:39:36 -0300 Subject: [PATCH 134/237] WIP implement tags feed overflow menu --- .../tagsfeed/ReaderTagsFeedMoreMenu.kt | 112 ++++++++++++++++++ .../tagsfeed/ReaderTagsFeedPostListItem.kt | 33 ++++-- 2 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedMoreMenu.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedMoreMenu.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedMoreMenu.kt new file mode 100644 index 000000000000..718f63f8164d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedMoreMenu.kt @@ -0,0 +1,112 @@ +package org.wordpress.android.ui.reader.views.compose.tagsfeed + +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import me.saket.cascade.CascadeColumnScope +import me.saket.cascade.CascadeDropdownMenu +import org.wordpress.android.ui.compose.components.menu.dropdown.MenuColors +import org.wordpress.android.ui.compose.unit.Margin +import org.wordpress.android.ui.compose.utils.horizontalFadingEdges +import org.wordpress.android.ui.compose.utils.uiStringText +import org.wordpress.android.ui.utils.UiString + +@Composable +fun ReaderTagsFeedMoreMenu( + expanded: Boolean, +) { + Row( + modifier = Modifier + .fillMaxSize() +// .horizontalScroll(scrollState) +// .horizontalFadingEdges(scrollState, startEdgeSize = 0.dp) + .padding(start = Margin.ExtraLarge.value), + verticalAlignment = Alignment.CenterVertically, + ) { + CascadeDropdownMenu( + modifier = Modifier + .background(MenuColors.itemBackgroundColor()), + expanded = expanded, +// fixedWidth = cascadeMenuWidth, + fixedWidth = 200.dp, +// onDismissRequest = { isMenuVisible = false }, + onDismissRequest = {}, + offset = DpOffset( +// x = if (LocalLayoutDirection.current == LayoutDirection.Rtl) cascadeMenuWidth else 0.dp, + x = if (LocalLayoutDirection.current == LayoutDirection.Rtl) 200.dp else 0.dp, + y = 0.dp + ) + ) { +// val onMenuItemSingleClick: (MenuElementData.Item.Single) -> Unit = { clickedItem -> +// isMenuVisible = false +// onSingleItemClick(clickedItem) +// } +// menuItems.forEach { element -> +// MenuElementComposable(element = element, onMenuItemSingleClick = onMenuItemSingleClick) +// } + androidx.compose.material3.DropdownMenuItem( + modifier = Modifier + .background(MenuColors.itemBackgroundColor()), + onClick = { + }, + text = { + Text( + text = uiStringText(UiString.UiStringText("Text 1")), + style = MaterialTheme.typography.bodyLarge, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Normal, + maxLines = 1, + ) + }, +// leadingIcon = if (element.leadingIcon != NO_ICON) { +// { +// Icon( +// painter = painterResource(id = element.leadingIcon), +// contentDescription = null, +// ) +// } +// } else null, + ) + androidx.compose.material3.DropdownMenuItem( + modifier = Modifier + .background(MenuColors.itemBackgroundColor()), + onClick = { + }, + text = { + Text( + text = uiStringText(UiString.UiStringText("Text 2")), + style = MaterialTheme.typography.bodyLarge, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Normal, + maxLines = 1, + ) + }, +// leadingIcon = if (element.leadingIcon != NO_ICON) { +// { +// Icon( +// painter = painterResource(id = element.leadingIcon), +// contentDescription = null, +// ) +// } +// } else null, + ) + } + } +} 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 8457cf8083e9..8892ad455a1d 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 @@ -1,6 +1,7 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme @@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -25,10 +27,14 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -218,16 +224,25 @@ fun ReaderTagsFeedPostListItem( } Spacer(Modifier.weight(1f)) // More menu ("…") - IconButton( - modifier = Modifier.size(24.dp), - onClick = { - onPostMoreMenuClick() - }, + Column( + horizontalAlignment = Alignment.End, ) { - Icon( - painter = painterResource(R.drawable.ic_more_ellipsis_horizontal_squares), - contentDescription = stringResource(R.string.show_more_desc), - tint = secondaryElementColor, + var isMenuVisible by remember { mutableStateOf(false) } + IconButton( + modifier = Modifier.size(24.dp), + onClick = { + onPostMoreMenuClick() + isMenuVisible = !isMenuVisible + }, + ) { + Icon( + painter = painterResource(R.drawable.ic_more_ellipsis_horizontal_squares), + contentDescription = stringResource(R.string.show_more_desc), + tint = secondaryElementColor, + ) + } + ReaderTagsFeedMoreMenu( + expanded = isMenuVisible, ) } } From 2b60f3aff214d28faad09da8a45dcd2ba31270d9 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 9 May 2024 13:45:04 -0300 Subject: [PATCH 135/237] Fix existing unit tests --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 12 ++-- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 67 +++++++++++++------ .../ReaderTagsFeedUiStateMapperTest.kt | 18 +++-- 3 files changed, 64 insertions(+), 33 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 8f6b0bb9c716..c07c2cfa82d7 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 @@ -1,12 +1,10 @@ package org.wordpress.android.ui.reader.viewmodels.tagsfeed -import android.util.Log import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -26,7 +24,6 @@ import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject import javax.inject.Named -@OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class ReaderTagsFeedViewModel @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, @@ -89,8 +86,6 @@ class ReaderTagsFeedViewModel @Inject constructor( /** * 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. - * - * Can be used for retrying a failed fetch, for instance. */ @Suppress("SwallowedException") private suspend fun fetchTag(tag: ReaderTag) { @@ -166,7 +161,8 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun onItemEnteredView(item: TagFeedItem) { + @VisibleForTesting + fun onItemEnteredView(item: TagFeedItem) { if (item.postList != PostList.Initial) { // do nothing as it's still loading or already loaded return @@ -181,7 +177,7 @@ class ReaderTagsFeedViewModel @Inject constructor( // TODO } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @VisibleForTesting fun onTagClick(readerTag: ReaderTag) { _actionEvents.value = ActionEvent.OpenTagPostsFeed(readerTag) } @@ -190,7 +186,7 @@ class ReaderTagsFeedViewModel @Inject constructor( // TODO } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @VisibleForTesting fun onSiteClick(postItem: TagsFeedPostItem) { launch { findPost(postItem.postId, postItem.blogId)?.let { 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 125453190f92..281137a82de2 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.viewmodels.tagsfeed.ReaderTagsFeedUiStateMapper @@ -82,7 +82,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } @Test - fun `given valid tag, when fetchTag, then UI state should update properly`() = testCollectingUiStates { + fun `given valid tag, when loaded, then UI state should update properly`() = testCollectingUiStates { // Given val tag = ReaderTestUtils.createTag("tag") val posts = ReaderPostList().apply { @@ -92,12 +92,15 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(100) posts } + mockMapInitialTagFeedItems() mockMapLoadingTagFeedItems() mockMapLoadedTagFeedItems() // When viewModel.start(listOf(tag)) advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag)) + advanceUntilIdle() // Then assertThat(collectedUiStates).contains( @@ -108,7 +111,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } @Test - fun `given invalid tag, when fetchTag, then UI state should update properly`() = testCollectingUiStates { + fun `given invalid tag, when loaded, then UI state should update properly`() = testCollectingUiStates { // Given val tag = ReaderTestUtils.createTag("tag") val error = ReaderPostFetchException("error") @@ -116,12 +119,15 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(100) throw error } + mockMapInitialTagFeedItems() mockMapLoadingTagFeedItems() mockMapErrorTagFeedItems() // When viewModel.start(listOf(tag)) advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag)) + advanceUntilIdle() // Then assertThat(collectedUiStates).contains( @@ -133,7 +139,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Suppress("LongMethod") @Test - fun `given valid tags, when start, then UI state should update properly`() = testCollectingUiStates { + fun `given valid tags, when loaded, then UI state should update properly`() = testCollectingUiStates { // Given val tag1 = ReaderTestUtils.createTag("tag1") val tag2 = ReaderTestUtils.createTag("tag2") @@ -151,12 +157,17 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(200) posts2 } + mockMapInitialTagFeedItems() mockMapLoadingTagFeedItems() mockMapLoadedTagFeedItems() // When viewModel.start(listOf(tag1, tag2)) advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag2)) + advanceUntilIdle() // Then assertThat(collectedUiStates).contains( @@ -171,7 +182,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Suppress("LongMethod") @Test - fun `given valid and invalid tags, when fetchAll, then UI state should update properly`() = testCollectingUiStates { + fun `given valid and invalid tags, when loaded, then UI state should update properly`() = testCollectingUiStates { // Given val tag1 = ReaderTestUtils.createTag("tag1") val tag2 = ReaderTestUtils.createTag("tag2") @@ -187,6 +198,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(200) throw error2 } + mockMapInitialTagFeedItems() mockMapLoadingTagFeedItems() mockMapLoadedTagFeedItems() mockMapErrorTagFeedItems() @@ -194,6 +206,10 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { // When viewModel.start(listOf(tag1, tag2)) advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag2)) + advanceUntilIdle() // Then assertThat(collectedUiStates).contains( @@ -250,7 +266,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(200) posts2 } - mockMapLoadingTagFeedItems() + mockMapInitialTagFeedItems() mockMapLoadedTagFeedItems() // When @@ -281,38 +297,47 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertThat(collectedUiStates).last().isInstanceOf(ReaderTagsFeedViewModel.UiState.Empty::class.java) } - private fun mockMapLoadingTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any())) + private fun mockMapInitialTagFeedItems() { + whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any(), any())) .thenAnswer { val tags = it.getArgument>(0) ReaderTagsFeedViewModel.UiState.Loaded( - tags.map { tag -> - ReaderTagsFeedViewModel.TagFeedItem( - tagChip = ReaderTagsFeedViewModel.TagChip( - tag = tag, - onTagClick = {}, - ), - postList = ReaderTagsFeedViewModel.PostList.Initial, - ) - } + tags.map { tag -> getInitialTagFeedItem(tag) } ) } } - private fun mockMapLoadedTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any(),)) + private fun mockMapLoadingTagFeedItems() { + whenever(readerTagsFeedUiStateMapper.mapLoadingTagFeedItem(any(), any(), any())) .thenAnswer { - getLoadedTagFeedItem(it.getArgument(0)) + val tag = it.getArgument(0) + ReaderTagsFeedViewModel.TagFeedItem( + ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.PostList.Loading + ) } } + private fun mockMapLoadedTagFeedItems() { + whenever( + readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any(), any()) + ).thenAnswer { + getLoadedTagFeedItem(it.getArgument(0)) + } + } + private fun mockMapErrorTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any(),)) + whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any(), any())) .thenAnswer { getErrorTagFeedItem(it.getArgument(0)) } } + private fun getInitialTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem( + ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.PostList.Initial + ) + private fun getLoadedTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem( ReaderTagsFeedViewModel.TagChip(tag, {}), ReaderTagsFeedViewModel.PostList.Loaded(listOf()) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index 737cd43ffe37..ee446059dc6d 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -56,6 +56,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val onPostCardClick: (TagsFeedPostItem) -> Unit = {} val onPostLikeClick = {} val onPostMoreMenuClick = {} + val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} val dateLine = "dateLine" val numberLikesText = "numberLikesText" @@ -79,6 +80,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { onPostCardClick = onPostCardClick, onPostLikeClick = onPostLikeClick, onPostMoreMenuClick = onPostMoreMenuClick, + onItemEnteredView = onItemEnteredView, ) // Then val expected = ReaderTagsFeedViewModel.TagFeedItem( @@ -106,6 +108,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { ) ) ), + onItemEnteredView = onItemEnteredView, ) assertEquals(expected, actual) } @@ -123,12 +126,14 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val errorType = ReaderTagsFeedViewModel.ErrorType.Default val onTagClick: (ReaderTag) -> Unit = {} val onRetryClick = {} + val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} // When val actual = classToTest.mapErrorTagFeedItem( tag = readerTag, errorType = errorType, onTagClick = onTagClick, onRetryClick = onRetryClick, + onItemEnteredView = onItemEnteredView, ) // Then @@ -140,7 +145,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { postList = ReaderTagsFeedViewModel.PostList.Error( type = errorType, onRetryClick = onRetryClick, - ) + ), + onItemEnteredView = onItemEnteredView, ) assertEquals(expected, actual) } @@ -149,6 +155,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { fun `Should map loading posts UI state correctly`() { // Given val onTagClick: (ReaderTag) -> Unit = {} + val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} val tag1 = ReaderTag( "tag", "tag", @@ -166,9 +173,10 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val tags = listOf(tag1, tag2) // When - val actual = classToTest.mapLoadingPostsUiState( + val actual = classToTest.mapInitialPostsUiState( tags = tags, onTagClick = onTagClick, + onItemEnteredView = onItemEnteredView, ) // Then @@ -179,14 +187,16 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { tag = tag1, onTagClick = onTagClick, ), - postList = ReaderTagsFeedViewModel.PostList.Loading, + postList = ReaderTagsFeedViewModel.PostList.Initial, + onItemEnteredView = onItemEnteredView, ), ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = tag2, onTagClick = onTagClick, ), - postList = ReaderTagsFeedViewModel.PostList.Loading, + postList = ReaderTagsFeedViewModel.PostList.Initial, + onItemEnteredView = onItemEnteredView, ) ) ) From c977d71291de1a1ca1940ce667363343cae5a95a Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 9 May 2024 13:57:50 -0300 Subject: [PATCH 136/237] Move post fetching for tags to IO thread --- .../android/ui/reader/repository/ReaderPostRepository.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt index 7fbb8e88556e..503682566dbb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt @@ -3,7 +3,9 @@ package org.wordpress.android.ui.reader.repository import com.android.volley.VolleyError import com.wordpress.rest.RestRequest import dagger.Reusable +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext import org.json.JSONObject import org.wordpress.android.WordPress.Companion.getRestClientUtilsV1_2 import org.wordpress.android.datasets.ReaderPostTable @@ -11,6 +13,7 @@ import org.wordpress.android.datasets.ReaderTagTable import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.modules.IO_THREAD import org.wordpress.android.ui.reader.ReaderConstants import org.wordpress.android.ui.reader.actions.ReaderActions import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResultListener @@ -23,6 +26,7 @@ import org.wordpress.android.util.LocaleManagerWrapper import org.wordpress.android.util.UrlUtils import java.util.Locale import javax.inject.Inject +import javax.inject.Named import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -30,13 +34,14 @@ import kotlin.coroutines.resumeWithException class ReaderPostRepository @Inject constructor( private val localeManagerWrapper: LocaleManagerWrapper, private val localSource: ReaderPostLocalSource, + @Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher, ) { /** * Fetches and returns the most recent posts for the passed tag, respecting the maxPosts limit. * It always fetches the most recent posts, saves them to the local DB and returns the latest from that cache. */ - suspend fun fetchNewerPostsForTag(tag: ReaderTag, maxPosts: Int = 10): ReaderPostList { - return suspendCancellableCoroutine { cont -> + suspend fun fetchNewerPostsForTag(tag: ReaderTag, maxPosts: Int = 10): ReaderPostList = withContext(ioDispatcher) { + suspendCancellableCoroutine { cont -> val resultListener = UpdateResultListener { result -> if (result == ReaderActions.UpdateResult.FAILED) { cont.resumeWithException( From 91bc85811c5e29f61dc6e661b2bf5c089198fb4e Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 9 May 2024 22:27:39 -0300 Subject: [PATCH 137/237] WIP implement more menu using existing Reader logic --- .../ui/reader/ReaderTagsFeedFragment.kt | 32 ++++++++ .../android/ui/reader/ReaderTypes.java | 3 +- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 2 +- .../tagsfeed/ReaderTagsFeedViewModel.kt | 74 +++++++++++++++++-- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 2 +- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 60 ++++++++------- 6 files changed, 137 insertions(+), 36 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 5dea6134f1a9..717d32dec6b6 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 @@ -1,7 +1,9 @@ package org.wordpress.android.ui.reader import android.os.Bundle +import android.view.Gravity import android.view.View +import androidx.appcompat.widget.ListPopupWindow import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.core.view.ViewCompat.animate @@ -12,12 +14,14 @@ 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.analytics.AnalyticsTracker import org.wordpress.android.databinding.ReaderTagFeedFragmentLayoutBinding import org.wordpress.android.models.ReaderTag import org.wordpress.android.ui.ActivityLauncher 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.adapters.ReaderMenuAdapter import org.wordpress.android.ui.reader.comments.ThreadedCommentsActionSource import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel @@ -28,6 +32,7 @@ import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent import org.wordpress.android.ui.reader.views.compose.tagsfeed.ReaderTagsFeed +import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.viewmodel.observeEvent import org.wordpress.android.widgets.WPSnackbar @@ -62,6 +67,9 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme @Inject lateinit var readerTracker: ReaderTracker + @Inject + lateinit var uiHelpers: UiHelpers + // binding private lateinit var binding: ReaderTagFeedFragmentLayoutBinding @@ -79,6 +87,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme observeActionEvents() observeNavigationEvents() observeErrorMessageEvents() + observeOpenMoreMenuEvents() } private fun observeSubFilterViewModel(savedInstanceState: Bundle?) { @@ -259,6 +268,29 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } + private fun observeOpenMoreMenuEvents() { + viewModel.openMoreMenuEvents.observe(viewLifecycleOwner) { + val readerCardUiState = it.readerCardUiState + val blogId = readerCardUiState.blogId + val postId = readerCardUiState.postId + val anchorView = binding.composeView.findViewWithTag("$blogId$postId") + readerTracker.track(AnalyticsTracker.Stat.POST_CARD_MORE_TAPPED) + val listPopup = ListPopupWindow(anchorView.context) + listPopup.width = anchorView.context.resources.getDimensionPixelSize(R.dimen.menu_item_width) + listPopup.setAdapter(ReaderMenuAdapter(anchorView.context, uiHelpers, it.readerPostCardActions)) + listPopup.setDropDownGravity(Gravity.END) + listPopup.anchorView = anchorView + listPopup.isModal = true + listPopup.setOnItemClickListener { _, _, position, _ -> + listPopup.dismiss() + val item = it.readerPostCardActions[position] + item.onClicked?.invoke(postId, blogId, item.type) + } + listPopup.setOnDismissListener { readerCardUiState.onMoreDismissed.invoke(readerCardUiState) } + listPopup.show() + } + } + private fun showBookmarkSavedLocallyDialog( bookmarkDialog: ReaderNavigationEvents.ShowBookmarkedSavedOnlyLocallyDialog ) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java index 28376be451dd..93aefb553cc2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java @@ -9,7 +9,8 @@ public enum ReaderPostListType { TAG_FOLLOWED(ReaderTracker.SOURCE_FOLLOWING), // list posts in a followed tag TAG_PREVIEW(ReaderTracker.SOURCE_TAG_PREVIEW), // list posts in a specific tag BLOG_PREVIEW(ReaderTracker.SOURCE_SITE_PREVIEW), // list posts in a specific blog/feed - SEARCH_RESULTS(ReaderTracker.SOURCE_SEARCH); // list posts matching a specific search keyword or phrase + SEARCH_RESULTS(ReaderTracker.SOURCE_SEARCH), // list posts matching a specific search keyword or phrase + TAGS_FEED(ReaderTracker.SOURCE_TAGS_FEED); // list posts in the tags feed private final String mSource; 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 8f6cb3f34e2b..eab44f3c2bba 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 @@ -19,7 +19,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( onSiteClick: (TagsFeedPostItem) -> Unit, onPostCardClick: (TagsFeedPostItem) -> Unit, onPostLikeClick: (TagsFeedPostItem) -> Unit, - onPostMoreMenuClick: () -> Unit, + onPostMoreMenuClick: (TagsFeedPostItem) -> Unit, ) = ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = tag, 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 cec69fbe913c..17b96b82e139 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 @@ -13,13 +13,22 @@ 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.reader.ReaderTypes +import org.wordpress.android.ui.reader.discover.FEATURED_IMAGE_HEIGHT_WIDTH_RATION +import org.wordpress.android.ui.reader.discover.PHOTON_WIDTH_QUALITY_RATION +import org.wordpress.android.ui.reader.discover.ReaderCardUiState import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents +import org.wordpress.android.ui.reader.discover.ReaderPostCardAction +import org.wordpress.android.ui.reader.discover.ReaderPostCardActionType import org.wordpress.android.ui.reader.discover.ReaderPostCardActionsHandler +import org.wordpress.android.ui.reader.discover.ReaderPostMoreButtonUiStateBuilder +import org.wordpress.android.ui.reader.discover.ReaderPostUiStateBuilder 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.util.DisplayUtilsWrapper import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent @@ -34,6 +43,9 @@ class ReaderTagsFeedViewModel @Inject constructor( private val readerPostCardActionsHandler: ReaderPostCardActionsHandler, private val postLikeUseCase: PostLikeUseCase, private val readerPostTableWrapper: ReaderPostTableWrapper, + private val readerPostMoreButtonUiStateBuilder: ReaderPostMoreButtonUiStateBuilder, + private val readerPostUiStateBuilder: ReaderPostUiStateBuilder, + private val displayUtilsWrapper: DisplayUtilsWrapper, ) : ScopedViewModel(bgDispatcher) { private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow @@ -47,6 +59,9 @@ class ReaderTagsFeedViewModel @Inject constructor( private val _errorMessageEvents = MediatorLiveData>() val errorMessageEvents: LiveData> = _errorMessageEvents + private val _openMoreMenuEvents = SingleLiveEvent() + val openMoreMenuEvents: LiveData = _openMoreMenuEvents + private var hasInitialized = false /** @@ -269,10 +284,10 @@ 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 { @@ -317,8 +332,50 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun onPostMoreMenuClick() { - // TODO + private fun onPostMoreMenuClick(postItem: TagsFeedPostItem) { + launch { + findPost(postItem.postId, postItem.blogId)?.let { post -> + val items = readerPostMoreButtonUiStateBuilder.buildMoreMenuItems( + post = post, + includeBookmark = true, + onButtonClicked = ::onMoreMenuButtonClicked, + ) + val photonWidth: Int = (displayUtilsWrapper.getDisplayPixelWidth() * PHOTON_WIDTH_QUALITY_RATION).toInt() + val photonHeight: Int = (photonWidth * FEATURED_IMAGE_HEIGHT_WIDTH_RATION).toInt() + _openMoreMenuEvents.postValue( + MoreMenuUiState( + readerCardUiState = readerPostUiStateBuilder.mapPostToNewUiState( + source = ReaderTracker.SOURCE_TAGS_FEED, + post = post, + photonWidth = photonWidth, + photonHeight = photonHeight, + postListType = ReaderTypes.ReaderPostListType.TAGS_FEED, + onButtonClicked = {_, _, _ -> }, + onItemClicked = {_, _ -> }, + onItemRendered = {}, + onMoreButtonClicked = {}, + onMoreDismissed = {}, + onVideoOverlayClicked = {_, _ ->}, + onPostHeaderViewClicked = {_, _ -> }, + ), + readerPostCardActions = items, + ) + ) + } + } + } + + private fun onMoreMenuButtonClicked(postId: Long, blogId: Long, type: ReaderPostCardActionType) { + launch { + findPost(postId, blogId)?.let { + readerPostCardActionsHandler.onAction( + it, + type, + isBookmarkList = false, + source = ReaderTracker.SOURCE_DISCOVER + ) + } + } } private fun findPost(postId: Long, blogId: Long): ReaderPost? { @@ -368,4 +425,9 @@ class ReaderTagsFeedViewModel @Inject constructor( data object NoContent : ErrorType } + + data class MoreMenuUiState( + val readerCardUiState: ReaderCardUiState.ReaderPostNewUiState, + val readerPostCardActions: List, + ) } 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 0b84fb09ed4c..0c927bbf3f9f 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 @@ -418,7 +418,7 @@ data class TagsFeedPostItem( val onSiteClick: (TagsFeedPostItem) -> Unit, val onPostCardClick: (TagsFeedPostItem) -> Unit, val onPostLikeClick: (TagsFeedPostItem) -> Unit, - val onPostMoreMenuClick: () -> Unit, + val onPostMoreMenuClick: (TagsFeedPostItem) -> Unit, ) @Preview 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 8892ad455a1d..e44b3e481142 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 @@ -1,7 +1,8 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed +import android.annotation.SuppressLint import android.content.res.Configuration -import androidx.compose.foundation.background +import android.widget.Button import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme @@ -17,24 +18,18 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -42,6 +37,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import coil.compose.AsyncImage import coil.request.ImageRequest import org.wordpress.android.R @@ -50,6 +46,7 @@ import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin +@SuppressLint("ResourceType") @Composable fun ReaderTagsFeedPostListItem( item: TagsFeedPostItem, @@ -224,27 +221,36 @@ fun ReaderTagsFeedPostListItem( } Spacer(Modifier.weight(1f)) // More menu ("…") - Column( - horizontalAlignment = Alignment.End, - ) { - var isMenuVisible by remember { mutableStateOf(false) } - IconButton( - modifier = Modifier.size(24.dp), - onClick = { - onPostMoreMenuClick() - isMenuVisible = !isMenuVisible - }, - ) { - Icon( - painter = painterResource(R.drawable.ic_more_ellipsis_horizontal_squares), - contentDescription = stringResource(R.string.show_more_desc), - tint = secondaryElementColor, - ) + AndroidView( + factory = { context -> + Button(context).apply { + text = "..." + tag = "${item.blogId}${item.postId}" + setOnClickListener { onPostMoreMenuClick(item) } + } } - ReaderTagsFeedMoreMenu( - expanded = isMenuVisible, - ) - } + ) +// Column( +// horizontalAlignment = Alignment.End, +// ) { +// var isMenuVisible by remember { mutableStateOf(false) } +// IconButton( +// modifier = Modifier.size(24.dp), +// onClick = { +// onPostMoreMenuClick() +// isMenuVisible = !isMenuVisible +// }, +// ) { +// Icon( +// painter = painterResource(R.drawable.ic_more_ellipsis_horizontal_squares), +// contentDescription = stringResource(R.string.show_more_desc), +// tint = secondaryElementColor, +// ) +// } +// ReaderTagsFeedMoreMenu( +// expanded = isMenuVisible, +// ) +// } } } } From 840f167f9ce2ec4a25c41ce6330c02fa4d36be33 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 9 May 2024 23:47:46 -0300 Subject: [PATCH 138/237] Fix more menu button using AndroidView in ReaderTagsFeedPostListItem --- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) 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 e44b3e481142..17068f352705 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 @@ -2,7 +2,8 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.annotation.SuppressLint import android.content.res.Configuration -import android.widget.Button +import android.view.ViewGroup +import android.widget.ImageView import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme @@ -38,6 +39,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat import coil.compose.AsyncImage import coil.request.ImageRequest import org.wordpress.android.R @@ -45,6 +47,8 @@ import org.wordpress.android.ui.compose.modifiers.conditionalThen import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin +import org.wordpress.android.util.extensions.getColorResIdFromAttribute +import org.wordpress.android.util.extensions.getDrawableResIdFromAttribute @SuppressLint("ResourceType") @Composable @@ -220,37 +224,33 @@ fun ReaderTagsFeedPostListItem( ) } Spacer(Modifier.weight(1f)) - // More menu ("…") + // More menu ("…"). It's an AndroidView because we must have a way to get the view and inflate the existing + // menu, which is a ListPopupWindow and requires an achor. AndroidView( factory = { context -> - Button(context).apply { - text = "..." + ImageView(context).apply { + layoutParams = ViewGroup.LayoutParams( + context.resources.getDimensionPixelSize(R.dimen.reader_post_card_new_more_icon), + context.resources.getDimensionPixelSize(R.dimen.reader_post_card_new_more_icon) + ) + setImageResource(R.drawable.ic_more_ellipsis_horizontal_squares) + contentDescription = context.resources.getString(R.string.show_more_desc) + setBackgroundResource( + context.getDrawableResIdFromAttribute( + com.google.android.material.R.attr.selectableItemBackgroundBorderless + ) + ) + setColorFilter( + ContextCompat.getColor( + context, + context.getColorResIdFromAttribute(R.attr.wpColorOnSurfaceMedium) + ) + ) tag = "${item.blogId}${item.postId}" setOnClickListener { onPostMoreMenuClick(item) } } } ) -// Column( -// horizontalAlignment = Alignment.End, -// ) { -// var isMenuVisible by remember { mutableStateOf(false) } -// IconButton( -// modifier = Modifier.size(24.dp), -// onClick = { -// onPostMoreMenuClick() -// isMenuVisible = !isMenuVisible -// }, -// ) { -// Icon( -// painter = painterResource(R.drawable.ic_more_ellipsis_horizontal_squares), -// contentDescription = stringResource(R.string.show_more_desc), -// tint = secondaryElementColor, -// ) -// } -// ReaderTagsFeedMoreMenu( -// expanded = isMenuVisible, -// ) -// } } } } From 4acb2b9248382fcdc9d32e5995b1103129bd0e7c Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 10 May 2024 12:39:13 -0300 Subject: [PATCH 139/237] Improve item update (review comment) --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 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 8f24e741b42d..139c0c1179cf 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 @@ -154,14 +154,13 @@ class ReaderTagsFeedViewModel @Inject constructor( val updatedLoadedData = getLoadedData(uiState) // At this point, all tag feed items already exist in the UI. - // We need it's index to update it and keep it in place. - val existingIndex = updatedLoadedData.indexOfFirst { it.tagChip.tag == updatedItem.tagChip.tag } - - // Remove the current row(s) of this tag, so we don't have duplicates. - updatedLoadedData.removeAll { it.tagChip.tag == updatedItem.tagChip.tag } - - // Add the updated item in the correct position. - updatedLoadedData.add(existingIndex, updatedItem) + // We need it's index to update it and keep it in the same place. + updatedLoadedData.indexOfFirst { it.tagChip.tag == updatedItem.tagChip.tag } + .takeIf { it >= 0 } + ?.let { existingIndex -> + // Update item + updatedLoadedData[existingIndex] = updatedItem + } UiState.Loaded(updatedLoadedData) } From 6097c61633cccdfae7705e6dd5770cb731c8442b Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 9 May 2024 16:17:05 -0300 Subject: [PATCH 140/237] Implment pull-to-refresh in tags feed --- .../ui/reader/ReaderTagsFeedFragment.kt | 4 + .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 8 +- .../tagsfeed/ReaderTagsFeedViewModel.kt | 48 +++++++--- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 94 ++++++++++++------- 4 files changed, 103 insertions(+), 51 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 5dea6134f1a9..fd66bbe0b089 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 @@ -109,6 +109,10 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme is ActionEvent.OpenTagPostsFeed -> { subFilterViewModel.setSubfilterFromTag(it.readerTag) } + + ActionEvent.RefreshTagsFeed -> { + subFilterViewModel.updateTagsAndSites() + } } } } 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 396150e18891..8511ed7fae36 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 @@ -77,11 +77,13 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapInitialPostsUiState( tags: List, + 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, @@ -90,7 +92,9 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( postList = ReaderTagsFeedViewModel.PostList.Initial, onItemEnteredView = onItemEnteredView, ) - } + }, + isRefreshing = isRefreshing, + onRefresh = onRefresh, ) fun mapLoadingTagFeedItem( 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 139c0c1179cf..35b2d3d29b77 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 @@ -55,11 +55,11 @@ class ReaderTagsFeedViewModel @Inject constructor( * [UiState]s: [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty]. */ fun start(tags: List) { - // 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()) { @@ -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 + ) } } @@ -162,8 +168,15 @@ class ReaderTagsFeedViewModel @Inject constructor( updatedLoadedData[existingIndex] = updatedItem } - UiState.Loaded(updatedLoadedData) + (uiState as? UiState.Loaded)?.copy(data = updatedLoadedData) ?: UiState.Loaded(updatedLoadedData) + } + } + + private fun onRefresh() { + _uiStateFlow.update { + (it as? UiState.Loaded)?.copy(isRefreshing = true) ?: it } + _actionEvents.value = ActionEvent.RefreshTagsFeed } @VisibleForTesting @@ -286,15 +299,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. @@ -348,11 +361,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) : UiState() + + data class Loaded( + val data: List, + val isRefreshing: Boolean = false, + val onRefresh: () -> Unit = {}, + ) : UiState() data object Loading : UiState() 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 6a89eb1584ed..bb8d68337001 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 @@ -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 @@ -77,46 +81,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), + ) } } @@ -272,9 +296,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 From 028a43440479029a5149ac66bfcd8ec202a6ffda Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 9 May 2024 16:22:15 -0300 Subject: [PATCH 141/237] Show 5 post skeletons in loading state --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) 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 bb8d68337001..b917a579862c 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 @@ -62,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( @@ -172,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 { - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) + items(LOADING_POSTS_COUNT) { ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) } } } @@ -264,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)) } } } From 1d94e49dfa5b9d26f609a3481a789b40593d04b3 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 9 May 2024 17:27:53 -0300 Subject: [PATCH 142/237] Fix existing unit tests --- .../ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt | 2 +- .../viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt | 7 ++++++- 2 files changed, 7 insertions(+), 2 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 0db161ad16f8..b34ee3403737 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 @@ -406,7 +406,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } private fun mockMapInitialTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any(), any())) + whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any(), any(), any(), any())) .thenAnswer { val tags = it.getArgument>(0) ReaderTagsFeedViewModel.UiState.Loaded( diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index d5a4309d4086..b23b5781ba64 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -157,6 +157,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { // Given val onTagClick: (ReaderTag) -> Unit = {} val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} + val onRefresh: () -> Unit = {} val tag1 = ReaderTag( "tag", "tag", @@ -176,8 +177,10 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { // When val actual = classToTest.mapInitialPostsUiState( tags = tags, + isRefreshing = true, onTagClick = onTagClick, onItemEnteredView = onItemEnteredView, + onRefresh = onRefresh, ) // Then @@ -199,7 +202,9 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { postList = ReaderTagsFeedViewModel.PostList.Initial, onItemEnteredView = onItemEnteredView, ) - ) + ), + isRefreshing = true, + onRefresh = onRefresh, ) assertEquals(expected, actual) } From 4d139d828749d6e434ca7e58f9741977b0e982b3 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 9 May 2024 17:44:19 -0300 Subject: [PATCH 143/237] Add unit tests in ViewModel for refresh --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 3 +- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 131 ++++++++++++++++++ 2 files changed, 133 insertions(+), 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 35b2d3d29b77..586d9fa5357d 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 @@ -172,7 +172,8 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun onRefresh() { + @VisibleForTesting + fun onRefresh() { _uiStateFlow.update { (it as? UiState.Loaded)?.copy(isRefreshing = true) ?: it } 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 b34ee3403737..84e494c2eb1d 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 @@ -20,6 +20,7 @@ import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper +import org.wordpress.android.getOrAwaitValue import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderPostList import org.wordpress.android.models.ReaderTag @@ -288,11 +289,16 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { posts2 } mockMapInitialTagFeedItems() + mockMapLoadingTagFeedItems() mockMapLoadedTagFeedItems() // When viewModel.start(listOf(tag1, tag2)) advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag2)) + advanceUntilIdle() val firstCollectedStates = collectedUiStates.toList() Mockito.clearInvocations(readerPostRepository) @@ -304,6 +310,54 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { verifyNoInteractions(readerPostRepository) } + @Suppress("LongMethod") + @Test + fun `given tags fetched, when start again refreshing, then move back to initial state`() = testCollectingUiStates { + // Given + val tag1 = ReaderTestUtils.createTag("tag1") + val tag2 = ReaderTestUtils.createTag("tag2") + val posts1 = ReaderPostList().apply { + add(ReaderPost()) + } + val posts2 = ReaderPostList().apply { + add(ReaderPost()) + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { + delay(100) + posts1 + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag2)).doSuspendableAnswer { + delay(200) + posts2 + } + mockMapInitialTagFeedItems() + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems() + + // When + viewModel.start(listOf(tag1, tag2)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag2)) + advanceUntilIdle() + + viewModel.onRefresh() + + // Then + viewModel.start(listOf(tag1, tag2)) + advanceUntilIdle() + + val loadedState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.Loaded + assertThat(loadedState.data).isEqualTo( + listOf( + getInitialTagFeedItem(tag1), + getInitialTagFeedItem(tag2) + ) + ) + assertThat(loadedState.isRefreshing).isFalse() + } + @Suppress("LongMethod") @Test fun `given no tags requested, when start, then UI state should update properly`() = testCollectingUiStates { @@ -318,6 +372,83 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertThat(collectedUiStates).last().isInstanceOf(ReaderTagsFeedViewModel.UiState.Empty::class.java) } + @Suppress("LongMethod") + @Test + fun `given tags fetched, when refreshing, then update isRefreshing status`() = testCollectingUiStates { + // Given + val tag1 = ReaderTestUtils.createTag("tag1") + val tag2 = ReaderTestUtils.createTag("tag2") + val posts1 = ReaderPostList().apply { + add(ReaderPost()) + } + val posts2 = ReaderPostList().apply { + add(ReaderPost()) + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { + delay(100) + posts1 + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag2)).doSuspendableAnswer { + delay(200) + posts2 + } + mockMapInitialTagFeedItems() + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems() + + // When + viewModel.start(listOf(tag1, tag2)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag2)) + advanceUntilIdle() + + // Then + viewModel.onRefresh() + + val loadedState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.Loaded + assertThat(loadedState.isRefreshing).isTrue() + } + + @Suppress("LongMethod") + @Test + fun `given tags fetched, when refreshing, then RefreshTagsFeed action is posted`() = testCollectingUiStates { + // Given + val tag1 = ReaderTestUtils.createTag("tag1") + val tag2 = ReaderTestUtils.createTag("tag2") + val posts1 = ReaderPostList().apply { + add(ReaderPost()) + } + val posts2 = ReaderPostList().apply { + add(ReaderPost()) + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { + delay(100) + posts1 + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag2)).doSuspendableAnswer { + delay(200) + posts2 + } + mockMapInitialTagFeedItems() + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems() + + // When + viewModel.start(listOf(tag1, tag2)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag2)) + advanceUntilIdle() + + // Then + viewModel.onRefresh() + + val action = viewModel.actionEvents.getOrAwaitValue() + assertThat(action).isEqualTo(ActionEvent.RefreshTagsFeed) + } @Test fun `Should update UI immediately when like button is tapped`() = testCollectingUiStates { // Given From 03d53da3a67a3bb134d744a8b254048cd4fb8675 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 9 May 2024 18:11:20 -0300 Subject: [PATCH 144/237] Show the feature image instead of blog avatar --- .../reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt | 2 +- .../viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt | 4 ++-- 2 files changed, 3 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 8511ed7fae36..8088fffd39cd 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 @@ -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 "", diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index b23b5781ba64..49f11f698f2e 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -35,7 +35,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { blogName = "Name" title = "Title" excerpt = "Excerpt" - blogImageUrl = "url" + featuredImage = "url" numLikes = 5 numReplies = 10 isLikedByCurrentUser = true @@ -95,7 +95,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { postDateLine = dateLine, postTitle = readerPost.title, postExcerpt = readerPost.excerpt, - postImageUrl = readerPost.blogImageUrl, + postImageUrl = readerPost.featuredImage, postNumberOfLikesText = numberLikesText, postNumberOfCommentsText = numberCommentsText, isPostLiked = readerPost.isLikedByCurrentUser, From 7fdb8b3efff7e84625d6ec10bd7f76c90516ef73 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 10 May 2024 13:37:05 -0300 Subject: [PATCH 145/237] Implement bookmarked post info dialog --- .../ui/reader/ReaderTagsFeedFragment.kt | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 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 717d32dec6b6..0ebd02c79bac 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 @@ -3,6 +3,7 @@ package org.wordpress.android.ui.reader import android.os.Bundle import android.view.Gravity import android.view.View +import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.ListPopupWindow import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -11,6 +12,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.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R @@ -73,6 +75,8 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme // binding private lateinit var binding: ReaderTagFeedFragmentLayoutBinding + private var bookmarksSavedLocallyDialog: AlertDialog? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = ReaderTagFeedFragmentLayoutBinding.bind(view) @@ -90,6 +94,11 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme observeOpenMoreMenuEvents() } + override fun onDestroy() { + super.onDestroy() + bookmarksSavedLocallyDialog?.dismiss() + } + private fun observeSubFilterViewModel(savedInstanceState: Bundle?) { subFilterViewModel = SubFilterViewModelProvider.getSubFilterViewModelForTag( this, @@ -296,23 +305,23 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme ) { // TODO show bookmark saved dialog? bookmarkDialog.buttonLabel -// if (bookmarksSavedLocallyDialog == null) { -// MaterialAlertDialogBuilder(requireActivity()) -// .setTitle(getString(bookmarkDialog.title)) -// .setMessage(getString(bookmarkDialog.message)) -// .setPositiveButton(getString(bookmarkDialog.buttonLabel)) { _, _ -> -// bookmarkDialog.okButtonAction.invoke() -// } -// .setOnDismissListener { -// bookmarksSavedLocallyDialog = null -// } -// .setCancelable(false) -// .create() -// .let { -// bookmarksSavedLocallyDialog = it -// it.show() -// } -// } + if (bookmarksSavedLocallyDialog == null) { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(getString(bookmarkDialog.title)) + .setMessage(getString(bookmarkDialog.message)) + .setPositiveButton(getString(bookmarkDialog.buttonLabel)) { _, _ -> + bookmarkDialog.okButtonAction.invoke() + } + .setOnDismissListener { + bookmarksSavedLocallyDialog = null + } + .setCancelable(false) + .create() + .let { + bookmarksSavedLocallyDialog = it + it.show() + } + } } override fun getScrollableViewForUniqueIdProvision(): View { From 5b45a34604907df5c77ec076a8b727b266472a66 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 10 May 2024 14:13:32 -0300 Subject: [PATCH 146/237] Implement subscribe action snackbar --- .../ui/reader/ReaderTagsFeedFragment.kt | 20 +++++++++++++++++++ .../tagsfeed/ReaderTagsFeedViewModel.kt | 17 ++++++++++++++++ 2 files changed, 37 insertions(+) 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 0ebd02c79bac..66664d2a899d 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 @@ -277,6 +277,26 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } + private fun observeSnackbarEvents() { + viewModel.snackbarEvents.observeEvent(viewLifecycleOwner) { snackbarMessageHolder -> + activity?.findViewById(R.id.coordinator)?.let { coordinator -> + with(snackbarMessageHolder) { + val snackbar = WPSnackbar.make( + coordinator, + uiHelpers.getTextOfUiString(requireContext(), message), + Snackbar.LENGTH_LONG + ) + if (buttonTitle != null) { + snackbar.setAction(uiHelpers.getTextOfUiString(requireContext(), buttonTitle)) { + buttonAction.invoke() + } + } + snackbar.show() + } + } + } + } + private fun observeOpenMoreMenuEvents() { viewModel.openMoreMenuEvents.observe(viewLifecycleOwner) { val readerCardUiState = it.readerCardUiState 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 17b96b82e139..a57d09bb9eb1 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 @@ -3,6 +3,7 @@ package org.wordpress.android.ui.reader.viewmodels.tagsfeed import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow @@ -13,6 +14,7 @@ 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.ReaderTypes import org.wordpress.android.ui.reader.discover.FEATURED_IMAGE_HEIGHT_WIDTH_RATION import org.wordpress.android.ui.reader.discover.PHOTON_WIDTH_QUALITY_RATION @@ -56,9 +58,16 @@ class ReaderTagsFeedViewModel @Inject constructor( private val _navigationEvents = MediatorLiveData>() val navigationEvents: LiveData> = _navigationEvents + // Unlike the snackbarEvents observable which only expects messages from ReaderPostCardActionsHandler, + // this observable is controlled by this ViewModel. private val _errorMessageEvents = MediatorLiveData>() val errorMessageEvents: LiveData> = _errorMessageEvents + // This observable just expects messages from ReaderPostCardActionsHandler. Nothing is directly triggered + // from this ViewModel. + private val _snackbarEvents = MediatorLiveData>() + val snackbarEvents: LiveData> = _snackbarEvents + private val _openMoreMenuEvents = SingleLiveEvent() val openMoreMenuEvents: LiveData = _openMoreMenuEvents @@ -84,7 +93,9 @@ class ReaderTagsFeedViewModel @Inject constructor( if (!hasInitialized) { hasInitialized = true + readerPostCardActionsHandler.initScope(viewModelScope) initNavigationEvents() + initSnackbarEvents() } // Initially add all tags to the list with the posts loading UI @@ -110,6 +121,12 @@ class ReaderTagsFeedViewModel @Inject constructor( } } + private fun initSnackbarEvents() { + _snackbarEvents.addSource(readerPostCardActionsHandler.snackbarEvents) { event -> + _snackbarEvents.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. From ca7cf669f429878d17cda4fb96e0f549c9c9eea7 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 10 May 2024 15:12:05 -0300 Subject: [PATCH 147/237] Update unit tests --- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 21 +++++++++++++++++++ .../ReaderTagsFeedUiStateMapperTest.kt | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) 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 f81950465a4e..3894acccfe46 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 @@ -24,9 +24,12 @@ 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.pages.SnackbarMessageHolder 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.discover.ReaderPostMoreButtonUiStateBuilder +import org.wordpress.android.ui.reader.discover.ReaderPostUiStateBuilder 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 +37,7 @@ import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedUiState import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem +import org.wordpress.android.util.DisplayUtilsWrapper import org.wordpress.android.viewmodel.Event import kotlin.test.assertIs @@ -54,9 +58,21 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Mock lateinit var postLikeUseCase: PostLikeUseCase + @Mock + lateinit var readerPostMoreButtonUiStateBuilder: ReaderPostMoreButtonUiStateBuilder + + @Mock + lateinit var readerPostUiStateBuilder: ReaderPostUiStateBuilder + + @Mock + lateinit var displayUtilsWrapper: DisplayUtilsWrapper + @Mock lateinit var navigationEvents: MediatorLiveData> + @Mock + lateinit var snackbarEvents: MediatorLiveData> + private lateinit var viewModel: ReaderTagsFeedViewModel private val collectedUiStates: MutableList = mutableListOf() @@ -81,9 +97,14 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { readerPostCardActionsHandler = readerPostCardActionsHandler, readerPostTableWrapper = readerPostTableWrapper, postLikeUseCase = postLikeUseCase, + readerPostMoreButtonUiStateBuilder = readerPostMoreButtonUiStateBuilder, + readerPostUiStateBuilder = readerPostUiStateBuilder, + displayUtilsWrapper = displayUtilsWrapper, ) whenever(readerPostCardActionsHandler.navigationEvents) .thenReturn(navigationEvents) + whenever(readerPostCardActionsHandler.snackbarEvents) + .thenReturn(snackbarEvents) observeActionEvents() observeNavigationEvents() } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index dd58f3f52b9a..ec6dd4b5b97f 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -55,7 +55,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val onSiteClick: (TagsFeedPostItem) -> Unit = {} val onPostCardClick: (TagsFeedPostItem) -> Unit = {} val onPostLikeClick: (TagsFeedPostItem) -> Unit = {} - val onPostMoreMenuClick = {} + val onPostMoreMenuClick: (TagsFeedPostItem) -> Unit = {} val dateLine = "dateLine" val numberLikesText = "numberLikesText" From 03f8a6371b117aef593c0b1a81ffb1b6c04fd04b Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 10 May 2024 15:44:32 -0300 Subject: [PATCH 148/237] Fix detekt --- .../ui/reader/ReaderTagsFeedFragment.kt | 1 + .../tagsfeed/ReaderTagsFeedViewModel.kt | 12 +- .../tagsfeed/ReaderTagsFeedMoreMenu.kt | 112 ------------------ 3 files changed, 7 insertions(+), 118 deletions(-) delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedMoreMenu.kt 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 66664d2a899d..32481d49f9b6 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 @@ -91,6 +91,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme observeActionEvents() observeNavigationEvents() observeErrorMessageEvents() + observeSnackbarEvents() observeOpenMoreMenuEvents() } 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 a57d09bb9eb1..cfa5b00a74eb 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 @@ -357,8 +357,8 @@ class ReaderTagsFeedViewModel @Inject constructor( includeBookmark = true, onButtonClicked = ::onMoreMenuButtonClicked, ) - val photonWidth: Int = (displayUtilsWrapper.getDisplayPixelWidth() * PHOTON_WIDTH_QUALITY_RATION).toInt() - val photonHeight: Int = (photonWidth * FEATURED_IMAGE_HEIGHT_WIDTH_RATION).toInt() + val photonWidth = (displayUtilsWrapper.getDisplayPixelWidth() * PHOTON_WIDTH_QUALITY_RATION).toInt() + val photonHeight = (photonWidth * FEATURED_IMAGE_HEIGHT_WIDTH_RATION).toInt() _openMoreMenuEvents.postValue( MoreMenuUiState( readerCardUiState = readerPostUiStateBuilder.mapPostToNewUiState( @@ -367,13 +367,13 @@ class ReaderTagsFeedViewModel @Inject constructor( photonWidth = photonWidth, photonHeight = photonHeight, postListType = ReaderTypes.ReaderPostListType.TAGS_FEED, - onButtonClicked = {_, _, _ -> }, - onItemClicked = {_, _ -> }, + onButtonClicked = { _, _, _ -> }, + onItemClicked = { _, _ -> }, onItemRendered = {}, onMoreButtonClicked = {}, onMoreDismissed = {}, - onVideoOverlayClicked = {_, _ ->}, - onPostHeaderViewClicked = {_, _ -> }, + onVideoOverlayClicked = { _, _ -> }, + onPostHeaderViewClicked = { _, _ -> }, ), readerPostCardActions = items, ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedMoreMenu.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedMoreMenu.kt deleted file mode 100644 index 718f63f8164d..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedMoreMenu.kt +++ /dev/null @@ -1,112 +0,0 @@ -package org.wordpress.android.ui.reader.views.compose.tagsfeed - -import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MenuDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import me.saket.cascade.CascadeColumnScope -import me.saket.cascade.CascadeDropdownMenu -import org.wordpress.android.ui.compose.components.menu.dropdown.MenuColors -import org.wordpress.android.ui.compose.unit.Margin -import org.wordpress.android.ui.compose.utils.horizontalFadingEdges -import org.wordpress.android.ui.compose.utils.uiStringText -import org.wordpress.android.ui.utils.UiString - -@Composable -fun ReaderTagsFeedMoreMenu( - expanded: Boolean, -) { - Row( - modifier = Modifier - .fillMaxSize() -// .horizontalScroll(scrollState) -// .horizontalFadingEdges(scrollState, startEdgeSize = 0.dp) - .padding(start = Margin.ExtraLarge.value), - verticalAlignment = Alignment.CenterVertically, - ) { - CascadeDropdownMenu( - modifier = Modifier - .background(MenuColors.itemBackgroundColor()), - expanded = expanded, -// fixedWidth = cascadeMenuWidth, - fixedWidth = 200.dp, -// onDismissRequest = { isMenuVisible = false }, - onDismissRequest = {}, - offset = DpOffset( -// x = if (LocalLayoutDirection.current == LayoutDirection.Rtl) cascadeMenuWidth else 0.dp, - x = if (LocalLayoutDirection.current == LayoutDirection.Rtl) 200.dp else 0.dp, - y = 0.dp - ) - ) { -// val onMenuItemSingleClick: (MenuElementData.Item.Single) -> Unit = { clickedItem -> -// isMenuVisible = false -// onSingleItemClick(clickedItem) -// } -// menuItems.forEach { element -> -// MenuElementComposable(element = element, onMenuItemSingleClick = onMenuItemSingleClick) -// } - androidx.compose.material3.DropdownMenuItem( - modifier = Modifier - .background(MenuColors.itemBackgroundColor()), - onClick = { - }, - text = { - Text( - text = uiStringText(UiString.UiStringText("Text 1")), - style = MaterialTheme.typography.bodyLarge, - overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.Normal, - maxLines = 1, - ) - }, -// leadingIcon = if (element.leadingIcon != NO_ICON) { -// { -// Icon( -// painter = painterResource(id = element.leadingIcon), -// contentDescription = null, -// ) -// } -// } else null, - ) - androidx.compose.material3.DropdownMenuItem( - modifier = Modifier - .background(MenuColors.itemBackgroundColor()), - onClick = { - }, - text = { - Text( - text = uiStringText(UiString.UiStringText("Text 2")), - style = MaterialTheme.typography.bodyLarge, - overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.Normal, - maxLines = 1, - ) - }, -// leadingIcon = if (element.leadingIcon != NO_ICON) { -// { -// Icon( -// painter = painterResource(id = element.leadingIcon), -// contentDescription = null, -// ) -// } -// } else null, - ) - } - } -} From 465599238052a76053989860faabf23223f3944c Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 10 May 2024 17:12:22 -0300 Subject: [PATCH 149/237] Fix tags dropdown menu item label --- WordPress/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 4ea278d95dd3..d5b1681152a2 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1701,7 +1701,7 @@ Saved Liked Automattic - Tags + Your Tags Lists 0 Blogs 1 Blog From 4bda70ee2cf9b3278c9246e7588a64056e985b21 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 10 May 2024 19:14:52 -0300 Subject: [PATCH 150/237] Update Loaded item UI --- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 127 +++++++++++------- 1 file changed, 81 insertions(+), 46 deletions(-) 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 8457cf8083e9..9609ed3cbb60 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 @@ -4,6 +4,7 @@ import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -25,11 +27,15 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -39,11 +45,16 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import org.wordpress.android.R -import org.wordpress.android.ui.compose.modifiers.conditionalThen import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin +private val ITEM_MAX_WIDTH = 320.dp +private val ITEM_HEIGHT = 150.dp // TODO thomashortadev do we want SP instead of DP? to change based on font size) +private const val ITEM_WIDTH_PERCENTAGE = 0.8f +private const val CONTENT_MAX_LINES = 3 +private const val TITLE_MAX_LINES = 2 + @Composable fun ReaderTagsFeedPostListItem( item: TagsFeedPostItem, @@ -55,10 +66,17 @@ fun ReaderTagsFeedPostListItem( val secondaryElementColor = baseColor.copy( alpha = 0.6F ) + + val localConfiguration = LocalConfiguration.current + val screenWidth = remember(localConfiguration) { + localConfiguration.screenWidthDp.dp + } + Column( modifier = Modifier - .width(240.dp) - .height(340.dp) + .widthIn(max = ITEM_MAX_WIDTH) + .width(screenWidth * ITEM_WIDTH_PERCENTAGE) + .height(ITEM_HEIGHT) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -95,51 +113,67 @@ fun ReaderTagsFeedPostListItem( color = secondaryElementColor, ) } - // Post title - Text( - modifier = Modifier - .padding(top = Margin.Medium.value) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { onPostCardClick(item) }, - ), - text = postTitle, - style = MaterialTheme.typography.titleMedium, - color = baseColor, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - // Post excerpt - Text( - modifier = Modifier - .padding( - top = Margin.Small.value, - bottom = Margin.Medium.value, + + Spacer(modifier = Modifier.height(Margin.Small.value)) + + // Post content row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Margin.Medium.value), + verticalAlignment = Alignment.CenterVertically, + ) { + // Post title and excerpt Column + Column( + modifier = Modifier.weight(1f), + ) { + // TODO thomashortadev improve this to avoid an initial composition with the wrong value + var excerptMaxLines by remember { mutableIntStateOf(2) } + + // Post title + Text( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onPostCardClick(item) }, + ), + text = postTitle, + style = MaterialTheme.typography.titleMedium, + color = baseColor, + maxLines = TITLE_MAX_LINES, + overflow = TextOverflow.Ellipsis, + onTextLayout = { layoutResult -> + excerptMaxLines = CONTENT_MAX_LINES - layoutResult.lineCount + }, ) - .conditionalThen( - predicate = postImageUrl.isBlank(), - other = Modifier.height(180.dp) + Spacer(Modifier.height(Margin.Medium.value)) + // Post excerpt + Text( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onPostCardClick(item) }, + ), + text = postExcerpt, + style = MaterialTheme.typography.bodySmall, + color = primaryElementColor, + maxLines = excerptMaxLines, // TODO thomashortadev max lines should be (3 - title_lines_used) + overflow = TextOverflow.Ellipsis, ) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, + } + + // Post image + if (postImageUrl.isNotBlank()) { + PostImage( + imageUrl = postImageUrl, onClick = { onPostCardClick(item) }, - ), - text = postExcerpt, - style = MaterialTheme.typography.bodySmall, - color = primaryElementColor, - maxLines = if (!postImageUrl.isBlank()) 2 else 10, - overflow = TextOverflow.Ellipsis, - ) - // Post image - if (!postImageUrl.isBlank()) { - PostImage( - imageUrl = postImageUrl, - onClick = { onPostCardClick(item) }, - ) + ) + } } + Spacer(Modifier.weight(1f)) + Row( modifier = Modifier .fillMaxWidth(), @@ -172,7 +206,9 @@ fun ReaderTagsFeedPostListItem( maxLines = 1, ) } - Spacer(Modifier.height(Margin.Medium.value)) + + Spacer(Modifier.height(Margin.Small.value)) + Row( modifier = Modifier .fillMaxWidth() @@ -242,8 +278,7 @@ fun PostImage( ) { AsyncImage( modifier = modifier - .fillMaxWidth() - .height(150.dp) + .size(64.dp) .clip(RoundedCornerShape(corner = CornerSize(8.dp))) .clickable { onClick() }, model = ImageRequest.Builder(LocalContext.current) From 26282f247758637f8774115fe80afdfc010250aa Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 10 May 2024 19:25:51 -0300 Subject: [PATCH 151/237] Update height of the "more from" button --- .../android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 3 ++- .../views/compose/tagsfeed/ReaderTagsFeedPostListItem.kt | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 b917a579862c..e64966ae6b06 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 @@ -63,6 +63,7 @@ import org.wordpress.android.ui.utils.UiString import org.wordpress.android.util.AppLog private const val LOADING_POSTS_COUNT = 5 +val READER_TAGS_FEED_ITEM_HEIGHT = 150.dp // TODO thomashortadev do we want SP? to change based on font size) @Composable fun ReaderTagsFeed(uiState: UiState) { @@ -305,7 +306,7 @@ private fun PostListLoaded( ) Box( modifier = Modifier - .height(340.dp) + .height(READER_TAGS_FEED_ITEM_HEIGHT) .padding( start = Margin.ExtraLarge.value, end = Margin.ExtraLarge.value, 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 9609ed3cbb60..3a04bad08d92 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 @@ -50,7 +50,6 @@ import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin private val ITEM_MAX_WIDTH = 320.dp -private val ITEM_HEIGHT = 150.dp // TODO thomashortadev do we want SP instead of DP? to change based on font size) private const val ITEM_WIDTH_PERCENTAGE = 0.8f private const val CONTENT_MAX_LINES = 3 private const val TITLE_MAX_LINES = 2 @@ -76,7 +75,7 @@ fun ReaderTagsFeedPostListItem( modifier = Modifier .widthIn(max = ITEM_MAX_WIDTH) .width(screenWidth * ITEM_WIDTH_PERCENTAGE) - .height(ITEM_HEIGHT) + .height(READER_TAGS_FEED_ITEM_HEIGHT) ) { Row( modifier = Modifier.fillMaxWidth(), From 018c335085fcbff19cae6c2f873b1e316662634f Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 10 May 2024 20:16:06 -0300 Subject: [PATCH 152/237] Remove unused navigation events from ReaderTagsFeedFragment --- .../ui/reader/ReaderTagsFeedFragment.kt | 37 +------------------ 1 file changed, 1 insertion(+), 36 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 1b2bf0513c41..3adf14884eb0 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 @@ -210,49 +210,15 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme is ReaderNavigationEvents.SharePost -> ReaderActivityLauncher.sharePost(context, event.post) is ReaderNavigationEvents.OpenPost -> ReaderActivityLauncher.openPost(context, event.post) - is ReaderNavigationEvents.ShowReaderComments -> ReaderActivityLauncher.showReaderComments( - context, - event.blogId, - event.postId, - ThreadedCommentsActionSource.READER_POST_CARD.sourceDescription - ) - - is ReaderNavigationEvents.ShowNoSitesToReblog -> ReaderActivityLauncher.showNoSiteToReblog(activity) - is ReaderNavigationEvents.ShowSitePickerForResult -> ActivityLauncher.showSitePickerForResult( - this@ReaderTagsFeedFragment, - event.preselectedSite, - event.mode - ) - - is ReaderNavigationEvents.OpenEditorForReblog -> ActivityLauncher.openEditorForReblog( - activity, - event.site, - event.post, - event.source - ) - - is ReaderNavigationEvents.ShowBookmarkedTab -> ActivityLauncher.viewSavedPostsListInReader(activity) is ReaderNavigationEvents.ShowBookmarkedSavedOnlyLocallyDialog -> { showBookmarkSavedLocallyDialog(event) } - is ReaderNavigationEvents.ShowPostsByTag -> ReaderActivityLauncher.showReaderTagPreview( - context, - event.tag, - ReaderTracker.SOURCE_DISCOVER, - readerTracker - ) - - is ReaderNavigationEvents.ShowVideoViewer -> ReaderActivityLauncher.showReaderVideoViewer( - context, - event.videoUrl - ) - is ReaderNavigationEvents.ShowBlogPreview -> ReaderActivityLauncher.showReaderBlogOrFeedPreview( context, event.siteId, event.feedId, event.isFollowed, - ReaderTracker.SOURCE_DISCOVER, + ReaderTracker.SOURCE_TAGS_FEED, readerTracker ) @@ -268,7 +234,6 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme ReaderActivityLauncher.OpenUrlType.INTERNAL ) - is ReaderNavigationEvents.ShowReaderSubs -> ReaderActivityLauncher.showReaderSubs(context) else -> Unit // Do Nothing } } From ef3eab2c82c87dfc75ad6771e913d19073635f7d Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 10 May 2024 20:21:18 -0300 Subject: [PATCH 153/237] Update post loading skeleton --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 9 +- .../tagsfeed/ReaderTagsFeedComposeUtils.kt | 28 ++++ .../tagsfeed/ReaderTagsFeedPostListItem.kt | 53 ++++---- .../ReaderTagsFeedPostListItemLoading.kt | 120 +++++++++++------- 4 files changed, 131 insertions(+), 79 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedComposeUtils.kt 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 e64966ae6b06..9586475cefc3 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 @@ -62,9 +62,6 @@ 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 -val READER_TAGS_FEED_ITEM_HEIGHT = 150.dp // TODO thomashortadev do we want SP? to change based on font size) - @Composable fun ReaderTagsFeed(uiState: UiState) { Box( @@ -179,7 +176,7 @@ private fun Loading() { horizontalArrangement = Arrangement.spacedBy(Margin.Large.value), contentPadding = PaddingValues(horizontal = Margin.Large.value), ) { - items(LOADING_POSTS_COUNT) { + items(ReaderTagsFeedComposeUtils.LOADING_POSTS_COUNT) { ReaderTagsFeedPostListItemLoading() } } @@ -271,7 +268,7 @@ private fun PostListLoading() { end = Margin.Large.value ), ) { - items(LOADING_POSTS_COUNT) { + items(ReaderTagsFeedComposeUtils.LOADING_POSTS_COUNT) { ReaderTagsFeedPostListItemLoading() } } @@ -306,7 +303,7 @@ private fun PostListLoaded( ) Box( modifier = Modifier - .height(READER_TAGS_FEED_ITEM_HEIGHT) + .height(ReaderTagsFeedComposeUtils.POST_ITEM_HEIGHT) .padding( start = Margin.ExtraLarge.value, end = Margin.ExtraLarge.value, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedComposeUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedComposeUtils.kt new file mode 100644 index 000000000000..9574d9cef17e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedComposeUtils.kt @@ -0,0 +1,28 @@ +package org.wordpress.android.ui.reader.views.compose.tagsfeed + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min + +object ReaderTagsFeedComposeUtils { + const val LOADING_POSTS_COUNT = 5 + + const val POST_ITEM_TITLE_MAX_LINES = 2 + val POST_ITEM_HEIGHT = 150.dp // TODO thomashortadev do we want SP? to change based on font size) + val POST_ITEM_IMAGE_SIZE = 64.dp + private val POST_ITEM_MAX_WIDTH = 320.dp + private const val POST_ITEM_WIDTH_PERCENTAGE = 0.8f + + val PostItemWidth: Dp + @Composable + get() { + val localConfiguration = LocalConfiguration.current + val screenWidth = remember(localConfiguration) { + localConfiguration.screenWidthDp.dp + } + return min((screenWidth * POST_ITEM_WIDTH_PERCENTAGE), POST_ITEM_MAX_WIDTH) + } +} 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 3a04bad08d92..d73690b703cb 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 @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -35,7 +34,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -49,10 +47,7 @@ import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin -private val ITEM_MAX_WIDTH = 320.dp -private const val ITEM_WIDTH_PERCENTAGE = 0.8f -private const val CONTENT_MAX_LINES = 3 -private const val TITLE_MAX_LINES = 2 +private const val CONTENT_TOTAL_LINES = 3 @Composable fun ReaderTagsFeedPostListItem( @@ -66,16 +61,10 @@ fun ReaderTagsFeedPostListItem( alpha = 0.6F ) - val localConfiguration = LocalConfiguration.current - val screenWidth = remember(localConfiguration) { - localConfiguration.screenWidthDp.dp - } - Column( modifier = Modifier - .widthIn(max = ITEM_MAX_WIDTH) - .width(screenWidth * ITEM_WIDTH_PERCENTAGE) - .height(READER_TAGS_FEED_ITEM_HEIGHT) + .width(ReaderTagsFeedComposeUtils.PostItemWidth) + .height(ReaderTagsFeedComposeUtils.POST_ITEM_HEIGHT) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -124,6 +113,7 @@ fun ReaderTagsFeedPostListItem( // Post title and excerpt Column Column( modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(Margin.Medium.value), ) { // TODO thomashortadev improve this to avoid an initial composition with the wrong value var excerptMaxLines by remember { mutableIntStateOf(2) } @@ -139,13 +129,13 @@ fun ReaderTagsFeedPostListItem( text = postTitle, style = MaterialTheme.typography.titleMedium, color = baseColor, - maxLines = TITLE_MAX_LINES, + maxLines = ReaderTagsFeedComposeUtils.POST_ITEM_TITLE_MAX_LINES, overflow = TextOverflow.Ellipsis, onTextLayout = { layoutResult -> - excerptMaxLines = CONTENT_MAX_LINES - layoutResult.lineCount + excerptMaxLines = CONTENT_TOTAL_LINES - layoutResult.lineCount }, ) - Spacer(Modifier.height(Margin.Medium.value)) + // Post excerpt Text( modifier = Modifier @@ -173,6 +163,7 @@ fun ReaderTagsFeedPostListItem( Spacer(Modifier.weight(1f)) + // Likes and comments row Row( modifier = Modifier .fillMaxWidth(), @@ -208,6 +199,7 @@ fun ReaderTagsFeedPostListItem( Spacer(Modifier.height(Margin.Small.value)) + // Actions row Row( modifier = Modifier .fillMaxWidth() @@ -277,7 +269,7 @@ fun PostImage( ) { AsyncImage( modifier = modifier - .size(64.dp) + .size(ReaderTagsFeedComposeUtils.POST_ITEM_IMAGE_SIZE) .clip(RoundedCornerShape(corner = CornerSize(8.dp))) .clickable { onClick() }, model = ImageRequest.Builder(LocalContext.current) @@ -298,12 +290,12 @@ fun ReaderTagsFeedPostListItemPreview() { modifier = Modifier .fillMaxWidth() .fillMaxHeight() - .padding(top = 16.dp, bottom = 16.dp) ) { LazyRow( modifier = Modifier .fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 24.dp), + contentPadding = PaddingValues(24.dp), + horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), ) { item { ReaderTagsFeedPostListItem( @@ -340,7 +332,8 @@ fun ReaderTagsFeedPostListItemPreview() { onPostMoreMenuClick = {}, ) ) - Spacer(Modifier.width(24.dp)) + } + item { ReaderTagsFeedPostListItem( item = TagsFeedPostItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + @@ -375,7 +368,8 @@ fun ReaderTagsFeedPostListItemPreview() { onPostMoreMenuClick = {}, ) ) - Spacer(Modifier.width(24.dp)) + } + item { ReaderTagsFeedPostListItem( item = TagsFeedPostItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + @@ -396,7 +390,8 @@ fun ReaderTagsFeedPostListItemPreview() { onPostMoreMenuClick = {}, ) ) - Spacer(Modifier.width(24.dp)) + } + item { ReaderTagsFeedPostListItem( item = TagsFeedPostItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + @@ -417,7 +412,8 @@ fun ReaderTagsFeedPostListItemPreview() { onPostMoreMenuClick = {}, ) ) - Spacer(Modifier.width(24.dp)) + } + item { ReaderTagsFeedPostListItem( item = TagsFeedPostItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + @@ -439,7 +435,8 @@ fun ReaderTagsFeedPostListItemPreview() { onPostMoreMenuClick = {}, ) ) - Spacer(Modifier.width(24.dp)) + } + item { ReaderTagsFeedPostListItem( item = TagsFeedPostItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer" + @@ -461,7 +458,8 @@ fun ReaderTagsFeedPostListItemPreview() { onPostMoreMenuClick = {}, ) ) - Spacer(Modifier.width(24.dp)) + } + item { ReaderTagsFeedPostListItem( item = TagsFeedPostItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + @@ -495,7 +493,8 @@ fun ReaderTagsFeedPostListItemPreview() { onPostMoreMenuClick = {}, ) ) - Spacer(Modifier.width(24.dp)) + } + item { ReaderTagsFeedPostListItem( item = TagsFeedPostItem( siteName = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pellentesque" + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt index 5f58c3164ff9..2d87e5901bd9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt @@ -3,14 +3,16 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.RoundedCornerShape @@ -26,16 +28,17 @@ import org.wordpress.android.ui.compose.unit.Margin @Composable fun ReaderTagsFeedPostListItemLoading() { - val backgroundColor = if (isSystemInDarkTheme()) { + val contentColor = if (isSystemInDarkTheme()) { AppColor.White.copy(alpha = 0.12F) } else { AppColor.Black.copy(alpha = 0.08F) } Column( modifier = Modifier - .width(240.dp) - .height(340.dp), + .width(ReaderTagsFeedComposeUtils.PostItemWidth) + .height(ReaderTagsFeedComposeUtils.POST_ITEM_HEIGHT) ) { + // Site info placeholder Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -45,54 +48,83 @@ fun ReaderTagsFeedPostListItemLoading() { .width(99.dp) .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)) - .background(backgroundColor), + .background(contentColor), ) } + + Spacer(modifier = Modifier.weight(1f)) + + // Content row placeholder + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Margin.Medium.value), + verticalAlignment = Alignment.CenterVertically, + ) { + // Post title and excerpt Column placeholder + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(Margin.Medium.value), + ) { + // Title placeholder + Box( + modifier = Modifier + .fillMaxWidth(0.75f) + .height(18.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(contentColor), + ) + + // Excerpt placeholder + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(Margin.Small.value), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(contentColor), + ) + Box( + modifier = Modifier + .fillMaxWidth(0.9f) + .height(8.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(contentColor), + ) + } + } + + // Image placeholder + Box( + modifier = Modifier + .size(ReaderTagsFeedComposeUtils.POST_ITEM_IMAGE_SIZE) + .clip(shape = RoundedCornerShape(8.dp)) + .background(contentColor), + ) + } + + Spacer(Modifier.weight(1f)) + + // Likes and comments placeholder Box( modifier = Modifier - .padding(top = Margin.Large.value) - .width(204.dp) - .height(18.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(backgroundColor), - ) - Box( - modifier = Modifier - .padding(top = Margin.Large.value) - .width(140.dp) - .height(18.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(backgroundColor), - ) - Box( - modifier = Modifier - .padding(top = Margin.Large.value) - .fillMaxWidth() - .height(150.dp) - .clip(shape = RoundedCornerShape(8.dp)) - .background(backgroundColor), - ) - Box( - modifier = Modifier - .padding( - start = Margin.Small.value, - top = Margin.Large.value, - ) .width(170.dp) .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)) - .background(backgroundColor), + .background(contentColor), ) + + Spacer(Modifier.height(Margin.Medium.value)) + + // Actions placeholder Box( modifier = Modifier - .padding( - start = Margin.Small.value, - top = Margin.Large.value, - ) .width(170.dp) .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)) - .background(backgroundColor), + .background(contentColor), ) } } @@ -110,14 +142,10 @@ fun ReaderTagsFeedPostListItemLoadingPreview() { LazyRow( modifier = Modifier .fillMaxWidth(), + contentPadding = PaddingValues(24.dp), + horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), ) { - item { - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) - ReaderTagsFeedPostListItemLoading() - Spacer(Modifier.width(12.dp)) + items(5) { ReaderTagsFeedPostListItemLoading() } } From c12c8849aa8e067a85f83864643564fcc421278c Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 10 May 2024 21:41:18 -0300 Subject: [PATCH 154/237] Implement action for more from tag button --- .../ui/reader/ReaderTagsFeedFragment.kt | 13 ++++-- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 24 +++++++---- .../tagsfeed/ReaderTagsFeedViewModel.kt | 31 +++++++++----- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 13 +++--- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 40 ++++++++++++------- .../ReaderTagsFeedUiStateMapperTest.kt | 30 +++++++++----- 6 files changed, 100 insertions(+), 51 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 3adf14884eb0..451e08e858f1 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 @@ -19,12 +19,10 @@ import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.databinding.ReaderTagFeedFragmentLayoutBinding import org.wordpress.android.models.ReaderTag -import org.wordpress.android.ui.ActivityLauncher 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.adapters.ReaderMenuAdapter -import org.wordpress.android.ui.reader.comments.ThreadedCommentsActionSource import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider @@ -125,10 +123,19 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme private fun observeActionEvents() { viewModel.actionEvents.observe(viewLifecycleOwner) { when (it) { - is ActionEvent.OpenTagPostsFeed -> { + is ActionEvent.FilterTagPostsFeed -> { subFilterViewModel.setSubfilterFromTag(it.readerTag) } + is ActionEvent.OpenTagPostList -> { + ReaderActivityLauncher.showReaderTagPreview( + context, + it.readerTag, + ReaderTracker.SOURCE_TAGS_FEED, + readerTracker, + ) + } + ActionEvent.RefreshTagsFeed -> { subFilterViewModel.updateTagsAndSites() } 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 3a3f7eadac6d..8869fb386a6d 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 @@ -15,7 +15,8 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapLoadedTagFeedItem( tag: ReaderTag, posts: ReaderPostList, - onTagClick: (ReaderTag) -> Unit, + onTagChipClick: (ReaderTag) -> Unit, + onMoreFromTagClick: (ReaderTag) -> Unit, onSiteClick: (TagsFeedPostItem) -> Unit, onPostCardClick: (TagsFeedPostItem) -> Unit, onPostLikeClick: (TagsFeedPostItem) -> Unit, @@ -24,7 +25,8 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( ) = ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = tag, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, ), postList = ReaderTagsFeedViewModel.PostList.Loaded( posts.map { @@ -59,14 +61,16 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapErrorTagFeedItem( tag: ReaderTag, errorType: ReaderTagsFeedViewModel.ErrorType, - onTagClick: (ReaderTag) -> Unit, + onTagChipClick: (ReaderTag) -> Unit, + onMoreFromTagClick: (ReaderTag) -> Unit, onRetryClick: () -> Unit, onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit, ): ReaderTagsFeedViewModel.TagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = tag, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, ), postList = ReaderTagsFeedViewModel.PostList.Error( type = errorType, @@ -78,7 +82,8 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapInitialPostsUiState( tags: List, isRefreshing: Boolean, - onTagClick: (ReaderTag) -> Unit, + onTagChipClick: (ReaderTag) -> Unit, + onMoreFromTagClick: (ReaderTag) -> Unit, onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit, onRefresh: () -> Unit, ): ReaderTagsFeedViewModel.UiState.Loaded = @@ -87,7 +92,8 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = tag, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, ), postList = ReaderTagsFeedViewModel.PostList.Initial, onItemEnteredView = onItemEnteredView, @@ -99,13 +105,15 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( fun mapLoadingTagFeedItem( tag: ReaderTag, - onTagClick: (ReaderTag) -> Unit, + onTagChipClick: (ReaderTag) -> Unit, + onMoreFromTagClick: (ReaderTag) -> Unit, onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit, ): ReaderTagsFeedViewModel.TagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = tag, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, ), postList = ReaderTagsFeedViewModel.PostList.Loading, onItemEnteredView = onItemEnteredView, 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 74f2ea3d1e99..57cd43912962 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 @@ -103,7 +103,8 @@ class ReaderTagsFeedViewModel @Inject constructor( readerTagsFeedUiStateMapper.mapInitialPostsUiState( tags, false, - ::onTagClick, + ::onTagChipClick, + ::onMoreFromTagClick, ::onItemEnteredView, ::onRefresh ) @@ -137,7 +138,8 @@ class ReaderTagsFeedViewModel @Inject constructor( updateTagFeedItem( readerTagsFeedUiStateMapper.mapLoadingTagFeedItem( tag = tag, - onTagClick = ::onTagClick, + onTagChipClick = ::onTagChipClick, + onMoreFromTagClick = ::onMoreFromTagClick, onItemEnteredView = ::onItemEnteredView, ) ) @@ -149,7 +151,8 @@ class ReaderTagsFeedViewModel @Inject constructor( readerTagsFeedUiStateMapper.mapLoadedTagFeedItem( tag = tag, posts = posts, - onTagClick = ::onTagClick, + onTagChipClick = ::onTagChipClick, + onMoreFromTagClick = ::onMoreFromTagClick, onSiteClick = ::onSiteClick, onPostCardClick = ::onPostCardClick, onPostLikeClick = ::onPostLikeClick, @@ -160,7 +163,8 @@ class ReaderTagsFeedViewModel @Inject constructor( readerTagsFeedUiStateMapper.mapErrorTagFeedItem( tag = tag, errorType = ErrorType.NoContent, - onTagClick = ::onTagClick, + onTagChipClick = ::onTagChipClick, + onMoreFromTagClick = ::onMoreFromTagClick, onRetryClick = ::onRetryClick, onItemEnteredView = ::onItemEnteredView, ) @@ -169,7 +173,8 @@ class ReaderTagsFeedViewModel @Inject constructor( readerTagsFeedUiStateMapper.mapErrorTagFeedItem( tag = tag, errorType = ErrorType.Default, - onTagClick = ::onTagClick, + onTagChipClick = ::onTagChipClick, + onMoreFromTagClick = ::onMoreFromTagClick, onRetryClick = ::onRetryClick, onItemEnteredView = ::onItemEnteredView, ) @@ -229,8 +234,13 @@ class ReaderTagsFeedViewModel @Inject constructor( } @VisibleForTesting - fun onTagClick(readerTag: ReaderTag) { - _actionEvents.value = ActionEvent.OpenTagPostsFeed(readerTag) + fun onTagChipClick(readerTag: ReaderTag) { + _actionEvents.value = ActionEvent.FilterTagPostsFeed(readerTag) + } + + @VisibleForTesting + fun onMoreFromTagClick(readerTag: ReaderTag) { + _actionEvents.value = ActionEvent.OpenTagPostList(readerTag) } private fun onRetryClick() { @@ -435,7 +445,9 @@ class ReaderTagsFeedViewModel @Inject constructor( } sealed class ActionEvent { - data class OpenTagPostsFeed(val readerTag: ReaderTag) : ActionEvent() + data class FilterTagPostsFeed(val readerTag: ReaderTag) : ActionEvent() + + data class OpenTagPostList(val readerTag: ReaderTag) : ActionEvent() data object RefreshTagsFeed : ActionEvent() } @@ -466,7 +478,8 @@ class ReaderTagsFeedViewModel @Inject constructor( data class TagChip( val tag: ReaderTag, - val onTagClick: (ReaderTag) -> Unit, + val onTagChipClick: (ReaderTag) -> Unit, + val onMoreFromTagClick: (ReaderTag) -> Unit, ) sealed class PostList { 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 717743db9c86..7bde55ca54cc 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 @@ -124,7 +124,7 @@ private fun Loaded(uiState: UiState.Loaded) { start = Margin.Large.value, ), text = UiString.UiStringText(tagChip.tag.tagTitle), - onClick = { tagChip.onTagClick(tagChip.tag) }, + onClick = { tagChip.onTagChipClick(tagChip.tag) }, height = 36.dp, ) Spacer(modifier = Modifier.height(Margin.Large.value)) @@ -318,8 +318,7 @@ private fun PostListLoaded( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), onClick = { - tagChip.onTagClick(tagChip.tag) - AppLog.e(AppLog.T.READER, "RL-> Tag clicked") + tagChip.onMoreFromTagClick(tagChip.tag) } ), verticalArrangement = Arrangement.Center, @@ -556,19 +555,19 @@ fun ReaderTagsFeedLoaded() { uiState = UiState.Loaded( data = listOf( TagFeedItem( - tagChip = TagChip(readerTag, {}), + tagChip = TagChip(readerTag, {}, {}), postList = postListLoaded ), TagFeedItem( - tagChip = TagChip(readerTag, {}), + tagChip = TagChip(readerTag, {}, {}), postList = PostList.Initial, ), TagFeedItem( - tagChip = TagChip(readerTag, {}), + tagChip = TagChip(readerTag, {}, {}), postList = PostList.Error(ErrorType.Default, {}), ), TagFeedItem( - tagChip = TagChip(readerTag, {}), + tagChip = TagChip(readerTag, {}, {}), postList = PostList.Error(ErrorType.NoContent, {}), ), ) 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 4ebbb16ee179..4de9d4c46b69 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 @@ -252,14 +252,24 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } @Test - fun `Should emit OpenTagPostsFeed when onTagClick is called`() { + fun `Should emit OpenTagPostsFeed when onTagChipClick is called`() { // When - viewModel.onTagClick(tag) + viewModel.onTagChipClick(tag) // Then - assertIs(actionEvents.first()) + assertIs(actionEvents.first()) } + @Test + fun `Should emit OpenTagPostsFeed when onMoreFromTagClick is called`() { + // When + viewModel.onMoreFromTagClick(tag) + + // Then + assertIs(actionEvents.first()) + } + + @Test fun `Should emit ShowBlogPreview when onSiteClick is called`() = test { // Given @@ -372,10 +382,10 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { val loadedState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.Loaded assertThat(loadedState.data).isEqualTo( listOf( - getInitialTagFeedItem(tag1), - getInitialTagFeedItem(tag2) - ) + getInitialTagFeedItem(tag1), + getInitialTagFeedItem(tag2) ) + ) assertThat(loadedState.isRefreshing).isFalse() } @@ -558,7 +568,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } private fun mockMapInitialTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any(), any(), any(), any())) + whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any(), any(), any(), any(), any())) .thenAnswer { val tags = it.getArgument>(0) ReaderTagsFeedViewModel.UiState.Loaded( @@ -568,11 +578,11 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } private fun mockMapLoadingTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapLoadingTagFeedItem(any(), any(), any())) + whenever(readerTagsFeedUiStateMapper.mapLoadingTagFeedItem(any(), any(), any(), any())) .thenAnswer { val tag = it.getArgument(0) ReaderTagsFeedViewModel.TagFeedItem( - ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.TagChip(tag, {}, {}), ReaderTagsFeedViewModel.PostList.Loading ) } @@ -580,32 +590,34 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { private fun mockMapLoadedTagFeedItems(items: List = emptyList()) { whenever( - readerTagsFeedUiStateMapper.mapLoadedTagFeedItem(any(), any(), any(), any(), any(), any(), any(), any()) + readerTagsFeedUiStateMapper.mapLoadedTagFeedItem( + any(), any(), any(), any(), any(), any(), any(), any(), any() + ) ).thenAnswer { getLoadedTagFeedItem(it.getArgument(0), items) } } private fun mockMapErrorTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any(), any())) + whenever(readerTagsFeedUiStateMapper.mapErrorTagFeedItem(any(), any(), any(), any(), any(), any())) .thenAnswer { getErrorTagFeedItem(it.getArgument(0)) } } private fun getInitialTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem( - ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.TagChip(tag, {}, {}), ReaderTagsFeedViewModel.PostList.Initial ) private fun getLoadedTagFeedItem(tag: ReaderTag, items: List = emptyList()) = ReaderTagsFeedViewModel.TagFeedItem( - ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.TagChip(tag, {}, {}), ReaderTagsFeedViewModel.PostList.Loaded(items) ) private fun getErrorTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem( - ReaderTagsFeedViewModel.TagChip(tag, {}), + ReaderTagsFeedViewModel.TagChip(tag, {}, {}), ReaderTagsFeedViewModel.PostList.Error( ReaderTagsFeedViewModel.ErrorType.Default, {} ), diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index 24c03b7836cb..d6da0aae93d6 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -51,7 +51,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { "endpoint", ReaderTagType.FOLLOWED, ) - val onTagClick = { _: ReaderTag -> } + val onTagChipClick = { _: ReaderTag -> } + val onMoreFromTagClick = { _: ReaderTag -> } val onSiteClick: (TagsFeedPostItem) -> Unit = {} val onPostCardClick: (TagsFeedPostItem) -> Unit = {} val onPostLikeClick: (TagsFeedPostItem) -> Unit = {} @@ -75,7 +76,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val actual = classToTest.mapLoadedTagFeedItem( tag = readerTag, posts = postList, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, onSiteClick = onSiteClick, onPostCardClick = onPostCardClick, onPostLikeClick = onPostLikeClick, @@ -86,7 +88,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val expected = ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = readerTag, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, ), postList = ReaderTagsFeedViewModel.PostList.Loaded( listOf( @@ -125,14 +128,16 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { ReaderTagType.FOLLOWED, ) val errorType = ReaderTagsFeedViewModel.ErrorType.Default - val onTagClick: (ReaderTag) -> Unit = {} + val onTagChipClick: (ReaderTag) -> Unit = {} + val onMoreFromTagClick: (ReaderTag) -> Unit = {} val onRetryClick = {} val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} // When val actual = classToTest.mapErrorTagFeedItem( tag = readerTag, errorType = errorType, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, onRetryClick = onRetryClick, onItemEnteredView = onItemEnteredView, ) @@ -141,7 +146,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val expected = ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = readerTag, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, ), postList = ReaderTagsFeedViewModel.PostList.Error( type = errorType, @@ -155,7 +161,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { @Test fun `Should map loading posts UI state correctly`() { // Given - val onTagClick: (ReaderTag) -> Unit = {} + val onTagChipClick: (ReaderTag) -> Unit = {} + val onMoreFromTagClick: (ReaderTag) -> Unit = {} val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} val onRefresh: () -> Unit = {} val tag1 = ReaderTag( @@ -178,7 +185,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val actual = classToTest.mapInitialPostsUiState( tags = tags, isRefreshing = true, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, onItemEnteredView = onItemEnteredView, onRefresh = onRefresh, ) @@ -189,7 +197,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = tag1, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, ), postList = ReaderTagsFeedViewModel.PostList.Initial, onItemEnteredView = onItemEnteredView, @@ -197,7 +206,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = tag2, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, ), postList = ReaderTagsFeedViewModel.PostList.Initial, onItemEnteredView = onItemEnteredView, From bcf0c9f61943cde4a73ccbc5c6c553422ecc95e8 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 10 May 2024 21:43:14 -0300 Subject: [PATCH 155/237] Improve excerpt rendering and item height --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 2 +- .../tagsfeed/ReaderTagsFeedComposeUtils.kt | 12 +- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 194 +++++++++++------- .../ReaderTagsFeedPostListItemLoading.kt | 2 +- 4 files changed, 134 insertions(+), 76 deletions(-) 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 9586475cefc3..78fcb3fb70e5 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 @@ -303,7 +303,7 @@ private fun PostListLoaded( ) Box( modifier = Modifier - .height(ReaderTagsFeedComposeUtils.POST_ITEM_HEIGHT) + .height(ReaderTagsFeedComposeUtils.PostItemHeight) .padding( start = Margin.ExtraLarge.value, end = Margin.ExtraLarge.value, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedComposeUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedComposeUtils.kt index 9574d9cef17e..1549deb7ee34 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedComposeUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedComposeUtils.kt @@ -3,19 +3,29 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min +import androidx.compose.ui.unit.sp object ReaderTagsFeedComposeUtils { const val LOADING_POSTS_COUNT = 5 const val POST_ITEM_TITLE_MAX_LINES = 2 - val POST_ITEM_HEIGHT = 150.dp // TODO thomashortadev do we want SP? to change based on font size) val POST_ITEM_IMAGE_SIZE = 64.dp + private val POST_ITEM_HEIGHT = 150.sp // use SP to scale with text size, which is the main content of the item private val POST_ITEM_MAX_WIDTH = 320.dp private const val POST_ITEM_WIDTH_PERCENTAGE = 0.8f + val PostItemHeight: Dp + @Composable + get() { + with(LocalDensity.current) { + return POST_ITEM_HEIGHT.toDp() + } + } + val PostItemWidth: Dp @Composable get() { 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 d73690b703cb..1acd33470e5e 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 @@ -6,6 +6,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -26,19 +27,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest @@ -46,8 +48,10 @@ import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin +import kotlin.math.min -private const val CONTENT_TOTAL_LINES = 3 +private const val CONTENT_TOTAL_LINES_WITH_INTERACTIONS = 3 +private const val CONTENT_TOTAL_LINES_NO_INTERACTIONS = 4 @Composable fun ReaderTagsFeedPostListItem( @@ -61,10 +65,12 @@ fun ReaderTagsFeedPostListItem( alpha = 0.6F ) + val hasInteractions = postNumberOfLikesText.isNotBlank() || postNumberOfCommentsText.isNotBlank() + Column( modifier = Modifier .width(ReaderTagsFeedComposeUtils.PostItemWidth) - .height(ReaderTagsFeedComposeUtils.POST_ITEM_HEIGHT) + .height(ReaderTagsFeedComposeUtils.PostItemHeight) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -110,47 +116,16 @@ fun ReaderTagsFeedPostListItem( horizontalArrangement = Arrangement.spacedBy(Margin.Medium.value), verticalAlignment = Alignment.CenterVertically, ) { - // Post title and excerpt Column - Column( + // Post text content + PostTextContent( + title = postTitle, + excerpt = postExcerpt, + onClick = { onPostCardClick(item) }, + titleColor = baseColor, + excerptColor = primaryElementColor, + hasInteractions = hasInteractions, modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(Margin.Medium.value), - ) { - // TODO thomashortadev improve this to avoid an initial composition with the wrong value - var excerptMaxLines by remember { mutableIntStateOf(2) } - - // Post title - Text( - modifier = Modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { onPostCardClick(item) }, - ), - text = postTitle, - style = MaterialTheme.typography.titleMedium, - color = baseColor, - maxLines = ReaderTagsFeedComposeUtils.POST_ITEM_TITLE_MAX_LINES, - overflow = TextOverflow.Ellipsis, - onTextLayout = { layoutResult -> - excerptMaxLines = CONTENT_TOTAL_LINES - layoutResult.lineCount - }, - ) - - // Post excerpt - Text( - modifier = Modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { onPostCardClick(item) }, - ), - text = postExcerpt, - style = MaterialTheme.typography.bodySmall, - color = primaryElementColor, - maxLines = excerptMaxLines, // TODO thomashortadev max lines should be (3 - title_lines_used) - overflow = TextOverflow.Ellipsis, - ) - } + ) // Post image if (postImageUrl.isNotBlank()) { @@ -164,46 +139,49 @@ fun ReaderTagsFeedPostListItem( Spacer(Modifier.weight(1f)) // Likes and comments row - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - // Number of likes - Text( - text = postNumberOfLikesText, - style = MaterialTheme.typography.bodyMedium, - color = secondaryElementColor, - maxLines = 1, - ) - Spacer(Modifier.height(Margin.Medium.value)) - // "•" separator. We should only show it if likes *and* comments text is not empty. - if (postNumberOfLikesText.isNotBlank() && postNumberOfCommentsText.isNotBlank()) { + if (hasInteractions) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + // Number of likes Text( - modifier = Modifier.padding( - horizontal = Margin.Small.value - ), - text = "•", + text = postNumberOfLikesText, + style = MaterialTheme.typography.bodyMedium, + color = secondaryElementColor, + maxLines = 1, + ) + Spacer(Modifier.height(Margin.Medium.value)) + // "•" separator. We should only show it if likes *and* comments text is not empty. + if (postNumberOfLikesText.isNotBlank() && postNumberOfCommentsText.isNotBlank()) { + Text( + modifier = Modifier.padding( + horizontal = Margin.Small.value + ), + text = "•", + style = MaterialTheme.typography.bodyMedium, + color = secondaryElementColor, + ) + } + // Number of comments + Text( + text = postNumberOfCommentsText, style = MaterialTheme.typography.bodyMedium, color = secondaryElementColor, + maxLines = 1, ) } - // Number of comments - Text( - text = postNumberOfCommentsText, - style = MaterialTheme.typography.bodyMedium, - color = secondaryElementColor, - maxLines = 1, - ) - } - Spacer(Modifier.height(Margin.Small.value)) + Spacer(Modifier.height(Margin.Small.value)) + } // Actions row Row( modifier = Modifier .fillMaxWidth() .height(24.dp), + verticalAlignment = Alignment.CenterVertically, ) { // Like action TextButton( @@ -281,6 +259,76 @@ fun PostImage( ) } +// Post title and excerpt Column +@Composable +fun PostTextContent( + title: String, + excerpt: String, + titleColor: Color, + excerptColor: Color, + hasInteractions: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val totalLines = if (hasInteractions) { + CONTENT_TOTAL_LINES_WITH_INTERACTIONS + } else { + CONTENT_TOTAL_LINES_NO_INTERACTIONS + } + + BoxWithConstraints( + modifier = modifier, + ) { + val density = LocalDensity.current + val maxWidthPx = with(density) { + maxWidth.toPx().toInt() + } + + val textMeasurer = rememberTextMeasurer() + val textLayoutResult = textMeasurer.measure( + text = title, + style = MaterialTheme.typography.titleMedium, + constraints = Constraints(maxWidth = maxWidthPx), + ) + val titleLines = min(textLayoutResult.lineCount, ReaderTagsFeedComposeUtils.POST_ITEM_TITLE_MAX_LINES) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(Margin.Medium.value), + ) { + // Post title + Text( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick, + ), + text = title, + style = MaterialTheme.typography.titleMedium, + color = titleColor, + maxLines = ReaderTagsFeedComposeUtils.POST_ITEM_TITLE_MAX_LINES, + overflow = TextOverflow.Ellipsis, + ) + + // Post excerpt + Text( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick, + ), + text = excerpt, + style = MaterialTheme.typography.bodySmall, + color = excerptColor, + maxLines = totalLines - titleLines, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt index 2d87e5901bd9..13996f66e30f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt @@ -36,7 +36,7 @@ fun ReaderTagsFeedPostListItemLoading() { Column( modifier = Modifier .width(ReaderTagsFeedComposeUtils.PostItemWidth) - .height(ReaderTagsFeedComposeUtils.POST_ITEM_HEIGHT) + .height(ReaderTagsFeedComposeUtils.PostItemHeight) ) { // Site info placeholder Row( From 4af884238647e73afd2e6653df2e531743a812ad Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 10 May 2024 23:51:29 -0300 Subject: [PATCH 156/237] Implement tags feed empty state button action --- .../android/ui/reader/ReaderTagsFeedFragment.kt | 13 +++++++++++++ .../viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt | 7 +++++-- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 13 +++++++++++-- 3 files changed, 29 insertions(+), 4 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 451e08e858f1..b450b9b3b7d1 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 @@ -24,6 +24,7 @@ import org.wordpress.android.ui.compose.theme.AppThemeWithoutBackground import org.wordpress.android.ui.main.WPMainActivity import org.wordpress.android.ui.reader.adapters.ReaderMenuAdapter import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsFragment import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel import org.wordpress.android.ui.reader.subfilter.SubFilterViewModelProvider import org.wordpress.android.ui.reader.subfilter.SubfilterListItem @@ -139,6 +140,17 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme ActionEvent.RefreshTagsFeed -> { subFilterViewModel.updateTagsAndSites() } + + ActionEvent.ShowTagsList -> { + val readerInterestsFragment = childFragmentManager.findFragmentByTag(ReaderInterestsFragment.TAG) + if (readerInterestsFragment == null) { + (parentFragment as? ReaderFragment)?.childFragmentManager?.beginTransaction()?.replace( + R.id.interests_fragment_container, + ReaderInterestsFragment(), + ReaderInterestsFragment.TAG + )?.commitNow() + } + } } } } @@ -220,6 +232,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme is ReaderNavigationEvents.ShowBookmarkedSavedOnlyLocallyDialog -> { showBookmarkSavedLocallyDialog(event) } + is ReaderNavigationEvents.ShowBlogPreview -> ReaderActivityLauncher.showReaderBlogOrFeedPreview( context, event.siteId, 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 57cd43912962..d2202664976d 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 @@ -229,8 +229,9 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun onOpenTagsListClick() { - // TODO + @VisibleForTesting + fun onOpenTagsListClick() { + _actionEvents.value = ActionEvent.ShowTagsList } @VisibleForTesting @@ -450,6 +451,8 @@ class ReaderTagsFeedViewModel @Inject constructor( data class OpenTagPostList(val readerTag: ReaderTag) : ActionEvent() data object RefreshTagsFeed : ActionEvent() + + data object ShowTagsList : ActionEvent() } sealed class UiState { 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 4de9d4c46b69..05fec0dacb34 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 @@ -252,7 +252,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } @Test - fun `Should emit OpenTagPostsFeed when onTagChipClick is called`() { + fun `Should emit FilterTagPostsFeed when onTagChipClick is called`() { // When viewModel.onTagChipClick(tag) @@ -261,7 +261,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } @Test - fun `Should emit OpenTagPostsFeed when onMoreFromTagClick is called`() { + fun `Should emit OpenTagPostList when onMoreFromTagClick is called`() { // When viewModel.onMoreFromTagClick(tag) @@ -269,6 +269,14 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertIs(actionEvents.first()) } + @Test + fun `Should emit ShowTagsList when onOpenTagsListClick is called`() { + // When + viewModel.onOpenTagsListClick() + + // Then + assertIs(actionEvents.first()) + } @Test fun `Should emit ShowBlogPreview when onSiteClick is called`() = test { @@ -480,6 +488,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { val action = viewModel.actionEvents.getOrAwaitValue() assertThat(action).isEqualTo(ActionEvent.RefreshTagsFeed) } + @Test fun `Should update UI immediately when like button is tapped`() = testCollectingUiStates { // Given From d03fe7e83735bdeae9f8072da192fdfb01a43e71 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Sat, 11 May 2024 01:49:46 -0300 Subject: [PATCH 157/237] Fix detekt --- .../reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapper.kt | 2 ++ .../android/ui/reader/views/compose/tagsfeed/ReaderTagsFeed.kt | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) 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 8869fb386a6d..eb1b31ac932a 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 @@ -58,6 +58,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( onItemEnteredView = onItemEnteredView, ) + @Suppress("LongParameterList") fun mapErrorTagFeedItem( tag: ReaderTag, errorType: ReaderTagsFeedViewModel.ErrorType, @@ -79,6 +80,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( onItemEnteredView = onItemEnteredView, ) + @Suppress("LongParameterList") fun mapInitialPostsUiState( tags: List, isRefreshing: Boolean, 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 7bde55ca54cc..d866c039dd4b 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 @@ -60,7 +60,6 @@ import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewMod import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.UiState 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 From 938505b68dc168fe5a36df6975ae9ebfe09198fc Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 13 May 2024 11:31:43 -0300 Subject: [PATCH 158/237] Apply PR suggestions --- .../ui/reader/ReaderTagsFeedFragment.kt | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 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 b450b9b3b7d1..be58939ceb43 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 @@ -269,19 +269,21 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme private fun observeSnackbarEvents() { viewModel.snackbarEvents.observeEvent(viewLifecycleOwner) { snackbarMessageHolder -> - activity?.findViewById(R.id.coordinator)?.let { coordinator -> - with(snackbarMessageHolder) { - val snackbar = WPSnackbar.make( - coordinator, - uiHelpers.getTextOfUiString(requireContext(), message), - Snackbar.LENGTH_LONG - ) - if (buttonTitle != null) { - snackbar.setAction(uiHelpers.getTextOfUiString(requireContext(), buttonTitle)) { - buttonAction.invoke() + if (isAdded) { + activity?.findViewById(R.id.coordinator)?.let { coordinator -> + with(snackbarMessageHolder) { + val snackbar = WPSnackbar.make( + coordinator, + uiHelpers.getTextOfUiString(requireContext(), message), + Snackbar.LENGTH_LONG + ) + if (buttonTitle != null) { + snackbar.setAction(uiHelpers.getTextOfUiString(requireContext(), buttonTitle)) { + buttonAction.invoke() + } } + snackbar.show() } - snackbar.show() } } } @@ -293,20 +295,22 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme val blogId = readerCardUiState.blogId val postId = readerCardUiState.postId val anchorView = binding.composeView.findViewWithTag("$blogId$postId") - readerTracker.track(AnalyticsTracker.Stat.POST_CARD_MORE_TAPPED) - val listPopup = ListPopupWindow(anchorView.context) - listPopup.width = anchorView.context.resources.getDimensionPixelSize(R.dimen.menu_item_width) - listPopup.setAdapter(ReaderMenuAdapter(anchorView.context, uiHelpers, it.readerPostCardActions)) - listPopup.setDropDownGravity(Gravity.END) - listPopup.anchorView = anchorView - listPopup.isModal = true - listPopup.setOnItemClickListener { _, _, position, _ -> - listPopup.dismiss() - val item = it.readerPostCardActions[position] - item.onClicked?.invoke(postId, blogId, item.type) + if (anchorView != null) { + readerTracker.track(AnalyticsTracker.Stat.POST_CARD_MORE_TAPPED) + val listPopup = ListPopupWindow(anchorView.context) + listPopup.width = anchorView.context.resources.getDimensionPixelSize(R.dimen.menu_item_width) + listPopup.setAdapter(ReaderMenuAdapter(anchorView.context, uiHelpers, it.readerPostCardActions)) + listPopup.setDropDownGravity(Gravity.END) + listPopup.anchorView = anchorView + listPopup.isModal = true + listPopup.setOnItemClickListener { _, _, position, _ -> + listPopup.dismiss() + val item = it.readerPostCardActions[position] + item.onClicked?.invoke(postId, blogId, item.type) + } + listPopup.setOnDismissListener { readerCardUiState.onMoreDismissed.invoke(readerCardUiState) } + listPopup.show() } - listPopup.setOnDismissListener { readerCardUiState.onMoreDismissed.invoke(readerCardUiState) } - listPopup.show() } } From 8967d4cc60937df549b62edcdd6bfe1c22801ce9 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 13 May 2024 14:51:22 -0300 Subject: [PATCH 159/237] Remove reblog todo --- .../ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt | 5 ----- 1 file changed, 5 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 d2202664976d..8c0455a08be8 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 @@ -113,11 +113,6 @@ class ReaderTagsFeedViewModel @Inject constructor( private fun initNavigationEvents() { _navigationEvents.addSource(readerPostCardActionsHandler.navigationEvents) { event -> - // TODO reblog supported in this screen? See ReaderPostDetailViewModel and ReaderDiscoverViewModel -// val target = event.peekContent() -// if (target is ReaderNavigationEvents.ShowSitePickerForResult) { -// pendingReblogPost = target.post -// } _navigationEvents.value = event } } From 919df05606529687851a2983aa3d741806be9520 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 13 May 2024 16:54:48 -0300 Subject: [PATCH 160/237] Improve number of title + excerpt lines --- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 53 ++++++++++----- .../ReaderTagsFeedPostListItemLoading.kt | 64 +++++++++++++------ 2 files changed, 83 insertions(+), 34 deletions(-) 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 1acd33470e5e..d8ed691ff05e 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 @@ -48,10 +48,9 @@ import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin -import kotlin.math.min -private const val CONTENT_TOTAL_LINES_WITH_INTERACTIONS = 3 -private const val CONTENT_TOTAL_LINES_NO_INTERACTIONS = 4 +private const val CONTENT_TOTAL_LINES_WITH_INTERACTIONS = 4 +private const val CONTENT_TOTAL_LINES_NO_INTERACTIONS = 5 @Composable fun ReaderTagsFeedPostListItem( @@ -112,7 +111,9 @@ fun ReaderTagsFeedPostListItem( // Post content row Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .weight(1f), horizontalArrangement = Arrangement.spacedBy(Margin.Medium.value), verticalAlignment = Alignment.CenterVertically, ) { @@ -124,7 +125,9 @@ fun ReaderTagsFeedPostListItem( titleColor = baseColor, excerptColor = primaryElementColor, hasInteractions = hasInteractions, - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .fillMaxHeight(), ) // Post image @@ -136,8 +139,6 @@ fun ReaderTagsFeedPostListItem( } } - Spacer(Modifier.weight(1f)) - // Likes and comments row if (hasInteractions) { Row( @@ -270,10 +271,12 @@ fun PostTextContent( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val totalLines = if (hasInteractions) { - CONTENT_TOTAL_LINES_WITH_INTERACTIONS - } else { - CONTENT_TOTAL_LINES_NO_INTERACTIONS + val totalLines = remember(hasInteractions) { + if (hasInteractions) { + CONTENT_TOTAL_LINES_WITH_INTERACTIONS + } else { + CONTENT_TOTAL_LINES_NO_INTERACTIONS + } } BoxWithConstraints( @@ -285,16 +288,28 @@ fun PostTextContent( } val textMeasurer = rememberTextMeasurer() - val textLayoutResult = textMeasurer.measure( + val titleLayoutResult = textMeasurer.measure( text = title, style = MaterialTheme.typography.titleMedium, + maxLines = ReaderTagsFeedComposeUtils.POST_ITEM_TITLE_MAX_LINES, + overflow = TextOverflow.Ellipsis, constraints = Constraints(maxWidth = maxWidthPx), ) - val titleLines = min(textLayoutResult.lineCount, ReaderTagsFeedComposeUtils.POST_ITEM_TITLE_MAX_LINES) + val titleLines = titleLayoutResult.lineCount + + // Check if excerpt is ellipsized + val excerptMaxLines = totalLines - titleLines + val excerptLayoutResult = textMeasurer.measure( + text = excerpt, + style = MaterialTheme.typography.bodySmall, + maxLines = excerptMaxLines, + overflow = TextOverflow.Ellipsis, + constraints = Constraints(maxWidth = maxWidthPx), + ) + val isExcerptFullHeight = excerptLayoutResult.lineCount == excerptMaxLines Column( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(Margin.Medium.value), ) { // Post title Text( @@ -311,6 +326,10 @@ fun PostTextContent( overflow = TextOverflow.Ellipsis, ) + Spacer( + if (isExcerptFullHeight) Modifier.weight(1f) else Modifier.height(Margin.Medium.value) + ) + // Post excerpt Text( modifier = Modifier @@ -322,9 +341,13 @@ fun PostTextContent( text = excerpt, style = MaterialTheme.typography.bodySmall, color = excerptColor, - maxLines = totalLines - titleLines, + maxLines = excerptMaxLines, overflow = TextOverflow.Ellipsis, ) + + if (isExcerptFullHeight) { + Spacer(Modifier.weight(1f)) + } } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt index 13996f66e30f..208d1f0efa21 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt @@ -36,11 +36,14 @@ fun ReaderTagsFeedPostListItemLoading() { Column( modifier = Modifier .width(ReaderTagsFeedComposeUtils.PostItemWidth) - .height(ReaderTagsFeedComposeUtils.PostItemHeight) + .height(ReaderTagsFeedComposeUtils.PostItemHeight), + verticalArrangement = Arrangement.SpaceBetween, ) { // Site info placeholder Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .height(24.dp), verticalAlignment = Alignment.CenterVertically, ) { Box( @@ -52,28 +55,39 @@ fun ReaderTagsFeedPostListItemLoading() { ) } - Spacer(modifier = Modifier.weight(1f)) - // Content row placeholder Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Margin.Medium.value), verticalAlignment = Alignment.CenterVertically, ) { // Post title and excerpt Column placeholder Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(Margin.Medium.value), + modifier = Modifier + .weight(1f), ) { // Title placeholder Box( modifier = Modifier - .fillMaxWidth(0.75f) + .fillMaxWidth(0.9f) .height(18.dp) .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), ) + Spacer(modifier = Modifier.height(Margin.Medium.value)) + + Box( + modifier = Modifier + .fillMaxWidth(0.8f) + .height(18.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(contentColor), + ) + + Spacer(modifier = Modifier.height(Margin.Medium.value)) + // Excerpt placeholder Column( modifier = Modifier.fillMaxWidth(), @@ -93,6 +107,20 @@ fun ReaderTagsFeedPostListItemLoading() { .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), ) + Box( + modifier = Modifier + .fillMaxWidth(0.85f) + .height(8.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(contentColor), + ) + Box( + modifier = Modifier + .fillMaxWidth(0.8f) + .height(8.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(contentColor), + ) } } @@ -105,24 +133,22 @@ fun ReaderTagsFeedPostListItemLoading() { ) } - Spacer(Modifier.weight(1f)) - // Likes and comments placeholder - Box( - modifier = Modifier - .width(170.dp) - .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(contentColor), - ) +// Box( +// modifier = Modifier +// .width(170.dp) +// .height(12.dp) +// .clip(shape = RoundedCornerShape(16.dp)) +// .background(contentColor), +// ) - Spacer(Modifier.height(Margin.Medium.value)) +// Spacer(Modifier.height(Margin.Medium.value)) // Actions placeholder Box( modifier = Modifier .width(170.dp) - .height(8.dp) + .height(16.dp) .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), ) From 3b0e0ae62486ceb82ea13c9dad9aadaee7072c39 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 13 May 2024 20:29:38 -0300 Subject: [PATCH 161/237] Workaround to update tags feed after changing subscribed status from tag posts list --- .../ui/reader/ReaderActivityLauncher.java | 12 ++++- .../ui/reader/ReaderPostListFragment.java | 7 +++ .../ui/reader/ReaderTagsFeedFragment.kt | 53 ++++++++++++++++--- .../tagsfeed/ReaderTagsFeedViewModel.kt | 14 +++-- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 37 ++++++++----- 5 files changed, 95 insertions(+), 28 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java index 32b2261e2527..d8167a10c791 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java @@ -175,11 +175,19 @@ public static void showReaderTagPreview(Context context, @NonNull ReaderTag tag, tag.getTagSlug(), source ); - Intent intent = new Intent(context, ReaderPostListActivity.class); + final Intent intent = createReaderTagPreviewIntent(context, tag, source); + context.startActivity(intent); + } + + @NonNull + public static Intent createReaderTagPreviewIntent(@NonNull final Context context, + @NonNull final ReaderTag tag, + @NonNull final String source) { + final Intent intent = new Intent(context, ReaderPostListActivity.class); intent.putExtra(ReaderConstants.ARG_SOURCE, source); intent.putExtra(ReaderConstants.ARG_TAG, tag); intent.putExtra(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.TAG_PREVIEW); - context.startActivity(intent); + return intent; } public static void showReaderSearch(Context context) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index 822a2aa2f2be..98665420c1e2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -779,6 +779,13 @@ public void onAttach(@NonNull Context context) { } initReaderSubsActivityResultLauncher(); + + final Activity activity = getActivity(); + if (activity != null) { + final Intent intent = new Intent(); + intent.putExtra(ReaderTagsFeedFragment.RESULT_SHOULD_REFRESH_TAGS_FEED, true); + activity.setResult(Activity.RESULT_OK, intent); + } } private void initReaderSubsActivityResultLauncher() { 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 be58939ceb43..b96d71fef96b 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 @@ -1,8 +1,14 @@ package org.wordpress.android.ui.reader +import android.app.Activity +import android.content.Context +import android.content.Intent import android.os.Bundle import android.view.Gravity import android.view.View +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.ListPopupWindow import androidx.compose.runtime.collectAsState @@ -17,6 +23,7 @@ import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.databinding.ReaderTagFeedFragmentLayoutBinding import org.wordpress.android.models.ReaderTag import org.wordpress.android.ui.ViewPagerFragment @@ -76,6 +83,8 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme private var bookmarksSavedLocallyDialog: AlertDialog? = null + private var readerPostListActivityResultLauncher: ActivityResultLauncher? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = ReaderTagFeedFragmentLayoutBinding.bind(view) @@ -99,6 +108,11 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme bookmarksSavedLocallyDialog?.dismiss() } + override fun onAttach(context: Context) { + super.onAttach(context) + initReaderPostListActivityResultLauncher() + } + private fun observeSubFilterViewModel(savedInstanceState: Bundle?) { subFilterViewModel = SubFilterViewModelProvider.getSubFilterViewModelForTag( this, @@ -109,7 +123,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme // TODO not triggered when there's no internet, so the error/no connection UI is not shown. subFilterViewModel.subFilters.observe(viewLifecycleOwner) { subFilters -> val tags = subFilters.filterIsInstance().map { it.tag } - viewModel.start(tags) + viewModel.onTagsChanged(tags) } subFilterViewModel.currentSubFilter.observe(viewLifecycleOwner) { subFilter -> @@ -129,15 +143,22 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } is ActionEvent.OpenTagPostList -> { - ReaderActivityLauncher.showReaderTagPreview( - context, - it.readerTag, - ReaderTracker.SOURCE_TAGS_FEED, - readerTracker, + if (!isAdded) { + return@observe + } + readerTracker.trackTag( + Stat.READER_TAG_PREVIEWED, + it.readerTag.tagSlug, + ReaderTracker.SOURCE_TAGS_FEED + ) + readerPostListActivityResultLauncher?.launch( + ReaderActivityLauncher.createReaderTagPreviewIntent( + requireActivity(), it.readerTag, ReaderTracker.SOURCE_TAGS_FEED + ) ) } - ActionEvent.RefreshTagsFeed -> { + ActionEvent.RefreshTags -> { subFilterViewModel.updateTagsAndSites() } @@ -338,6 +359,22 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } } + private fun initReaderPostListActivityResultLauncher() { + readerPostListActivityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + val data = result.data + if (data != null) { + val shouldRefreshTagsFeed = data.getBooleanExtra(RESULT_SHOULD_REFRESH_TAGS_FEED, false) + if (shouldRefreshTagsFeed) { + viewModel.onBackFromTagDetails() + } + } + } + } + } + override fun getScrollableViewForUniqueIdProvision(): View { return binding.composeView } @@ -347,6 +384,8 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme } companion object { + const val RESULT_SHOULD_REFRESH_TAGS_FEED = "RESULT_SHOULD_REFRESH_TAGS_FEED" + private const val ARG_TAGS_FEED_TAG = "tags_feed_tag" private const val POST_LIST_FADE_DURATION = 250L private const val POST_LIST_FADE_IN_DELAY = 300L 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 8c0455a08be8..3e01330a1114 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 @@ -78,8 +78,8 @@ class ReaderTagsFeedViewModel @Inject constructor( * [uiStateFlow] are expected when calling this method for each tag, since each can go through the following * [UiState]s: [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty]. */ - fun start(tags: List) { - // don't start again if the tags match, unless the user requested a refresh + fun onTagsChanged(tags: List) { + // don't fetch tags 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 @@ -98,7 +98,7 @@ class ReaderTagsFeedViewModel @Inject constructor( initSnackbarEvents() } - // Initially add all tags to the list with the posts loading UI + // Add tags to the list with the posts loading UI _uiStateFlow.update { readerTagsFeedUiStateMapper.mapInitialPostsUiState( tags, @@ -209,7 +209,11 @@ class ReaderTagsFeedViewModel @Inject constructor( _uiStateFlow.update { (it as? UiState.Loaded)?.copy(isRefreshing = true) ?: it } - _actionEvents.value = ActionEvent.RefreshTagsFeed + _actionEvents.value = ActionEvent.RefreshTags + } + + fun onBackFromTagDetails() { + _actionEvents.value = ActionEvent.RefreshTags } @VisibleForTesting @@ -445,7 +449,7 @@ class ReaderTagsFeedViewModel @Inject constructor( data class OpenTagPostList(val readerTag: ReaderTag) : ActionEvent() - data object RefreshTagsFeed : ActionEvent() + data object RefreshTags : ActionEvent() data object ShowTagsList : ActionEvent() } 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 05fec0dacb34..637e216049b5 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 @@ -126,7 +126,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { mockMapLoadedTagFeedItems() // When - viewModel.start(listOf(tag)) + viewModel.onTagsChanged(listOf(tag)) advanceUntilIdle() viewModel.onItemEnteredView(getInitialTagFeedItem(tag)) advanceUntilIdle() @@ -153,7 +153,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { mockMapErrorTagFeedItems() // When - viewModel.start(listOf(tag)) + viewModel.onTagsChanged(listOf(tag)) advanceUntilIdle() viewModel.onItemEnteredView(getInitialTagFeedItem(tag)) advanceUntilIdle() @@ -191,7 +191,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { mockMapLoadedTagFeedItems() // When - viewModel.start(listOf(tag1, tag2)) + viewModel.onTagsChanged(listOf(tag1, tag2)) advanceUntilIdle() viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) advanceUntilIdle() @@ -233,7 +233,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { mockMapErrorTagFeedItems() // When - viewModel.start(listOf(tag1, tag2)) + viewModel.onTagsChanged(listOf(tag1, tag2)) advanceUntilIdle() viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) advanceUntilIdle() @@ -332,7 +332,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { mockMapLoadedTagFeedItems() // When - viewModel.start(listOf(tag1, tag2)) + viewModel.onTagsChanged(listOf(tag1, tag2)) advanceUntilIdle() viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) advanceUntilIdle() @@ -342,7 +342,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { Mockito.clearInvocations(readerPostRepository) // Then - viewModel.start(listOf(tag1, tag2)) + viewModel.onTagsChanged(listOf(tag1, tag2)) advanceUntilIdle() assertThat(collectedUiStates).isEqualTo(firstCollectedStates) // still same states, nothing new emitted @@ -374,7 +374,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { mockMapLoadedTagFeedItems() // When - viewModel.start(listOf(tag1, tag2)) + viewModel.onTagsChanged(listOf(tag1, tag2)) advanceUntilIdle() viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) advanceUntilIdle() @@ -384,7 +384,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { viewModel.onRefresh() // Then - viewModel.start(listOf(tag1, tag2)) + viewModel.onTagsChanged(listOf(tag1, tag2)) advanceUntilIdle() val loadedState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.Loaded @@ -404,7 +404,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { val tags = emptyList() // When - viewModel.start(tags) + viewModel.onTagsChanged(tags) advanceUntilIdle() // Then @@ -436,7 +436,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { mockMapLoadedTagFeedItems() // When - viewModel.start(listOf(tag1, tag2)) + viewModel.onTagsChanged(listOf(tag1, tag2)) advanceUntilIdle() viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) advanceUntilIdle() @@ -475,7 +475,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { mockMapLoadedTagFeedItems() // When - viewModel.start(listOf(tag1, tag2)) + viewModel.onTagsChanged(listOf(tag1, tag2)) advanceUntilIdle() viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) advanceUntilIdle() @@ -486,7 +486,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { viewModel.onRefresh() val action = viewModel.actionEvents.getOrAwaitValue() - assertThat(action).isEqualTo(ActionEvent.RefreshTagsFeed) + assertThat(action).isEqualTo(ActionEvent.RefreshTags) } @Test @@ -521,7 +521,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } // When - viewModel.start(listOf(tag)) + viewModel.onTagsChanged(listOf(tag)) advanceUntilIdle() viewModel.onItemEnteredView(getInitialTagFeedItem(tag)) advanceUntilIdle() @@ -568,7 +568,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { .thenReturn(flowOf()) // When - viewModel.start(listOf(tag)) + viewModel.onTagsChanged(listOf(tag)) advanceUntilIdle() viewModel.onPostLikeClick(tagsFeedPostItem) @@ -576,6 +576,15 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { verify(postLikeUseCase).perform(any(), any(), any()) } + @Test + fun `Should emit RefreshTags when onBackFromTagDetails is called`() { + // When + viewModel.onBackFromTagDetails() + + // Then + assertIs(actionEvents.first()) + } + private fun mockMapInitialTagFeedItems() { whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any(), any(), any(), any(), any())) .thenAnswer { From 84d60fc1bdda8de719f65ae072a5df1f76e9d346 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 14 May 2024 15:12:58 -0300 Subject: [PATCH 162/237] Add blog name fallback in case of blank names --- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 29 +++--- .../ReaderTagsFeedUiStateMapperTest.kt | 94 +++++++++++++++++++ 2 files changed, 110 insertions(+), 13 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 8088fffd39cd..ef834f5fe610 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 @@ -5,11 +5,13 @@ import org.wordpress.android.models.ReaderTag import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.util.DateTimeUtilsWrapper +import org.wordpress.android.util.UrlUtilsWrapper import javax.inject.Inject class ReaderTagsFeedUiStateMapper @Inject constructor( private val dateTimeUtilsWrapper: DateTimeUtilsWrapper, private val readerUtilsWrapper: ReaderUtilsWrapper, + private val urlUtilsWrapper: UrlUtilsWrapper, ) { @Suppress("LongParameterList") fun mapLoadedTagFeedItem( @@ -27,25 +29,26 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( onTagClick = onTagClick, ), postList = ReaderTagsFeedViewModel.PostList.Loaded( - posts.map { + posts.map { post -> TagsFeedPostItem( - siteName = it.blogName, + siteName = post.blogName.takeIf { it.isNotBlank() } + ?: post.blogUrl.let { urlUtilsWrapper.removeScheme(it) }, postDateLine = dateTimeUtilsWrapper.javaDateToTimeSpan( - it.getDisplayDate(dateTimeUtilsWrapper) + post.getDisplayDate(dateTimeUtilsWrapper) ), - postTitle = it.title, - postExcerpt = it.excerpt, - postImageUrl = it.featuredImage, - postNumberOfLikesText = if (it.numLikes > 0) readerUtilsWrapper.getShortLikeLabelText( - numLikes = it.numLikes + postTitle = post.title, + postExcerpt = post.excerpt, + postImageUrl = post.featuredImage, + postNumberOfLikesText = if (post.numLikes > 0) readerUtilsWrapper.getShortLikeLabelText( + numLikes = post.numLikes ) else "", - postNumberOfCommentsText = if (it.numReplies > 0) readerUtilsWrapper.getShortCommentLabelText( - numComments = it.numReplies + postNumberOfCommentsText = if (post.numReplies > 0) readerUtilsWrapper.getShortCommentLabelText( + numComments = post.numReplies ) else "", - isPostLiked = it.isLikedByCurrentUser, + isPostLiked = post.isLikedByCurrentUser, isLikeButtonEnabled = true, - postId = it.postId, - blogId = it.blogId, + postId = post.postId, + blogId = post.blogId, onSiteClick = onSiteClick, onPostCardClick = onPostCardClick, onPostLikeClick = onPostLikeClick, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index 49f11f698f2e..b22842c9b933 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -14,6 +14,7 @@ import org.wordpress.android.models.ReaderTagType import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.util.DateTimeUtilsWrapper +import org.wordpress.android.util.UrlUtilsWrapper import java.util.Date @OptIn(ExperimentalCoroutinesApi::class) @@ -22,9 +23,12 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { private val readerUtilsWrapper = mock() + private val urlUtilsWrapper = mock() + private val classToTest: ReaderTagsFeedUiStateMapper = ReaderTagsFeedUiStateMapper( dateTimeUtilsWrapper = dateTimeUtilsWrapper, readerUtilsWrapper = readerUtilsWrapper, + urlUtilsWrapper = urlUtilsWrapper, ) @Suppress("LongMethod") @@ -114,6 +118,96 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { assertEquals(expected, actual) } + @Suppress("LongMethod") + @Test + fun `Should map loaded TagFeedItem correctly with blank blog name`() { + // Given + val readerPost = ReaderPost().apply { + blogName = "" + blogUrl = "https://blogurl.wordpress.com" + title = "Title" + excerpt = "Excerpt" + featuredImage = "url" + numLikes = 5 + numReplies = 10 + isLikedByCurrentUser = true + datePublished = "" + } + val postList = ReaderPostList().apply { + add(readerPost) + } + val readerTag = ReaderTag( + "tag", + "tag", + "tag", + "endpoint", + ReaderTagType.FOLLOWED, + ) + val onTagClick = { _: ReaderTag -> } + val onSiteClick: (TagsFeedPostItem) -> Unit = {} + val onPostCardClick: (TagsFeedPostItem) -> Unit = {} + val onPostLikeClick: (TagsFeedPostItem) -> Unit = {} + val onPostMoreMenuClick = {} + val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} + + val dateLine = "dateLine" + val numberLikesText = "numberLikesText" + val numberCommentsText = "numberCommentsText" + + // When + whenever(dateTimeUtilsWrapper.dateFromIso8601(any())) + .thenReturn(Date(0)) + whenever(dateTimeUtilsWrapper.javaDateToTimeSpan(any())) + .thenReturn(dateLine) + whenever(readerUtilsWrapper.getShortLikeLabelText(readerPost.numLikes)) + .thenReturn(numberLikesText) + whenever(readerUtilsWrapper.getShortCommentLabelText(readerPost.numReplies)) + .thenReturn(numberCommentsText) + whenever(urlUtilsWrapper.removeScheme(readerPost.blogUrl)) + .thenReturn("blogurl.wordpress.com") + + val actual = classToTest.mapLoadedTagFeedItem( + tag = readerTag, + posts = postList, + onTagClick = onTagClick, + onSiteClick = onSiteClick, + onPostCardClick = onPostCardClick, + onPostLikeClick = onPostLikeClick, + onPostMoreMenuClick = onPostMoreMenuClick, + onItemEnteredView = onItemEnteredView, + ) + // Then + val expected = ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = readerTag, + onTagClick = onTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Loaded( + listOf( + TagsFeedPostItem( + siteName = "blogurl.wordpress.com", + postDateLine = dateLine, + postTitle = readerPost.title, + postExcerpt = readerPost.excerpt, + postImageUrl = readerPost.featuredImage, + postNumberOfLikesText = numberLikesText, + postNumberOfCommentsText = numberCommentsText, + isPostLiked = readerPost.isLikedByCurrentUser, + isLikeButtonEnabled = true, + postId = 0L, + blogId = 0L, + onSiteClick = onSiteClick, + onPostLikeClick = onPostLikeClick, + onPostCardClick = onPostCardClick, + onPostMoreMenuClick = onPostMoreMenuClick, + ) + ) + ), + onItemEnteredView = onItemEnteredView, + ) + assertEquals(expected, actual) + } + @Test fun `Should map error TagFeedItem correctly`() { // Given From 5724e46b0c18af447f4d9833a5a275a7489b0786 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 14 May 2024 15:14:24 -0300 Subject: [PATCH 163/237] Change tags feed item content total lines to 3 At most 2 for title, an the rest for excerpt, vertically aligning the content block between the site tile row and bottom metadata/buttons --- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 77 +++++++------------ 1 file changed, 28 insertions(+), 49 deletions(-) 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 d8ed691ff05e..950d25951830 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 @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -49,8 +50,8 @@ import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin -private const val CONTENT_TOTAL_LINES_WITH_INTERACTIONS = 4 -private const val CONTENT_TOTAL_LINES_NO_INTERACTIONS = 5 +private const val CONTENT_TOTAL_LINES = 3 + @Composable fun ReaderTagsFeedPostListItem( @@ -69,10 +70,13 @@ fun ReaderTagsFeedPostListItem( Column( modifier = Modifier .width(ReaderTagsFeedComposeUtils.PostItemWidth) - .height(ReaderTagsFeedComposeUtils.PostItemHeight) + .height(ReaderTagsFeedComposeUtils.PostItemHeight), + verticalArrangement = Arrangement.spacedBy(Margin.Small.value), ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .heightIn(min = 24.dp) + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { // Site name @@ -107,8 +111,6 @@ fun ReaderTagsFeedPostListItem( ) } - Spacer(modifier = Modifier.height(Margin.Small.value)) - // Post content row Row( modifier = Modifier @@ -124,10 +126,8 @@ fun ReaderTagsFeedPostListItem( onClick = { onPostCardClick(item) }, titleColor = baseColor, excerptColor = primaryElementColor, - hasInteractions = hasInteractions, modifier = Modifier - .weight(1f) - .fillMaxHeight(), + .weight(1f), ) // Post image @@ -141,6 +141,8 @@ fun ReaderTagsFeedPostListItem( // Likes and comments row if (hasInteractions) { + val interactionTextStyle = MaterialTheme.typography.bodySmall + Row( modifier = Modifier .fillMaxWidth(), @@ -149,7 +151,7 @@ fun ReaderTagsFeedPostListItem( // Number of likes Text( text = postNumberOfLikesText, - style = MaterialTheme.typography.bodyMedium, + style = interactionTextStyle, color = secondaryElementColor, maxLines = 1, ) @@ -161,20 +163,18 @@ fun ReaderTagsFeedPostListItem( horizontal = Margin.Small.value ), text = "•", - style = MaterialTheme.typography.bodyMedium, + style = interactionTextStyle, color = secondaryElementColor, ) } // Number of comments Text( text = postNumberOfCommentsText, - style = MaterialTheme.typography.bodyMedium, + style = interactionTextStyle, color = secondaryElementColor, maxLines = 1, ) } - - Spacer(Modifier.height(Margin.Small.value)) } // Actions row @@ -267,18 +267,9 @@ fun PostTextContent( excerpt: String, titleColor: Color, excerptColor: Color, - hasInteractions: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val totalLines = remember(hasInteractions) { - if (hasInteractions) { - CONTENT_TOTAL_LINES_WITH_INTERACTIONS - } else { - CONTENT_TOTAL_LINES_NO_INTERACTIONS - } - } - BoxWithConstraints( modifier = modifier, ) { @@ -288,28 +279,24 @@ fun PostTextContent( } val textMeasurer = rememberTextMeasurer() - val titleLayoutResult = textMeasurer.measure( - text = title, - style = MaterialTheme.typography.titleMedium, - maxLines = ReaderTagsFeedComposeUtils.POST_ITEM_TITLE_MAX_LINES, - overflow = TextOverflow.Ellipsis, - constraints = Constraints(maxWidth = maxWidthPx), - ) - val titleLines = titleLayoutResult.lineCount + val titleStyle = MaterialTheme.typography.titleMedium - // Check if excerpt is ellipsized - val excerptMaxLines = totalLines - titleLines - val excerptLayoutResult = textMeasurer.measure( - text = excerpt, - style = MaterialTheme.typography.bodySmall, - maxLines = excerptMaxLines, - overflow = TextOverflow.Ellipsis, - constraints = Constraints(maxWidth = maxWidthPx), - ) - val isExcerptFullHeight = excerptLayoutResult.lineCount == excerptMaxLines + val excerptMaxLines = remember(title, titleStyle, maxWidthPx) { + val titleLayoutResult = textMeasurer.measure( + text = title, + style = titleStyle, + maxLines = ReaderTagsFeedComposeUtils.POST_ITEM_TITLE_MAX_LINES, + overflow = TextOverflow.Ellipsis, + constraints = Constraints(maxWidth = maxWidthPx), + ) + + val titleLines = titleLayoutResult.lineCount + CONTENT_TOTAL_LINES - titleLines + } Column( modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(Margin.Small.value), ) { // Post title Text( @@ -326,10 +313,6 @@ fun PostTextContent( overflow = TextOverflow.Ellipsis, ) - Spacer( - if (isExcerptFullHeight) Modifier.weight(1f) else Modifier.height(Margin.Medium.value) - ) - // Post excerpt Text( modifier = Modifier @@ -344,10 +327,6 @@ fun PostTextContent( maxLines = excerptMaxLines, overflow = TextOverflow.Ellipsis, ) - - if (isExcerptFullHeight) { - Spacer(Modifier.weight(1f)) - } } } } From ec75730d300954fbd7e6b26ad4726f231d9aa8d3 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 14 May 2024 15:41:55 -0300 Subject: [PATCH 164/237] Update loading skeleton to match new layout --- .../ReaderTagsFeedPostListItemLoading.kt | 88 ++++++------------- 1 file changed, 25 insertions(+), 63 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt index 208d1f0efa21..b7636d2e1fdc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -48,7 +47,7 @@ fun ReaderTagsFeedPostListItemLoading() { ) { Box( modifier = Modifier - .width(99.dp) + .width(150.dp) .height(8.dp) .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), @@ -66,62 +65,23 @@ fun ReaderTagsFeedPostListItemLoading() { Column( modifier = Modifier .weight(1f), + verticalArrangement = Arrangement.spacedBy(Margin.Medium.value), ) { // Title placeholder Box( modifier = Modifier - .fillMaxWidth(0.9f) - .height(18.dp) + .fillMaxWidth(0.95f) + .height(16.dp) .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), ) - - Spacer(modifier = Modifier.height(Margin.Medium.value)) - Box( modifier = Modifier .fillMaxWidth(0.8f) - .height(18.dp) + .height(16.dp) .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), ) - - Spacer(modifier = Modifier.height(Margin.Medium.value)) - - // Excerpt placeholder - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(Margin.Small.value), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(contentColor), - ) - Box( - modifier = Modifier - .fillMaxWidth(0.9f) - .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(contentColor), - ) - Box( - modifier = Modifier - .fillMaxWidth(0.85f) - .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(contentColor), - ) - Box( - modifier = Modifier - .fillMaxWidth(0.8f) - .height(8.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(contentColor), - ) - } } // Image placeholder @@ -133,25 +93,27 @@ fun ReaderTagsFeedPostListItemLoading() { ) } - // Likes and comments placeholder -// Box( -// modifier = Modifier -// .width(170.dp) -// .height(12.dp) -// .clip(shape = RoundedCornerShape(16.dp)) -// .background(contentColor), -// ) - -// Spacer(Modifier.height(Margin.Medium.value)) - - // Actions placeholder - Box( + // Likes and comments + actions placeholder + Column( modifier = Modifier - .width(170.dp) - .height(16.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(contentColor), - ) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(Margin.MediumLarge.value), + ) { + Box( + modifier = Modifier + .fillMaxWidth(0.6f) + .height(8.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(contentColor), + ) + Box( + modifier = Modifier + .fillMaxWidth(0.5f) + .height(8.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .background(contentColor), + ) + } } } From e3515ce507a5c6e6e61993cd74510826478c73cb Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 14 May 2024 16:02:14 -0300 Subject: [PATCH 165/237] Bump thin line in skeleton to 10dp --- .../tagsfeed/ReaderTagsFeedPostListItemLoading.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt index b7636d2e1fdc..1926a5585d5f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/tagsfeed/ReaderTagsFeedPostListItemLoading.kt @@ -25,6 +25,9 @@ import org.wordpress.android.ui.compose.theme.AppColor import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin +private val ThinLineHeight = 10.dp +private val ThickLineHeight = 16.dp + @Composable fun ReaderTagsFeedPostListItemLoading() { val contentColor = if (isSystemInDarkTheme()) { @@ -48,7 +51,7 @@ fun ReaderTagsFeedPostListItemLoading() { Box( modifier = Modifier .width(150.dp) - .height(8.dp) + .height(ThinLineHeight) .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), ) @@ -71,14 +74,14 @@ fun ReaderTagsFeedPostListItemLoading() { Box( modifier = Modifier .fillMaxWidth(0.95f) - .height(16.dp) + .height(ThickLineHeight) .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), ) Box( modifier = Modifier .fillMaxWidth(0.8f) - .height(16.dp) + .height(ThickLineHeight) .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), ) @@ -102,14 +105,14 @@ fun ReaderTagsFeedPostListItemLoading() { Box( modifier = Modifier .fillMaxWidth(0.6f) - .height(8.dp) + .height(ThinLineHeight) .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), ) Box( modifier = Modifier .fillMaxWidth(0.5f) - .height(8.dp) + .height(ThinLineHeight) .clip(shape = RoundedCornerShape(16.dp)) .background(contentColor), ) From c96931a519b5f2fa5fe3dd1e39e691d95b80b04b Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 14 May 2024 17:47:43 -0300 Subject: [PATCH 166/237] Implement reader_dropdown_menu_item_tapped event for tags item --- .../android/ui/reader/tracker/ReaderTracker.kt | 1 + .../android/ui/reader/ReaderTrackerTest.kt | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt index c6fc4e1006b9..fa440fcfcc28 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt @@ -404,6 +404,7 @@ class ReaderTracker @Inject constructor( readerTag.isA8C -> "a8c" readerTag.isListTopic -> "list" readerTag.isP2 -> "p2" + readerTag.isTags -> "tags" else -> null }?.let { trackingId -> analyticsTrackerWrapper.track( diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTrackerTest.kt index c45df778107b..71221349351d 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTrackerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderTrackerTest.kt @@ -325,6 +325,23 @@ class ReaderTrackerTest { ) } + @Test + fun `Should track dropdown menu tags feed item tapped`() { + tracker.trackDropdownMenuItemTapped( + ReaderTag( + "slug", + "displayName", + "title", + null, + ReaderTagType.TAGS, + ) + ) + verify(analyticsTrackerWrapper).track( + stat = AnalyticsTracker.Stat.READER_DROPDOWN_MENU_ITEM_TAPPED, + properties = mapOf("id" to "tags"), + ) + } + @Test fun `Should track post with reading preferences returned from ReadingPreferencesTracker`() { val post = ReaderPost() From 72ff59ebf53b829e98c0cbbd262b022ed7b67d7c Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 14 May 2024 18:22:23 -0300 Subject: [PATCH 167/237] Implement reader_tags_feed_shown analytics event --- .../wordpress/android/ui/reader/tracker/ReaderTracker.kt | 6 +++++- .../org/wordpress/android/analytics/AnalyticsTracker.java | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt index fa440fcfcc28..5cc3c683b3a8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt @@ -88,6 +88,7 @@ class ReaderTracker @Inject constructor( ReaderTab.A8C -> analyticsTrackerWrapper.track(AnalyticsTracker.Stat.READER_A8C_SHOWN) ReaderTab.P2 -> analyticsTrackerWrapper.track(AnalyticsTracker.Stat.READER_P2_SHOWN) ReaderTab.CUSTOM -> analyticsTrackerWrapper.track(AnalyticsTracker.Stat.READER_CUSTOM_TAB_SHOWN) + ReaderTab.TAGS_FEED -> analyticsTrackerWrapper.track(AnalyticsTracker.Stat.READER_TAGS_FEED_SHOWN) } appPrefsWrapper.setReaderActiveTab(readerTab) } @@ -516,7 +517,8 @@ enum class ReaderTab( SAVED(4, ReaderTracker.SOURCE_SAVED), CUSTOM(5, ReaderTracker.SOURCE_CUSTOM), A8C(6, ReaderTracker.SOURCE_A8C), - P2(7, ReaderTracker.SOURCE_P2); + P2(7, ReaderTracker.SOURCE_P2), + TAGS_FEED(8, ReaderTracker.SOURCE_TAGS_FEED); companion object { fun fromId(id: Int): ReaderTab { @@ -528,6 +530,7 @@ enum class ReaderTab( A8C.id -> A8C P2.id -> P2 CUSTOM.id -> CUSTOM + TAGS_FEED.id -> TAGS_FEED else -> throw RuntimeException("Unexpected ReaderTab id") } } @@ -541,6 +544,7 @@ enum class ReaderTab( readerTag.isDiscover -> DISCOVER readerTag.isA8C -> A8C readerTag.isP2 -> P2 + readerTag.isTags -> TAGS_FEED else -> CUSTOM } } diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index f9b368cc757d..fd71ec4d2a65 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -771,6 +771,7 @@ public enum Stat { READER_LIKED_SHOWN, READER_SAVED_LIST_SHOWN, READER_CUSTOM_TAB_SHOWN, + READER_TAGS_FEED_SHOWN, READER_DISCOVER_SHOWN, READER_DISCOVER_PAGINATED, READER_DISCOVER_TOPIC_TAPPED, From b781cd51d850ebf8855ce369e05a6749edc02d20 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 14 May 2024 20:43:30 -0300 Subject: [PATCH 168/237] Add type parameter to reader_filter_sheet_item_selected event --- .../ui/reader/subfilter/SubFilterViewModel.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index 5f806bb6ad8b..e5afa691219f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -317,7 +317,21 @@ class SubFilterViewModel @Inject constructor( fun onSubfilterSelected(subfilterListItem: SubfilterListItem) { // We should not track subfilter selected if we're clearing a filter that is currently applied. if (!subfilterListItem.isClearingFilter) { - readerTracker.track(Stat.READER_FILTER_SHEET_ITEM_SELECTED) + val filterItemType = when(subfilterListItem.type) { + SubfilterListItem.ItemType.SITE -> FilterItemType.Blog + SubfilterListItem.ItemType.TAG -> FilterItemType.Tag + else -> null + } + if (filterItemType != null) { + readerTracker.track( + Stat.READER_FILTER_SHEET_ITEM_SELECTED, + mutableMapOf("type" to filterItemType.trackingValue) + ) + } else { + readerTracker.track( + Stat.READER_FILTER_SHEET_ITEM_SELECTED, + ) + } } changeSubfilter(subfilterListItem, true, mTagFragmentStartedWith) } @@ -425,4 +439,10 @@ class SubFilterViewModel @Inject constructor( return SUBFILTER_VM_BASE_KEY + tag.keyString } } + + sealed class FilterItemType(val trackingValue: String) { + object Tag : FilterItemType("tag") + + object Blog : FilterItemType("blog") + } } From 8f2ec7b3d4ad2ad7399d7a512997cfce26b507f0 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 14 May 2024 21:01:25 -0300 Subject: [PATCH 169/237] Implement reader_tag_header_tapped analytics event --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 3 +++ .../viewmodels/ReaderTagsFeedViewModelTest.kt | 15 +++++++++++++++ .../android/analytics/AnalyticsTracker.java | 1 + 3 files changed, 19 insertions(+) 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 3e01330a1114..14430a44a05d 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 @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderTag @@ -48,6 +49,7 @@ class ReaderTagsFeedViewModel @Inject constructor( private val readerPostMoreButtonUiStateBuilder: ReaderPostMoreButtonUiStateBuilder, private val readerPostUiStateBuilder: ReaderPostUiStateBuilder, private val displayUtilsWrapper: DisplayUtilsWrapper, + private val readerTracker: ReaderTracker, ) : ScopedViewModel(bgDispatcher) { private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow @@ -235,6 +237,7 @@ class ReaderTagsFeedViewModel @Inject constructor( @VisibleForTesting fun onTagChipClick(readerTag: ReaderTag) { + readerTracker.track(AnalyticsTracker.Stat.READER_TAG_HEADER_TAPPED) _actionEvents.value = ActionEvent.FilterTagPostsFeed(readerTag) } 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 637e216049b5..076cb4316ae9 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 @@ -19,6 +19,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest +import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper import org.wordpress.android.getOrAwaitValue import org.wordpress.android.models.ReaderPost @@ -34,6 +35,7 @@ import org.wordpress.android.ui.reader.discover.ReaderPostUiStateBuilder 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.viewmodels.tagsfeed.ReaderTagsFeedUiStateMapper import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent @@ -68,6 +70,9 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Mock lateinit var displayUtilsWrapper: DisplayUtilsWrapper + @Mock + lateinit var readerTracker: ReaderTracker + @Mock lateinit var navigationEvents: MediatorLiveData> @@ -101,6 +106,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { readerPostMoreButtonUiStateBuilder = readerPostMoreButtonUiStateBuilder, readerPostUiStateBuilder = readerPostUiStateBuilder, displayUtilsWrapper = displayUtilsWrapper, + readerTracker = readerTracker, ) whenever(readerPostCardActionsHandler.navigationEvents) .thenReturn(navigationEvents) @@ -260,6 +266,15 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertIs(actionEvents.first()) } + @Test + fun `Should track READER_TAG_HEADER_TAPPED when onTagChipClick is called`() { + // When + viewModel.onTagChipClick(tag) + + // Then + verify(readerTracker).track(AnalyticsTracker.Stat.READER_TAG_HEADER_TAPPED) + } + @Test fun `Should emit OpenTagPostList when onMoreFromTagClick is called`() { // When diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index fd71ec4d2a65..466cd9b5bd81 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -88,6 +88,7 @@ public enum Stat { READER_READING_PREFERENCES_FEEDBACK_TAPPED, READER_READING_PREFERENCES_ITEM_TAPPED, READER_READING_PREFERENCES_SAVED, + READER_TAG_HEADER_TAPPED, STATS_ACCESSED, STATS_ACCESS_ERROR, STATS_PERIOD_ACCESSED, From a51f36a1fe16adfb219bd50db9cc05082224d6ca Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 14 May 2024 21:18:30 -0300 Subject: [PATCH 170/237] Update SubFilterViewModel unit tests --- .../ui/reader/subfilter/SubFilterViewModel.kt | 4 +-- .../subfilter/SubFilterViewModelTest.kt | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index e5afa691219f..4009d7195f0a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -441,8 +441,8 @@ class SubFilterViewModel @Inject constructor( } sealed class FilterItemType(val trackingValue: String) { - object Tag : FilterItemType("tag") + data object Tag : FilterItemType("tag") - object Blog : FilterItemType("blog") + data object Blog : FilterItemType("blog") } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt index dea51ed31922..f36ef208f0e2 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt @@ -20,8 +20,10 @@ import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.fluxc.model.AccountModel import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.getOrAwaitValue +import org.wordpress.android.models.ReaderBlog import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType.BOOKMARKED +import org.wordpress.android.models.ReaderTagType.TAGS import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.reader.ReaderSubsActivity import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType @@ -447,6 +449,34 @@ class SubFilterViewModelTest : BaseUnitTest() { verify(readerTracker).track(AnalyticsTracker.Stat.READER_FILTER_SHEET_ITEM_SELECTED) } + @Test + fun `Should track READER_FILTER_SHEET_ITEM_SELECTED with parameters if type is SITE`() { + val siteFilter = Site( + blog = ReaderBlog(), + onClickAction = {}, + ) + viewModel.onSubfilterSelected(siteFilter) + verify(readerTracker).track( + stat = AnalyticsTracker.Stat.READER_FILTER_SHEET_ITEM_SELECTED, + properties = mutableMapOf("type" to "blog"), + ) + } + + @Test + fun `Should track READER_FILTER_SHEET_ITEM_SELECTED with parameters if type is TAG`() { + val tagFilter = Tag( + tag = ReaderTag( + "", "", "", "", TAGS + ), + onClickAction = {}, + ) + viewModel.onSubfilterSelected(tagFilter) + verify(readerTracker).track( + stat = AnalyticsTracker.Stat.READER_FILTER_SHEET_ITEM_SELECTED, + properties = mutableMapOf("type" to "tag"), + ) + } + @Test fun `Should propagate title container visibility state properly`() { listOf(true, false).forEach { isTitleContainerVisible -> From 5b00f51688f5782228e0b40f5f41ed16594a4f76 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 14 May 2024 23:09:43 -0300 Subject: [PATCH 171/237] Add type parameter to reader_filter_sheet_cleared event --- .../ui/reader/subfilter/SubFilterViewModel.kt | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index 4009d7195f0a..040b527b434e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -217,7 +217,15 @@ class SubFilterViewModel @Inject constructor( } fun setDefaultSubfilter(isClearingFilter: Boolean) { - readerTracker.track(Stat.READER_FILTER_SHEET_CLEARED) + val filterItemType = FilterItemType.fromSubfilterListItem(getCurrentSubfilterValue()) + if (filterItemType != null) { + readerTracker.track( + Stat.READER_FILTER_SHEET_CLEARED, + mutableMapOf(FilterItemType.trackingEntry(filterItemType)) + ) + } else { + readerTracker.track(Stat.READER_FILTER_SHEET_CLEARED) + } updateSubfilter( filter = SiteAll( onClickAction = ::onSubfilterClicked, @@ -317,20 +325,14 @@ class SubFilterViewModel @Inject constructor( fun onSubfilterSelected(subfilterListItem: SubfilterListItem) { // We should not track subfilter selected if we're clearing a filter that is currently applied. if (!subfilterListItem.isClearingFilter) { - val filterItemType = when(subfilterListItem.type) { - SubfilterListItem.ItemType.SITE -> FilterItemType.Blog - SubfilterListItem.ItemType.TAG -> FilterItemType.Tag - else -> null - } + val filterItemType = FilterItemType.fromSubfilterListItem(subfilterListItem) if (filterItemType != null) { readerTracker.track( Stat.READER_FILTER_SHEET_ITEM_SELECTED, - mutableMapOf("type" to filterItemType.trackingValue) + mutableMapOf(FilterItemType.trackingEntry(filterItemType)) ) } else { - readerTracker.track( - Stat.READER_FILTER_SHEET_ITEM_SELECTED, - ) + readerTracker.track(Stat.READER_FILTER_SHEET_ITEM_SELECTED,) } } changeSubfilter(subfilterListItem, true, mTagFragmentStartedWith) @@ -444,5 +446,17 @@ class SubFilterViewModel @Inject constructor( data object Tag : FilterItemType("tag") data object Blog : FilterItemType("blog") + + companion object { + fun fromSubfilterListItem(subfilterListItem: SubfilterListItem): FilterItemType? = + when(subfilterListItem.type) { + SubfilterListItem.ItemType.SITE -> Blog + SubfilterListItem.ItemType.TAG -> Tag + else -> null + } + + fun trackingEntry(filterItemType: FilterItemType): Pair = + "type" to filterItemType.trackingValue + } } } From efd86638a3e1f87c43bb6c7b6a2caccd156a2d6d Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 15 May 2024 00:15:06 -0300 Subject: [PATCH 172/237] Track reader_post_card_tapped event when reader tags feed post is clicked --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 7 ++- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 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 14430a44a05d..740f8a4601fe 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 @@ -261,9 +261,14 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun onPostCardClick(postItem: TagsFeedPostItem) { + @VisibleForTesting + fun onPostCardClick(postItem: TagsFeedPostItem) { launch { findPost(postItem.postId, postItem.blogId)?.let { + readerTracker.trackBlog( + AnalyticsTracker.Stat.READER_POST_CARD_TAPPED, + it.blogId, it.feedId, it.isFollowedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED, + ) readerPostCardActionsHandler.handleOnItemClicked( it, ReaderTracker.SOURCE_TAGS_FEED 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 076cb4316ae9..3481a6bdba1a 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 @@ -600,6 +600,49 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertIs(actionEvents.first()) } + @Test + fun `Should track READER_POST_CARD_TAPPED when onPostCardClick is called`() = testCollectingUiStates { + // Given + val blogId = 123L + val feedId = 456L + val isFollowedByCurrentUser = true + whenever(readerPostTableWrapper.getBlogPost(any(), any(), any())) + .thenReturn(ReaderPost().apply { + this.blogId = blogId + this.feedId = feedId + this.isFollowedByCurrentUser = isFollowedByCurrentUser + }) + // When + viewModel.onPostCardClick( + postItem = TagsFeedPostItem( + siteName = "", + postDateLine = "", + postTitle = "", + postExcerpt = "", + postImageUrl = "", + postNumberOfLikesText = "", + postNumberOfCommentsText = "", + isPostLiked = true, + isLikeButtonEnabled = true, + postId = 123L, + blogId = 123L, + onSiteClick = {}, + onPostCardClick = {}, + onPostLikeClick = {}, + onPostMoreMenuClick = {} + ) + ) + + // Then + verify(readerTracker).trackBlog( + stat = AnalyticsTracker.Stat.READER_POST_CARD_TAPPED, + blogId = blogId, + feedId = feedId, + isFollowed = isFollowedByCurrentUser, + source = ReaderTracker.SOURCE_TAGS_FEED, + ) + } + private fun mockMapInitialTagFeedItems() { whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any(), any(), any(), any(), any())) .thenAnswer { From 74a085b60b806d7a19ab6d33ab8affa502637447 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 15 May 2024 00:26:39 -0300 Subject: [PATCH 173/237] Fix tags feed more menu actions source for analytics events --- .../ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 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 740f8a4601fe..ca30b3caac7a 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 @@ -438,7 +438,7 @@ class ReaderTagsFeedViewModel @Inject constructor( it, type, isBookmarkList = false, - source = ReaderTracker.SOURCE_DISCOVER + source = ReaderTracker.SOURCE_TAGS_FEED, ) } } From 44f56f1a95b3d131c181e78dcf5c750110b04a80 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 15 May 2024 00:34:39 -0300 Subject: [PATCH 174/237] Track reader_tags_feed_more_from_tag_tapped event when reader tags feed more from tag button is tapped --- .../viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt | 1 + .../reader/viewmodels/ReaderTagsFeedViewModelTest.kt | 10 ++++++++++ .../wordpress/android/analytics/AnalyticsTracker.java | 1 + 3 files changed, 12 insertions(+) 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 ca30b3caac7a..c0888203a6e1 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 @@ -243,6 +243,7 @@ class ReaderTagsFeedViewModel @Inject constructor( @VisibleForTesting fun onMoreFromTagClick(readerTag: ReaderTag) { + readerTracker.track(AnalyticsTracker.Stat.READER_TAGS_FEED_MORE_FROM_TAG_TAPPED) _actionEvents.value = ActionEvent.OpenTagPostList(readerTag) } 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 3481a6bdba1a..177653660bd4 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 @@ -284,6 +284,16 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertIs(actionEvents.first()) } + @Test + fun `Should track READER_TAGS_FEED_MORE_FROM_TAG_TAPPED when onMoreFromTagClick is called`() { + // When + viewModel.onMoreFromTagClick(tag) + + // Then + verify(readerTracker).track(AnalyticsTracker.Stat.READER_TAGS_FEED_MORE_FROM_TAG_TAPPED) + } + + @Test fun `Should emit ShowTagsList when onOpenTagsListClick is called`() { // When diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index 466cd9b5bd81..5a0b9d62675f 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -89,6 +89,7 @@ public enum Stat { READER_READING_PREFERENCES_ITEM_TAPPED, READER_READING_PREFERENCES_SAVED, READER_TAG_HEADER_TAPPED, + READER_TAGS_FEED_MORE_FROM_TAG_TAPPED, STATS_ACCESSED, STATS_ACCESS_ERROR, STATS_PERIOD_ACCESSED, From 66cef71a45cba0e0b58e4a8788c024150d8aa704 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 15 May 2024 13:17:25 -0300 Subject: [PATCH 175/237] Change analytics event name from READER_TAG_HEADER_TAPPED to READER_TAGS_FEED_HEADER_TAPPED --- .../ui/reader/viewmodels/tagsfeed/ReaderTagsFeedViewModel.kt | 2 +- .../ui/reader/viewmodels/ReaderTagsFeedViewModelTest.kt | 4 ++-- .../org/wordpress/android/analytics/AnalyticsTracker.java | 2 +- 3 files changed, 4 insertions(+), 4 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 c0888203a6e1..525270715e31 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 @@ -237,7 +237,7 @@ class ReaderTagsFeedViewModel @Inject constructor( @VisibleForTesting fun onTagChipClick(readerTag: ReaderTag) { - readerTracker.track(AnalyticsTracker.Stat.READER_TAG_HEADER_TAPPED) + readerTracker.track(AnalyticsTracker.Stat.READER_TAGS_FEED_HEADER_TAPPED) _actionEvents.value = ActionEvent.FilterTagPostsFeed(readerTag) } 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 177653660bd4..d939380018e5 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 @@ -267,12 +267,12 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } @Test - fun `Should track READER_TAG_HEADER_TAPPED when onTagChipClick is called`() { + fun `Should track READER_TAGS_FEED_HEADER_TAPPED when onTagChipClick is called`() { // When viewModel.onTagChipClick(tag) // Then - verify(readerTracker).track(AnalyticsTracker.Stat.READER_TAG_HEADER_TAPPED) + verify(readerTracker).track(AnalyticsTracker.Stat.READER_TAGS_FEED_HEADER_TAPPED) } @Test diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index 5a0b9d62675f..c1bab072d19f 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -88,7 +88,7 @@ public enum Stat { READER_READING_PREFERENCES_FEEDBACK_TAPPED, READER_READING_PREFERENCES_ITEM_TAPPED, READER_READING_PREFERENCES_SAVED, - READER_TAG_HEADER_TAPPED, + READER_TAGS_FEED_HEADER_TAPPED, READER_TAGS_FEED_MORE_FROM_TAG_TAPPED, STATS_ACCESSED, STATS_ACCESS_ERROR, From 50d47f7fc3ea531027a72d64658a72dc020fe534 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 15 May 2024 13:52:36 -0300 Subject: [PATCH 176/237] Change reader filter sheet events type parameter values --- .../android/ui/reader/subfilter/SubFilterViewModel.kt | 4 ++-- .../android/ui/reader/subfilter/SubFilterViewModelTest.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index 040b527b434e..eefb58a67f82 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -443,9 +443,9 @@ class SubFilterViewModel @Inject constructor( } sealed class FilterItemType(val trackingValue: String) { - data object Tag : FilterItemType("tag") + data object Tag : FilterItemType("topic") - data object Blog : FilterItemType("blog") + data object Blog : FilterItemType("site") companion object { fun fromSubfilterListItem(subfilterListItem: SubfilterListItem): FilterItemType? = diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt index f36ef208f0e2..bad452b02fd0 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt @@ -458,7 +458,7 @@ class SubFilterViewModelTest : BaseUnitTest() { viewModel.onSubfilterSelected(siteFilter) verify(readerTracker).track( stat = AnalyticsTracker.Stat.READER_FILTER_SHEET_ITEM_SELECTED, - properties = mutableMapOf("type" to "blog"), + properties = mutableMapOf("type" to "site"), ) } @@ -473,7 +473,7 @@ class SubFilterViewModelTest : BaseUnitTest() { viewModel.onSubfilterSelected(tagFilter) verify(readerTracker).track( stat = AnalyticsTracker.Stat.READER_FILTER_SHEET_ITEM_SELECTED, - properties = mutableMapOf("type" to "tag"), + properties = mutableMapOf("type" to "topic"), ) } From c9347c8be1f458b21af0eb7b0251a60fe18eb4be Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 15 May 2024 15:52:37 -0300 Subject: [PATCH 177/237] Fix unit tests --- .../tagsfeed/ReaderTagsFeedUiStateMapperTest.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index 4f5c3ddf62e5..c82f8e9ff3af 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -146,11 +146,12 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { "endpoint", ReaderTagType.FOLLOWED, ) - val onTagClick = { _: ReaderTag -> } + val onTagChipClick = { _: ReaderTag -> } + val onMoreFromTagClick = { _: ReaderTag -> } val onSiteClick: (TagsFeedPostItem) -> Unit = {} val onPostCardClick: (TagsFeedPostItem) -> Unit = {} val onPostLikeClick: (TagsFeedPostItem) -> Unit = {} - val onPostMoreMenuClick = {} + val onPostMoreMenuClick: (TagsFeedPostItem) -> Unit = {} val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} val dateLine = "dateLine" @@ -172,7 +173,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val actual = classToTest.mapLoadedTagFeedItem( tag = readerTag, posts = postList, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, onSiteClick = onSiteClick, onPostCardClick = onPostCardClick, onPostLikeClick = onPostLikeClick, @@ -183,7 +185,8 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val expected = ReaderTagsFeedViewModel.TagFeedItem( tagChip = ReaderTagsFeedViewModel.TagChip( tag = readerTag, - onTagClick = onTagClick, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, ), postList = ReaderTagsFeedViewModel.PostList.Loaded( listOf( From a78d3dcd76f1f86a0c1ded9f9e0e10b9fc0a5a5e Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 15 May 2024 17:45:30 -0300 Subject: [PATCH 178/237] Add proper semantics to loaded item --- .../tagsfeed/ReaderTagsFeedPostListItem.kt | 46 ++++++++++++++++++- WordPress/src/main/res/values/strings.xml | 5 ++ 2 files changed, 50 insertions(+), 1 deletion(-) 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 fa3198d4b7a1..d7ed424ef3cd 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 @@ -33,6 +33,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -40,6 +41,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -76,7 +82,8 @@ fun ReaderTagsFeedPostListItem( Column( modifier = Modifier .width(ReaderTagsFeedComposeUtils.PostItemWidth) - .height(ReaderTagsFeedComposeUtils.PostItemHeight), + .height(ReaderTagsFeedComposeUtils.PostItemHeight) + .itemSemanticsModifier(item), verticalArrangement = Arrangement.spacedBy(Margin.Small.value), ) { Row( @@ -260,6 +267,43 @@ fun ReaderTagsFeedPostListItem( } } +private fun Modifier.itemSemanticsModifier(item: TagsFeedPostItem): Modifier = composed { + val openPostActionLabel = stringResource(R.string.reader_tags_feed_action_label_open_post) + val openBlogActionLabel = stringResource(R.string.reader_tags_feed_action_label_open_blog) + + val likeStateDescription = if (item.isPostLiked) stringResource(R.string.mnu_comment_liked) else null + val likeActionLabel = if (item.isPostLiked) { + stringResource(R.string.reader_tags_feed_action_label_unlike_post) + } else { + stringResource(R.string.reader_tags_feed_action_label_like_post) + } + + val openMenuActionLabel = stringResource(R.string.reader_tags_feed_action_label_open_menu) + + clearAndSetSemantics { + contentDescription = "${item.siteName}, ${item.postDateLine}, ${item.postTitle}" + customActions = listOf( + CustomAccessibilityAction(openPostActionLabel) { + item.onPostCardClick(item) + true + }, + CustomAccessibilityAction(openBlogActionLabel) { + item.onSiteClick(item) + true + }, + CustomAccessibilityAction(likeActionLabel) { + item.onPostLikeClick(item) + true + }, + CustomAccessibilityAction(openMenuActionLabel) { + item.onPostMoreMenuClick(item) + true + }, + ) + likeStateDescription?.let { stateDescription = it } + } +} + @Composable fun PostImage( imageUrl: String, diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index d5b1681152a2..e9ec2d338432 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2332,6 +2332,11 @@ We couldn\'t load posts from this tag right now We couldn\'t find any posts tagged %s right now Retry + open post + open blog + like post + remove post like + open menu No connection From aba2d9855f2cc3087583fc9f6d4fc3d96a2aa824 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 15 May 2024 18:09:03 -0300 Subject: [PATCH 179/237] Fix semantics for loading/empty/error states --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) 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 9c85b58d8ad8..45996523486b 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 @@ -41,6 +41,9 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -145,8 +148,14 @@ private fun Loaded(uiState: UiState.Loaded) { @Composable private fun Loading() { + val fetchingPostsLabel = stringResource(id = R.string.posts_fetching) + LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .clearAndSetSemantics { + contentDescription = fetchingPostsLabel + }, userScrollEnabled = false, ) { val numberOfLoadingRows = 3 @@ -257,9 +266,13 @@ private fun Empty(uiState: UiState.Empty) { @Composable private fun PostListLoading() { + val loadingLabel = stringResource(id = R.string.loading) LazyRow( modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .clearAndSetSemantics { + contentDescription = loadingLabel + }, userScrollEnabled = false, horizontalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), contentPadding = PaddingValues( @@ -362,6 +375,7 @@ private fun PostListError( modifier = Modifier .height(250.dp) .fillMaxWidth() + .semantics(mergeDescendants = true) {} .padding(start = 60.dp, end = 60.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { From c2fa5924ddbb0ae4d5d68dca61ee1bd98c1265e2 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 16 May 2024 16:08:57 -0300 Subject: [PATCH 180/237] Add reader announcement card feature config --- WordPress/build.gradle | 1 + .../config/ReaderAnnouncementCardFeatureConfig.kt | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/util/config/ReaderAnnouncementCardFeatureConfig.kt diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 2ac338ca6eca..ee393d97a0b3 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -152,6 +152,7 @@ android { buildConfigField "boolean", "READER_READING_PREFERENCES", "false" buildConfigField "boolean", "READER_READING_PREFERENCES_FEEDBACK", "false" buildConfigField "boolean", "READER_TAGS_FEED", "false" + buildConfigField "boolean", "READER_ANNOUNCEMENT_CARD", "true" // Override these constants in jetpack product flavor to enable/ disable features buildConfigField "boolean", "ENABLE_SITE_CREATION", "true" diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/ReaderAnnouncementCardFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderAnnouncementCardFeatureConfig.kt new file mode 100644 index 000000000000..9c9a87bb2645 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderAnnouncementCardFeatureConfig.kt @@ -0,0 +1,15 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.Feature +import javax.inject.Inject + +private const val READER_ANNOUNCEMENT_CARD_REMOTE_FIELD = "reader_announcement_card" +@Feature(remoteField = READER_ANNOUNCEMENT_CARD_REMOTE_FIELD, defaultValue = false) +class ReaderAnnouncementCardFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.READER_ANNOUNCEMENT_CARD, + READER_ANNOUNCEMENT_CARD_REMOTE_FIELD, +) From d63c192b51380ee6c061a9bb5749c7c63ab6a64a Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 16 May 2024 16:15:48 -0300 Subject: [PATCH 181/237] Implement onRetry function to fetch tag posts --- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 2 +- .../tagsfeed/ReaderTagsFeedViewModel.kt | 45 ++++++++++++++----- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 2 +- 3 files changed, 37 insertions(+), 12 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 283341978ef4..cfac4b72a7b1 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 @@ -67,7 +67,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( errorType: ReaderTagsFeedViewModel.ErrorType, onTagChipClick: (ReaderTag) -> Unit, onMoreFromTagClick: (ReaderTag) -> Unit, - onRetryClick: () -> Unit, + onRetryClick: (ReaderTag) -> Unit, onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit, ): ReaderTagsFeedViewModel.TagFeedItem = ReaderTagsFeedViewModel.TagFeedItem( 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 525270715e31..ef95b93946e9 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 @@ -202,7 +202,9 @@ class ReaderTagsFeedViewModel @Inject constructor( updatedLoadedData[existingIndex] = updatedItem } - (uiState as? UiState.Loaded)?.copy(data = updatedLoadedData) ?: UiState.Loaded(updatedLoadedData) + (uiState as? UiState.Loaded)?.copy(data = updatedLoadedData) ?: UiState.Loaded( + updatedLoadedData + ) } } @@ -247,8 +249,10 @@ class ReaderTagsFeedViewModel @Inject constructor( _actionEvents.value = ActionEvent.OpenTagPostList(readerTag) } - private fun onRetryClick() { - // TODO + private fun onRetryClick(readerTag: ReaderTag) { + launch { + fetchTag(readerTag) + } } @VisibleForTesting @@ -256,7 +260,13 @@ class ReaderTagsFeedViewModel @Inject constructor( launch { findPost(postItem.postId, postItem.blogId)?.let { _navigationEvents.postValue( - Event(ReaderNavigationEvents.ShowBlogPreview(it.blogId, it.feedId, it.isFollowedByCurrentUser)) + Event( + ReaderNavigationEvents.ShowBlogPreview( + it.blogId, + it.feedId, + it.isFollowedByCurrentUser + ) + ) ) } } @@ -268,7 +278,10 @@ class ReaderTagsFeedViewModel @Inject constructor( findPost(postItem.postId, postItem.blogId)?.let { readerTracker.trackBlog( AnalyticsTracker.Stat.READER_POST_CARD_TAPPED, - it.blogId, it.feedId, it.isFollowedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED, + it.blogId, + it.feedId, + it.isFollowedByCurrentUser, + ReaderTracker.SOURCE_TAGS_FEED, ) readerPostCardActionsHandler.handleOnItemClicked( it, @@ -349,7 +362,10 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun findTagFeedItemToUpdate(uiState: UiState.Loaded, postItemToUpdate: TagsFeedPostItem) = + 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 @@ -359,7 +375,11 @@ class ReaderTagsFeedViewModel @Inject constructor( private fun likePostRemote(postItem: TagsFeedPostItem, isPostLikedUpdated: Boolean) { launch { findPost(postItem.postId, postItem.blogId)?.let { post -> - postLikeUseCase.perform(post, !post.isLikedByCurrentUser, ReaderTracker.SOURCE_TAGS_FEED).collect { + 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. @@ -407,7 +427,8 @@ class ReaderTagsFeedViewModel @Inject constructor( includeBookmark = true, onButtonClicked = ::onMoreMenuButtonClicked, ) - val photonWidth = (displayUtilsWrapper.getDisplayPixelWidth() * PHOTON_WIDTH_QUALITY_RATION).toInt() + val photonWidth = + (displayUtilsWrapper.getDisplayPixelWidth() * PHOTON_WIDTH_QUALITY_RATION).toInt() val photonHeight = (photonWidth * FEATURED_IMAGE_HEIGHT_WIDTH_RATION).toInt() _openMoreMenuEvents.postValue( MoreMenuUiState( @@ -432,7 +453,11 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - private fun onMoreMenuButtonClicked(postId: Long, blogId: Long, type: ReaderPostCardActionType) { + private fun onMoreMenuButtonClicked( + postId: Long, + blogId: Long, + type: ReaderPostCardActionType + ) { launch { findPost(postId, blogId)?.let { readerPostCardActionsHandler.onAction( @@ -502,7 +527,7 @@ class ReaderTagsFeedViewModel @Inject constructor( data class Error( val type: ErrorType, - val onRetryClick: () -> Unit + val onRetryClick: (ReaderTag) -> Unit ) : PostList() } 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 45996523486b..9df5d725b7bb 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 @@ -417,7 +417,7 @@ private fun PostListError( ) Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) Button( - onClick = { postList.onRetryClick() }, + onClick = { postList.onRetryClick(tagChip.tag) }, modifier = Modifier .height(36.dp) .width(114.dp), From 4642929faad89947e3498aee8e847c782877b403 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 16 May 2024 16:46:30 -0300 Subject: [PATCH 182/237] Change Error layout height to match post item height --- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 9df5d725b7bb..ef85b60a357c 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 @@ -14,8 +14,10 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items @@ -373,13 +375,14 @@ private fun PostListError( ) { Column( modifier = Modifier - .height(250.dp) + .heightIn(min = ReaderTagsFeedComposeUtils.PostItemHeight) .fillMaxWidth() .semantics(mergeDescendants = true) {} .padding(start = 60.dp, end = 60.dp), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, ) { - Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) + Spacer(modifier = Modifier.height(Margin.Medium.value)) Icon( modifier = Modifier .drawBehind { @@ -392,7 +395,7 @@ private fun PostListError( tint = MaterialTheme.colors.onSurface, contentDescription = null ) - Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value)) + Spacer(modifier = Modifier.height(Margin.ExtraMediumLarge.value)) val tagName = tagChip.tag.tagDisplayName Text( text = stringResource(id = R.string.reader_tags_feed_error_title, tagName), @@ -400,7 +403,7 @@ private fun PostListError( color = MaterialTheme.colors.onSurface, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.height(Margin.Medium.value)) + Spacer(modifier = Modifier.height(Margin.Small.value)) val errorMessage = when (postList.type) { is ErrorType.Default -> stringResource(R.string.reader_tags_feed_loading_error_description) is ErrorType.NoContent -> stringResource(R.string.reader_tags_feed_no_content_error_description, tagName) @@ -415,12 +418,12 @@ private fun PostListError( }, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.height(Margin.ExtraLarge.value)) + Spacer(modifier = Modifier.height(Margin.Large.value)) Button( onClick = { postList.onRetryClick(tagChip.tag) }, modifier = Modifier .height(36.dp) - .width(114.dp), + .widthIn(min = 114.dp), elevation = ButtonDefaults.elevation( defaultElevation = 0.dp, pressedElevation = 0.dp, From 42fa1c8f19b6b320c1b1bca4cdb61ef214e1e857 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 16 May 2024 17:03:59 -0300 Subject: [PATCH 183/237] Add show Reader card announcement methods to AppPrefs --- .../java/org/wordpress/android/ui/prefs/AppPrefs.java | 9 +++++++++ .../org/wordpress/android/ui/prefs/AppPrefsWrapper.kt | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index 0396fb813f35..1c472a5e2200 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -204,6 +204,7 @@ public enum DeletablePrefKey implements PrefKey { SHOULD_HIDE_DYNAMIC_CARD, PINNED_SITE_IDS, READER_READING_PREFERENCES_JSON, + SHOULD_SHOW_READER_ANNOUNCEMENT_CARD, } /** @@ -1780,6 +1781,14 @@ public static void setPinnedSiteLocalIds(@NonNull final String ids) { setString(DeletablePrefKey.PINNED_SITE_IDS, ids); } + public static boolean getShouldShowReaderAnnouncementCard() { + return prefs().getBoolean(DeletablePrefKey.SHOULD_SHOW_READER_ANNOUNCEMENT_CARD.name(), true); + } + + public static void setShouldShowReaderAnnouncementCard(final boolean shouldShow) { + prefs().edit().putBoolean(DeletablePrefKey.SHOULD_SHOW_READER_ANNOUNCEMENT_CARD.name(), shouldShow).apply(); + } + @Nullable public static String getReaderReadingPreferencesJson() { return getString(DeletablePrefKey.READER_READING_PREFERENCES_JSON, null); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt index 81714fba6d47..c185a8f8b3e8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt @@ -453,6 +453,11 @@ class AppPrefsWrapper @Inject constructor() { fun setBookmarkPostsPseudoIdsUpdated() = AppPrefs.setBookmarkPostsPseudoIdsUpdated() + fun shouldShowReaderAnnouncementCard(): Boolean = AppPrefs.getShouldShowReaderAnnouncementCard() + + fun setShouldShowReaderAnnouncementCard(shouldShow: Boolean) = + AppPrefs.setShouldShowReaderAnnouncementCard(shouldShow) + fun getAllPrefs(): Map = AppPrefs.getAllPrefs() fun setString(prefKey: PrefKey, value: String) { From b028ecda8edbd31d6234894e8591a9f2541dc1b9 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 16 May 2024 17:15:47 -0300 Subject: [PATCH 184/237] Fix and add unit tests for retry --- .../tagsfeed/ReaderTagsFeedViewModel.kt | 3 +- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 50 +++++++++++++++++-- .../ReaderTagsFeedUiStateMapperTest.kt | 2 +- 3 files changed, 50 insertions(+), 5 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 ef95b93946e9..30a21829e1bf 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 @@ -249,7 +249,8 @@ class ReaderTagsFeedViewModel @Inject constructor( _actionEvents.value = ActionEvent.OpenTagPostList(readerTag) } - private fun onRetryClick(readerTag: ReaderTag) { + @VisibleForTesting + fun onRetryClick(readerTag: ReaderTag) { launch { fetchTag(readerTag) } 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 d939380018e5..06368089184f 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 @@ -653,6 +653,52 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { ) } + @Test + fun `should fetch again when onRetryClick is called`() = testCollectingUiStates { + // Given + val tag = ReaderTestUtils.createTag("tag") + val posts = ReaderPostList().apply { + add(ReaderPost()) + } + val error = ReaderPostFetchException("error") + whenever(readerPostRepository.fetchNewerPostsForTag(tag)).doSuspendableAnswer { + delay(100) + throw error + }.doSuspendableAnswer { + delay(100) + posts + } + + mockMapInitialTagFeedItems() + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems() + mockMapErrorTagFeedItems() + + viewModel.onTagsChanged(listOf(tag)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag)) + advanceUntilIdle() + + assertThat(collectedUiStates.last()).isEqualTo( + ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf(getErrorTagFeedItem(tag)) + ) + ) + + // When + viewModel.onRetryClick(tag) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag)) + advanceUntilIdle() + + // Then + assertThat(collectedUiStates).contains( + ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf(getLoadedTagFeedItem(tag)) + ) + ) + } + private fun mockMapInitialTagFeedItems() { whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any(), any(), any(), any(), any())) .thenAnswer { @@ -704,9 +750,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { private fun getErrorTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem( ReaderTagsFeedViewModel.TagChip(tag, {}, {}), - ReaderTagsFeedViewModel.PostList.Error( - ReaderTagsFeedViewModel.ErrorType.Default, {} - ), + ReaderTagsFeedViewModel.PostList.Error(ReaderTagsFeedViewModel.ErrorType.Default, {}), ) private fun testCollectingUiStates(block: suspend TestScope.() -> Unit) = test { diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index c82f8e9ff3af..e0bfbde05d8c 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -227,7 +227,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { val errorType = ReaderTagsFeedViewModel.ErrorType.Default val onTagChipClick: (ReaderTag) -> Unit = {} val onMoreFromTagClick: (ReaderTag) -> Unit = {} - val onRetryClick = {} + val onRetryClick: (ReaderTag) -> Unit = {} val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} // When val actual = classToTest.mapErrorTagFeedItem( From 17b1525ffab7a926b9c752ea12e576a68daab5bc Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 16 May 2024 20:08:01 -0300 Subject: [PATCH 185/237] Refactor no connection error message string key --- .../promptslist/compose/BloggingPromptsListScreen.kt | 4 ++-- WordPress/src/main/res/values-ar/strings.xml | 4 ++-- WordPress/src/main/res/values-cs/strings.xml | 4 ++-- WordPress/src/main/res/values-de/strings.xml | 4 ++-- WordPress/src/main/res/values-en-rCA/strings.xml | 4 ++-- WordPress/src/main/res/values-en-rGB/strings.xml | 4 ++-- WordPress/src/main/res/values-es-rCO/strings.xml | 4 ++-- WordPress/src/main/res/values-es/strings.xml | 4 ++-- WordPress/src/main/res/values-fr-rCA/strings.xml | 4 ++-- WordPress/src/main/res/values-fr/strings.xml | 4 ++-- WordPress/src/main/res/values-gl/strings.xml | 4 ++-- WordPress/src/main/res/values-he/strings.xml | 4 ++-- WordPress/src/main/res/values-id/strings.xml | 4 ++-- WordPress/src/main/res/values-it/strings.xml | 4 ++-- WordPress/src/main/res/values-ja/strings.xml | 4 ++-- WordPress/src/main/res/values-ko/strings.xml | 4 ++-- WordPress/src/main/res/values-lv/strings.xml | 4 ++-- WordPress/src/main/res/values-nl/strings.xml | 4 ++-- WordPress/src/main/res/values-pt-rBR/strings.xml | 4 ++-- WordPress/src/main/res/values-ro/strings.xml | 4 ++-- WordPress/src/main/res/values-ru/strings.xml | 4 ++-- WordPress/src/main/res/values-sq/strings.xml | 4 ++-- WordPress/src/main/res/values-sv/strings.xml | 4 ++-- WordPress/src/main/res/values-tr/strings.xml | 4 ++-- WordPress/src/main/res/values-zh-rCN/strings.xml | 4 ++-- WordPress/src/main/res/values-zh-rHK/strings.xml | 4 ++-- WordPress/src/main/res/values-zh-rTW/strings.xml | 4 ++-- WordPress/src/main/res/values/strings.xml | 5 +++-- 28 files changed, 57 insertions(+), 56 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/compose/BloggingPromptsListScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/compose/BloggingPromptsListScreen.kt index e499fad2f4b2..a809a3bdc48c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/compose/BloggingPromptsListScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/compose/BloggingPromptsListScreen.kt @@ -112,8 +112,8 @@ private fun FetchErrorContent() { @Composable private fun NetworkErrorContent() { EmptyContent( - title = stringResource(R.string.blogging_prompts_list_error_network_title), - subtitle = stringResource(R.string.blogging_prompts_list_error_network_subtitle), + title = stringResource(R.string.no_connection_error_title), + subtitle = stringResource(R.string.no_connection_error_description), image = R.drawable.img_illustration_cloud_off_152dp, modifier = Modifier.fillMaxSize(), ) diff --git a/WordPress/src/main/res/values-ar/strings.xml b/WordPress/src/main/res/values-ar/strings.xml index 8a48702ee148..4afff1755719 100644 --- a/WordPress/src/main/res/values-ar/strings.xml +++ b/WordPress/src/main/res/values-ar/strings.xml @@ -534,14 +534,14 @@ Language: ar ستنتقل ميزة التنبيهات إلى Jetpack ستنتقل ميزة القارئ إلى تطبيق Jetpack التبديل إلى تطبيق Jetpack الجديد - يتعذر تحميل هذا المحتوى حاليًا. + يتعذر تحميل هذا المحتوى حاليًا. حدث خطأ في أثناء تحميل المطالبات. عذرًا لا توجد مطالبات بعد %d من الإجابات إجابة واحدة ستنتقل ميزة الإحصاءات لديك إلى تطبيق Jetpack - تحقق من اتصالك بالشبكة وحاول مرة أخرى. + تحقق من اتصالك بالشبكة وحاول مرة أخرى. 0 من الإجابات ✓ تم الرد المطالبات diff --git a/WordPress/src/main/res/values-cs/strings.xml b/WordPress/src/main/res/values-cs/strings.xml index a7e128c3e6cb..d8b4b56bf66b 100644 --- a/WordPress/src/main/res/values-cs/strings.xml +++ b/WordPress/src/main/res/values-cs/strings.xml @@ -148,8 +148,8 @@ Language: cs_CZ Čtečka se přesouvá do aplikace Jetpack Vaše statistiky se přesouvají do aplikace Jetpack Přepněte na novou aplikaci Jetpack - Zkontrolujte připojení k síti a zkuste to znovu. - Momentálně tento obsah nelze načíst + Zkontrolujte připojení k síti a zkuste to znovu. + Momentálně tento obsah nelze načíst Došlo k chybě při načítání to se mi líbí Jejda Zatím žádné výzvy diff --git a/WordPress/src/main/res/values-de/strings.xml b/WordPress/src/main/res/values-de/strings.xml index 995411887988..5f2d7106d702 100644 --- a/WordPress/src/main/res/values-de/strings.xml +++ b/WordPress/src/main/res/values-de/strings.xml @@ -549,8 +549,8 @@ Language: de Der Reader wird in die Jetpack-App verschoben Deine Statistiken werden in die Jetpack-App verschoben Zur neuen Jetpack-App wechseln - Prüfe deine Netzwerkverbindung und versuche es erneut. - Dieser Inhalt kann gerade nicht geladen werden + Prüfe deine Netzwerkverbindung und versuche es erneut. + Dieser Inhalt kann gerade nicht geladen werden Beim Laden der Schreibanregungen ist ein Fehler aufgetreten. Ups Noch keine Schreibanregungen diff --git a/WordPress/src/main/res/values-en-rCA/strings.xml b/WordPress/src/main/res/values-en-rCA/strings.xml index bf42b4a96689..44e41f34c9d7 100644 --- a/WordPress/src/main/res/values-en-rCA/strings.xml +++ b/WordPress/src/main/res/values-en-rCA/strings.xml @@ -491,8 +491,8 @@ Language: en_CA Stats, Reader, Notifications and other features will soon move to the Jetpack mobile app. There was an error loading prompts. Oops - Check your network connection and try again. - Unable to load this content right now + Check your network connection and try again. + Unable to load this content right now No prompts yet 1 answer %d answers diff --git a/WordPress/src/main/res/values-en-rGB/strings.xml b/WordPress/src/main/res/values-en-rGB/strings.xml index 7f5704278c21..1e5b512815c1 100644 --- a/WordPress/src/main/res/values-en-rGB/strings.xml +++ b/WordPress/src/main/res/values-en-rGB/strings.xml @@ -549,8 +549,8 @@ Language: en_GB Reader is moving to the Jetpack app Your stats are moving to the Jetpack app Switch to the new Jetpack app - Check your network connection and try again. - Unable to load this content right now + Check your network connection and try again. + Unable to load this content right now There was an error loading prompts. Oops No prompts yet diff --git a/WordPress/src/main/res/values-es-rCO/strings.xml b/WordPress/src/main/res/values-es-rCO/strings.xml index a09dd4e8652c..eac1fde19f77 100644 --- a/WordPress/src/main/res/values-es-rCO/strings.xml +++ b/WordPress/src/main/res/values-es-rCO/strings.xml @@ -549,8 +549,8 @@ Language: es_CO El lector se está trasladando a la aplicación de Jetpack La estadísticas se están trasladado a la aplicación de Jetpack Cambiar a la nueva aplicación de Jetpack - Comprueba tu conexión a la red e inténtalo de nuevo. - En este momento no se ha podido cargar este contenido + Comprueba tu conexión a la red e inténtalo de nuevo. + En este momento no se ha podido cargar este contenido Se ha producido un error al cargar las indicaciones. ¡Vaya! Todavía no hay sugerencias diff --git a/WordPress/src/main/res/values-es/strings.xml b/WordPress/src/main/res/values-es/strings.xml index d44702d9f362..3041cd1330c5 100644 --- a/WordPress/src/main/res/values-es/strings.xml +++ b/WordPress/src/main/res/values-es/strings.xml @@ -549,8 +549,8 @@ Language: es El lector se está trasladando a la aplicación de Jetpack La estadísticas se están trasladado a la aplicación de Jetpack Cambiar a la nueva aplicación de Jetpack - Comprueba tu conexión a la red e inténtalo de nuevo. - En este momento no se ha podido cargar este contenido + Comprueba tu conexión a la red e inténtalo de nuevo. + En este momento no se ha podido cargar este contenido Se ha producido un error al cargar las indicaciones. ¡Vaya! Todavía no hay sugerencias diff --git a/WordPress/src/main/res/values-fr-rCA/strings.xml b/WordPress/src/main/res/values-fr-rCA/strings.xml index bb762e3bd384..99007a63887d 100644 --- a/WordPress/src/main/res/values-fr-rCA/strings.xml +++ b/WordPress/src/main/res/values-fr-rCA/strings.xml @@ -542,8 +542,8 @@ Language: fr Le lecteur va être déplacé dans l’application Jetpack Les statistiques vont être déplacées dans l’application Jetpack Passer à la nouvelle application Jetpack - Vérifiez votre connexion réseau et réessayez. - Impossible de charger ce contenu pour le moment + Vérifiez votre connexion réseau et réessayez. + Impossible de charger ce contenu pour le moment Un problème est survenu lors du chargement des incitations. Oups Pas encore d’incitation diff --git a/WordPress/src/main/res/values-fr/strings.xml b/WordPress/src/main/res/values-fr/strings.xml index bb762e3bd384..99007a63887d 100644 --- a/WordPress/src/main/res/values-fr/strings.xml +++ b/WordPress/src/main/res/values-fr/strings.xml @@ -542,8 +542,8 @@ Language: fr Le lecteur va être déplacé dans l’application Jetpack Les statistiques vont être déplacées dans l’application Jetpack Passer à la nouvelle application Jetpack - Vérifiez votre connexion réseau et réessayez. - Impossible de charger ce contenu pour le moment + Vérifiez votre connexion réseau et réessayez. + Impossible de charger ce contenu pour le moment Un problème est survenu lors du chargement des incitations. Oups Pas encore d’incitation diff --git a/WordPress/src/main/res/values-gl/strings.xml b/WordPress/src/main/res/values-gl/strings.xml index b4d23cc2778e..5006494cabc3 100644 --- a/WordPress/src/main/res/values-gl/strings.xml +++ b/WordPress/src/main/res/values-gl/strings.xml @@ -556,9 +556,9 @@ Language: gl_ES Os avisos estanse trasladando á aplicación de Jetpack O lector estase trasladando á aplicación de Jetpack Produciuse un erro ao cargar as indicacións. - Neste momento non se puido cargar este contido + Neste momento non se puido cargar este contido A estatísticas estanse trasladado á aplicación de Jetpack - Comproba a túa conexión á rede e inténtao de novo. + Comproba a túa conexión á rede e inténtao de novo. Peticións ✓ Respondido pechar diff --git a/WordPress/src/main/res/values-he/strings.xml b/WordPress/src/main/res/values-he/strings.xml index 212e233c0619..9a13d85610f7 100644 --- a/WordPress/src/main/res/values-he/strings.xml +++ b/WordPress/src/main/res/values-he/strings.xml @@ -542,8 +542,8 @@ Language: he_IL הכלי Reader עובר לאפליקציה של Jetpack הנתונים הסטטיסטיים שלך עוברים לאפליקציה של Jetpack לעבור לאפליקציה החדשה של Jetpack - יש לבדוק את החיבור לרשת ולנסות שוב. - אין אפשרות לטעון את התוכן כרגע + יש לבדוק את החיבור לרשת ולנסות שוב. + אין אפשרות לטעון את התוכן כרגע אירעה שגיאה בטעינת ההצעות. אופס עדיין אין הצעות diff --git a/WordPress/src/main/res/values-id/strings.xml b/WordPress/src/main/res/values-id/strings.xml index d5fff60fe9fe..8233297376fd 100644 --- a/WordPress/src/main/res/values-id/strings.xml +++ b/WordPress/src/main/res/values-id/strings.xml @@ -548,8 +548,8 @@ Language: id Reader akan berpindah ke aplikasi Jetpack Statistik Anda akan berpindah ke aplikasi Jetpack Beralih ke aplikasi Jetpack baru - Periksa koneksi internet Anda dan coba lagi. - Tidak dapat memuat konten ini sekarang + Periksa koneksi internet Anda dan coba lagi. + Tidak dapat memuat konten ini sekarang Terjadi eror saat memuat prompt. Waduh Belum ada prompt diff --git a/WordPress/src/main/res/values-it/strings.xml b/WordPress/src/main/res/values-it/strings.xml index 86385c8715c0..b1c509251ad5 100644 --- a/WordPress/src/main/res/values-it/strings.xml +++ b/WordPress/src/main/res/values-it/strings.xml @@ -543,8 +543,8 @@ Language: it Le notifiche si stanno spostando sull\'app Jetpack Le tue statistiche si stanno spostando sull\'app Jetpack Passa alla nuova app Jetpack - Controlla la connessione di rete e riprova. - Impossibile caricare questo contenuto al momento + Controlla la connessione di rete e riprova. + Impossibile caricare questo contenuto al momento Si è verificato un errore durante il caricamento delle richieste. Ops Ancora nessuna richiesta diff --git a/WordPress/src/main/res/values-ja/strings.xml b/WordPress/src/main/res/values-ja/strings.xml index e0d1f587b197..443a856f6581 100644 --- a/WordPress/src/main/res/values-ja/strings.xml +++ b/WordPress/src/main/res/values-ja/strings.xml @@ -538,8 +538,8 @@ Language: ja_JP Reader は Jetpack アプリに移動しています 統計は Jetpack アプリに移動しています 新しい Jetpack アプリに切り替える - ネットワーク接続を確認して、もう一度お試しください。 - 現在このページを読み込めません + ネットワーク接続を確認して、もう一度お試しください。 + 現在このページを読み込めません プロンプトの読み込みでエラーが発生しました。 エラーです プロンプトはまだありません diff --git a/WordPress/src/main/res/values-ko/strings.xml b/WordPress/src/main/res/values-ko/strings.xml index 14fe72e23c0d..fa1858c54c0e 100644 --- a/WordPress/src/main/res/values-ko/strings.xml +++ b/WordPress/src/main/res/values-ko/strings.xml @@ -542,8 +542,8 @@ Language: ko_KR 젯팩 앱으로 리더 이동 중 젯팩 앱으로 통계 이동 중 새 젯팩 앱으로 전환 - 네트워크 연결을 확인하고 다시 시도하세요. - 지금 이 콘텐츠를 로드할 수 없습니다. + 네트워크 연결을 확인하고 다시 시도하세요. + 지금 이 콘텐츠를 로드할 수 없습니다. 프롬프트 로드 중 오류가 발생했습니다. 죄송합니다. 아직 프롬프트 없음 diff --git a/WordPress/src/main/res/values-lv/strings.xml b/WordPress/src/main/res/values-lv/strings.xml index 600711a3a7a2..20cccf2ded05 100644 --- a/WordPress/src/main/res/values-lv/strings.xml +++ b/WordPress/src/main/res/values-lv/strings.xml @@ -280,8 +280,8 @@ Language: lv Lasītājs pāriet uz Jetpack lietotni Jūsu statistika tiek pārvietota uz Jetpack lietotni Pārslēdzieties uz jauno Jetpack lietotni - Pārbaudiet tīkla savienojumu un mēģiniet vēlreiz. - Pašlaik nevar ielādēt šo saturu + Pārbaudiet tīkla savienojumu un mēģiniet vēlreiz. + Pašlaik nevar ielādēt šo saturu Ielādējot uzvednes, radās kļūda. Hmm… Vēl nav uzvedņu diff --git a/WordPress/src/main/res/values-nl/strings.xml b/WordPress/src/main/res/values-nl/strings.xml index 3f3e5c65075f..b13e6af15c54 100644 --- a/WordPress/src/main/res/values-nl/strings.xml +++ b/WordPress/src/main/res/values-nl/strings.xml @@ -549,8 +549,8 @@ Language: nl Reader zal naar de Jetpack-app worden verplaatst Je statistieken zullen naar de Jetpack-app worden verplaatst Schakel over naar de nieuwe Jetpack-app - Controleer je netwerkverbinding en probeer het nogmaals. - Deze inhoud kan momenteel niet geladen worden + Controleer je netwerkverbinding en probeer het nogmaals. + Deze inhoud kan momenteel niet geladen worden Er is een fout opgetreden bij het laden van opdrachten. Oeps Er zijn nog geen opdrachten diff --git a/WordPress/src/main/res/values-pt-rBR/strings.xml b/WordPress/src/main/res/values-pt-rBR/strings.xml index e4a4322da026..d276a53dab52 100644 --- a/WordPress/src/main/res/values-pt-rBR/strings.xml +++ b/WordPress/src/main/res/values-pt-rBR/strings.xml @@ -312,8 +312,8 @@ Language: pt_BR O Leitor está migrando para o aplicativo Jetpack Suas estatísticas estão migrando para o aplicativo Jetpack Mudar para o novo aplicativo do Jetpack - Verifique sua conexão de rede e tente novamente. - Não foi possível carregar esse conteúdo no momento + Verifique sua conexão de rede e tente novamente. + Não foi possível carregar esse conteúdo no momento Ocorreu um erro ao carregar as sugestões. Opa Nenhuma sugestão ainda diff --git a/WordPress/src/main/res/values-ro/strings.xml b/WordPress/src/main/res/values-ro/strings.xml index 36b4adb1ff35..b53e1f914ff9 100644 --- a/WordPress/src/main/res/values-ro/strings.xml +++ b/WordPress/src/main/res/values-ro/strings.xml @@ -549,8 +549,8 @@ Language: ro Cititorul a fost mutat în aplicația Jetpack Statisticile se mută în aplicația Jetpack Comută la noua aplicație Jetpack - Verifică conexiunea rețelei și încearcă din nou. - Nu pot să încarc acest conținut chiar acum + Verifică conexiunea rețelei și încearcă din nou. + Nu pot să încarc acest conținut chiar acum A fost o eroare la încărcarea îndemnurilor. Hopa Niciun îndemn încă diff --git a/WordPress/src/main/res/values-ru/strings.xml b/WordPress/src/main/res/values-ru/strings.xml index c93edc6bb2d2..fb236e7a2f07 100644 --- a/WordPress/src/main/res/values-ru/strings.xml +++ b/WordPress/src/main/res/values-ru/strings.xml @@ -547,8 +547,8 @@ Language: ru Чтиво переезжает в приложение Jetpack Статистика переезжает в приложение Jetpack Перейти в новое приложение Jetpack - Проверьте ваше подключение к сети и попробуйте снова. - Сейчас невозможно загрузить это содержимое + Проверьте ваше подключение к сети и попробуйте снова. + Сейчас невозможно загрузить это содержимое При загрузке подсказок возникла ошибка. Ой! Пока нет подсказок diff --git a/WordPress/src/main/res/values-sq/strings.xml b/WordPress/src/main/res/values-sq/strings.xml index 11a3d337c205..18f2adb9e44c 100644 --- a/WordPress/src/main/res/values-sq/strings.xml +++ b/WordPress/src/main/res/values-sq/strings.xml @@ -520,8 +520,8 @@ Language: sq_AL Lexuesi po kalohet te aplikacioni Jetpack Statistikat tuaja po kalojnë te aplikacioni Jetpack Kaloni në aplikacionin e ri Jetpack - Kontrolloni lidhjen tuaj në rrjet dhe riprovoni. - S’arrihet të ngarkohet kjo lëndë këtë çast + Kontrolloni lidhjen tuaj në rrjet dhe riprovoni. + S’arrihet të ngarkohet kjo lëndë këtë çast Pati një gabim në ngarkim cytjesh. Hëm Ende pa cytje diff --git a/WordPress/src/main/res/values-sv/strings.xml b/WordPress/src/main/res/values-sv/strings.xml index df5eade59e7d..a0fb595b94c8 100644 --- a/WordPress/src/main/res/values-sv/strings.xml +++ b/WordPress/src/main/res/values-sv/strings.xml @@ -549,8 +549,8 @@ Language: sv_SE Läsaren flyttar till Jetpack-appen Din statistik flyttas till Jetpack-appen Byt till den nya Jetpack-appen - Kontrollera din nätverksanslutning och försök igen. - Det går inte att ladda detta innehåll just nu + Kontrollera din nätverksanslutning och försök igen. + Det går inte att ladda detta innehåll just nu Det var ett problem att ladda in förslag. Hoppsan Inga förslag än diff --git a/WordPress/src/main/res/values-tr/strings.xml b/WordPress/src/main/res/values-tr/strings.xml index 6bdc41bf6bd1..4eefcfe02e32 100644 --- a/WordPress/src/main/res/values-tr/strings.xml +++ b/WordPress/src/main/res/values-tr/strings.xml @@ -546,8 +546,8 @@ Language: tr Okuyucu, Jetpack uygulamasına taşınıyor İstatistikleriniz Jetpack uygulamasına taşınıyor Yeni Jetpack uygulamasına geçiş yapın - Ağ bağlantınızı denetleyip yeniden deneyin. - Bu içerik şu an yüklenemiyor + Ağ bağlantınızı denetleyip yeniden deneyin. + Bu içerik şu an yüklenemiyor İstemler yüklenirken bir sorun çıktı. Eyvah Henüz bir istem yok diff --git a/WordPress/src/main/res/values-zh-rCN/strings.xml b/WordPress/src/main/res/values-zh-rCN/strings.xml index 3844b72060a9..9cefd4408257 100644 --- a/WordPress/src/main/res/values-zh-rCN/strings.xml +++ b/WordPress/src/main/res/values-zh-rCN/strings.xml @@ -540,8 +540,8 @@ Language: zh_CN 阅读器即将移至 Jetpack 应用 统计信息即将移至 Jetpack 应用 切换到新版 Jetpack 应用 - 请检查您的网络连接,然后重试。 - 现在无法加载此内容 + 请检查您的网络连接,然后重试。 + 现在无法加载此内容 加载提示时出错。 糟糕 尚无提示 diff --git a/WordPress/src/main/res/values-zh-rHK/strings.xml b/WordPress/src/main/res/values-zh-rHK/strings.xml index 7106eb840445..f6cce6eb1afa 100644 --- a/WordPress/src/main/res/values-zh-rHK/strings.xml +++ b/WordPress/src/main/res/values-zh-rHK/strings.xml @@ -542,8 +542,8 @@ Language: zh_TW 「閱讀器」將轉移至 Jetpack 應用程式 你的統計資料將轉移至 Jetpack 應用程式 切換至全新 Jetpack 應用程式 - 請檢查你的網路連線並再試一次。 - 目前無法載入此內容 + 請檢查你的網路連線並再試一次。 + 目前無法載入此內容 載入提示時發生錯誤。 糟糕 尚無提示 diff --git a/WordPress/src/main/res/values-zh-rTW/strings.xml b/WordPress/src/main/res/values-zh-rTW/strings.xml index 7106eb840445..f6cce6eb1afa 100644 --- a/WordPress/src/main/res/values-zh-rTW/strings.xml +++ b/WordPress/src/main/res/values-zh-rTW/strings.xml @@ -542,8 +542,8 @@ Language: zh_TW 「閱讀器」將轉移至 Jetpack 應用程式 你的統計資料將轉移至 Jetpack 應用程式 切換至全新 Jetpack 應用程式 - 請檢查你的網路連線並再試一次。 - 目前無法載入此內容 + 請檢查你的網路連線並再試一次。 + 目前無法載入此內容 載入提示時發生錯誤。 糟糕 尚無提示 diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index e9ec2d338432..f7486889e865 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -122,6 +122,9 @@ <Experimental> + Unable to load this content right now + Check your network connection and try again. + %d selected @@ -4310,8 +4313,6 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> No prompts yet Oops There was an error loading prompts. - Unable to load this content right now - Check your network connection and try again. Content From 3934df93dfd2e978fd20913a408b3c2bc8ec446f Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 16 May 2024 20:17:05 -0300 Subject: [PATCH 186/237] Implement NoConnection state --- .../ui/reader/ReaderTagsFeedFragment.kt | 1 + .../tagsfeed/ReaderTagsFeedViewModel.kt | 54 ++++++++++-- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 84 ++++++++++++++----- 3 files changed, 111 insertions(+), 28 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 b96d71fef96b..b154d8b3890d 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 @@ -101,6 +101,7 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme observeErrorMessageEvents() observeSnackbarEvents() observeOpenMoreMenuEvents() + viewModel.onViewCreated() } override fun onDestroy() { 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 30a21829e1bf..2a0344f7ef56 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 @@ -6,6 +6,7 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -32,6 +33,7 @@ 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.DisplayUtilsWrapper +import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent @@ -50,6 +52,7 @@ class ReaderTagsFeedViewModel @Inject constructor( private val readerPostUiStateBuilder: ReaderPostUiStateBuilder, private val displayUtilsWrapper: DisplayUtilsWrapper, private val readerTracker: ReaderTracker, + private val networkUtilsWrapper: NetworkUtilsWrapper, ) : ScopedViewModel(bgDispatcher) { private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow @@ -75,6 +78,16 @@ class ReaderTagsFeedViewModel @Inject constructor( private var hasInitialized = false + fun onViewCreated() { + if (!hasInitialized) { + hasInitialized = true + readerPostCardActionsHandler.initScope(viewModelScope) + initNavigationEvents() + initSnackbarEvents() + initUiState() + } + } + /** * Fetch multiple tag posts in parallel. Each tag load causes a new state to be emitted, so multiple emissions of * [uiStateFlow] are expected when calling this method for each tag, since each can go through the following @@ -93,13 +106,6 @@ class ReaderTagsFeedViewModel @Inject constructor( return } - if (!hasInitialized) { - hasInitialized = true - readerPostCardActionsHandler.initScope(viewModelScope) - initNavigationEvents() - initSnackbarEvents() - } - // Add tags to the list with the posts loading UI _uiStateFlow.update { readerTagsFeedUiStateMapper.mapInitialPostsUiState( @@ -125,6 +131,27 @@ class ReaderTagsFeedViewModel @Inject constructor( } } + private fun initUiState() { + _uiStateFlow.value = if (networkUtilsWrapper.isNetworkAvailable()) { + UiState.Loading + } else { + UiState.NoConnection(::onNoConnectionRetryClick) + } + } + + private fun onNoConnectionRetryClick() { + _uiStateFlow.value = UiState.Loading + if (networkUtilsWrapper.isNetworkAvailable()) { + _actionEvents.value = ActionEvent.RefreshTags + } else { + // delay a bit before returning to NoConnection for a better feedback to the user + launch { + delay(NO_CONNECTION_DELAY) + _uiStateFlow.value = UiState.NoConnection(::onNoConnectionRetryClick) + } + } + } + /** * 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. @@ -210,6 +237,11 @@ class ReaderTagsFeedViewModel @Inject constructor( @VisibleForTesting fun onRefresh() { + if (!networkUtilsWrapper.isNetworkAvailable()) { + _errorMessageEvents.postValue(Event(R.string.no_network_message)) + return + } + _uiStateFlow.update { (it as? UiState.Loaded)?.copy(isRefreshing = true) ?: it } @@ -217,6 +249,8 @@ class ReaderTagsFeedViewModel @Inject constructor( } fun onBackFromTagDetails() { + if (!networkUtilsWrapper.isNetworkAvailable()) return + _actionEvents.value = ActionEvent.RefreshTags } @@ -501,6 +535,8 @@ class ReaderTagsFeedViewModel @Inject constructor( data object Loading : UiState() data class Empty(val onOpenTagsListClick: () -> Unit) : UiState() + + data class NoConnection(val onRetryClick: () -> Unit) : UiState() } data class TagFeedItem( @@ -542,4 +578,8 @@ class ReaderTagsFeedViewModel @Inject constructor( val readerCardUiState: ReaderCardUiState.ReaderPostNewUiState, val readerPostCardActions: List, ) + + companion object { + private const val NO_CONNECTION_DELAY = 500L + } } 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 ef85b60a357c..789bf55173d7 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 @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults @@ -78,6 +79,7 @@ fun ReaderTagsFeed(uiState: UiState) { is UiState.Loading -> Loading() is UiState.Loaded -> Loaded(uiState) is UiState.Empty -> Empty(uiState) + is UiState.NoConnection -> NoConnection(uiState) is UiState.Initial -> { // no-op } @@ -134,7 +136,7 @@ private fun Loaded(uiState: UiState.Loaded) { when (postList) { is PostList.Initial, is PostList.Loading -> PostListLoading() is PostList.Loaded -> PostListLoaded(postList, tagChip, backgroundColor) - is PostList.Error -> PostListError(backgroundColor, tagChip, postList) + is PostList.Error -> PostListError(postList, tagChip, backgroundColor) } Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value)) } @@ -266,6 +268,28 @@ private fun Empty(uiState: UiState.Empty) { } } +@Composable +fun NoConnection(uiState: UiState.NoConnection) { + val backgroundColor = if (isSystemInDarkTheme()) { + AppColor.White.copy(alpha = 0.12F) + } else { + AppColor.Black.copy(alpha = 0.08F) + } + + Box(modifier = Modifier.fillMaxSize()) { + ErrorMessage( + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(), + backgroundColor = backgroundColor, + titleText = stringResource(R.string.no_connection_error_title), + descriptionText = stringResource(R.string.no_connection_error_description), + actionText = stringResource(R.string.reader_tags_feed_error_retry), + onActionClick = uiState.onRetryClick, + ) + } +} + @Composable private fun PostListLoading() { val loadingLabel = stringResource(id = R.string.loading) @@ -369,47 +393,65 @@ private fun PostListLoaded( @Composable private fun PostListError( - backgroundColor: Color, - tagChip: TagChip, postList: PostList.Error, + tagChip: TagChip, + backgroundColor: Color, ) { - Column( + val tagName = tagChip.tag.tagDisplayName + val errorMessage = when (postList.type) { + is ErrorType.Default -> stringResource(R.string.reader_tags_feed_loading_error_description) + is ErrorType.NoContent -> stringResource(R.string.reader_tags_feed_no_content_error_description, tagName) + } + + ErrorMessage( modifier = Modifier .heightIn(min = ReaderTagsFeedComposeUtils.PostItemHeight) - .fillMaxWidth() + .fillMaxWidth(), + backgroundColor = backgroundColor, + titleText = stringResource(id = R.string.reader_tags_feed_error_title, tagName), + descriptionText = errorMessage, + actionText = stringResource(R.string.reader_tags_feed_error_retry), + onActionClick = { postList.onRetryClick(tagChip.tag) } + ) +} + +@Composable +private fun ErrorMessage( + backgroundColor: Color, + titleText: String, + descriptionText: String, + actionText: String, + modifier: Modifier = Modifier, + onActionClick: () -> Unit, +) { + Column( + modifier = modifier .semantics(mergeDescendants = true) {} .padding(start = 60.dp, end = 60.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - Spacer(modifier = Modifier.height(Margin.Medium.value)) Icon( modifier = Modifier - .drawBehind { - drawCircle( - color = backgroundColor, - radius = this.size.maxDimension - ) - }, + .background( + color = backgroundColor, + shape = CircleShape + ) + .padding(Margin.Medium.value), painter = painterResource(R.drawable.ic_wifi_off_24px), tint = MaterialTheme.colors.onSurface, contentDescription = null ) Spacer(modifier = Modifier.height(Margin.ExtraMediumLarge.value)) - val tagName = tagChip.tag.tagDisplayName Text( - text = stringResource(id = R.string.reader_tags_feed_error_title, tagName), + text = titleText, style = androidx.compose.material3.MaterialTheme.typography.labelLarge, color = MaterialTheme.colors.onSurface, textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(Margin.Small.value)) - val errorMessage = when (postList.type) { - is ErrorType.Default -> stringResource(R.string.reader_tags_feed_loading_error_description) - is ErrorType.NoContent -> stringResource(R.string.reader_tags_feed_no_content_error_description, tagName) - } Text( - text = errorMessage, + text = descriptionText, style = androidx.compose.material3.MaterialTheme.typography.bodySmall, color = if (isSystemInDarkTheme()) { AppColor.White.copy(alpha = 0.4F) @@ -420,7 +462,7 @@ private fun PostListError( ) Spacer(modifier = Modifier.height(Margin.Large.value)) Button( - onClick = { postList.onRetryClick(tagChip.tag) }, + onClick = onActionClick, modifier = Modifier .height(36.dp) .widthIn(min = 114.dp), @@ -440,7 +482,7 @@ private fun PostListError( style = androidx.compose.material3.MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Medium, color = MaterialTheme.colors.surface, - text = stringResource(R.string.reader_tags_feed_error_retry), + text = actionText, ) } } From 1251497bd543eae64ad43520de5a18286a2c19b3 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 16 May 2024 20:17:26 -0300 Subject: [PATCH 187/237] Improve snackbar code for better positioning --- .../ui/reader/ReaderTagsFeedFragment.kt | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 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 b154d8b3890d..531c9742fc38 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 @@ -283,8 +283,8 @@ 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() + if (isAdded) { + WPSnackbar.make(binding.root, getString(stringRes), Snackbar.LENGTH_LONG).show() } } } @@ -292,20 +292,18 @@ class ReaderTagsFeedFragment : ViewPagerFragment(R.layout.reader_tag_feed_fragme private fun observeSnackbarEvents() { viewModel.snackbarEvents.observeEvent(viewLifecycleOwner) { snackbarMessageHolder -> if (isAdded) { - activity?.findViewById(R.id.coordinator)?.let { coordinator -> - with(snackbarMessageHolder) { - val snackbar = WPSnackbar.make( - coordinator, - uiHelpers.getTextOfUiString(requireContext(), message), - Snackbar.LENGTH_LONG - ) - if (buttonTitle != null) { - snackbar.setAction(uiHelpers.getTextOfUiString(requireContext(), buttonTitle)) { - buttonAction.invoke() - } + with(snackbarMessageHolder) { + val snackbar = WPSnackbar.make( + binding.root, + uiHelpers.getTextOfUiString(requireContext(), message), + Snackbar.LENGTH_LONG + ) + if (buttonTitle != null) { + snackbar.setAction(uiHelpers.getTextOfUiString(requireContext(), buttonTitle)) { + buttonAction.invoke() } - snackbar.show() } + snackbar.show() } } } From 9643e599410b1b00c640d4f1f34232e5623efacd Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 16 May 2024 23:51:20 -0300 Subject: [PATCH 188/237] WIP reader announcement card UI --- .../views/compose/ReaderAnnouncementCard.kt | 172 ++++++++++++++++++ WordPress/src/main/res/values/strings.xml | 1 + 2 files changed, 173 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt new file mode 100644 index 000000000000..48bd920b7598 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt @@ -0,0 +1,172 @@ +package org.wordpress.android.ui.reader.views.compose + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.wordpress.android.R +import org.wordpress.android.designsystem.footnote +import org.wordpress.android.ui.compose.theme.AppColor +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.unit.Margin + +@Composable +fun ReaderAnnouncementCard( + items: List +) { + val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value), + ) { + // Title + Text( + text = stringResource(R.string.reader_announcement_card_title), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + // Items + LazyColumn( + modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value) + ) { + items( + items = items, + ) { + ReaderAnnouncementCardItem(it) + } + } + // Done button + Button( + modifier = Modifier + .padding( +// vertical = Margin.Small.value, + horizontal = Margin.Large.value, + ) + .fillMaxWidth(), + onClick = { }, + elevation = ButtonDefaults.elevation(0.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colorScheme.onSurface, + ), + ) { + Text( + text = stringResource(id = R.string.reader_btn_done), + color = MaterialTheme.colorScheme.surface, + ) + } + } +} + +@Composable +private fun ReaderAnnouncementCardItem(data: ReaderAnnouncementCardItemData) { + val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black + Row( + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minWidth = 54.dp, minHeight = 54.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val iconBackgroundColor = MaterialTheme.colorScheme.onSurface + Icon( + modifier = Modifier + .padding( + start = Margin.Large.value, + end = Margin.Large.value + ) + .drawBehind { + drawCircle( + color = iconBackgroundColor, + radius = this.size.maxDimension, + ) + }, + painter = painterResource(data.iconRes), + tint = MaterialTheme.colorScheme.surface, + contentDescription = null + ) + Column(verticalArrangement = Arrangement.Center) { + Text( + modifier = Modifier.padding( + start = Margin.Large.value, + ), + text = stringResource(data.titleRes), + style = MaterialTheme.typography.labelLarge, + color = baseColor, + ) + val secondaryElementColor = baseColor.copy( + alpha = 0.6F + ) + Text( + modifier = Modifier.padding( + start = Margin.Large.value, + ), + text = stringResource(data.titleRes), + style = MaterialTheme.typography.footnote, + color = secondaryElementColor, + ) + } + } +} + +data class ReaderAnnouncementCardItemData( + @DrawableRes val iconRes: Int, + @StringRes val titleRes: Int, + @StringRes val descriptionRes: Int, +) + + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun ReaderTagsFeedPostListItemPreview() { + AppTheme { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + ReaderAnnouncementCard( + items = listOf( + ReaderAnnouncementCardItemData( + iconRes = R.drawable.ic_wifi_off_24px, + titleRes = R.string.reader_tags_display_name, + descriptionRes = R.string.reader_tags_feed_loading_error_description, + ), + ReaderAnnouncementCardItemData( + iconRes = R.drawable.ic_wifi_off_24px, + titleRes = R.string.reader_tags_display_name, + descriptionRes = R.string.reader_tags_feed_loading_error_description, + ), + ReaderAnnouncementCardItemData( + iconRes = R.drawable.ic_wifi_off_24px, + titleRes = R.string.reader_tags_display_name, + descriptionRes = R.string.reader_tags_feed_loading_error_description, + ), + ) + ) + } + } +} diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index d5b1681152a2..e5c8176cc52e 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1709,6 +1709,7 @@ 0 Tags 1 Tag %d Tags + New in Reader Reading Preferences reading,colors,fonts From 1c3dddccb2901b12b7bf3962743a0051622fe387 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 17 May 2024 00:08:52 -0300 Subject: [PATCH 189/237] Fix reader announcement card text style --- .../android/ui/reader/views/compose/ReaderAnnouncementCard.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt index 48bd920b7598..1f112b4b6022 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt @@ -63,7 +63,6 @@ fun ReaderAnnouncementCard( Button( modifier = Modifier .padding( -// vertical = Margin.Small.value, horizontal = Margin.Large.value, ) .fillMaxWidth(), @@ -76,6 +75,7 @@ fun ReaderAnnouncementCard( Text( text = stringResource(id = R.string.reader_btn_done), color = MaterialTheme.colorScheme.surface, + style = MaterialTheme.typography.labelLarge, ) } } From 2b7e362d2bb3e55eb2b2971f2c334cce1e5acba8 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 17 May 2024 01:16:46 -0300 Subject: [PATCH 190/237] WIP Reader announcement card UI integration --- .../android/ui/reader/ReaderFragment.kt | 16 ++++++++++++++++ .../ui/reader/viewmodels/ReaderViewModel.kt | 4 ++++ .../views/compose/ReaderAnnouncementCard.kt | 1 - .../main/res/layout/reader_fragment_layout.xml | 12 ++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 474ef518d722..7322e9473740 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -53,6 +53,7 @@ import org.wordpress.android.ui.reader.subfilter.SubfilterCategory import org.wordpress.android.ui.reader.subfilter.SubfilterListItem import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState +import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCard import org.wordpress.android.ui.reader.views.compose.ReaderTopAppBar import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterType import org.wordpress.android.ui.utils.UiHelpers @@ -99,6 +100,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding = ReaderFragmentLayoutBinding.bind(view).apply { initTopAppBar() + initAnnouncementCard() initViewModel(savedInstanceState) } } @@ -181,6 +183,20 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView } } + private fun ReaderFragmentLayoutBinding.initAnnouncementCard() { + readerAnnouncementCardComposeView.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val announcementCardState by viewModel.announcementCardState.observeAsState() + val state = announcementCardState ?: return@setContent + + AppTheme { + ReaderAnnouncementCard(items = listOf()) + } + } + } + } + private fun ReaderFragmentLayoutBinding.initViewModel(savedInstanceState: Bundle?) { viewModel = ViewModelProvider(this@ReaderFragment, viewModelFactory)[ReaderViewModel::class.java] startReaderViewModel(savedInstanceState) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 97106e3b3326..30905a9c013f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -43,6 +43,7 @@ import org.wordpress.android.ui.reader.utils.DateProvider import org.wordpress.android.ui.reader.utils.ReaderTopBarMenuHelper import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState.TabUiState +import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCardItemData import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterSelectedItem import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterType import org.wordpress.android.ui.utils.UiString @@ -93,6 +94,9 @@ class ReaderViewModel @Inject constructor( private val _topBarUiState = MutableLiveData() val topBarUiState: LiveData = _topBarUiState.distinct() + private val _announcementCardState = MutableLiveData() + val announcementCardState: LiveData = _announcementCardState + private val _updateTags = MutableLiveData>() val updateTags: LiveData> = _updateTags diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt index 1f112b4b6022..6ae5bcac59df 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt @@ -36,7 +36,6 @@ import org.wordpress.android.ui.compose.unit.Margin fun ReaderAnnouncementCard( items: List ) { - val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value), diff --git a/WordPress/src/main/res/layout/reader_fragment_layout.xml b/WordPress/src/main/res/layout/reader_fragment_layout.xml index 64d03853edf3..6d5eef001958 100644 --- a/WordPress/src/main/res/layout/reader_fragment_layout.xml +++ b/WordPress/src/main/res/layout/reader_fragment_layout.xml @@ -18,6 +18,18 @@ + + + + + + Date: Fri, 17 May 2024 10:51:07 -0300 Subject: [PATCH 191/237] Add and update unit tests for no connection scenarios --- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) 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 06368089184f..83baaa4a6c28 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 @@ -15,10 +15,12 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.doSuspendableAnswer +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest +import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper import org.wordpress.android.getOrAwaitValue @@ -41,9 +43,11 @@ import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewMod import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.util.DisplayUtilsWrapper +import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.viewmodel.Event import kotlin.test.assertIs +@Suppress("LargeClass") @OptIn(ExperimentalCoroutinesApi::class) class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Mock @@ -79,12 +83,16 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Mock lateinit var snackbarEvents: MediatorLiveData> + @Mock + lateinit var networkUtilsWrapper: NetworkUtilsWrapper + private lateinit var viewModel: ReaderTagsFeedViewModel private val collectedUiStates: MutableList = mutableListOf() private val actionEvents = mutableListOf() private val readerNavigationEvents = mutableListOf>() + private val errorMessageEvents = mutableListOf>() val tag = ReaderTag( "tag", @@ -107,6 +115,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { readerPostUiStateBuilder = readerPostUiStateBuilder, displayUtilsWrapper = displayUtilsWrapper, readerTracker = readerTracker, + networkUtilsWrapper = networkUtilsWrapper, ) whenever(readerPostCardActionsHandler.navigationEvents) .thenReturn(navigationEvents) @@ -114,6 +123,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { .thenReturn(snackbarEvents) observeActionEvents() observeNavigationEvents() + observeErrorMessageEvents() } @Test @@ -386,6 +396,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { val posts2 = ReaderPostList().apply { add(ReaderPost()) } + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { delay(100) posts1 @@ -448,6 +459,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { val posts2 = ReaderPostList().apply { add(ReaderPost()) } + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { delay(100) posts1 @@ -487,6 +499,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { val posts2 = ReaderPostList().apply { add(ReaderPost()) } + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { delay(100) posts1 @@ -514,6 +527,46 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertThat(action).isEqualTo(ActionEvent.RefreshTags) } + @Suppress("LongMethod") + @Test + fun `given tags fetched and no connection, when refreshing, then show error message`() = testCollectingUiStates { + // Given + val tag1 = ReaderTestUtils.createTag("tag1") + val tag2 = ReaderTestUtils.createTag("tag2") + val posts1 = ReaderPostList().apply { + add(ReaderPost()) + } + val posts2 = ReaderPostList().apply { + add(ReaderPost()) + } + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false) + whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { + delay(100) + posts1 + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag2)).doSuspendableAnswer { + delay(200) + posts2 + } + mockMapInitialTagFeedItems() + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems() + + // When + viewModel.onTagsChanged(listOf(tag1, tag2)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag2)) + advanceUntilIdle() + + // Then + viewModel.onRefresh() + + val messageRes = errorMessageEvents.last().peekContent() + assertThat(messageRes).isEqualTo(R.string.no_network_message) + } + @Test fun `Should update UI immediately when like button is tapped`() = testCollectingUiStates { // Given @@ -603,6 +656,8 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Test fun `Should emit RefreshTags when onBackFromTagDetails is called`() { + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) + // When viewModel.onBackFromTagDetails() @@ -610,6 +665,17 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertIs(actionEvents.first()) } + @Test + fun `Should not emit RefreshTags when onBackFromTagDetails is called with no connection`() { + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false) + + // When + viewModel.onBackFromTagDetails() + + // Then + actionEvents.isEmpty() + } + @Test fun `Should track READER_POST_CARD_TAPPED when onPostCardClick is called`() = testCollectingUiStates { // Given @@ -699,6 +765,80 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { ) } + @Test + fun `when calling onViewCreated multiple times, then initialize handler once`() = test { + // When + viewModel.onViewCreated() + viewModel.onViewCreated() + + // Then + verify(readerPostCardActionsHandler, times(1)).initScope(any()) + } + + @Test + fun `given connection on, when onViewCreated, then init UI state with Loading`() = testCollectingUiStates { + // Given + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) + + // When + viewModel.onViewCreated() + + // Then + assertThat(collectedUiStates.last()).isEqualTo(ReaderTagsFeedViewModel.UiState.Loading) + } + + @Test + fun `given connection off, when onViewCreated, then init UI state with NoConnection`() = testCollectingUiStates { + // Given + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false) + + // When + viewModel.onViewCreated() + + // Then + assertThat(collectedUiStates.last()).isInstanceOf(ReaderTagsFeedViewModel.UiState.NoConnection::class.java) + } + + @Test + fun `given NoConnectionState and connection off, when onRetryClick, then UI state is NoConnection`() = + testCollectingUiStates { + // Given + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false) + viewModel.onViewCreated() + val noConnectionState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.NoConnection + + // When + noConnectionState.onRetryClick() + advanceUntilIdle() + + // Then + val lastStates = collectedUiStates.takeLast(2) + assertThat(lastStates[0]).isEqualTo(ReaderTagsFeedViewModel.UiState.Loading) + assertThat(lastStates[1]).isInstanceOf(ReaderTagsFeedViewModel.UiState.NoConnection::class.java) + } + + @Test + fun `given NoConnectionState and connection on, when onRetryClick, then refresh is requested`() = + testCollectingUiStates { + // Given + whenever(networkUtilsWrapper.isNetworkAvailable()) + .thenReturn(false) + .thenReturn(true) + viewModel.onViewCreated() + val noConnectionState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.NoConnection + + // When + noConnectionState.onRetryClick() + advanceUntilIdle() + + // Then + val lastState = collectedUiStates.last() + assertThat(lastState).isEqualTo(ReaderTagsFeedViewModel.UiState.Loading) + viewModel.actionEvents.getOrAwaitValue().let { + assertThat(it).isEqualTo(ActionEvent.RefreshTags) + } + } + private fun mockMapInitialTagFeedItems() { whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any(), any(), any(), any(), any())) .thenAnswer { @@ -773,4 +913,10 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { it?.let { readerNavigationEvents.add(it) } } } + + private fun observeErrorMessageEvents() { + viewModel.errorMessageEvents.observeForever { + it?.let { errorMessageEvents.add(it) } + } + } } From 5c9f9bc714a1d5e8b634db23111150469176891a Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 17 May 2024 15:05:20 -0300 Subject: [PATCH 192/237] Fix scrolling of reader announcement card --- .../views/compose/ReaderAnnouncementCard.kt | 8 +-- .../res/layout/reader_fragment_layout.xml | 55 ++++++++++++------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt index 6ae5bcac59df..e2e0c9c0a74c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt @@ -37,7 +37,9 @@ fun ReaderAnnouncementCard( items: List ) { Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(Margin.ExtraLarge.value), verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value), ) { // Title @@ -61,9 +63,6 @@ fun ReaderAnnouncementCard( // Done button Button( modifier = Modifier - .padding( - horizontal = Margin.Large.value, - ) .fillMaxWidth(), onClick = { }, elevation = ButtonDefaults.elevation(0.dp), @@ -145,7 +144,6 @@ fun ReaderTagsFeedPostListItemPreview() { Box( modifier = Modifier .fillMaxWidth() - .padding(16.dp) ) { ReaderAnnouncementCard( items = listOf( diff --git a/WordPress/src/main/res/layout/reader_fragment_layout.xml b/WordPress/src/main/res/layout/reader_fragment_layout.xml index 6d5eef001958..081157fd6aa8 100644 --- a/WordPress/src/main/res/layout/reader_fragment_layout.xml +++ b/WordPress/src/main/res/layout/reader_fragment_layout.xml @@ -18,30 +18,45 @@ - + android:layout_height="match_parent" + android:paddingBottom="56dp" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + - - - - - From b45ef93d255bdb75919e8ebb05aad73e97a0933f Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 17 May 2024 16:22:47 -0300 Subject: [PATCH 193/237] Add real data to reader announcement card --- .../android/ui/reader/ReaderFragment.kt | 11 ++++--- .../ui/reader/viewmodels/ReaderViewModel.kt | 31 +++++++++++++++++-- .../views/compose/ReaderAnnouncementCard.kt | 2 +- .../usecases/TagsAndCategoriesUseCase.kt | 2 +- ...c_tag_white_24dp.xml => ic_reader_tag.xml} | 0 .../main/res/drawable/ic_reader_tag_24dp.xml | 9 ++++++ WordPress/src/main/res/values/strings.xml | 4 +++ .../usecases/TagsAndCategoriesUseCaseTest.kt | 2 +- 8 files changed, 51 insertions(+), 10 deletions(-) rename WordPress/src/main/res/drawable/{ic_tag_white_24dp.xml => ic_reader_tag.xml} (100%) create mode 100644 WordPress/src/main/res/drawable/ic_reader_tag_24dp.xml diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 7322e9473740..d73cb51bee1d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -187,11 +187,12 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView readerAnnouncementCardComposeView.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - val announcementCardState by viewModel.announcementCardState.observeAsState() - val state = announcementCardState ?: return@setContent - - AppTheme { - ReaderAnnouncementCard(items = listOf()) + val announcementCardUiState by viewModel.announcementCardState.observeAsState() + val state = announcementCardUiState ?: return@setContent + if (state.shouldShow) { + AppTheme { + ReaderAnnouncementCard(items = state.items) + } } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 30905a9c013f..3f978734d438 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -52,6 +52,7 @@ import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.util.QuickStartUtils import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.UrlUtilsWrapper +import org.wordpress.android.util.config.ReaderAnnouncementCardFeatureConfig import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig import org.wordpress.android.util.distinct import org.wordpress.android.viewmodel.Event @@ -82,6 +83,7 @@ class ReaderViewModel @Inject constructor( private val urlUtilsWrapper: UrlUtilsWrapper, private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig, // todo: annnmarie removed this private val getFollowedTagsUseCase: GetFollowedTagsUseCase + private val readerAnnouncementCardFeatureConfig: ReaderAnnouncementCardFeatureConfig, ) : ScopedViewModel(mainDispatcher) { private var initialized: Boolean = false private var wasPaused: Boolean = false @@ -94,8 +96,8 @@ class ReaderViewModel @Inject constructor( private val _topBarUiState = MutableLiveData() val topBarUiState: LiveData = _topBarUiState.distinct() - private val _announcementCardState = MutableLiveData() - val announcementCardState: LiveData = _announcementCardState + private val _announcementCardState = MutableLiveData() + val announcementCardState: LiveData = _announcementCardState private val _updateTags = MutableLiveData>() val updateTags: LiveData> = _updateTags @@ -129,6 +131,7 @@ class ReaderViewModel @Inject constructor( if (initialized) return loadTabs(savedInstanceState) if (jetpackBrandingUtils.shouldShowJetpackPoweredBottomSheet()) showJetpackPoweredBottomSheet() + loadAnnouncementCard() } fun onSaveInstanceState(out: Bundle) { @@ -142,6 +145,25 @@ class ReaderViewModel @Inject constructor( // _showJetpackPoweredBottomSheet.value = Event(true) } + private fun loadAnnouncementCard() { + _announcementCardState.value = AnnouncementCardUiState( + shouldShow = readerAnnouncementCardFeatureConfig.isEnabled() && + appPrefsWrapper.shouldShowReaderAnnouncementCard(), + items = listOf( + ReaderAnnouncementCardItemData( + iconRes = R.drawable.ic_reader_tag, + titleRes = R.string.reader_announcement_card_tags_stream_title, + descriptionRes = R.string.reader_announcement_card_tags_stream_description, + ), + ReaderAnnouncementCardItemData( + iconRes = R.drawable.ic_reader_preferences, + titleRes = R.string.reader_announcement_card_reading_preferences_title, + descriptionRes = R.string.reader_announcement_card_reading_preferences_description, + ), + ), + ) + } + @JvmOverloads fun loadTabs(savedInstanceState: Bundle? = null) { launch { @@ -567,6 +589,11 @@ class ReaderViewModel @Inject constructor( val duration: Int = QUICK_START_PROMPT_DURATION ) + data class AnnouncementCardUiState( + val shouldShow: Boolean, + val items: List, + ) + companion object { private const val QUICK_START_PROMPT_DURATION = 5000 private const val FILTER_UPDATE_DELAY = 50L diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt index e2e0c9c0a74c..d8ff402a26dc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt @@ -121,7 +121,7 @@ private fun ReaderAnnouncementCardItem(data: ReaderAnnouncementCardItemData) { modifier = Modifier.padding( start = Margin.Large.value, ), - text = stringResource(data.titleRes), + text = stringResource(data.descriptionRes), style = MaterialTheme.typography.footnote, color = secondaryElementColor, ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/TagsAndCategoriesUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/TagsAndCategoriesUseCase.kt index c9503dde80e4..86f2f112a70d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/TagsAndCategoriesUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/TagsAndCategoriesUseCase.kt @@ -200,7 +200,7 @@ class TagsAndCategoriesUseCase } private fun getIcon(type: String) = - if (type == "tag") R.drawable.ic_tag_white_24dp else R.drawable.ic_folder_white_24dp + if (type == "tag") R.drawable.ic_reader_tag else R.drawable.ic_folder_white_24dp private fun onLinkClick() { analyticsTracker.track(AnalyticsTracker.Stat.STATS_TAGS_AND_CATEGORIES_VIEW_MORE_TAPPED) diff --git a/WordPress/src/main/res/drawable/ic_tag_white_24dp.xml b/WordPress/src/main/res/drawable/ic_reader_tag.xml similarity index 100% rename from WordPress/src/main/res/drawable/ic_tag_white_24dp.xml rename to WordPress/src/main/res/drawable/ic_reader_tag.xml diff --git a/WordPress/src/main/res/drawable/ic_reader_tag_24dp.xml b/WordPress/src/main/res/drawable/ic_reader_tag_24dp.xml new file mode 100644 index 000000000000..9abc506e9e2b --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_reader_tag_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index e5c8176cc52e..91d91bd5278c 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1710,6 +1710,10 @@ 1 Tag %d Tags New in Reader + Tags stream + Tap the dropdown at the top and select Tags to access streams from your followed tags. + Reading Preferences + Choose colors and fonts that suit you. When you’re reading a post tap the AA icon at the top of the screen. Reading Preferences reading,colors,fonts diff --git a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/TagsAndCategoriesUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/TagsAndCategoriesUseCaseTest.kt index 264262dd7f86..cc3cf536c3cc 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/TagsAndCategoriesUseCaseTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/TagsAndCategoriesUseCaseTest.kt @@ -246,7 +246,7 @@ class TagsAndCategoriesUseCaseTest : BaseUnitTest() { } else { assertThat(item.barWidth).isNull() } - assertThat(item.icon).isEqualTo(R.drawable.ic_tag_white_24dp) + assertThat(item.icon).isEqualTo(R.drawable.ic_reader_tag) assertThat(item.contentDescription).isEqualTo(contentDescription) } From e37d1dbcab4e34db28e88c0624cb57660c43ccab Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 17 May 2024 16:57:45 -0300 Subject: [PATCH 194/237] Implement reader announcement card done button --- .../android/ui/reader/ReaderFragment.kt | 10 ++- .../ui/reader/viewmodels/ReaderViewModel.kt | 5 ++ .../views/compose/ReaderAnnouncementCard.kt | 85 +++++++++++-------- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index d73cb51bee1d..022c9627faab 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -189,10 +189,12 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView setContent { val announcementCardUiState by viewModel.announcementCardState.observeAsState() val state = announcementCardUiState ?: return@setContent - if (state.shouldShow) { - AppTheme { - ReaderAnnouncementCard(items = state.items) - } + AppTheme { + ReaderAnnouncementCard( + shouldShow = state.shouldShow, + items = state.items, + onAnnouncementCardDoneClick = { viewModel.onAnnouncementCardDoneClick() } + ) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 3f978734d438..f1c70ba13531 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -164,6 +164,11 @@ class ReaderViewModel @Inject constructor( ) } + fun onAnnouncementCardDoneClick() { + appPrefsWrapper.setShouldShowReaderAnnouncementCard(false) + loadAnnouncementCard() + } + @JvmOverloads fun loadTabs(savedInstanceState: Bundle? = null) { launch { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt index d8ff402a26dc..1cb42d94ee34 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt @@ -3,6 +3,9 @@ package org.wordpress.android.ui.reader.views.compose import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandIn +import androidx.compose.animation.shrinkOut import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -34,47 +37,57 @@ import org.wordpress.android.ui.compose.unit.Margin @Composable fun ReaderAnnouncementCard( - items: List + shouldShow: Boolean, + items: List, + onAnnouncementCardDoneClick: () -> Unit, ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(Margin.ExtraLarge.value), - verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value), + AnimatedVisibility( + visible = shouldShow, + enter = expandIn(), + exit = shrinkOut( + shrinkTowards = Alignment.TopCenter, + ), ) { - // Title - Text( - text = stringResource(R.string.reader_announcement_card_title), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - // Items - LazyColumn( + Column( modifier = Modifier - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value) - ) { - items( - items = items, - ) { - ReaderAnnouncementCardItem(it) - } - } - // Done button - Button( - modifier = Modifier - .fillMaxWidth(), - onClick = { }, - elevation = ButtonDefaults.elevation(0.dp), - colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colorScheme.onSurface, - ), + .fillMaxWidth() + .padding(Margin.ExtraLarge.value), + verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value), ) { + // Title Text( - text = stringResource(id = R.string.reader_btn_done), - color = MaterialTheme.colorScheme.surface, + text = stringResource(R.string.reader_announcement_card_title), style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, ) + // Items + LazyColumn( + modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value) + ) { + items( + items = items, + ) { + ReaderAnnouncementCardItem(it) + } + } + // Done button + Button( + modifier = Modifier + .fillMaxWidth(), + onClick = { onAnnouncementCardDoneClick() }, + elevation = ButtonDefaults.elevation(0.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colorScheme.onSurface, + ), + ) { + Text( + text = stringResource(id = R.string.reader_btn_done), + color = MaterialTheme.colorScheme.surface, + style = MaterialTheme.typography.labelLarge, + ) + } } } } @@ -146,6 +159,7 @@ fun ReaderTagsFeedPostListItemPreview() { .fillMaxWidth() ) { ReaderAnnouncementCard( + shouldShow = false, items = listOf( ReaderAnnouncementCardItemData( iconRes = R.drawable.ic_wifi_off_24px, @@ -162,7 +176,8 @@ fun ReaderTagsFeedPostListItemPreview() { titleRes = R.string.reader_tags_display_name, descriptionRes = R.string.reader_tags_feed_loading_error_description, ), - ) + ), + onAnnouncementCardDoneClick = {}, ) } } From 2c7c46bacbc2fd7273549da2515e03cfc8a6aa91 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 17 May 2024 17:18:32 -0300 Subject: [PATCH 195/237] Implement done button analytics event --- .../wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt | 2 ++ .../java/org/wordpress/android/analytics/AnalyticsTracker.java | 1 + 2 files changed, 3 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index f1c70ba13531..a3609bda6274 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -18,6 +18,7 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode.MAIN import org.wordpress.android.BuildConfig import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.fluxc.store.QuickStartStore import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask @@ -165,6 +166,7 @@ class ReaderViewModel @Inject constructor( } fun onAnnouncementCardDoneClick() { + readerTracker.track(AnalyticsTracker.Stat.READER_ANNOUNCEMENT_CARD_DISMISSED) appPrefsWrapper.setShouldShowReaderAnnouncementCard(false) loadAnnouncementCard() } diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index f9b368cc757d..38bf1079891f 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -88,6 +88,7 @@ public enum Stat { READER_READING_PREFERENCES_FEEDBACK_TAPPED, READER_READING_PREFERENCES_ITEM_TAPPED, READER_READING_PREFERENCES_SAVED, + READER_ANNOUNCEMENT_CARD_DISMISSED, STATS_ACCESSED, STATS_ACCESS_ERROR, STATS_PERIOD_ACCESSED, From 4855ec7153e7eca9299a0a991c84bef2203c9dc5 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 17 May 2024 17:48:59 -0300 Subject: [PATCH 196/237] Add mechanism to initialize observer only once Also making sure it works for Fragment recreation by the system (such as screen rotation) and ViewModel destruction due to owner being destroyed. --- .../android/ui/reader/ReaderFragment.kt | 27 +++++++++++++++++-- .../ui/reader/subfilter/SubFilterViewModel.kt | 11 +++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 474ef518d722..70267519379e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle +import android.util.Log import android.view.View import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher @@ -96,6 +97,14 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView private var readerSubsActivityResultLauncher: ActivityResultLauncher? = null + /** + * Stores the keys for [SubFilterViewModel] that have already been initialized (observers) and started. + * It is only cleared when: + * - the [ReaderFragment] is destroyed, to allow proper initialization in cases like screen rotation + * - the [SubFilterViewModel] is cleared, to allow proper initialization when the ViewModel is created again + */ + private val initializedSubFilterViewModelKeys: MutableList = mutableListOf() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding = ReaderFragmentLayoutBinding.bind(view).apply { initTopAppBar() @@ -444,11 +453,21 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView } override fun getSubFilterViewModelForTag(tag: ReaderTag, savedInstanceState: Bundle?): SubFilterViewModel { + val keyForTag = SubFilterViewModel.getViewModelKeyForTag(tag) return ViewModelProvider(getSubFilterViewModelOwner(), viewModelFactory)[ - SubFilterViewModel.getViewModelKeyForTag(tag), + keyForTag, SubFilterViewModel::class.java ].also { - it.initSubFilterViewModel(tag, savedInstanceState) + // only initialize if it hasn't been initialized yet + if (keyForTag !in initializedSubFilterViewModelKeys) { + it.initSubFilterViewModel(tag, savedInstanceState) + + // setup mechanism for proper one-time initialization and clearing + initializedSubFilterViewModelKeys.add(keyForTag) + it.onCleared.observeEvent(viewLifecycleOwner) { + initializedSubFilterViewModelKeys.remove(keyForTag) + } + } } } @@ -458,6 +477,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView currentSubFilter.observe( viewLifecycleOwner ) { subfilterListItem: SubfilterListItem -> + Log.d("ReaderFragment", "currentSubFilter.observe") onSubfilterSelected(subfilterListItem) viewModel.onSubFilterItemSelected(subfilterListItem) } @@ -466,6 +486,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView updateTagsAndSites.observe( viewLifecycleOwner ) { event: Event> -> + Log.d("ReaderFragment", "updateTagsAndSites.observe") event.applyIfNotHandled { if (NetworkUtils.isNetworkAvailable(activity)) { ReaderUpdateServiceStarter.startService(activity, this) @@ -503,6 +524,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView bottomSheetUiState.observe( viewLifecycleOwner ) { event: Event -> + Log.d("ReaderFragment", "bottomSheetUiState.observe") event.applyIfNotHandled { val fm = childFragmentManager var bottomSheet = fm.findFragmentByTag(SUBFILTER_BOTTOM_SHEET_TAG) as SubfilterBottomSheetFragment? @@ -524,6 +546,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView bottomSheetAction.observe( viewLifecycleOwner ) { event: Event -> + Log.d("ReaderFragment", "bottomSheetAction.observe") event.applyIfNotHandled { when (this) { is OpenSubsAtPage -> { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index eefb58a67f82..d35c18d86cb8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -38,7 +38,6 @@ import org.wordpress.android.util.UrlUtils import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent -import java.util.Comparator import java.util.EnumSet import javax.inject.Inject import javax.inject.Named @@ -70,6 +69,9 @@ class SubFilterViewModel @Inject constructor( private val _updateTagsAndSites = MutableLiveData>>() val updateTagsAndSites: LiveData>> = _updateTagsAndSites + private val _onCleared = MutableLiveData>() + val onCleared: LiveData> = _onCleared + private val _isTitleContainerVisible = MutableLiveData(true) val isTitleContainerVisible: LiveData = _isTitleContainerVisible @@ -139,12 +141,12 @@ class SubFilterViewModel @Inject constructor( blog.organizationId == organization.orgId } ?: false } - }.sortedWith(Comparator { blog1, blog2 -> + }.sortedWith { blog1, blog2 -> // sort followed blogs by name/domain to match display val blogOneName = getBlogNameForComparison(blog1) val blogTwoName = getBlogNameForComparison(blog2) blogOneName.compareTo(blogTwoName, true) - }) + } filterList.addAll( followedBlogs.map { blog -> @@ -414,7 +416,7 @@ class SubFilterViewModel @Inject constructor( loadSubFilters() } - @Suppress("unused", "UNUSED_PARAMETER") + @Suppress("unused") @Subscribe(threadMode = ThreadMode.MAIN) fun onEventMainThread(event: ReaderEvents.FollowedBlogsFetched) { if(event.didChange()) { @@ -426,6 +428,7 @@ class SubFilterViewModel @Inject constructor( override fun onCleared() { super.onCleared() eventBusWrapper.unregister(this) + _onCleared.value = Event(Unit) } companion object { From da1d249573ec12c8fc7dea61cddce6a06aba612e Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 17 May 2024 18:20:28 -0300 Subject: [PATCH 197/237] Fix item selected tracked when first opening Subscriptions/Tags feeds --- .../ui/reader/subfilter/SubFilterViewModel.kt | 6 +++--- .../subfilter/SubFilterViewModelTest.kt | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index d35c18d86cb8..a586c810e68f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -325,8 +325,8 @@ class SubFilterViewModel @Inject constructor( } fun onSubfilterSelected(subfilterListItem: SubfilterListItem) { - // We should not track subfilter selected if we're clearing a filter that is currently applied. - if (!subfilterListItem.isClearingFilter) { + // We should only track the selection of a subfilter if it's a tracked item (meaning it's a valid tag or site) + if (subfilterListItem.isTrackedItem) { val filterItemType = FilterItemType.fromSubfilterListItem(subfilterListItem) if (filterItemType != null) { readerTracker.track( @@ -334,7 +334,7 @@ class SubFilterViewModel @Inject constructor( mutableMapOf(FilterItemType.trackingEntry(filterItemType)) ) } else { - readerTracker.track(Stat.READER_FILTER_SHEET_ITEM_SELECTED,) + readerTracker.track(Stat.READER_FILTER_SHEET_ITEM_SELECTED) } } changeSubfilter(subfilterListItem, true, mTagFragmentStartedWith) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt index bad452b02fd0..7c8f2807e5f6 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt @@ -430,9 +430,8 @@ class SubFilterViewModelTest : BaseUnitTest() { } @Test - fun `Should NOT track READER_FILTER_SHEET_ITEM_SELECTED if clearing filter when onSubfilterSelected is called`() { + fun `Should NOT track READER_FILTER_SHEET_ITEM_SELECTED if SiteAll when onSubfilterSelected is called`() { val filter = SiteAll( - isClearingFilter = true, onClickAction = {}, ) viewModel.onSubfilterSelected(filter) @@ -440,13 +439,17 @@ class SubFilterViewModelTest : BaseUnitTest() { } @Test - fun `Should track READER_FILTER_SHEET_ITEM_SELECTED if NOT clearing filter when onSubfilterSelected is called`() { - val filter = SiteAll( - isClearingFilter = false, - onClickAction = {}, - ) + fun `Should NOT track READER_FILTER_SHEET_ITEM_SELECTED if Divider when onSubfilterSelected is called`() { + val filter = SubfilterListItem.Divider viewModel.onSubfilterSelected(filter) - verify(readerTracker).track(AnalyticsTracker.Stat.READER_FILTER_SHEET_ITEM_SELECTED) + verify(readerTracker, times(0)).track(AnalyticsTracker.Stat.READER_FILTER_SHEET_ITEM_SELECTED) + } + + @Test + fun `Should NOT track READER_FILTER_SHEET_ITEM_SELECTED if SectionTitle when onSubfilterSelected is called`() { + val filter = SubfilterListItem.SectionTitle(UiStringText("test")) + viewModel.onSubfilterSelected(filter) + verify(readerTracker, times(0)).track(AnalyticsTracker.Stat.READER_FILTER_SHEET_ITEM_SELECTED) } @Test From b8b3dac7837f1665c6b6f3ed304901c3f72e34a6 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 17 May 2024 22:15:55 -0300 Subject: [PATCH 198/237] Implement unit tests for reader announcement card --- .../reader/viewmodels/ReaderViewModelTest.kt | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index 02a0a53f8ed6..ade8e129f30c 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -39,12 +39,16 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.QuickStartRead import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.TopBarUiState +import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCardItemData import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.UrlUtilsWrapper +import org.wordpress.android.util.config.ReaderAnnouncementCardFeatureConfig import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig import org.wordpress.android.viewmodel.Event import java.util.Date +import kotlin.test.assertFalse +import kotlin.test.assertTrue private const val DUMMY_CURRENT_TIME: Long = 10000000000 @@ -89,6 +93,9 @@ class ReaderViewModelTest : BaseUnitTest() { @Mock lateinit var readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig + @Mock + lateinit var readerAnnouncementCardFeatureConfig: ReaderAnnouncementCardFeatureConfig + private val emptyReaderTagList = ReaderTagList() private val nonEmptyReaderTagList = createNonMockedNonEmptyReaderTagList() @@ -112,6 +119,7 @@ class ReaderViewModelTest : BaseUnitTest() { ReaderTopBarMenuHelper(readerTagsFeedFeatureConfig), urlUtilsWrapper, readerTagsFeedFeatureConfig, + readerAnnouncementCardFeatureConfig, ) whenever(dateProvider.getCurrentDate()).thenReturn(Date(DUMMY_CURRENT_TIME)) @@ -502,6 +510,57 @@ class ReaderViewModelTest : BaseUnitTest() { assertThat(showJetpackOverlayEvent.last().peekContent()).isTrue } + @Test + fun `Should load announcement card correctly`() = testWithNonEmptyTags { + triggerContentDisplay() + val observers = initObservers() + + val announcementCardUiState = observers.announcementCardStateEvents.first() + + val tagsFeedItem = announcementCardUiState.items[0] + assertThat(tagsFeedItem.iconRes).isEqualTo(R.drawable.ic_reader_tag) + assertThat(tagsFeedItem.titleRes).isEqualTo(R.string.reader_announcement_card_tags_stream_title) + assertThat(tagsFeedItem.descriptionRes).isEqualTo(R.string.reader_announcement_card_tags_stream_description) + + val readerPreferencesItem = announcementCardUiState.items[1] + assertThat(readerPreferencesItem.iconRes).isEqualTo(R.drawable.ic_reader_preferences) + assertThat(readerPreferencesItem.titleRes).isEqualTo(R.string.reader_announcement_card_reading_preferences_title) + assertThat(readerPreferencesItem.descriptionRes).isEqualTo(R.string.reader_announcement_card_reading_preferences_description) + } + + @Test + fun `Should show announcement card if feature flag is enabled and app preference returns true`() = + testWithNonEmptyTags { + whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) + whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(true) + triggerContentDisplay() + val observers = initObservers() + + val announcementCardUiState = observers.announcementCardStateEvents.first() + assertTrue(announcementCardUiState.shouldShow) + } + + @Test + fun `Should NOT show announcement card if feature flag is disabled`() = testWithNonEmptyTags { + whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(false) + triggerContentDisplay() + val observers = initObservers() + + val announcementCardUiState = observers.announcementCardStateEvents.first() + assertFalse(announcementCardUiState.shouldShow) + } + + @Test + fun `Should NOT show announcement card if app preference returns false`() = testWithNonEmptyTags { + whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) + whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(false) + triggerContentDisplay() + val observers = initObservers() + + val announcementCardUiState = observers.announcementCardStateEvents.first() + assertFalse(announcementCardUiState.shouldShow) + } + private fun assertQsFollowSiteTaskStarted( observers: Observers, isSettingsSupported: Boolean = true @@ -546,13 +605,19 @@ class ReaderViewModelTest : BaseUnitTest() { // tabNavigationEvents.add(it.peekContent()) // } - return Observers(uiStates, quickStartReaderPrompts, tabNavigationEvents) + val announcementCardStateEvents = mutableListOf() + viewModel.announcementCardState.observeForever { + announcementCardStateEvents.add(it) + } + + return Observers(uiStates, quickStartReaderPrompts, tabNavigationEvents, announcementCardStateEvents) } private data class Observers( val uiStates: List, val quickStartReaderPrompts: List>, - val tabNavigationEvents: List + val tabNavigationEvents: List, + val announcementCardStateEvents: List, ) private fun triggerContentDisplay( From c80c9830c730564eebdc5b34fda91e78369946a7 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 17 May 2024 22:29:27 -0300 Subject: [PATCH 199/237] Fix dark theme colors --- .../views/compose/ReaderAnnouncementCard.kt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt index 1cb42d94ee34..78e87dcb923c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt @@ -41,6 +41,8 @@ fun ReaderAnnouncementCard( items: List, onAnnouncementCardDoneClick: () -> Unit, ) { + val primaryColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black + val secondaryColor = if (isSystemInDarkTheme()) AppColor.Black else AppColor.White AnimatedVisibility( visible = shouldShow, enter = expandIn(), @@ -58,7 +60,7 @@ fun ReaderAnnouncementCard( Text( text = stringResource(R.string.reader_announcement_card_title), style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurface, + color = primaryColor, ) // Items LazyColumn( @@ -79,12 +81,12 @@ fun ReaderAnnouncementCard( onClick = { onAnnouncementCardDoneClick() }, elevation = ButtonDefaults.elevation(0.dp), colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colorScheme.onSurface, + backgroundColor = primaryColor, ), ) { Text( text = stringResource(id = R.string.reader_btn_done), - color = MaterialTheme.colorScheme.surface, + color = secondaryColor, style = MaterialTheme.typography.labelLarge, ) } @@ -94,14 +96,15 @@ fun ReaderAnnouncementCard( @Composable private fun ReaderAnnouncementCardItem(data: ReaderAnnouncementCardItemData) { - val baseColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black + val primaryColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black + val secondaryColor = if (isSystemInDarkTheme()) AppColor.Black else AppColor.White Row( modifier = Modifier .fillMaxWidth() .defaultMinSize(minWidth = 54.dp, minHeight = 54.dp), verticalAlignment = Alignment.CenterVertically, ) { - val iconBackgroundColor = MaterialTheme.colorScheme.onSurface + val iconBackgroundColor = primaryColor Icon( modifier = Modifier .padding( @@ -115,7 +118,7 @@ private fun ReaderAnnouncementCardItem(data: ReaderAnnouncementCardItemData) { ) }, painter = painterResource(data.iconRes), - tint = MaterialTheme.colorScheme.surface, + tint = secondaryColor, contentDescription = null ) Column(verticalArrangement = Arrangement.Center) { @@ -125,9 +128,9 @@ private fun ReaderAnnouncementCardItem(data: ReaderAnnouncementCardItemData) { ), text = stringResource(data.titleRes), style = MaterialTheme.typography.labelLarge, - color = baseColor, + color = primaryColor, ) - val secondaryElementColor = baseColor.copy( + val secondaryElementColor = primaryColor.copy( alpha = 0.6F ) Text( From 06f28a776902e211bfcf95e25cb23d7b94c64796 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 17 May 2024 22:34:57 -0300 Subject: [PATCH 200/237] Fix detekt --- .../android/ui/reader/viewmodels/ReaderViewModelTest.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index ade8e129f30c..d60dc62e7141 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -39,7 +39,6 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.QuickStartRead import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.TopBarUiState -import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCardItemData import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.UrlUtilsWrapper @@ -524,8 +523,12 @@ class ReaderViewModelTest : BaseUnitTest() { val readerPreferencesItem = announcementCardUiState.items[1] assertThat(readerPreferencesItem.iconRes).isEqualTo(R.drawable.ic_reader_preferences) - assertThat(readerPreferencesItem.titleRes).isEqualTo(R.string.reader_announcement_card_reading_preferences_title) - assertThat(readerPreferencesItem.descriptionRes).isEqualTo(R.string.reader_announcement_card_reading_preferences_description) + assertThat(readerPreferencesItem.titleRes).isEqualTo( + R.string.reader_announcement_card_reading_preferences_title + ) + assertThat(readerPreferencesItem.descriptionRes).isEqualTo( + R.string.reader_announcement_card_reading_preferences_description + ) } @Test From 590230f154c84a7d5a558faf82e4887777e35adb Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 20 May 2024 08:39:43 -0300 Subject: [PATCH 201/237] Revert "Add mechanism to initialize observer only once" This reverts commit 4855ec7153e7eca9299a0a991c84bef2203c9dc5. --- .../android/ui/reader/ReaderFragment.kt | 27 ++----------------- .../ui/reader/subfilter/SubFilterViewModel.kt | 11 +++----- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 70267519379e..474ef518d722 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle -import android.util.Log import android.view.View import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher @@ -97,14 +96,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView private var readerSubsActivityResultLauncher: ActivityResultLauncher? = null - /** - * Stores the keys for [SubFilterViewModel] that have already been initialized (observers) and started. - * It is only cleared when: - * - the [ReaderFragment] is destroyed, to allow proper initialization in cases like screen rotation - * - the [SubFilterViewModel] is cleared, to allow proper initialization when the ViewModel is created again - */ - private val initializedSubFilterViewModelKeys: MutableList = mutableListOf() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding = ReaderFragmentLayoutBinding.bind(view).apply { initTopAppBar() @@ -453,21 +444,11 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView } override fun getSubFilterViewModelForTag(tag: ReaderTag, savedInstanceState: Bundle?): SubFilterViewModel { - val keyForTag = SubFilterViewModel.getViewModelKeyForTag(tag) return ViewModelProvider(getSubFilterViewModelOwner(), viewModelFactory)[ - keyForTag, + SubFilterViewModel.getViewModelKeyForTag(tag), SubFilterViewModel::class.java ].also { - // only initialize if it hasn't been initialized yet - if (keyForTag !in initializedSubFilterViewModelKeys) { - it.initSubFilterViewModel(tag, savedInstanceState) - - // setup mechanism for proper one-time initialization and clearing - initializedSubFilterViewModelKeys.add(keyForTag) - it.onCleared.observeEvent(viewLifecycleOwner) { - initializedSubFilterViewModelKeys.remove(keyForTag) - } - } + it.initSubFilterViewModel(tag, savedInstanceState) } } @@ -477,7 +458,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView currentSubFilter.observe( viewLifecycleOwner ) { subfilterListItem: SubfilterListItem -> - Log.d("ReaderFragment", "currentSubFilter.observe") onSubfilterSelected(subfilterListItem) viewModel.onSubFilterItemSelected(subfilterListItem) } @@ -486,7 +466,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView updateTagsAndSites.observe( viewLifecycleOwner ) { event: Event> -> - Log.d("ReaderFragment", "updateTagsAndSites.observe") event.applyIfNotHandled { if (NetworkUtils.isNetworkAvailable(activity)) { ReaderUpdateServiceStarter.startService(activity, this) @@ -524,7 +503,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView bottomSheetUiState.observe( viewLifecycleOwner ) { event: Event -> - Log.d("ReaderFragment", "bottomSheetUiState.observe") event.applyIfNotHandled { val fm = childFragmentManager var bottomSheet = fm.findFragmentByTag(SUBFILTER_BOTTOM_SHEET_TAG) as SubfilterBottomSheetFragment? @@ -546,7 +524,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView bottomSheetAction.observe( viewLifecycleOwner ) { event: Event -> - Log.d("ReaderFragment", "bottomSheetAction.observe") event.applyIfNotHandled { when (this) { is OpenSubsAtPage -> { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index a586c810e68f..2f8164966098 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -38,6 +38,7 @@ import org.wordpress.android.util.UrlUtils import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent +import java.util.Comparator import java.util.EnumSet import javax.inject.Inject import javax.inject.Named @@ -69,9 +70,6 @@ class SubFilterViewModel @Inject constructor( private val _updateTagsAndSites = MutableLiveData>>() val updateTagsAndSites: LiveData>> = _updateTagsAndSites - private val _onCleared = MutableLiveData>() - val onCleared: LiveData> = _onCleared - private val _isTitleContainerVisible = MutableLiveData(true) val isTitleContainerVisible: LiveData = _isTitleContainerVisible @@ -141,12 +139,12 @@ class SubFilterViewModel @Inject constructor( blog.organizationId == organization.orgId } ?: false } - }.sortedWith { blog1, blog2 -> + }.sortedWith(Comparator { blog1, blog2 -> // sort followed blogs by name/domain to match display val blogOneName = getBlogNameForComparison(blog1) val blogTwoName = getBlogNameForComparison(blog2) blogOneName.compareTo(blogTwoName, true) - } + }) filterList.addAll( followedBlogs.map { blog -> @@ -416,7 +414,7 @@ class SubFilterViewModel @Inject constructor( loadSubFilters() } - @Suppress("unused") + @Suppress("unused", "UNUSED_PARAMETER") @Subscribe(threadMode = ThreadMode.MAIN) fun onEventMainThread(event: ReaderEvents.FollowedBlogsFetched) { if(event.didChange()) { @@ -428,7 +426,6 @@ class SubFilterViewModel @Inject constructor( override fun onCleared() { super.onCleared() eventBusWrapper.unregister(this) - _onCleared.value = Event(Unit) } companion object { From 88b734768e3d5fac4a862c463321401eaf04d508 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 20 May 2024 09:49:37 -0300 Subject: [PATCH 202/237] Initialize subfilter observers as fields to keep unique reference This makes sure that the Lifecycle and ViewModel Owners handle everything correctly when observing LiveData, which avoids duplicate observers and allows for correct auto removal/re-observation due to lifecycle and owner state change. --- .../android/ui/reader/ReaderFragment.kt | 180 ++++++++++-------- .../ui/reader/subfilter/SubFilterViewModel.kt | 16 +- 2 files changed, 108 insertions(+), 88 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 474ef518d722..ec1427374780 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import androidx.recyclerview.widget.RecyclerView @@ -96,6 +97,86 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView private var readerSubsActivityResultLauncher: ActivityResultLauncher? = null + private val wpMainActivityViewModel by lazy { + ViewModelProvider( + requireActivity(), + viewModelFactory + )[WPMainActivityViewModel::class.java] + } + + // region SubgroupFilterViewModel Observers + // we need a reference to the observers so they are properly handled by the lifecycle and ViewModel owners, avoiding + // duplication, and ensuring they are properly removed when the Fragment is destroyed + private val currentSubfilterObserver = Observer { subfilterListItem -> + viewModel.onSubFilterItemSelected(subfilterListItem) + } + + private val updateTagsAndSitesObserver = Observer>> { event -> + event.applyIfNotHandled { + if (NetworkUtils.isNetworkAvailable(activity)) { + ReaderUpdateServiceStarter.startService(activity, this) + } + } + } + + private val subFiltersObserver = Observer> { subFilters -> + val selectedTag = (viewModel.uiState.value as? ContentUiState)?.selectedReaderTag ?: return@Observer + viewModel.showTopBarFilterGroup( + selectedTag, + subFilters + ) + } + + private val bottomSheetUiStateObserver = Observer> { event -> + event.applyIfNotHandled { + val selectedTag = (viewModel.uiState.value as? ContentUiState)?.selectedReaderTag + ?: return@applyIfNotHandled + val viewModelKey = SubFilterViewModel.getViewModelKeyForTag(selectedTag) + + val fm = childFragmentManager + var bottomSheet = fm.findFragmentByTag(SUBFILTER_BOTTOM_SHEET_TAG) as SubfilterBottomSheetFragment? + if (isVisible && bottomSheet == null) { + val (title, category) = this as BottomSheetVisible + bottomSheet = newInstance( + viewModelKey, + category, + uiHelpers.getTextOfUiString(requireContext(), title) + ) + bottomSheet.show(childFragmentManager, SUBFILTER_BOTTOM_SHEET_TAG) + } else if (!isVisible && bottomSheet != null) { + bottomSheet.dismiss() + } + } + } + + private val bottomSheetActionObserver = Observer> { event -> + event.applyIfNotHandled { + when (this) { + is OpenSubsAtPage -> { + readerSubsActivityResultLauncher?.launch( + ReaderActivityLauncher.createIntentShowReaderSubs( + requireActivity(), + tabIndex + ) + ) + } + + is OpenLoginPage -> { + wpMainActivityViewModel.onOpenLoginPage() + } + + is OpenSearchPage -> { + ReaderActivityLauncher.showReaderSearch(requireActivity()) + } + + is OpenSuggestedTagsPage -> { + ReaderActivityLauncher.showReaderInterests(requireActivity()) + } + } + } + } + // endregion + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding = ReaderFragmentLayoutBinding.bind(view).apply { initTopAppBar() @@ -453,104 +534,41 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView } private fun SubFilterViewModel.initSubFilterViewModel(startedTag: ReaderTag, savedInstanceState: Bundle?) { - setupSubFilterBottomSheetObservers(startedTag) + bottomSheetUiState.observe( + viewLifecycleOwner, + bottomSheetUiStateObserver + ) + + bottomSheetAction.observe( + viewLifecycleOwner, + bottomSheetActionObserver + ) currentSubFilter.observe( - viewLifecycleOwner - ) { subfilterListItem: SubfilterListItem -> - onSubfilterSelected(subfilterListItem) - viewModel.onSubFilterItemSelected(subfilterListItem) - } + viewLifecycleOwner, + currentSubfilterObserver + ) updateTagsAndSites.observe( - viewLifecycleOwner - ) { event: Event> -> - event.applyIfNotHandled { - if (NetworkUtils.isNetworkAvailable(activity)) { - ReaderUpdateServiceStarter.startService(activity, this) - } - } - } + viewLifecycleOwner, + updateTagsAndSitesObserver + ) if (startedTag.isFilterable) { subFilters.observe( - viewLifecycleOwner - ) { subFilters: List -> - viewModel.showTopBarFilterGroup( - startedTag, - subFilters - ) - } + viewLifecycleOwner, + subFiltersObserver + ) updateTagsAndSites() } else { viewModel.hideTopBarFilterGroup(startedTag) } - // thomashortadev: not sure if always passing the same tags can cause problems start(startedTag, startedTag, savedInstanceState) } - private fun SubFilterViewModel.setupSubFilterBottomSheetObservers(startedTag: ReaderTag) { - val wpMainActivityViewModel = ViewModelProvider( - requireActivity(), - viewModelFactory - )[WPMainActivityViewModel::class.java] - - val viewModelKey = SubFilterViewModel.getViewModelKeyForTag(startedTag) - - bottomSheetUiState.observe( - viewLifecycleOwner - ) { event: Event -> - event.applyIfNotHandled { - val fm = childFragmentManager - var bottomSheet = fm.findFragmentByTag(SUBFILTER_BOTTOM_SHEET_TAG) as SubfilterBottomSheetFragment? - if (isVisible && bottomSheet == null) { - loadSubFilters() - val (title, category) = this as BottomSheetVisible - bottomSheet = newInstance( - viewModelKey, - category, - uiHelpers.getTextOfUiString(requireContext(), title) - ) - bottomSheet.show(childFragmentManager, SUBFILTER_BOTTOM_SHEET_TAG) - } else if (!isVisible && bottomSheet != null) { - bottomSheet.dismiss() - } - } - } - - bottomSheetAction.observe( - viewLifecycleOwner - ) { event: Event -> - event.applyIfNotHandled { - when (this) { - is OpenSubsAtPage -> { - readerSubsActivityResultLauncher?.launch( - ReaderActivityLauncher.createIntentShowReaderSubs( - requireActivity(), - tabIndex - ) - ) - } - - is OpenLoginPage -> { - wpMainActivityViewModel.onOpenLoginPage() - } - - is OpenSearchPage -> { - ReaderActivityLauncher.showReaderSearch(requireActivity()) - } - - is OpenSuggestedTagsPage -> { - ReaderActivityLauncher.showReaderInterests(requireActivity()) - } - } - } - } - } - companion object { private const val SUBFILTER_BOTTOM_SHEET_TAG = "SUBFILTER_BOTTOM_SHEET_TAG" } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index 2f8164966098..c197234dad0e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -38,7 +38,6 @@ import org.wordpress.android.util.UrlUtils import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent -import java.util.Comparator import java.util.EnumSet import javax.inject.Inject import javax.inject.Named @@ -125,6 +124,7 @@ class SubFilterViewModel @Inject constructor( "" } } + fun loadSubFilters() { launch { val filterList = ArrayList() @@ -139,12 +139,12 @@ class SubFilterViewModel @Inject constructor( blog.organizationId == organization.orgId } ?: false } - }.sortedWith(Comparator { blog1, blog2 -> + }.sortedWith { blog1, blog2 -> // sort followed blogs by name/domain to match display val blogOneName = getBlogNameForComparison(blog1) val blogTwoName = getBlogNameForComparison(blog2) blogOneName.compareTo(blogTwoName, true) - }) + } filterList.addAll( followedBlogs.map { blog -> @@ -239,13 +239,14 @@ class SubFilterViewModel @Inject constructor( category: SubfilterCategory, ) { updateTagsAndSites() + loadSubFilters() _bottomSheetUiState.value = Event( BottomSheetVisible( UiStringRes(category.titleRes), category ) ) - val source = when(category) { + val source = when (category) { SubfilterCategory.SITES -> "blogs" SubfilterCategory.TAGS -> "tags" } @@ -353,6 +354,7 @@ class SubFilterViewModel @Inject constructor( } else { readerTracker.stop(ReaderTrackerType.SUBFILTERED_LIST) } + onSubfilterSelected(filter) _currentSubFilter.value = filter } @@ -414,10 +416,10 @@ class SubFilterViewModel @Inject constructor( loadSubFilters() } - @Suppress("unused", "UNUSED_PARAMETER") + @Suppress("unused") @Subscribe(threadMode = ThreadMode.MAIN) fun onEventMainThread(event: ReaderEvents.FollowedBlogsFetched) { - if(event.didChange()) { + if (event.didChange()) { AppLog.d(T.READER, "Subfilter bottom sheet > followed blogs changed") loadSubFilters() } @@ -449,7 +451,7 @@ class SubFilterViewModel @Inject constructor( companion object { fun fromSubfilterListItem(subfilterListItem: SubfilterListItem): FilterItemType? = - when(subfilterListItem.type) { + when (subfilterListItem.type) { SubfilterListItem.ItemType.SITE -> Blog SubfilterListItem.ItemType.TAG -> Tag else -> null From 456283f98c9ab51bc9e8c4d867e070e89250eb72 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 20 May 2024 11:38:33 -0300 Subject: [PATCH 203/237] Update unit tests --- .../ui/reader/subfilter/SubFilterViewModel.kt | 14 ++-- .../subfilter/SubFilterViewModelTest.kt | 69 ++++++++++++++++--- 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index c197234dad0e..8f41c16c70e7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -9,8 +9,8 @@ import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.wordpress.android.analytics.AnalyticsTracker.Stat -import org.wordpress.android.datasets.ReaderBlogTable -import org.wordpress.android.datasets.ReaderTagTable +import org.wordpress.android.datasets.ReaderBlogTableWrapper +import org.wordpress.android.datasets.wrappers.ReaderTagTableWrapper import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.models.ReaderBlog import org.wordpress.android.models.ReaderTag @@ -49,7 +49,9 @@ class SubFilterViewModel @Inject constructor( private val subfilterListItemMapper: SubfilterListItemMapper, private val eventBusWrapper: EventBusWrapper, private val accountStore: AccountStore, - private val readerTracker: ReaderTracker + private val readerTracker: ReaderTracker, + private val readerTagTableWrapper: ReaderTagTableWrapper, + private val readerBlogTableWrapper: ReaderBlogTableWrapper, ) : ScopedViewModel(bgDispatcher) { private val _subFilters = MutableLiveData>() val subFilters: LiveData> = _subFilters @@ -132,7 +134,7 @@ class SubFilterViewModel @Inject constructor( if (accountStore.hasAccessToken()) { val organization = mTagFragmentStartedWith?.organization - val followedBlogs = ReaderBlogTable.getFollowedBlogs().let { blogList -> + val followedBlogs = readerBlogTableWrapper.getFollowedBlogs().let { blogList -> // Filtering out all blogs not belonging to this VM organization if valid blogList.filter { blog -> organization?.let { @@ -157,7 +159,7 @@ class SubFilterViewModel @Inject constructor( ) } - val tags = ReaderTagTable.getFollowedTags() + val tags = readerTagTableWrapper.getFollowedTags() for (tag in tags) { filterList.add( @@ -354,8 +356,8 @@ class SubFilterViewModel @Inject constructor( } else { readerTracker.stop(ReaderTrackerType.SUBFILTERED_LIST) } - onSubfilterSelected(filter) _currentSubFilter.value = filter + onSubfilterSelected(filter) } fun onUserComesToReader() { diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt index 7c8f2807e5f6..c2a70cc8c21f 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt @@ -17,17 +17,20 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.datasets.ReaderBlogTableWrapper +import org.wordpress.android.datasets.wrappers.ReaderTagTableWrapper import org.wordpress.android.fluxc.model.AccountModel import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.getOrAwaitValue import org.wordpress.android.models.ReaderBlog import org.wordpress.android.models.ReaderTag +import org.wordpress.android.models.ReaderTagList import org.wordpress.android.models.ReaderTagType.BOOKMARKED import org.wordpress.android.models.ReaderTagType.TAGS +import org.wordpress.android.ui.Organization import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.reader.ReaderSubsActivity import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType -import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask import org.wordpress.android.ui.reader.subfilter.ActionType.OpenLoginPage import org.wordpress.android.ui.reader.subfilter.ActionType.OpenSubsAtPage @@ -78,7 +81,10 @@ class SubFilterViewModelTest : BaseUnitTest() { private lateinit var savedState: Bundle @Mock - private lateinit var filter: SubfilterListItem + private lateinit var readerTagTableWrapper: ReaderTagTableWrapper + + @Mock + private lateinit var readerBlogTableWrapper: ReaderBlogTableWrapper private lateinit var viewModel: SubFilterViewModel @@ -93,7 +99,9 @@ class SubFilterViewModelTest : BaseUnitTest() { subfilterListItemMapper, eventBusWrapper, accountStore, - readerTracker + readerTracker, + readerTagTableWrapper, + readerBlogTableWrapper, ) viewModel.start(initialTag, savedTag, savedState) @@ -102,6 +110,10 @@ class SubFilterViewModelTest : BaseUnitTest() { @Test fun `current subfilter is set back when we have a previous intance state`() { val json = "{\"blogId\":0,\"feedId\":0,\"tagSlug\":\"news\",\"tagType\":1,\"type\":4}" + val filter = Site( + blog = ReaderBlog(), + onClickAction = mock(), + ) whenever(savedState.getString(SubFilterViewModel.ARG_CURRENT_SUBFILTER_JSON)).thenReturn(json) whenever(subfilterListItemMapper.fromJson(eq(json), any(), any())).thenReturn(filter) @@ -113,7 +125,9 @@ class SubFilterViewModelTest : BaseUnitTest() { subfilterListItemMapper, eventBusWrapper, accountStore, - readerTracker + readerTracker, + readerTagTableWrapper, + readerBlogTableWrapper, ) viewModel.start(initialTag, savedTag, savedState) @@ -288,11 +302,9 @@ class SubFilterViewModelTest : BaseUnitTest() { @Test fun `view model updates the tags and sites and asks to show the bottom sheet when filters button is tapped`() { - var updateTasks: EnumSet? = null - var uiState: BottomSheetUiState? = null + var updateTasks: EnumSet? = null viewModel.updateTagsAndSites.observeForever { updateTasks = it.peekContent() } - viewModel.bottomSheetUiState.observeForever { uiState = it.peekContent() } viewModel.onSubFiltersListButtonClicked(SubfilterCategory.SITES) @@ -302,10 +314,47 @@ class SubFilterViewModelTest : BaseUnitTest() { UpdateTask.FOLLOWED_BLOGS ) ) + } + + @Test + fun `view model asks to show the bottom sheet when filters button is tapped`() { + var uiState: BottomSheetUiState? = null + + viewModel.bottomSheetUiState.observeForever { uiState = it.peekContent() } + + viewModel.onSubFiltersListButtonClicked(SubfilterCategory.SITES) assertThat(uiState).isInstanceOf(BottomSheetVisible::class.java) } + @Test + fun `view model updates subfilters when filters button is tapped`() = test { + whenever(initialTag.organization).thenReturn(Organization.NO_ORGANIZATION) + whenever(accountStore.hasAccessToken()).thenReturn(true) + whenever(readerTagTableWrapper.getFollowedTags()).thenReturn( + ReaderTagList().apply { + add(ReaderTag("a", "a", "a", "endpoint-a", TAGS)) + add(ReaderTag("b", "b", "b", "endpoint-b", TAGS)) + add(ReaderTag("c", "c", "c", "endpoint-c", TAGS)) + } + ) + + whenever(readerBlogTableWrapper.getFollowedBlogs()).thenReturn( + List(2) { + ReaderBlog().apply { organizationId = Organization.NO_ORGANIZATION.orgId } + } + ) + + var subFilters: List? = null + + viewModel.subFilters.observeForever { subFilters = it } + + viewModel.onSubFiltersListButtonClicked(SubfilterCategory.SITES) + advanceUntilIdle() + + assertThat(subFilters).hasSize(5) + } + @Test fun `view model hides the bottom sheet when it is cancelled`() { var uiState: BottomSheetUiState? = null @@ -320,7 +369,11 @@ class SubFilterViewModelTest : BaseUnitTest() { @Test fun `bottom sheet is hidden when a filter is tapped on`() { var uiState: BottomSheetUiState? = null - val filter: SubfilterListItem = mock() + val filter: SubfilterListItem = Site( + isSelected = false, + onClickAction = mock(), + blog = ReaderBlog(), + ) viewModel.setSubfilterFromTag(savedTag) From e0ebd70fac01a94e0dd733120abcc60e6b378fe5 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 20 May 2024 12:01:50 -0300 Subject: [PATCH 204/237] Remove unused resource --- WordPress/src/main/res/drawable/ic_reader_tag_24dp.xml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 WordPress/src/main/res/drawable/ic_reader_tag_24dp.xml diff --git a/WordPress/src/main/res/drawable/ic_reader_tag_24dp.xml b/WordPress/src/main/res/drawable/ic_reader_tag_24dp.xml deleted file mode 100644 index 9abc506e9e2b..000000000000 --- a/WordPress/src/main/res/drawable/ic_reader_tag_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - From d9b2bb101a8140f9e59cba704df2d08706c0b14c Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 20 May 2024 12:46:05 -0300 Subject: [PATCH 205/237] Fix BuildConfig constant being true --- WordPress/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/build.gradle b/WordPress/build.gradle index ee393d97a0b3..0a1ebf4622db 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -152,7 +152,7 @@ android { buildConfigField "boolean", "READER_READING_PREFERENCES", "false" buildConfigField "boolean", "READER_READING_PREFERENCES_FEEDBACK", "false" buildConfigField "boolean", "READER_TAGS_FEED", "false" - buildConfigField "boolean", "READER_ANNOUNCEMENT_CARD", "true" + buildConfigField "boolean", "READER_ANNOUNCEMENT_CARD", "false" // Override these constants in jetpack product flavor to enable/ disable features buildConfigField "boolean", "ENABLE_SITE_CREATION", "true" From 8c20b4be3e9018ade60ab9ba22d5586f6f2d845c Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 20 May 2024 12:46:26 -0300 Subject: [PATCH 206/237] Replace LazyColumn with Column for announcement items --- .../ui/reader/views/compose/ReaderAnnouncementCard.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt index 78e87dcb923c..1c4bb3877354 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt @@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material3.Icon @@ -63,14 +61,12 @@ fun ReaderAnnouncementCard( color = primaryColor, ) // Items - LazyColumn( + Column( modifier = Modifier .fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value) ) { - items( - items = items, - ) { + items.forEach { ReaderAnnouncementCardItem(it) } } From 9d1a4b99d8bcb332fca39e37b0b87053a86abf1a Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 20 May 2024 12:59:19 -0300 Subject: [PATCH 207/237] Add the Tags feed announcement item if it's enabled only --- .../ui/reader/viewmodels/ReaderViewModel.kt | 31 ++++++++++++------- .../reader/viewmodels/ReaderViewModelTest.kt | 27 +++++++++++++++- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index a3609bda6274..0af740b04eb7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -147,21 +147,30 @@ class ReaderViewModel @Inject constructor( } private fun loadAnnouncementCard() { - _announcementCardState.value = AnnouncementCardUiState( - shouldShow = readerAnnouncementCardFeatureConfig.isEnabled() && - appPrefsWrapper.shouldShowReaderAnnouncementCard(), - items = listOf( + val items = mutableListOf() + + if (readerTagsFeedFeatureConfig.isEnabled()) { + items.add( ReaderAnnouncementCardItemData( iconRes = R.drawable.ic_reader_tag, titleRes = R.string.reader_announcement_card_tags_stream_title, descriptionRes = R.string.reader_announcement_card_tags_stream_description, - ), - ReaderAnnouncementCardItemData( - iconRes = R.drawable.ic_reader_preferences, - titleRes = R.string.reader_announcement_card_reading_preferences_title, - descriptionRes = R.string.reader_announcement_card_reading_preferences_description, - ), - ), + ) + ) + } + + items.add( + ReaderAnnouncementCardItemData( + iconRes = R.drawable.ic_reader_preferences, + titleRes = R.string.reader_announcement_card_reading_preferences_title, + descriptionRes = R.string.reader_announcement_card_reading_preferences_description, + ) + ) + + _announcementCardState.value = AnnouncementCardUiState( + shouldShow = readerAnnouncementCardFeatureConfig.isEnabled() && + appPrefsWrapper.shouldShowReaderAnnouncementCard(), + items = items, ) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index d60dc62e7141..06657f8f67ca 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -510,12 +510,16 @@ class ReaderViewModelTest : BaseUnitTest() { } @Test - fun `Should load announcement card correctly`() = testWithNonEmptyTags { + fun `Should load announcement card correctly with tags item`() = testWithNonEmptyTags { + whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(true) + triggerContentDisplay() val observers = initObservers() val announcementCardUiState = observers.announcementCardStateEvents.first() + assertThat(announcementCardUiState.items).hasSize(2) + val tagsFeedItem = announcementCardUiState.items[0] assertThat(tagsFeedItem.iconRes).isEqualTo(R.drawable.ic_reader_tag) assertThat(tagsFeedItem.titleRes).isEqualTo(R.string.reader_announcement_card_tags_stream_title) @@ -531,6 +535,27 @@ class ReaderViewModelTest : BaseUnitTest() { ) } + @Test + fun `Should load announcement card correctly without tags item`() = testWithNonEmptyTags { + whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(false) + + triggerContentDisplay() + val observers = initObservers() + + val announcementCardUiState = observers.announcementCardStateEvents.first() + + assertThat(announcementCardUiState.items).hasSize(1) + + val readerPreferencesItem = announcementCardUiState.items[0] + assertThat(readerPreferencesItem.iconRes).isEqualTo(R.drawable.ic_reader_preferences) + assertThat(readerPreferencesItem.titleRes).isEqualTo( + R.string.reader_announcement_card_reading_preferences_title + ) + assertThat(readerPreferencesItem.descriptionRes).isEqualTo( + R.string.reader_announcement_card_reading_preferences_description + ) + } + @Test fun `Should show announcement card if feature flag is enabled and app preference returns true`() = testWithNonEmptyTags { From ff3f56105b043033b5c851f56a811d32b4d13ebe Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 20 May 2024 15:01:47 -0300 Subject: [PATCH 208/237] Remove unnecessary coroutine in tests --- .../android/ui/reader/subfilter/SubFilterViewModelTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt index c2a70cc8c21f..78ce1e841766 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt @@ -328,7 +328,7 @@ class SubFilterViewModelTest : BaseUnitTest() { } @Test - fun `view model updates subfilters when filters button is tapped`() = test { + fun `view model updates subfilters when filters button is tapped`() { whenever(initialTag.organization).thenReturn(Organization.NO_ORGANIZATION) whenever(accountStore.hasAccessToken()).thenReturn(true) whenever(readerTagTableWrapper.getFollowedTags()).thenReturn( @@ -350,7 +350,6 @@ class SubFilterViewModelTest : BaseUnitTest() { viewModel.subFilters.observeForever { subFilters = it } viewModel.onSubFiltersListButtonClicked(SubfilterCategory.SITES) - advanceUntilIdle() assertThat(subFilters).hasSize(5) } From 12e9ef704540160162af759d5c2bd1f64bbcedd1 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Mon, 20 May 2024 16:00:01 -0300 Subject: [PATCH 209/237] Fix unit tests --- .../ui/reader/subfilter/SubFilterViewModelTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt index 78ce1e841766..23e3a40f8b30 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModelTest.kt @@ -302,6 +302,8 @@ class SubFilterViewModelTest : BaseUnitTest() { @Test fun `view model updates the tags and sites and asks to show the bottom sheet when filters button is tapped`() { + mockReaderTableEmpty() + var updateTasks: EnumSet? = null viewModel.updateTagsAndSites.observeForever { updateTasks = it.peekContent() } @@ -318,6 +320,8 @@ class SubFilterViewModelTest : BaseUnitTest() { @Test fun `view model asks to show the bottom sheet when filters button is tapped`() { + mockReaderTableEmpty() + var uiState: BottomSheetUiState? = null viewModel.bottomSheetUiState.observeForever { uiState = it.peekContent() } @@ -539,4 +543,11 @@ class SubFilterViewModelTest : BaseUnitTest() { assertThat(viewModel.isTitleContainerVisible.getOrAwaitValue()).isEqualTo(isTitleContainerVisible) } } + + private fun mockReaderTableEmpty() { + whenever(initialTag.organization).thenReturn(Organization.NO_ORGANIZATION) + whenever(accountStore.hasAccessToken()).thenReturn(true) + whenever(readerTagTableWrapper.getFollowedTags()).thenReturn(ReaderTagList()) + whenever(readerBlogTableWrapper.getFollowedBlogs()).thenReturn(emptyList()) + } } From c1ab0caf990bd9aa3b8ae6801da5e9651c349dc1 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 21 May 2024 13:14:09 -0300 Subject: [PATCH 210/237] Update Loaded state with tags in a diff-style Keep existing tags content untouched and only add new tags in initial state for new tags, so the list updates in place. Also add placement animation to the list items for a better UX. --- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 27 +++++-- .../tagsfeed/ReaderTagsFeedViewModel.kt | 70 ++++++++++++------- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 47 ++++++++----- 3 files changed, 94 insertions(+), 50 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 cfac4b72a7b1..015a1433aec1 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 @@ -94,13 +94,10 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( ): ReaderTagsFeedViewModel.UiState.Loaded = ReaderTagsFeedViewModel.UiState.Loaded( data = tags.map { tag -> - ReaderTagsFeedViewModel.TagFeedItem( - tagChip = ReaderTagsFeedViewModel.TagChip( - tag = tag, - onTagChipClick = onTagChipClick, - onMoreFromTagClick = onMoreFromTagClick, - ), - postList = ReaderTagsFeedViewModel.PostList.Initial, + mapInitialTagFeedItem( + tag = tag, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, onItemEnteredView = onItemEnteredView, ) }, @@ -108,6 +105,22 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( onRefresh = onRefresh, ) + fun mapInitialTagFeedItem( + tag: ReaderTag, + onTagChipClick: (ReaderTag) -> Unit, + onMoreFromTagClick: (ReaderTag) -> Unit, + onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit, + ): ReaderTagsFeedViewModel.TagFeedItem = + ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = tag, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Initial, + onItemEnteredView = onItemEnteredView, + ) + fun mapLoadingTagFeedItem( tag: ReaderTag, onTagChipClick: (ReaderTag) -> Unit, 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 2a0344f7ef56..8ccfa74dd5fa 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 @@ -88,34 +88,41 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - /** - * Fetch multiple tag posts in parallel. Each tag load causes a new state to be emitted, so multiple emissions of - * [uiStateFlow] are expected when calling this method for each tag, since each can go through the following - * [UiState]s: [UiState.Initial], [UiState.Loaded], [UiState.Loading], [UiState.Empty]. - */ - fun onTagsChanged(tags: List) { - // don't fetch tags 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 + fun onTagsChanged(tags: List) = _uiStateFlow.update { currentState -> + when { + tags.isEmpty() -> { + UiState.Empty(::onOpenTagsListClick) } - } - if (tags.isEmpty()) { - _uiStateFlow.value = UiState.Empty(::onOpenTagsListClick) - return - } + currentState is UiState.Loaded -> { + val currentTags = currentState.data.map { it.tagChip.tag } + if (currentState.isRefreshing) { + readerTagsFeedUiStateMapper.mapInitialPostsUiState( + tags, + false, + ::onTagChipClick, + ::onMoreFromTagClick, + ::onItemEnteredView, + ::onRefresh + ) + } else if (currentTags != tags) { + updateLoadedStateWithTags(currentState, tags) + } else { + currentState + } + } - // Add tags to the list with the posts loading UI - _uiStateFlow.update { - readerTagsFeedUiStateMapper.mapInitialPostsUiState( - tags, - false, - ::onTagChipClick, - ::onMoreFromTagClick, - ::onItemEnteredView, - ::onRefresh - ) + else -> { + // Add tags to the list with the posts initial/loading UI + readerTagsFeedUiStateMapper.mapInitialPostsUiState( + tags, + false, + ::onTagChipClick, + ::onMoreFromTagClick, + ::onItemEnteredView, + ::onRefresh + ) + } } } @@ -139,6 +146,19 @@ class ReaderTagsFeedViewModel @Inject constructor( } } + private fun updateLoadedStateWithTags(state: UiState.Loaded, tags: List): UiState.Loaded { + val currentTagsMap = state.data.associateBy { it.tagChip.tag.tagSlug } + val updatedData = tags.map { tag -> + currentTagsMap[tag.tagSlug] ?: readerTagsFeedUiStateMapper.mapInitialTagFeedItem( + tag = tag, + onTagChipClick = ::onTagChipClick, + onMoreFromTagClick = ::onMoreFromTagClick, + onItemEnteredView = ::onItemEnteredView, + ) + } + return state.copy(data = updatedData) + } + private fun onNoConnectionRetryClick() { _uiStateFlow.value = UiState.Loading if (networkUtilsWrapper.isNetworkAvailable()) { 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 789bf55173d7..1394c2d971ce 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 @@ -1,6 +1,7 @@ package org.wordpress.android.ui.reader.views.compose.tagsfeed import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -87,7 +88,7 @@ fun ReaderTagsFeed(uiState: UiState) { } } -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable private fun Loaded(uiState: UiState.Loaded) { val pullRefreshState = rememberPullRefreshState( @@ -108,6 +109,7 @@ private fun Loaded(uiState: UiState.Loaded) { ) { items( items = uiState.data, + key = { it.tagChip.tag.tagSlug } ) { item -> val tagChip = item.tagChip val postList = item.postList @@ -121,24 +123,33 @@ private fun Loaded(uiState: UiState.Loaded) { } 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.onTagChipClick(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(postList, tagChip, backgroundColor) + + Column( + modifier = Modifier + .animateItemPlacement() + .fillMaxWidth() + .padding( + top = Margin.Large.value, + bottom = Margin.ExtraExtraMediumLarge.value, + ) + ) { + // Tag chip UI + ReaderFilterChip( + modifier = Modifier.padding( + start = Margin.Large.value, + ), + text = UiString.UiStringText(tagChip.tag.tagTitle), + onClick = { tagChip.onTagChipClick(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(postList, tagChip, backgroundColor) + } } - Spacer(modifier = Modifier.height(Margin.ExtraExtraMediumLarge.value)) } } From 681ab48ef429e53c4f667a8fda35552a68f20ab5 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 21 May 2024 13:48:55 -0300 Subject: [PATCH 211/237] Apply PR suggestion: rename loadAnnouncementCard to updateAnnouncementCard --- .../android/ui/reader/viewmodels/ReaderViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 0af740b04eb7..14ba95f13dbc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -132,7 +132,7 @@ class ReaderViewModel @Inject constructor( if (initialized) return loadTabs(savedInstanceState) if (jetpackBrandingUtils.shouldShowJetpackPoweredBottomSheet()) showJetpackPoweredBottomSheet() - loadAnnouncementCard() + updateAnnouncementCard() } fun onSaveInstanceState(out: Bundle) { @@ -146,7 +146,7 @@ class ReaderViewModel @Inject constructor( // _showJetpackPoweredBottomSheet.value = Event(true) } - private fun loadAnnouncementCard() { + private fun updateAnnouncementCard() { val items = mutableListOf() if (readerTagsFeedFeatureConfig.isEnabled()) { @@ -177,7 +177,7 @@ class ReaderViewModel @Inject constructor( fun onAnnouncementCardDoneClick() { readerTracker.track(AnalyticsTracker.Stat.READER_ANNOUNCEMENT_CARD_DISMISSED) appPrefsWrapper.setShouldShowReaderAnnouncementCard(false) - loadAnnouncementCard() + updateAnnouncementCard() } @JvmOverloads From e67a94627bb45bf927b9da8f3d1a1f4f328d751f Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 21 May 2024 16:17:31 -0300 Subject: [PATCH 212/237] Add mapper unit tests --- .../ReaderTagsFeedUiStateMapperTest.kt | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index e0bfbde05d8c..54df791458d7 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -256,7 +256,75 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { } @Test - fun `Should map loading posts UI state correctly`() { + fun `Should map loading TagFeedItem correctly`() { + // Given + val readerTag = ReaderTag( + "tag", + "tag", + "tag", + "endpoint", + ReaderTagType.FOLLOWED, + ) + val onTagChipClick: (ReaderTag) -> Unit = {} + val onMoreFromTagClick: (ReaderTag) -> Unit = {} + val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} + // When + val actual = classToTest.mapLoadingTagFeedItem( + tag = readerTag, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, + onItemEnteredView = onItemEnteredView, + ) + + // Then + val expected = ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = readerTag, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Loading, + onItemEnteredView = onItemEnteredView, + ) + assertEquals(expected, actual) + } + + @Test + fun `Should map initial TagFeedItem correctly`() { + // Given + val readerTag = ReaderTag( + "tag", + "tag", + "tag", + "endpoint", + ReaderTagType.FOLLOWED, + ) + val onTagChipClick: (ReaderTag) -> Unit = {} + val onMoreFromTagClick: (ReaderTag) -> Unit = {} + val onItemEnteredView: (ReaderTagsFeedViewModel.TagFeedItem) -> Unit = {} + // When + val actual = classToTest.mapInitialTagFeedItem( + tag = readerTag, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, + onItemEnteredView = onItemEnteredView, + ) + + // Then + val expected = ReaderTagsFeedViewModel.TagFeedItem( + tagChip = ReaderTagsFeedViewModel.TagChip( + tag = readerTag, + onTagChipClick = onTagChipClick, + onMoreFromTagClick = onMoreFromTagClick, + ), + postList = ReaderTagsFeedViewModel.PostList.Initial, + onItemEnteredView = onItemEnteredView, + ) + assertEquals(expected, actual) + } + + @Test + fun `Should map initial posts UI state correctly`() { // Given val onTagChipClick: (ReaderTag) -> Unit = {} val onMoreFromTagClick: (ReaderTag) -> Unit = {} From 13d03b121a468eb928bddfd9586e34e0c84640b5 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Tue, 21 May 2024 16:38:15 -0300 Subject: [PATCH 213/237] Update ReaderTagsFeedViewModel unit tests --- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 110 ++++++++++++++---- 1 file changed, 88 insertions(+), 22 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 83baaa4a6c28..992017556433 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 @@ -182,7 +182,6 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { ) } - @Suppress("LongMethod") @Test fun `given valid tags, when loaded, then UI state should update properly`() = testCollectingUiStates { // Given @@ -225,7 +224,6 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { ) } - @Suppress("LongMethod") @Test fun `given valid and invalid tags, when loaded, then UI state should update properly`() = testCollectingUiStates { // Given @@ -342,9 +340,8 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertIs>(readerNavigationEvents.first()) } - @Suppress("LongMethod") @Test - fun `given tags fetched, when start again, then nothing happens`() = testCollectingUiStates { + fun `given tags fetched, when onTagsChanged again, then nothing happens`() = testCollectingUiStates { // Given val tag1 = ReaderTestUtils.createTag("tag1") val tag2 = ReaderTestUtils.createTag("tag2") @@ -384,19 +381,23 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { verifyNoInteractions(readerPostRepository) } - @Suppress("LongMethod") @Test - fun `given tags fetched, when start again refreshing, then move back to initial state`() = testCollectingUiStates { + fun `given new tags fetched, when onTagsChanged again, then state updates`() = testCollectingUiStates { // Given - val tag1 = ReaderTestUtils.createTag("tag1") - val tag2 = ReaderTestUtils.createTag("tag2") + val tag1 = ReaderTestUtils.createTag("tag1") // will be present both times + val tag2 = ReaderTestUtils.createTag("tag2") // will be present only first time + val tag3 = ReaderTestUtils.createTag("tag3") // will be present only second time + val posts1 = ReaderPostList().apply { add(ReaderPost()) } val posts2 = ReaderPostList().apply { add(ReaderPost()) } - whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) + val posts3 = ReaderPostList().apply { + add(ReaderPost()) + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { delay(100) posts1 @@ -405,9 +406,15 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { delay(200) posts2 } + whenever(readerPostRepository.fetchNewerPostsForTag(tag3)).doSuspendableAnswer { + delay(300) + posts3 + } + mockMapInitialTagFeedItems() mockMapLoadingTagFeedItems() mockMapLoadedTagFeedItems() + mockMapInitialTagFeedItem() // When viewModel.onTagsChanged(listOf(tag1, tag2)) @@ -417,25 +424,31 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { viewModel.onItemEnteredView(getInitialTagFeedItem(tag2)) advanceUntilIdle() - viewModel.onRefresh() + assertThat(collectedUiStates.last()).isEqualTo( + ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf( + getLoadedTagFeedItem(tag1), + getLoadedTagFeedItem(tag2), + ) + ) + ) // Then - viewModel.onTagsChanged(listOf(tag1, tag2)) + viewModel.onTagsChanged(listOf(tag1, tag3)) advanceUntilIdle() - val loadedState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.Loaded - assertThat(loadedState.data).isEqualTo( - listOf( - getInitialTagFeedItem(tag1), - getInitialTagFeedItem(tag2) + assertThat(collectedUiStates.last()).isEqualTo( + ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf( + getLoadedTagFeedItem(tag1), // still loaded even without entering view + getInitialTagFeedItem(tag3), + ) ) ) - assertThat(loadedState.isRefreshing).isFalse() } - @Suppress("LongMethod") @Test - fun `given no tags requested, when start, then UI state should update properly`() = testCollectingUiStates { + fun `given no tags, when onTagsChanged, then UI state should update properly`() = testCollectingUiStates { // Given val tags = emptyList() @@ -447,7 +460,55 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertThat(collectedUiStates).last().isInstanceOf(ReaderTagsFeedViewModel.UiState.Empty::class.java) } - @Suppress("LongMethod") + @Test + fun `given tags fetched, when onTagsChanged again refreshing, then move back to initial state`() = + testCollectingUiStates { + // Given + val tag1 = ReaderTestUtils.createTag("tag1") + val tag2 = ReaderTestUtils.createTag("tag2") + val posts1 = ReaderPostList().apply { + add(ReaderPost()) + } + val posts2 = ReaderPostList().apply { + add(ReaderPost()) + } + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) + whenever(readerPostRepository.fetchNewerPostsForTag(tag1)).doSuspendableAnswer { + delay(100) + posts1 + } + whenever(readerPostRepository.fetchNewerPostsForTag(tag2)).doSuspendableAnswer { + delay(200) + posts2 + } + mockMapInitialTagFeedItems() + mockMapLoadingTagFeedItems() + mockMapLoadedTagFeedItems() + + // When + viewModel.onTagsChanged(listOf(tag1, tag2)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag1)) + advanceUntilIdle() + viewModel.onItemEnteredView(getInitialTagFeedItem(tag2)) + advanceUntilIdle() + + viewModel.onRefresh() + + // Then + viewModel.onTagsChanged(listOf(tag1, tag2)) + advanceUntilIdle() + + val loadedState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.Loaded + assertThat(loadedState.data).isEqualTo( + listOf( + getInitialTagFeedItem(tag1), + getInitialTagFeedItem(tag2) + ) + ) + assertThat(loadedState.isRefreshing).isFalse() + } + @Test fun `given tags fetched, when refreshing, then update isRefreshing status`() = testCollectingUiStates { // Given @@ -487,7 +548,6 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertThat(loadedState.isRefreshing).isTrue() } - @Suppress("LongMethod") @Test fun `given tags fetched, when refreshing, then RefreshTagsFeed action is posted`() = testCollectingUiStates { // Given @@ -527,7 +587,6 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { assertThat(action).isEqualTo(ActionEvent.RefreshTags) } - @Suppress("LongMethod") @Test fun `given tags fetched and no connection, when refreshing, then show error message`() = testCollectingUiStates { // Given @@ -877,6 +936,13 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } } + private fun mockMapInitialTagFeedItem() { + whenever(readerTagsFeedUiStateMapper.mapInitialTagFeedItem(any(), any(), any(), any())) + .thenAnswer { + getInitialTagFeedItem(it.getArgument(0)) + } + } + private fun getInitialTagFeedItem(tag: ReaderTag) = ReaderTagsFeedViewModel.TagFeedItem( ReaderTagsFeedViewModel.TagChip(tag, {}, {}), ReaderTagsFeedViewModel.PostList.Initial From 7587b30db2e0290d519ab029d7e12b3b9d196286 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 21 May 2024 22:06:49 -0300 Subject: [PATCH 214/237] Make reader announcement card scrollable as part of the post list --- .../res/layout/reader_fragment_layout.xml | 51 ++++++------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/WordPress/src/main/res/layout/reader_fragment_layout.xml b/WordPress/src/main/res/layout/reader_fragment_layout.xml index 081157fd6aa8..8bab85f1abe9 100644 --- a/WordPress/src/main/res/layout/reader_fragment_layout.xml +++ b/WordPress/src/main/res/layout/reader_fragment_layout.xml @@ -16,47 +16,26 @@ android:layout_height="wrap_content" app:layout_scrollFlags="scroll|enterAlways" /> - - - - - - + app:layout_scrollFlags="scroll|enterAlways" /> - + - + - + + From 6f3a6bbbf02efcac28bc020095b03af3b3909c4c Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 21 May 2024 23:00:21 -0300 Subject: [PATCH 215/237] Hide reader announcement card on Discover when empty state is shown --- .../ui/reader/discover/ReaderDiscoverViewModel.kt | 3 +++ .../ui/reader/viewmodels/ReaderViewModel.kt | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt index b50d61aa70ae..cac0f5ff301f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt @@ -154,11 +154,13 @@ class ReaderDiscoverViewModel @Inject constructor( // since new users have the dailyprompt tag followed by default, we need to ignore them when // checking if the user has any tags followed, so we show the onboarding state (ShowNoFollowedTags) if (userTags.filterNot { it.tagSlug == BLOGGING_PROMPT_TAG }.isEmpty()) { + parentViewModel.onFeedEmptyStateLoaded() _uiState.value = DiscoverUiState.EmptyUiState.ShowNoFollowedTagsUiState { parentViewModel.onShowReaderInterests() } } else { if (posts != null && posts.cards.isNotEmpty()) { + parentViewModel.onFeedContentLoaded() _uiState.value = DiscoverUiState.ContentUiState( convertCardsToUiStates(posts), reloadProgressVisibility = false, @@ -169,6 +171,7 @@ class ReaderDiscoverViewModel @Inject constructor( swipeToRefreshTriggered = false } } else { + parentViewModel.onFeedEmptyStateLoaded() _uiState.value = DiscoverUiState.EmptyUiState.ShowNoPostsUiState { _navigationEvents.value = Event(ShowReaderSubs) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 14ba95f13dbc..4bf77d16e5c3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -142,6 +142,20 @@ class ReaderViewModel @Inject constructor( } } + fun onFeedEmptyStateLoaded() { + hideAnnouncementCard() + } + + fun onFeedContentLoaded() { + updateAnnouncementCard() + } + + private fun hideAnnouncementCard() { + _announcementCardState.value = _announcementCardState.value?.copy( + shouldShow = false, + ) + } + private fun showJetpackPoweredBottomSheet() { // _showJetpackPoweredBottomSheet.value = Event(true) } From 00dd7ead5c7fafb90bac8b5408340378ab76db1a Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 21 May 2024 23:43:13 -0300 Subject: [PATCH 216/237] Show reader announcement card only on Discover feed --- .../android/ui/reader/viewmodels/ReaderViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 4bf77d16e5c3..d95db4a3071e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -180,9 +180,9 @@ class ReaderViewModel @Inject constructor( descriptionRes = R.string.reader_announcement_card_reading_preferences_description, ) ) - + val isDiscoverSelected = selectedReaderTag()?.isDiscover == true _announcementCardState.value = AnnouncementCardUiState( - shouldShow = readerAnnouncementCardFeatureConfig.isEnabled() && + shouldShow = isDiscoverSelected && readerAnnouncementCardFeatureConfig.isEnabled() && appPrefsWrapper.shouldShowReaderAnnouncementCard(), items = items, ) @@ -213,6 +213,7 @@ class ReaderViewModel @Inject constructor( } fun onTagChanged(selectedTag: ReaderTag?) { + updateAnnouncementCard() selectedTag?.let { trackReaderTabShownIfNecessary(it) } From 90fbfec90042e408873a636fabe89860f26e793e Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 22 May 2024 00:33:58 -0300 Subject: [PATCH 217/237] Hide reader announcement card when any feed but Discover is selected --- .../android/ui/reader/viewmodels/ReaderViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index d95db4a3071e..876beb4f2fee 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -132,7 +132,6 @@ class ReaderViewModel @Inject constructor( if (initialized) return loadTabs(savedInstanceState) if (jetpackBrandingUtils.shouldShowJetpackPoweredBottomSheet()) showJetpackPoweredBottomSheet() - updateAnnouncementCard() } fun onSaveInstanceState(out: Bundle) { @@ -213,7 +212,9 @@ class ReaderViewModel @Inject constructor( } fun onTagChanged(selectedTag: ReaderTag?) { - updateAnnouncementCard() + if (selectedTag?.isDiscover == false) { + hideAnnouncementCard() + } selectedTag?.let { trackReaderTabShownIfNecessary(it) } From 9d73d4a3d85fc3bf04114caae33d2cc275264eb8 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 22 May 2024 01:17:17 -0300 Subject: [PATCH 218/237] Update ReaderViewModel unit tests --- .../reader/viewmodels/ReaderViewModelTest.kt | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index 06657f8f67ca..64f90bae1d04 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -510,10 +510,10 @@ class ReaderViewModelTest : BaseUnitTest() { } @Test - fun `Should load announcement card correctly with tags item`() = testWithNonEmptyTags { + fun `Should update announcement card UI correctly with tags item`() = testWithNonEmptyTags { whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(true) - triggerContentDisplay() + viewModel.onFeedContentLoaded() val observers = initObservers() val announcementCardUiState = observers.announcementCardStateEvents.first() @@ -536,10 +536,10 @@ class ReaderViewModelTest : BaseUnitTest() { } @Test - fun `Should load announcement card correctly without tags item`() = testWithNonEmptyTags { + fun `Should update announcement card UI correctly without tags item onFeedContentLoaded`() = testWithNonEmptyTags { whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(false) - triggerContentDisplay() + viewModel.onFeedContentLoaded() val observers = initObservers() val announcementCardUiState = observers.announcementCardStateEvents.first() @@ -557,32 +557,41 @@ class ReaderViewModelTest : BaseUnitTest() { } @Test - fun `Should show announcement card if feature flag is enabled and app preference returns true`() = + fun `Should show announcement card if feature flag is enabled, app preference returns true and feed is Discover`() = testWithNonEmptyTags { - whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) - whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(true) - triggerContentDisplay() - val observers = initObservers() + val readerTag = ReaderTag("Discover", "Discover", "Discover", DISCOVER_PATH, ReaderTagType.DEFAULT) + whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) + whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(true) + val observers = initObservers() + triggerContentDisplay() + viewModel.updateSelectedContent(readerTag) + viewModel.onFeedContentLoaded() - val announcementCardUiState = observers.announcementCardStateEvents.first() - assertTrue(announcementCardUiState.shouldShow) - } + val announcementCardUiState = observers.announcementCardStateEvents.first() + assertTrue(announcementCardUiState.shouldShow) + } @Test fun `Should NOT show announcement card if feature flag is disabled`() = testWithNonEmptyTags { - whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(false) - triggerContentDisplay() - val observers = initObservers() + val readerTag = ReaderTag("Discover", "Discover", "Discover", DISCOVER_PATH, ReaderTagType.DEFAULT) + whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(false) + val observers = initObservers() + triggerContentDisplay() + viewModel.updateSelectedContent(readerTag) + viewModel.onFeedContentLoaded() - val announcementCardUiState = observers.announcementCardStateEvents.first() - assertFalse(announcementCardUiState.shouldShow) - } + val announcementCardUiState = observers.announcementCardStateEvents.first() + assertFalse(announcementCardUiState.shouldShow) + } @Test fun `Should NOT show announcement card if app preference returns false`() = testWithNonEmptyTags { + val readerTag = ReaderTag("Discover", "Discover", "Discover", DISCOVER_PATH, ReaderTagType.DEFAULT) whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(false) triggerContentDisplay() + viewModel.updateSelectedContent(readerTag) + viewModel.onFeedContentLoaded() val observers = initObservers() val announcementCardUiState = observers.announcementCardStateEvents.first() From 6c5176294431e2a109de309d3e6d96382af445b1 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Wed, 22 May 2024 17:33:24 -0300 Subject: [PATCH 219/237] Use inverted logic since it should only hide Blogs for Tags Feed --- .../wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 876beb4f2fee..69d24965b323 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -570,7 +570,7 @@ class ReaderViewModel @Inject constructor( } private fun shouldShowBlogsFilter(readerTag: ReaderTag): Boolean { - return readerTag.isFilterable && readerTag.isFollowedSites + return readerTag.isFilterable && !readerTag.isTags } private fun shouldShowTagsFilter(readerTag: ReaderTag): Boolean { From 14c08da9ca858e0c0e3a4f8f7a985c1870b0b422 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 23 May 2024 18:14:00 -0300 Subject: [PATCH 220/237] Create ReaderAnnouncementRepository --- .../ReaderAnnouncementRepository.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepository.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepository.kt new file mode 100644 index 000000000000..0a89094a2ca9 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepository.kt @@ -0,0 +1,57 @@ +package org.wordpress.android.ui.reader.repository + +import dagger.Reusable +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.ui.reader.tracker.ReaderTracker +import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCardItemData +import org.wordpress.android.util.config.ReaderAnnouncementCardFeatureConfig +import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig +import javax.inject.Inject + +@Reusable +class ReaderAnnouncementRepository @Inject constructor( + private val readerAnnouncementCardFeatureConfig: ReaderAnnouncementCardFeatureConfig, + private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig, + private val appPrefsWrapper: AppPrefsWrapper, + private val readerTracker: ReaderTracker, +) { + fun hasReaderAnnouncement(): Boolean { + return readerAnnouncementCardFeatureConfig.isEnabled() && appPrefsWrapper.shouldShowReaderAnnouncementCard() + } + + fun getReaderAnnouncementItems(): List { + if (!readerAnnouncementCardFeatureConfig.isEnabled() || !appPrefsWrapper.shouldShowReaderAnnouncementCard()) { + return emptyList() + } + + val items = mutableListOf() + + if (readerTagsFeedFeatureConfig.isEnabled()) { + items.add( + ReaderAnnouncementCardItemData( + iconRes = R.drawable.ic_reader_tag, + titleRes = R.string.reader_announcement_card_tags_stream_title, + descriptionRes = R.string.reader_announcement_card_tags_stream_description, + ) + ) + } + + items.add( + ReaderAnnouncementCardItemData( + iconRes = R.drawable.ic_reader_preferences, + titleRes = R.string.reader_announcement_card_reading_preferences_title, + descriptionRes = R.string.reader_announcement_card_reading_preferences_description, + ) + ) + + return items + } + + fun dismissReaderAnnouncement() { + readerTracker.track(AnalyticsTracker.Stat.READER_ANNOUNCEMENT_CARD_DISMISSED) + appPrefsWrapper.setShouldShowReaderAnnouncementCard(false) + } +} + From 59b9230a367b731319b696d9a2804d13f70680df Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 23 May 2024 18:14:14 -0300 Subject: [PATCH 221/237] Create ReaderAnnouncementCardView wrapper for Composable --- .../views/ReaderAnnouncementCardView.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt new file mode 100644 index 000000000000..dcf567c55e14 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt @@ -0,0 +1,44 @@ +package org.wordpress.android.ui.reader.views + +import android.content.Context +import android.util.AttributeSet +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.AbstractComposeView +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCard +import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCardItemData + +class ReaderAnnouncementCardView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AbstractComposeView(context, attrs, defStyleAttr) { + private val items: MutableState> = mutableStateOf(emptyList()) + + private val onDoneClickListener: MutableState = mutableStateOf(null) + + @Composable + override fun Content() { + AppTheme { + ReaderAnnouncementCard( + shouldShow = true, + items = items.value, + onAnnouncementCardDoneClick = { onDoneClickListener.value?.onDoneClick() } + ) + } + } + + fun setItems(items: List) { + this.items.value = items + } + + fun setOnDoneClickListener(listener: OnDoneClickListener) { + this.onDoneClickListener.value = listener + } + + interface OnDoneClickListener { + fun onDoneClick() + } +} From e2886c4e553f2baa96b139704f72c2baafa0a434 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 23 May 2024 18:14:50 -0300 Subject: [PATCH 222/237] Add Announcement card logic to ReaderPostAdapter --- .../ui/reader/adapters/ReaderPostAdapter.java | 112 ++++++++++++++---- 1 file changed, 87 insertions(+), 25 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java index 5ae4c5d68fa7..5237901a752e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java @@ -46,9 +46,11 @@ import org.wordpress.android.ui.reader.discover.viewholders.ReaderPostNewViewHolder; import org.wordpress.android.ui.reader.discover.viewholders.ReaderPostViewHolder; import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId; +import org.wordpress.android.ui.reader.repository.ReaderAnnouncementRepository; import org.wordpress.android.ui.reader.tracker.ReaderTab; import org.wordpress.android.ui.reader.tracker.ReaderTracker; import org.wordpress.android.ui.reader.utils.ReaderXPostUtils; +import org.wordpress.android.ui.reader.views.ReaderAnnouncementCardView; import org.wordpress.android.ui.reader.views.ReaderGapMarkerView; import org.wordpress.android.ui.reader.views.ReaderSiteHeaderView; import org.wordpress.android.ui.reader.views.ReaderTagHeaderView; @@ -117,9 +119,11 @@ public class ReaderPostAdapter extends RecyclerView.Adapter { + mReaderAnnouncementRepository.dismissReaderAnnouncement(); + notifyItemRemoved(getAnnouncementPosition()); + }); } } @@ -680,6 +728,10 @@ private boolean hasTagHeader() { return (getPostListType() == ReaderPostListType.TAG_PREVIEW) && !isEmpty(); } + private boolean hasAnnouncement() { + return mIsMainReader && mReaderAnnouncementRepository.hasReaderAnnouncement() && !isEmpty(); + } + private boolean isDiscover() { return mCurrentTag != null && mCurrentTag.isDiscover(); } @@ -766,6 +818,9 @@ private void loadPosts() { } private ReaderPost getItem(int position) { + if (position == getAnnouncementPosition() && hasAnnouncement()) { + return null; + } if (position == getHeaderPosition() && hasHeader()) { return null; } @@ -788,22 +843,27 @@ private ReaderPost getItem(int position) { } private int getItemPositionOffset() { - return hasHeader() ? 1 : 0; + int offset = 0; + if (hasAnnouncement()) offset++; + if (hasHeader()) offset++; + return offset; } private int getHeaderPosition() { - return hasHeader() ? 0 : -1; + int headerPosition = hasAnnouncement() ? 1 : 0; + return hasHeader() ? headerPosition : -1; + } + + private int getAnnouncementPosition() { + return hasAnnouncement() ? 0 : -1; } @Override public int getItemCount() { int size = mPosts.size(); - if (mGapMarkerPosition != -1) { - size++; - } - if (hasHeader()) { - size++; - } + if (mGapMarkerPosition != -1) size++; + if (hasHeader()) size++; + if (hasAnnouncement()) size++; return size; } @@ -824,6 +884,8 @@ public long getItemId(int position) { return ITEM_ID_HEADER; case VIEW_TYPE_GAP_MARKER: return ITEM_ID_GAP_MARKER; + case VIEW_TYPE_READER_ANNOUNCEMENT: + return ITEM_ID_READER_ANNOUNCEMENT; default: ReaderPost post = getItem(position); return post != null ? post.getStableId() : 0; @@ -892,7 +954,7 @@ protected void onCancelled() { @Override protected Boolean doInBackground(Void... params) { - int numExisting; + int numExisting = 0; switch (getPostListType()) { case TAG_PREVIEW: case TAG_FOLLOWED: @@ -909,7 +971,7 @@ protected Boolean doInBackground(Void... params) { numExisting = ReaderPostTable.getNumPostsInBlog(mCurrentBlogId); } break; - default: + case TAGS_FEED: return false; } From 46d172fa99e672e8845bc35667ef347fb9c71a7a Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 23 May 2024 18:59:49 -0300 Subject: [PATCH 223/237] Add Announcement card to Discover Feed --- .../ui/reader/discover/ReaderCardUiState.kt | 6 +++++ .../reader/discover/ReaderDiscoverAdapter.kt | 14 ++++++++++- .../discover/ReaderDiscoverViewModel.kt | 25 ++++++++++++++++++- .../ReaderAnnouncementCardViewHolder.kt | 21 ++++++++++++++++ .../views/ReaderAnnouncementCardView.kt | 8 ++++++ .../layout/reader_cardview_announcement.xml | 4 +++ 6 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderAnnouncementCardViewHolder.kt create mode 100644 WordPress/src/main/res/layout/reader_cardview_announcement.xml diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt index 5a1d8c33d812..6549e88cfe85 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderCardUiState.kt @@ -9,6 +9,7 @@ import org.wordpress.android.ui.reader.discover.ReaderPostCardAction.PrimaryActi import org.wordpress.android.ui.reader.discover.ReaderPostCardActionType.SPACER_NO_ACTION import org.wordpress.android.ui.reader.discover.interests.TagUiState import org.wordpress.android.ui.reader.models.ReaderImageList +import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCardItemData import org.wordpress.android.ui.reader.views.uistates.ReaderBlogSectionUiState import org.wordpress.android.ui.utils.UiDimen import org.wordpress.android.ui.utils.UiString @@ -175,6 +176,11 @@ sealed class ReaderCardUiState { } } } + + data class ReaderAnnouncementCardUiState( + val items: List, + val onDoneClick: () -> Unit, + ) : ReaderCardUiState() } data class ReaderPostActions( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverAdapter.kt index a94e2b2b1507..c6a5f2d409ec 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverAdapter.kt @@ -3,10 +3,12 @@ package org.wordpress.android.ui.reader.discover import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView.Adapter +import org.wordpress.android.ui.reader.discover.ReaderCardUiState.ReaderAnnouncementCardUiState import org.wordpress.android.ui.reader.discover.ReaderCardUiState.ReaderInterestsCardUiState import org.wordpress.android.ui.reader.discover.ReaderCardUiState.ReaderPostNewUiState import org.wordpress.android.ui.reader.discover.ReaderCardUiState.ReaderPostUiState import org.wordpress.android.ui.reader.discover.ReaderCardUiState.ReaderRecommendedBlogsCardUiState +import org.wordpress.android.ui.reader.discover.viewholders.ReaderAnnouncementCardViewHolder import org.wordpress.android.ui.reader.discover.viewholders.ReaderInterestsCardNewViewHolder import org.wordpress.android.ui.reader.discover.viewholders.ReaderInterestsCardViewHolder import org.wordpress.android.ui.reader.discover.viewholders.ReaderPostNewViewHolder @@ -24,6 +26,7 @@ private const val POST_VIEW_TYPE: Int = 1 private const val INTEREST_VIEW_TYPE: Int = 2 private const val RECOMMENDED_BLOGS_VIEW_TYPE: Int = 3 private const val POST_NEW_VIEW_TYPE: Int = 4 +private const val READER_ANNOUNCEMENT_TYPE: Int = 5 class ReaderDiscoverAdapter( private val uiHelpers: UiHelpers, @@ -43,6 +46,7 @@ class ReaderDiscoverAdapter( networkUtilsWrapper, parent ) + INTEREST_VIEW_TYPE -> { if (isReaderImprovementsEnabled) { ReaderInterestsCardNewViewHolder(uiHelpers, parent) @@ -50,6 +54,7 @@ class ReaderDiscoverAdapter( ReaderInterestsCardViewHolder(uiHelpers, parent) } } + RECOMMENDED_BLOGS_VIEW_TYPE -> if (isReaderImprovementsEnabled) { ReaderRecommendedBlogsCardNewViewHolder( @@ -60,6 +65,9 @@ class ReaderDiscoverAdapter( parent, imageManager, uiHelpers ) } + + READER_ANNOUNCEMENT_TYPE -> ReaderAnnouncementCardViewHolder(parent) + else -> throw NotImplementedError("Unknown ViewType") } } @@ -93,6 +101,7 @@ class ReaderDiscoverAdapter( is ReaderPostNewUiState -> POST_NEW_VIEW_TYPE is ReaderInterestsCardUiState -> INTEREST_VIEW_TYPE is ReaderRecommendedBlogsCardUiState -> RECOMMENDED_BLOGS_VIEW_TYPE + is ReaderAnnouncementCardUiState -> READER_ANNOUNCEMENT_TYPE } } @@ -115,14 +124,17 @@ class ReaderDiscoverAdapter( is ReaderPostUiState -> { oldItem.postId == (newItem as ReaderPostUiState).postId && oldItem.blogId == newItem.blogId } + is ReaderPostNewUiState -> { oldItem.postId == (newItem as ReaderPostNewUiState).postId && oldItem.blogId == newItem.blogId } + is ReaderRecommendedBlogsCardUiState -> { val newItemState = newItem as? ReaderRecommendedBlogsCardUiState oldItem.blogs.map { it.blogId to it.feedId } == newItemState?.blogs?.map { it.blogId to it.feedId } } - is ReaderInterestsCardUiState -> { + + is ReaderInterestsCardUiState, is ReaderAnnouncementCardUiState -> { oldItem == newItem } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt index cac0f5ff301f..cdabfddbf517 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt @@ -27,6 +27,7 @@ import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents.ShowPosts import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents.ShowReaderSubs import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents.ShowSitePickerForResult import org.wordpress.android.ui.reader.reblog.ReblogUseCase +import org.wordpress.android.ui.reader.repository.ReaderAnnouncementRepository import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication.Error import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication.Started @@ -62,6 +63,7 @@ class ReaderDiscoverViewModel @Inject constructor( displayUtilsWrapper: DisplayUtilsWrapper, private val getFollowedTagsUseCase: GetFollowedTagsUseCase, private val readerImprovementsFeatureConfig: ReaderImprovementsFeatureConfig, + private val readerAnnouncementRepository: ReaderAnnouncementRepository, @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, @Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher ) : ScopedViewModel(mainDispatcher) { @@ -161,8 +163,20 @@ class ReaderDiscoverViewModel @Inject constructor( } else { if (posts != null && posts.cards.isNotEmpty()) { parentViewModel.onFeedContentLoaded() + + val announcement = if (readerAnnouncementRepository.hasReaderAnnouncement()) { + listOf( + ReaderCardUiState.ReaderAnnouncementCardUiState( + readerAnnouncementRepository.getReaderAnnouncementItems(), + ::dismissAnnouncementCard + ) + ) + } else { + emptyList() + } + _uiState.value = DiscoverUiState.ContentUiState( - convertCardsToUiStates(posts), + announcement + convertCardsToUiStates(posts), reloadProgressVisibility = false, loadMoreProgressVisibility = false, ) @@ -181,6 +195,15 @@ class ReaderDiscoverViewModel @Inject constructor( } } + private fun dismissAnnouncementCard() { + readerAnnouncementRepository.dismissReaderAnnouncement() + _uiState.value = (_uiState.value as? DiscoverUiState.ContentUiState)?.let { contentUiState -> + contentUiState.copy( + cards = contentUiState.cards.filterNot { it is ReaderCardUiState.ReaderAnnouncementCardUiState } + ) + } + } + private fun observeFollowStatus() { // listen to changes on follow status for updating the reader recommended blogs state immediately _uiState.addSource(readerPostCardActionsHandler.followStatusUpdated) { data -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderAnnouncementCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderAnnouncementCardViewHolder.kt new file mode 100644 index 000000000000..c2c5bb83494d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderAnnouncementCardViewHolder.kt @@ -0,0 +1,21 @@ +package org.wordpress.android.ui.reader.discover.viewholders + +import android.view.ViewGroup +import org.wordpress.android.databinding.ReaderCardviewAnnouncementBinding +import org.wordpress.android.ui.reader.discover.ReaderCardUiState +import org.wordpress.android.util.extensions.viewBinding + +class ReaderAnnouncementCardViewHolder( + parentView: ViewGroup, +) : ReaderViewHolder( + parentView.viewBinding(ReaderCardviewAnnouncementBinding::inflate) +) { + override fun onBind(uiState: ReaderCardUiState) { + (uiState as? ReaderCardUiState.ReaderAnnouncementCardUiState)?.let { state -> + with(binding.root) { + setItems(state.items) + setOnDoneClickListener(state.onDoneClick) + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt index dcf567c55e14..6d4c69c88fab 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt @@ -38,6 +38,14 @@ class ReaderAnnouncementCardView @JvmOverloads constructor( this.onDoneClickListener.value = listener } + fun setOnDoneClickListener(block: () -> Unit) { + this.onDoneClickListener.value = object : OnDoneClickListener { + override fun onDoneClick() { + block() + } + } + } + interface OnDoneClickListener { fun onDoneClick() } diff --git a/WordPress/src/main/res/layout/reader_cardview_announcement.xml b/WordPress/src/main/res/layout/reader_cardview_announcement.xml new file mode 100644 index 000000000000..a225bc9e492b --- /dev/null +++ b/WordPress/src/main/res/layout/reader_cardview_announcement.xml @@ -0,0 +1,4 @@ + + From f0f15c321c1950b3882f354ac90aa2e567075dc5 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 23 May 2024 19:03:41 -0300 Subject: [PATCH 224/237] Update and add new unit tests to Discover ViewModel --- .../discover/ReaderDiscoverViewModelTest.kt | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt index 064409261b3c..338a6d1eea2b 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt @@ -54,6 +54,7 @@ import org.wordpress.android.ui.reader.discover.ReaderPostCardActionType.LIKE import org.wordpress.android.ui.reader.discover.ReaderPostCardActionType.REBLOG import org.wordpress.android.ui.reader.discover.interests.TagUiState import org.wordpress.android.ui.reader.reblog.ReblogUseCase +import org.wordpress.android.ui.reader.repository.ReaderAnnouncementRepository import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication.Error.NetworkUnavailable import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication.Started @@ -137,6 +138,9 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { @Mock private lateinit var readerImprovementsFeatureConfig: ReaderImprovementsFeatureConfig + @Mock + private lateinit var readerAnnouncementRepository: ReaderAnnouncementRepository + private val fakeDiscoverFeed = ReactiveMutableLiveData() private val fakeCommunicationChannel = MutableLiveData>() private val fakeNavigationFeed = MutableLiveData>() @@ -160,6 +164,7 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { displayUtilsWrapper, getFollowedTagsUseCase, readerImprovementsFeatureConfig, + readerAnnouncementRepository, testDispatcher(), testDispatcher() ) @@ -400,6 +405,55 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { assertThat(contentUiState.cards.first()).isInstanceOf(ReaderPostNewUiState::class.java) } + @Test + fun `if Announcement does not exist then ReaderAnnouncementCardUiState will not be present`() = test { + // Arrange + whenever(readerAnnouncementRepository.hasReaderAnnouncement()).thenReturn(false) + val uiStates = init(autoUpdateFeed = false).uiStates + // Act + fakeDiscoverFeed.value = createDummyReaderCardsList() // mock finished loading + // Assert + val contentUiState = uiStates.last() as ContentUiState + assertThat(contentUiState.cards.first()) + .isNotInstanceOf(ReaderCardUiState.ReaderAnnouncementCardUiState::class.java) + } + + @Test + fun `if Announcement exists then ReaderAnnouncementCardUiState will be present`() = test { + // Arrange + whenever(readerAnnouncementRepository.hasReaderAnnouncement()).thenReturn(true) + whenever(readerAnnouncementRepository.getReaderAnnouncementItems()).thenReturn(mock()) + val uiStates = init(autoUpdateFeed = false).uiStates + // Act + fakeDiscoverFeed.value = createDummyReaderCardsList() // mock finished loading + // Assert + val contentUiState = uiStates.last() as ContentUiState + assertThat(contentUiState.cards.first()) + .isInstanceOf(ReaderCardUiState.ReaderAnnouncementCardUiState::class.java) + } + + @Test + fun `clicking done on ReaderAnnouncementCardUiState dismisses and updates the ContentUiState`() = test { + // Arrange + whenever(readerAnnouncementRepository.hasReaderAnnouncement()).thenReturn(true) + whenever(readerAnnouncementRepository.getReaderAnnouncementItems()).thenReturn(mock()) + val uiStates = init(autoUpdateFeed = false).uiStates + + fakeDiscoverFeed.value = createDummyReaderCardsList() // mock finished loading + val contentUiState = uiStates.last() as ContentUiState + val announcementCard = contentUiState.cards.first() as ReaderCardUiState.ReaderAnnouncementCardUiState + + // Act + announcementCard.onDoneClick() + + // Assert + verify(readerAnnouncementRepository).dismissReaderAnnouncement() + + val newContentUiState = uiStates.last() as ContentUiState + assertThat(newContentUiState.cards.first()) + .isNotInstanceOf(ReaderCardUiState.ReaderAnnouncementCardUiState::class.java) + } + @Test fun `Discover data provider is started when the vm is started`() = test { // Act From dfeaf1d8deb4ad72632ee85f300e9b7f34a249b3 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 23 May 2024 19:22:26 -0300 Subject: [PATCH 225/237] Remove Announcement card from ReaderFragment --- .../android/ui/reader/ReaderFragment.kt | 19 ----- .../discover/ReaderDiscoverViewModel.kt | 4 -- .../ui/reader/viewmodels/ReaderViewModel.kt | 70 ++----------------- 3 files changed, 4 insertions(+), 89 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 3732f5d95b71..ec1427374780 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -54,7 +54,6 @@ import org.wordpress.android.ui.reader.subfilter.SubfilterCategory import org.wordpress.android.ui.reader.subfilter.SubfilterListItem import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState -import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCard import org.wordpress.android.ui.reader.views.compose.ReaderTopAppBar import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterType import org.wordpress.android.ui.utils.UiHelpers @@ -181,7 +180,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding = ReaderFragmentLayoutBinding.bind(view).apply { initTopAppBar() - initAnnouncementCard() initViewModel(savedInstanceState) } } @@ -264,23 +262,6 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView } } - private fun ReaderFragmentLayoutBinding.initAnnouncementCard() { - readerAnnouncementCardComposeView.apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - val announcementCardUiState by viewModel.announcementCardState.observeAsState() - val state = announcementCardUiState ?: return@setContent - AppTheme { - ReaderAnnouncementCard( - shouldShow = state.shouldShow, - items = state.items, - onAnnouncementCardDoneClick = { viewModel.onAnnouncementCardDoneClick() } - ) - } - } - } - } - private fun ReaderFragmentLayoutBinding.initViewModel(savedInstanceState: Bundle?) { viewModel = ViewModelProvider(this@ReaderFragment, viewModelFactory)[ReaderViewModel::class.java] startReaderViewModel(savedInstanceState) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt index cdabfddbf517..007f8e93b847 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt @@ -156,14 +156,11 @@ class ReaderDiscoverViewModel @Inject constructor( // since new users have the dailyprompt tag followed by default, we need to ignore them when // checking if the user has any tags followed, so we show the onboarding state (ShowNoFollowedTags) if (userTags.filterNot { it.tagSlug == BLOGGING_PROMPT_TAG }.isEmpty()) { - parentViewModel.onFeedEmptyStateLoaded() _uiState.value = DiscoverUiState.EmptyUiState.ShowNoFollowedTagsUiState { parentViewModel.onShowReaderInterests() } } else { if (posts != null && posts.cards.isNotEmpty()) { - parentViewModel.onFeedContentLoaded() - val announcement = if (readerAnnouncementRepository.hasReaderAnnouncement()) { listOf( ReaderCardUiState.ReaderAnnouncementCardUiState( @@ -185,7 +182,6 @@ class ReaderDiscoverViewModel @Inject constructor( swipeToRefreshTriggered = false } } else { - parentViewModel.onFeedEmptyStateLoaded() _uiState.value = DiscoverUiState.EmptyUiState.ShowNoPostsUiState { _navigationEvents.value = Event(ShowReaderSubs) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 69d24965b323..f06b8c225ef4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -18,7 +18,6 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode.MAIN import org.wordpress.android.BuildConfig import org.wordpress.android.R -import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.fluxc.store.QuickStartStore import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask @@ -44,7 +43,6 @@ import org.wordpress.android.ui.reader.utils.DateProvider import org.wordpress.android.ui.reader.utils.ReaderTopBarMenuHelper import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState.TabUiState -import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCardItemData import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterSelectedItem import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterType import org.wordpress.android.ui.utils.UiString @@ -53,7 +51,6 @@ import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.util.QuickStartUtils import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.UrlUtilsWrapper -import org.wordpress.android.util.config.ReaderAnnouncementCardFeatureConfig import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig import org.wordpress.android.util.distinct import org.wordpress.android.viewmodel.Event @@ -83,8 +80,6 @@ class ReaderViewModel @Inject constructor( private val readerTopBarMenuHelper: ReaderTopBarMenuHelper, private val urlUtilsWrapper: UrlUtilsWrapper, private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig, - // todo: annnmarie removed this private val getFollowedTagsUseCase: GetFollowedTagsUseCase - private val readerAnnouncementCardFeatureConfig: ReaderAnnouncementCardFeatureConfig, ) : ScopedViewModel(mainDispatcher) { private var initialized: Boolean = false private var wasPaused: Boolean = false @@ -97,9 +92,6 @@ class ReaderViewModel @Inject constructor( private val _topBarUiState = MutableLiveData() val topBarUiState: LiveData = _topBarUiState.distinct() - private val _announcementCardState = MutableLiveData() - val announcementCardState: LiveData = _announcementCardState - private val _updateTags = MutableLiveData>() val updateTags: LiveData> = _updateTags @@ -141,58 +133,10 @@ class ReaderViewModel @Inject constructor( } } - fun onFeedEmptyStateLoaded() { - hideAnnouncementCard() - } - - fun onFeedContentLoaded() { - updateAnnouncementCard() - } - - private fun hideAnnouncementCard() { - _announcementCardState.value = _announcementCardState.value?.copy( - shouldShow = false, - ) - } - private fun showJetpackPoweredBottomSheet() { // _showJetpackPoweredBottomSheet.value = Event(true) } - private fun updateAnnouncementCard() { - val items = mutableListOf() - - if (readerTagsFeedFeatureConfig.isEnabled()) { - items.add( - ReaderAnnouncementCardItemData( - iconRes = R.drawable.ic_reader_tag, - titleRes = R.string.reader_announcement_card_tags_stream_title, - descriptionRes = R.string.reader_announcement_card_tags_stream_description, - ) - ) - } - - items.add( - ReaderAnnouncementCardItemData( - iconRes = R.drawable.ic_reader_preferences, - titleRes = R.string.reader_announcement_card_reading_preferences_title, - descriptionRes = R.string.reader_announcement_card_reading_preferences_description, - ) - ) - val isDiscoverSelected = selectedReaderTag()?.isDiscover == true - _announcementCardState.value = AnnouncementCardUiState( - shouldShow = isDiscoverSelected && readerAnnouncementCardFeatureConfig.isEnabled() && - appPrefsWrapper.shouldShowReaderAnnouncementCard(), - items = items, - ) - } - - fun onAnnouncementCardDoneClick() { - readerTracker.track(AnalyticsTracker.Stat.READER_ANNOUNCEMENT_CARD_DISMISSED) - appPrefsWrapper.setShouldShowReaderAnnouncementCard(false) - updateAnnouncementCard() - } - @JvmOverloads fun loadTabs(savedInstanceState: Bundle? = null) { launch { @@ -212,9 +156,6 @@ class ReaderViewModel @Inject constructor( } fun onTagChanged(selectedTag: ReaderTag?) { - if (selectedTag?.isDiscover == false) { - hideAnnouncementCard() - } selectedTag?.let { trackReaderTabShownIfNecessary(it) } @@ -284,7 +225,7 @@ class ReaderViewModel @Inject constructor( // Determine if analytics should be bumped either due to tags changed or time elapsed since last bump val now = DateProvider().getCurrentDate().time val shouldBumpAnalytics = event.didChange() - || ( now - appPrefsWrapper.readerAnalyticsCountTagsTimestamp > ONE_HOUR_MILLIS) + || (now - appPrefsWrapper.readerAnalyticsCountTagsTimestamp > ONE_HOUR_MILLIS) if (shouldBumpAnalytics) { readerTracker.trackFollowedTagsCount(event.totalTags) @@ -481,7 +422,9 @@ class ReaderViewModel @Inject constructor( when (item) { is SubfilterListItem.SiteAll -> clearTopBarFilter() is SubfilterListItem.Site -> updateTopBarFilter(item.blog.name - .ifEmpty { urlUtilsWrapper.removeScheme(item.blog.url.ifEmpty { "" }) }, ReaderFilterType.BLOG) + .ifEmpty { urlUtilsWrapper.removeScheme(item.blog.url.ifEmpty { "" }) }, ReaderFilterType.BLOG + ) + is SubfilterListItem.Tag -> updateTopBarFilter(item.tag.tagDisplayName, ReaderFilterType.TAG) else -> Unit // do nothing } @@ -621,11 +564,6 @@ class ReaderViewModel @Inject constructor( val duration: Int = QUICK_START_PROMPT_DURATION ) - data class AnnouncementCardUiState( - val shouldShow: Boolean, - val items: List, - ) - companion object { private const val QUICK_START_PROMPT_DURATION = 5000 private const val FILTER_UPDATE_DELAY = 50L From c232b0124e722beda5c95fa14a96739a4265c713 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 23 May 2024 19:22:54 -0300 Subject: [PATCH 226/237] Update Announcement unit tests --- .../ReaderAnnouncementRepositoryTest.kt | 150 ++++++++++++++++++ .../reader/viewmodels/ReaderViewModelTest.kt | 104 +----------- 2 files changed, 151 insertions(+), 103 deletions(-) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepositoryTest.kt diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepositoryTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepositoryTest.kt new file mode 100644 index 000000000000..e6c3b5156833 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepositoryTest.kt @@ -0,0 +1,150 @@ +package org.wordpress.android.ui.reader.repository + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.ui.reader.tracker.ReaderTracker +import org.wordpress.android.util.config.ReaderAnnouncementCardFeatureConfig +import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig + +@RunWith(MockitoJUnitRunner::class) +class ReaderAnnouncementRepositoryTest { + @Mock + private lateinit var readerAnnouncementCardFeatureConfig: ReaderAnnouncementCardFeatureConfig + + @Mock + private lateinit var readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig + + @Mock + private lateinit var appPrefsWrapper: AppPrefsWrapper + + @Mock + private lateinit var readerTracker: ReaderTracker + + private lateinit var repository: ReaderAnnouncementRepository + + @Before + fun setUp() { + repository = ReaderAnnouncementRepository( + readerAnnouncementCardFeatureConfig, + readerTagsFeedFeatureConfig, + appPrefsWrapper, + readerTracker + ) + } + + @Test + fun `given feature config is off the hasReaderAnnouncement is false`() { + // Given + whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(false) + + // When + val hasAnnouncement = repository.hasReaderAnnouncement() + + // Then + assertThat(hasAnnouncement).isFalse() + } + + @Test + fun `given should show announcement in prefs is false the hasReaderAnnouncement is false`() { + // Given + whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) + whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(false) + + // When + val hasAnnouncement = repository.hasReaderAnnouncement() + + // Then + assertThat(hasAnnouncement).isFalse() + } + + @Test + fun `given feature config is on and should show announcement in prefs is true the hasReaderAnnouncement is true`() { + // Given + whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) + whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(true) + + // When + val hasAnnouncement = repository.hasReaderAnnouncement() + + // Then + assertThat(hasAnnouncement).isTrue() + } + + @Test + fun `given tags feed feature is off when getReaderAnnouncementItems then return single item`() { + // Given + whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) + whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(true) + whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(false) + + // When + val items = repository.getReaderAnnouncementItems() + + // Then + assertThat(items).hasSize(1) + + val readerPreferencesItem = items[0] + assertThat(readerPreferencesItem.iconRes).isEqualTo(R.drawable.ic_reader_preferences) + assertThat(readerPreferencesItem.titleRes).isEqualTo( + R.string.reader_announcement_card_reading_preferences_title + ) + assertThat(readerPreferencesItem.descriptionRes).isEqualTo( + R.string.reader_announcement_card_reading_preferences_description + ) + } + + @Test + fun `given tags feed feature is on when getReaderAnnouncementItems then return single item`() { + // Given + whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) + whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(true) + whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(true) + + // When + val items = repository.getReaderAnnouncementItems() + + // Then + assertThat(items).hasSize(2) + + val tagsFeedItem = items[0] + assertThat(tagsFeedItem.iconRes).isEqualTo(R.drawable.ic_reader_tag) + assertThat(tagsFeedItem.titleRes).isEqualTo(R.string.reader_announcement_card_tags_stream_title) + assertThat(tagsFeedItem.descriptionRes).isEqualTo(R.string.reader_announcement_card_tags_stream_description) + + val readerPreferencesItem = items[1] + assertThat(readerPreferencesItem.iconRes).isEqualTo(R.drawable.ic_reader_preferences) + assertThat(readerPreferencesItem.titleRes).isEqualTo( + R.string.reader_announcement_card_reading_preferences_title + ) + assertThat(readerPreferencesItem.descriptionRes).isEqualTo( + R.string.reader_announcement_card_reading_preferences_description + ) + } + + @Test + fun `when dismissReaderAnnouncement then track`() { + // When + repository.dismissReaderAnnouncement() + + // Then + verify(readerTracker).track(AnalyticsTracker.Stat.READER_ANNOUNCEMENT_CARD_DISMISSED) + } + + @Test + fun `when dismissReaderAnnouncement then set should show reader announcement card to false`() { + // When + repository.dismissReaderAnnouncement() + + // Then + verify(appPrefsWrapper).setShouldShowReaderAnnouncementCard(false) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt index 64f90bae1d04..4fdc83eca39d 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModelTest.kt @@ -42,12 +42,9 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.TopBarUiState import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.UrlUtilsWrapper -import org.wordpress.android.util.config.ReaderAnnouncementCardFeatureConfig import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig import org.wordpress.android.viewmodel.Event import java.util.Date -import kotlin.test.assertFalse -import kotlin.test.assertTrue private const val DUMMY_CURRENT_TIME: Long = 10000000000 @@ -92,9 +89,6 @@ class ReaderViewModelTest : BaseUnitTest() { @Mock lateinit var readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig - @Mock - lateinit var readerAnnouncementCardFeatureConfig: ReaderAnnouncementCardFeatureConfig - private val emptyReaderTagList = ReaderTagList() private val nonEmptyReaderTagList = createNonMockedNonEmptyReaderTagList() @@ -118,7 +112,6 @@ class ReaderViewModelTest : BaseUnitTest() { ReaderTopBarMenuHelper(readerTagsFeedFeatureConfig), urlUtilsWrapper, readerTagsFeedFeatureConfig, - readerAnnouncementCardFeatureConfig, ) whenever(dateProvider.getCurrentDate()).thenReturn(Date(DUMMY_CURRENT_TIME)) @@ -509,95 +502,6 @@ class ReaderViewModelTest : BaseUnitTest() { assertThat(showJetpackOverlayEvent.last().peekContent()).isTrue } - @Test - fun `Should update announcement card UI correctly with tags item`() = testWithNonEmptyTags { - whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(true) - - viewModel.onFeedContentLoaded() - val observers = initObservers() - - val announcementCardUiState = observers.announcementCardStateEvents.first() - - assertThat(announcementCardUiState.items).hasSize(2) - - val tagsFeedItem = announcementCardUiState.items[0] - assertThat(tagsFeedItem.iconRes).isEqualTo(R.drawable.ic_reader_tag) - assertThat(tagsFeedItem.titleRes).isEqualTo(R.string.reader_announcement_card_tags_stream_title) - assertThat(tagsFeedItem.descriptionRes).isEqualTo(R.string.reader_announcement_card_tags_stream_description) - - val readerPreferencesItem = announcementCardUiState.items[1] - assertThat(readerPreferencesItem.iconRes).isEqualTo(R.drawable.ic_reader_preferences) - assertThat(readerPreferencesItem.titleRes).isEqualTo( - R.string.reader_announcement_card_reading_preferences_title - ) - assertThat(readerPreferencesItem.descriptionRes).isEqualTo( - R.string.reader_announcement_card_reading_preferences_description - ) - } - - @Test - fun `Should update announcement card UI correctly without tags item onFeedContentLoaded`() = testWithNonEmptyTags { - whenever(readerTagsFeedFeatureConfig.isEnabled()).thenReturn(false) - - viewModel.onFeedContentLoaded() - val observers = initObservers() - - val announcementCardUiState = observers.announcementCardStateEvents.first() - - assertThat(announcementCardUiState.items).hasSize(1) - - val readerPreferencesItem = announcementCardUiState.items[0] - assertThat(readerPreferencesItem.iconRes).isEqualTo(R.drawable.ic_reader_preferences) - assertThat(readerPreferencesItem.titleRes).isEqualTo( - R.string.reader_announcement_card_reading_preferences_title - ) - assertThat(readerPreferencesItem.descriptionRes).isEqualTo( - R.string.reader_announcement_card_reading_preferences_description - ) - } - - @Test - fun `Should show announcement card if feature flag is enabled, app preference returns true and feed is Discover`() = - testWithNonEmptyTags { - val readerTag = ReaderTag("Discover", "Discover", "Discover", DISCOVER_PATH, ReaderTagType.DEFAULT) - whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) - whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(true) - val observers = initObservers() - triggerContentDisplay() - viewModel.updateSelectedContent(readerTag) - viewModel.onFeedContentLoaded() - - val announcementCardUiState = observers.announcementCardStateEvents.first() - assertTrue(announcementCardUiState.shouldShow) - } - - @Test - fun `Should NOT show announcement card if feature flag is disabled`() = testWithNonEmptyTags { - val readerTag = ReaderTag("Discover", "Discover", "Discover", DISCOVER_PATH, ReaderTagType.DEFAULT) - whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(false) - val observers = initObservers() - triggerContentDisplay() - viewModel.updateSelectedContent(readerTag) - viewModel.onFeedContentLoaded() - - val announcementCardUiState = observers.announcementCardStateEvents.first() - assertFalse(announcementCardUiState.shouldShow) - } - - @Test - fun `Should NOT show announcement card if app preference returns false`() = testWithNonEmptyTags { - val readerTag = ReaderTag("Discover", "Discover", "Discover", DISCOVER_PATH, ReaderTagType.DEFAULT) - whenever(readerAnnouncementCardFeatureConfig.isEnabled()).thenReturn(true) - whenever(appPrefsWrapper.shouldShowReaderAnnouncementCard()).thenReturn(false) - triggerContentDisplay() - viewModel.updateSelectedContent(readerTag) - viewModel.onFeedContentLoaded() - val observers = initObservers() - - val announcementCardUiState = observers.announcementCardStateEvents.first() - assertFalse(announcementCardUiState.shouldShow) - } - private fun assertQsFollowSiteTaskStarted( observers: Observers, isSettingsSupported: Boolean = true @@ -642,19 +546,13 @@ class ReaderViewModelTest : BaseUnitTest() { // tabNavigationEvents.add(it.peekContent()) // } - val announcementCardStateEvents = mutableListOf() - viewModel.announcementCardState.observeForever { - announcementCardStateEvents.add(it) - } - - return Observers(uiStates, quickStartReaderPrompts, tabNavigationEvents, announcementCardStateEvents) + return Observers(uiStates, quickStartReaderPrompts, tabNavigationEvents) } private data class Observers( val uiStates: List, val quickStartReaderPrompts: List>, val tabNavigationEvents: List, - val announcementCardStateEvents: List, ) private fun triggerContentDisplay( From 237588799e762d39c27ee5a871935e0d81962d06 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Thu, 23 May 2024 19:29:11 -0300 Subject: [PATCH 227/237] Remove the announcement compose view from reader_fragment_layout.xml --- WordPress/src/main/res/layout/reader_fragment_layout.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/WordPress/src/main/res/layout/reader_fragment_layout.xml b/WordPress/src/main/res/layout/reader_fragment_layout.xml index 8bab85f1abe9..64d03853edf3 100644 --- a/WordPress/src/main/res/layout/reader_fragment_layout.xml +++ b/WordPress/src/main/res/layout/reader_fragment_layout.xml @@ -16,12 +16,6 @@ android:layout_height="wrap_content" app:layout_scrollFlags="scroll|enterAlways" /> - - Date: Fri, 24 May 2024 11:27:13 -0300 Subject: [PATCH 228/237] Improve Item View Type logic --- .../ui/reader/adapters/ReaderPostAdapter.java | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java index 5237901a752e..7a9aec413396 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java @@ -226,28 +226,16 @@ private static class GapMarkerViewHolder extends RecyclerView.ViewHolder { @Override public int getItemViewType(int position) { - // first item logic - if (position == 0) { - // should check for announcement and headers - if (hasAnnouncement()) { - // first item is a ReaderAnnouncementView - return VIEW_TYPE_READER_ANNOUNCEMENT; - } else if (hasSiteHeader()) { - // first item is a ReaderSiteHeaderView - return VIEW_TYPE_SITE_HEADER; - } else if (hasTagHeader()) { - // first item is a ReaderTagHeaderView - return VIEW_TYPE_TAG_HEADER; - } + // announcement logic + if (getAnnouncementPosition() == position) { + return VIEW_TYPE_READER_ANNOUNCEMENT; } - // second item logic if we have a Reader Announcement - if (hasAnnouncement() && position == 1) { + // header logic + if (position == getHeaderPosition()){ if (hasSiteHeader()) { - // first item is a ReaderSiteHeaderView return VIEW_TYPE_SITE_HEADER; } else if (hasTagHeader()) { - // first item is a ReaderTagHeaderView return VIEW_TYPE_TAG_HEADER; } } From 9172623d3628082862e905f6cacafd599c2c0b66 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 24 May 2024 12:53:38 -0300 Subject: [PATCH 229/237] Fix detekt issue --- .../wordpress/android/ui/reader/adapters/ReaderPostAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java index 7a9aec413396..c59f610b3003 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java @@ -232,7 +232,7 @@ public int getItemViewType(int position) { } // header logic - if (position == getHeaderPosition()){ + if (position == getHeaderPosition()) { if (hasSiteHeader()) { return VIEW_TYPE_SITE_HEADER; } else if (hasTagHeader()) { From 6085d9cf628d9fe56eef85ffb1c14694afddf147 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 24 May 2024 13:26:42 -0300 Subject: [PATCH 230/237] Hide Announcement in Subscriptions feed when filtered --- .../android/ui/reader/adapters/ReaderPostAdapter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java index c59f610b3003..b576f43f2ad0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java @@ -717,7 +717,9 @@ private boolean hasTagHeader() { } private boolean hasAnnouncement() { - return mIsMainReader && mReaderAnnouncementRepository.hasReaderAnnouncement() && !isEmpty(); + return mIsMainReader && mReaderAnnouncementRepository.hasReaderAnnouncement() && !isEmpty() + && (getPostListType() != ReaderPostListType.BLOG_PREVIEW) + && (mCurrentTag != null && !mCurrentTag.isTagTopic()); } private boolean isDiscover() { From 16bc1458362340bcc8d402d290c37fd0f098747c Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 24 May 2024 12:25:56 -0300 Subject: [PATCH 231/237] Add Reader Announcement on Tags Feed --- .../tagsfeed/ReaderTagsFeedUiStateMapper.kt | 2 + .../tagsfeed/ReaderTagsFeedViewModel.kt | 78 +++++++++++++------ .../views/compose/tagsfeed/ReaderTagsFeed.kt | 49 ++++++------ 3 files changed, 82 insertions(+), 47 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 015a1433aec1..83c85bb7d9d5 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 @@ -86,6 +86,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( @Suppress("LongParameterList") fun mapInitialPostsUiState( tags: List, + announcementItem: ReaderTagsFeedViewModel.ReaderAnnouncementItem?, isRefreshing: Boolean, onTagChipClick: (ReaderTag) -> Unit, onMoreFromTagClick: (ReaderTag) -> Unit, @@ -101,6 +102,7 @@ class ReaderTagsFeedUiStateMapper @Inject constructor( onItemEnteredView = onItemEnteredView, ) }, + announcementItem = announcementItem, isRefreshing = isRefreshing, onRefresh = onRefresh, ) 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 8ccfa74dd5fa..79e902858692 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 @@ -28,9 +28,11 @@ import org.wordpress.android.ui.reader.discover.ReaderPostCardActionsHandler import org.wordpress.android.ui.reader.discover.ReaderPostMoreButtonUiStateBuilder import org.wordpress.android.ui.reader.discover.ReaderPostUiStateBuilder import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException +import org.wordpress.android.ui.reader.repository.ReaderAnnouncementRepository 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.ReaderAnnouncementCardItemData import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.util.DisplayUtilsWrapper import org.wordpress.android.util.NetworkUtilsWrapper @@ -53,6 +55,7 @@ class ReaderTagsFeedViewModel @Inject constructor( private val displayUtilsWrapper: DisplayUtilsWrapper, private val readerTracker: ReaderTracker, private val networkUtilsWrapper: NetworkUtilsWrapper, + private val readerAnnouncementRepository: ReaderAnnouncementRepository, ) : ScopedViewModel(bgDispatcher) { private val _uiStateFlow: MutableStateFlow = MutableStateFlow(UiState.Initial) val uiStateFlow: StateFlow = _uiStateFlow @@ -88,41 +91,45 @@ class ReaderTagsFeedViewModel @Inject constructor( } } - fun onTagsChanged(tags: List) = _uiStateFlow.update { currentState -> - when { - tags.isEmpty() -> { - UiState.Empty(::onOpenTagsListClick) - } + fun onTagsChanged(tags: List) { + return _uiStateFlow.update { currentState -> + when { + tags.isEmpty() -> { + UiState.Empty(::onOpenTagsListClick) + } + + currentState is UiState.Loaded -> { + val currentTags = currentState.data.map { it.tagChip.tag } + if (currentState.isRefreshing) { + readerTagsFeedUiStateMapper.mapInitialPostsUiState( + tags, + getAnnouncementItem(), + false, + ::onTagChipClick, + ::onMoreFromTagClick, + ::onItemEnteredView, + ::onRefresh + ) + } else if (currentTags != tags) { + updateLoadedStateWithTags(currentState, tags) + } else { + currentState + } + } - currentState is UiState.Loaded -> { - val currentTags = currentState.data.map { it.tagChip.tag } - if (currentState.isRefreshing) { + else -> { + // Add tags to the list with the posts initial/loading UI readerTagsFeedUiStateMapper.mapInitialPostsUiState( tags, + getAnnouncementItem(), false, ::onTagChipClick, ::onMoreFromTagClick, ::onItemEnteredView, ::onRefresh ) - } else if (currentTags != tags) { - updateLoadedStateWithTags(currentState, tags) - } else { - currentState } } - - else -> { - // Add tags to the list with the posts initial/loading UI - readerTagsFeedUiStateMapper.mapInitialPostsUiState( - tags, - false, - ::onTagChipClick, - ::onMoreFromTagClick, - ::onItemEnteredView, - ::onRefresh - ) - } } } @@ -146,6 +153,23 @@ class ReaderTagsFeedViewModel @Inject constructor( } } + private fun getAnnouncementItem(): ReaderAnnouncementItem? = + if (readerAnnouncementRepository.hasReaderAnnouncement()) { + ReaderAnnouncementItem( + items = readerAnnouncementRepository.getReaderAnnouncementItems(), + onDoneClicked = ::dismissAnnouncementItem, + ) + } else { + null + } + + private fun dismissAnnouncementItem() { + readerAnnouncementRepository.dismissReaderAnnouncement() + _uiStateFlow.update { + (it as? UiState.Loaded)?.copy(announcementItem = null) ?: it + } + } + private fun updateLoadedStateWithTags(state: UiState.Loaded, tags: List): UiState.Loaded { val currentTagsMap = state.data.associateBy { it.tagChip.tag.tagSlug } val updatedData = tags.map { tag -> @@ -548,6 +572,7 @@ class ReaderTagsFeedViewModel @Inject constructor( data class Loaded( val data: List, + val announcementItem: ReaderAnnouncementItem? = null, val isRefreshing: Boolean = false, val onRefresh: () -> Unit = {}, ) : UiState() @@ -559,6 +584,11 @@ class ReaderTagsFeedViewModel @Inject constructor( data class NoConnection(val onRetryClick: () -> Unit) : UiState() } + data class ReaderAnnouncementItem( + val items: List, + val onDoneClicked: () -> Unit, + ) + data class TagFeedItem( val tagChip: TagChip, val postList: PostList, 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 1394c2d971ce..5b131b5bc84e 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 @@ -65,6 +65,7 @@ import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewMod import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.TagChip import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.TagFeedItem import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.UiState +import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCard import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterChip import org.wordpress.android.ui.utils.UiString @@ -107,6 +108,16 @@ private fun Loaded(uiState: UiState.Loaded) { modifier = Modifier .fillMaxSize(), ) { + uiState.announcementItem?.let { announcementItem -> + item(key = "reader-announcement-card") { + ReaderAnnouncementCard( + shouldShow = true, + items = announcementItem.items, + onAnnouncementCardDoneClick = announcementItem.onDoneClicked, + ) + } + } + items( items = uiState.data, key = { it.tagChip.tag.tagSlug } @@ -611,33 +622,25 @@ fun ReaderTagsFeedLoaded() { ), ) ) - val readerTag = ReaderTag( - "Tag 1", - "Tag 1", - "Tag 1", - "Tag 1", - ReaderTagType.TAGS, - ) ReaderTagsFeed( uiState = UiState.Loaded( - data = listOf( + data = List(4) { + val tagName = "Tag ${it + 1}" TagFeedItem( - tagChip = TagChip(readerTag, {}, {}), + tagChip = TagChip( + tag = ReaderTag( + tagName, + tagName, + tagName, + tagName, + ReaderTagType.TAGS, + ), + onTagChipClick = {}, + onMoreFromTagClick = {}, + ), postList = postListLoaded - ), - TagFeedItem( - tagChip = TagChip(readerTag, {}, {}), - postList = PostList.Initial, - ), - TagFeedItem( - tagChip = TagChip(readerTag, {}, {}), - postList = PostList.Error(ErrorType.Default, {}), - ), - TagFeedItem( - tagChip = TagChip(readerTag, {}, {}), - postList = PostList.Error(ErrorType.NoContent, {}), - ), - ) + ) + } ) ) } From 6738e9bb1f70de75ee3e8bafb60b291155a56d3b Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 24 May 2024 12:43:25 -0300 Subject: [PATCH 232/237] Update mapper tests --- .../viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt index 54df791458d7..797e7ba280a2 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/tagsfeed/ReaderTagsFeedUiStateMapperTest.kt @@ -323,6 +323,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { assertEquals(expected, actual) } + @Suppress("LongMethod") @Test fun `Should map initial posts UI state correctly`() { // Given @@ -345,10 +346,15 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { ReaderTagType.FOLLOWED, ) val tags = listOf(tag1, tag2) + val announcementItem = ReaderTagsFeedViewModel.ReaderAnnouncementItem( + items = listOf(mock(), mock()), + onDoneClicked = {}, + ) // When val actual = classToTest.mapInitialPostsUiState( tags = tags, + announcementItem = announcementItem, isRefreshing = true, onTagChipClick = onTagChipClick, onMoreFromTagClick = onMoreFromTagClick, @@ -378,6 +384,7 @@ class ReaderTagsFeedUiStateMapperTest : BaseUnitTest() { onItemEnteredView = onItemEnteredView, ) ), + announcementItem = announcementItem, isRefreshing = true, onRefresh = onRefresh, ) From b37f1bf2f0374b31528e63d33f77cca476d67588 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 24 May 2024 12:51:12 -0300 Subject: [PATCH 233/237] Update unit tests for ReaderTagsFeedViewModel --- .../viewmodels/ReaderTagsFeedViewModelTest.kt | 90 +++++++++++++++++-- 1 file changed, 83 insertions(+), 7 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 992017556433..3bb004161a6e 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 @@ -14,7 +14,9 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doSuspendableAnswer +import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions @@ -35,12 +37,14 @@ import org.wordpress.android.ui.reader.discover.ReaderPostCardActionsHandler import org.wordpress.android.ui.reader.discover.ReaderPostMoreButtonUiStateBuilder import org.wordpress.android.ui.reader.discover.ReaderPostUiStateBuilder import org.wordpress.android.ui.reader.exceptions.ReaderPostFetchException +import org.wordpress.android.ui.reader.repository.ReaderAnnouncementRepository 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.viewmodels.tagsfeed.ReaderTagsFeedUiStateMapper import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel import org.wordpress.android.ui.reader.viewmodels.tagsfeed.ReaderTagsFeedViewModel.ActionEvent +import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCardItemData import org.wordpress.android.ui.reader.views.compose.tagsfeed.TagsFeedPostItem import org.wordpress.android.util.DisplayUtilsWrapper import org.wordpress.android.util.NetworkUtilsWrapper @@ -86,6 +90,9 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { @Mock lateinit var networkUtilsWrapper: NetworkUtilsWrapper + @Mock + lateinit var readerAnnouncementRepository: ReaderAnnouncementRepository + private lateinit var viewModel: ReaderTagsFeedViewModel private val collectedUiStates: MutableList = mutableListOf() @@ -116,6 +123,7 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { displayUtilsWrapper = displayUtilsWrapper, readerTracker = readerTracker, networkUtilsWrapper = networkUtilsWrapper, + readerAnnouncementRepository = readerAnnouncementRepository, ) whenever(readerPostCardActionsHandler.navigationEvents) .thenReturn(navigationEvents) @@ -126,6 +134,69 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { observeErrorMessageEvents() } + @Test + fun `when tags changed, then UI state should update properly`() = testCollectingUiStates { + // Given + val tag = ReaderTestUtils.createTag("tag") + mockMapInitialTagFeedItems() + + // When + viewModel.onTagsChanged(listOf(tag)) + advanceUntilIdle() + + // Then + assertThat(collectedUiStates).contains( + ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf(getInitialTagFeedItem(tag)), + ) + ) + } + + @Test + fun `given has announcement, when tags changed, then UI state should update properly`() = testCollectingUiStates { + // Given + val tag = ReaderTestUtils.createTag("tag") + val announcementItems = listOf(mock(), mock()) + mockMapInitialTagFeedItems() + whenever(readerAnnouncementRepository.hasReaderAnnouncement()).thenReturn(true) + whenever(readerAnnouncementRepository.getReaderAnnouncementItems()).thenReturn(announcementItems) + + // When + viewModel.onTagsChanged(listOf(tag)) + advanceUntilIdle() + + // Then + val loadedState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.Loaded + assertThat(loadedState.data).isEqualTo(listOf(getInitialTagFeedItem(tag))) + assertThat(loadedState.announcementItem!!.items).isEqualTo(announcementItems) + } + + @Test + fun `given has announcement, when done clicked, then UI state should update properly`() = testCollectingUiStates { + // Given + val tag = ReaderTestUtils.createTag("tag") + val announcementItems = listOf(mock(), mock()) + mockMapInitialTagFeedItems() + whenever(readerAnnouncementRepository.hasReaderAnnouncement()).thenReturn(true) + whenever(readerAnnouncementRepository.getReaderAnnouncementItems()).thenReturn(announcementItems) + + viewModel.onTagsChanged(listOf(tag)) + advanceUntilIdle() + + // When + val loadedState = collectedUiStates.last() as ReaderTagsFeedViewModel.UiState.Loaded + loadedState.announcementItem!!.onDoneClicked() + advanceUntilIdle() + + // Then + verify(readerAnnouncementRepository).dismissReaderAnnouncement() + assertThat(collectedUiStates.last()).isEqualTo( + ReaderTagsFeedViewModel.UiState.Loaded( + data = listOf(getInitialTagFeedItem(tag)), + ) + ) + } + @Test fun `given valid tag, when loaded, then UI state should update properly`() = testCollectingUiStates { // Given @@ -899,13 +970,18 @@ class ReaderTagsFeedViewModelTest : BaseUnitTest() { } private fun mockMapInitialTagFeedItems() { - whenever(readerTagsFeedUiStateMapper.mapInitialPostsUiState(any(), any(), any(), any(), any(), any())) - .thenAnswer { - val tags = it.getArgument>(0) - ReaderTagsFeedViewModel.UiState.Loaded( - tags.map { tag -> getInitialTagFeedItem(tag) } - ) - } + whenever( + readerTagsFeedUiStateMapper.mapInitialPostsUiState( + any(), anyOrNull(), any(), any(), any(), any(), any() + ) + ).thenAnswer { + val tags = it.getArgument>(0) + val announcementItem = it.getArgument(1) + ReaderTagsFeedViewModel.UiState.Loaded( + data = tags.map { tag -> getInitialTagFeedItem(tag) }, + announcementItem = announcementItem, + ) + } } private fun mockMapLoadingTagFeedItems() { From 7d83aa22f5ef9d8b4afdcd7cd2c6ba03084cb5ab Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 24 May 2024 12:57:14 -0300 Subject: [PATCH 234/237] Remove shouldShow from ReaderAnnouncementCard --- .../views/ReaderAnnouncementCardView.kt | 1 - .../views/compose/ReaderAnnouncementCard.kt | 73 ++++++++----------- .../views/compose/tagsfeed/ReaderTagsFeed.kt | 1 - 3 files changed, 30 insertions(+), 45 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt index 6d4c69c88fab..6d631b517325 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderAnnouncementCardView.kt @@ -23,7 +23,6 @@ class ReaderAnnouncementCardView @JvmOverloads constructor( override fun Content() { AppTheme { ReaderAnnouncementCard( - shouldShow = true, items = items.value, onAnnouncementCardDoneClick = { onDoneClickListener.value?.onDoneClick() } ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt index 1c4bb3877354..b531c6bd1ce0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/compose/ReaderAnnouncementCard.kt @@ -3,9 +3,6 @@ package org.wordpress.android.ui.reader.views.compose import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandIn -import androidx.compose.animation.shrinkOut import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -35,57 +32,48 @@ import org.wordpress.android.ui.compose.unit.Margin @Composable fun ReaderAnnouncementCard( - shouldShow: Boolean, items: List, onAnnouncementCardDoneClick: () -> Unit, ) { val primaryColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black val secondaryColor = if (isSystemInDarkTheme()) AppColor.Black else AppColor.White - AnimatedVisibility( - visible = shouldShow, - enter = expandIn(), - exit = shrinkOut( - shrinkTowards = Alignment.TopCenter, - ), + Column( + modifier = Modifier + .fillMaxWidth() + .padding(Margin.ExtraLarge.value), + verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value), ) { + // Title + Text( + text = stringResource(R.string.reader_announcement_card_title), + style = MaterialTheme.typography.labelLarge, + color = primaryColor, + ) + // Items Column( modifier = Modifier - .fillMaxWidth() - .padding(Margin.ExtraLarge.value), - verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value), + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value) + ) { + items.forEach { + ReaderAnnouncementCardItem(it) + } + } + // Done button + Button( + modifier = Modifier + .fillMaxWidth(), + onClick = { onAnnouncementCardDoneClick() }, + elevation = ButtonDefaults.elevation(0.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = primaryColor, + ), ) { - // Title Text( - text = stringResource(R.string.reader_announcement_card_title), + text = stringResource(id = R.string.reader_btn_done), + color = secondaryColor, style = MaterialTheme.typography.labelLarge, - color = primaryColor, ) - // Items - Column( - modifier = Modifier - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value) - ) { - items.forEach { - ReaderAnnouncementCardItem(it) - } - } - // Done button - Button( - modifier = Modifier - .fillMaxWidth(), - onClick = { onAnnouncementCardDoneClick() }, - elevation = ButtonDefaults.elevation(0.dp), - colors = ButtonDefaults.buttonColors( - backgroundColor = primaryColor, - ), - ) { - Text( - text = stringResource(id = R.string.reader_btn_done), - color = secondaryColor, - style = MaterialTheme.typography.labelLarge, - ) - } } } } @@ -158,7 +146,6 @@ fun ReaderTagsFeedPostListItemPreview() { .fillMaxWidth() ) { ReaderAnnouncementCard( - shouldShow = false, items = listOf( ReaderAnnouncementCardItemData( iconRes = R.drawable.ic_wifi_off_24px, 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 5b131b5bc84e..7b30f1f0aa0b 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 @@ -111,7 +111,6 @@ private fun Loaded(uiState: UiState.Loaded) { uiState.announcementItem?.let { announcementItem -> item(key = "reader-announcement-card") { ReaderAnnouncementCard( - shouldShow = true, items = announcementItem.items, onAnnouncementCardDoneClick = announcementItem.onDoneClicked, ) From 5d8db54ef9ccfed47db189c550c5260ce3254e38 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 24 May 2024 15:51:32 -0300 Subject: [PATCH 235/237] Turn on Reader Tags Feed FF by default --- .../android/util/config/ReaderTagsFeedFeatureConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/ReaderTagsFeedFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderTagsFeedFeatureConfig.kt index a41e596b5b2f..acaa667ee3d4 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/config/ReaderTagsFeedFeatureConfig.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderTagsFeedFeatureConfig.kt @@ -6,7 +6,7 @@ import javax.inject.Inject private const val READER_TAGS_FEED_REMOTE_FIELD = "reader_tags_feed" -@Feature(remoteField = READER_TAGS_FEED_REMOTE_FIELD, defaultValue = false) +@Feature(remoteField = READER_TAGS_FEED_REMOTE_FIELD, defaultValue = true) class ReaderTagsFeedFeatureConfig @Inject constructor( appConfig: AppConfig ) : FeatureConfig( From eb93505ff8d1d703a984d8cb310d82ea16af03e1 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 24 May 2024 15:51:46 -0300 Subject: [PATCH 236/237] Turn on Reader Announcement FF by default --- .../android/util/config/ReaderAnnouncementCardFeatureConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/ReaderAnnouncementCardFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderAnnouncementCardFeatureConfig.kt index 9c9a87bb2645..84c58133a465 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/config/ReaderAnnouncementCardFeatureConfig.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderAnnouncementCardFeatureConfig.kt @@ -5,7 +5,7 @@ import org.wordpress.android.annotation.Feature import javax.inject.Inject private const val READER_ANNOUNCEMENT_CARD_REMOTE_FIELD = "reader_announcement_card" -@Feature(remoteField = READER_ANNOUNCEMENT_CARD_REMOTE_FIELD, defaultValue = false) +@Feature(remoteField = READER_ANNOUNCEMENT_CARD_REMOTE_FIELD, defaultValue = true) class ReaderAnnouncementCardFeatureConfig @Inject constructor( appConfig: AppConfig ) : FeatureConfig( From 911be72d3f82c84673fde5b70b205725b3a16a85 Mon Sep 17 00:00:00 2001 From: Thomas Horta Date: Fri, 24 May 2024 16:00:05 -0300 Subject: [PATCH 237/237] Rename Announcement repo to ReaderAnnouncementHelper --- .../ui/reader/adapters/ReaderPostAdapter.java | 10 +++++----- .../reader/discover/ReaderDiscoverViewModel.kt | 10 +++++----- .../ReaderAnnouncementHelper.kt} | 4 ++-- .../discover/ReaderDiscoverViewModelTest.kt | 18 +++++++++--------- .../ReaderAnnouncementHelperTest.kt} | 8 ++++---- 5 files changed, 25 insertions(+), 25 deletions(-) rename WordPress/src/main/java/org/wordpress/android/ui/reader/{repository/ReaderAnnouncementRepository.kt => utils/ReaderAnnouncementHelper.kt} (95%) rename WordPress/src/test/java/org/wordpress/android/ui/reader/{repository/ReaderAnnouncementRepositoryTest.kt => utils/ReaderAnnouncementHelperTest.kt} (96%) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java index b576f43f2ad0..af30a157d08c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java @@ -46,7 +46,7 @@ import org.wordpress.android.ui.reader.discover.viewholders.ReaderPostNewViewHolder; import org.wordpress.android.ui.reader.discover.viewholders.ReaderPostViewHolder; import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId; -import org.wordpress.android.ui.reader.repository.ReaderAnnouncementRepository; +import org.wordpress.android.ui.reader.utils.ReaderAnnouncementHelper; import org.wordpress.android.ui.reader.tracker.ReaderTab; import org.wordpress.android.ui.reader.tracker.ReaderTracker; import org.wordpress.android.ui.reader.utils.ReaderXPostUtils; @@ -135,7 +135,7 @@ public class ReaderPostAdapter extends RecyclerView.Adapter { - mReaderAnnouncementRepository.dismissReaderAnnouncement(); + mReaderAnnouncementHelper.dismissReaderAnnouncement(); notifyItemRemoved(getAnnouncementPosition()); }); } @@ -717,7 +717,7 @@ private boolean hasTagHeader() { } private boolean hasAnnouncement() { - return mIsMainReader && mReaderAnnouncementRepository.hasReaderAnnouncement() && !isEmpty() + return mIsMainReader && mReaderAnnouncementHelper.hasReaderAnnouncement() && !isEmpty() && (getPostListType() != ReaderPostListType.BLOG_PREVIEW) && (mCurrentTag != null && !mCurrentTag.isTagTopic()); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt index 007f8e93b847..7408e7f23735 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt @@ -27,7 +27,7 @@ import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents.ShowPosts import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents.ShowReaderSubs import org.wordpress.android.ui.reader.discover.ReaderNavigationEvents.ShowSitePickerForResult import org.wordpress.android.ui.reader.reblog.ReblogUseCase -import org.wordpress.android.ui.reader.repository.ReaderAnnouncementRepository +import org.wordpress.android.ui.reader.utils.ReaderAnnouncementHelper import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication.Error import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication.Started @@ -63,7 +63,7 @@ class ReaderDiscoverViewModel @Inject constructor( displayUtilsWrapper: DisplayUtilsWrapper, private val getFollowedTagsUseCase: GetFollowedTagsUseCase, private val readerImprovementsFeatureConfig: ReaderImprovementsFeatureConfig, - private val readerAnnouncementRepository: ReaderAnnouncementRepository, + private val readerAnnouncementHelper: ReaderAnnouncementHelper, @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, @Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher ) : ScopedViewModel(mainDispatcher) { @@ -161,10 +161,10 @@ class ReaderDiscoverViewModel @Inject constructor( } } else { if (posts != null && posts.cards.isNotEmpty()) { - val announcement = if (readerAnnouncementRepository.hasReaderAnnouncement()) { + val announcement = if (readerAnnouncementHelper.hasReaderAnnouncement()) { listOf( ReaderCardUiState.ReaderAnnouncementCardUiState( - readerAnnouncementRepository.getReaderAnnouncementItems(), + readerAnnouncementHelper.getReaderAnnouncementItems(), ::dismissAnnouncementCard ) ) @@ -192,7 +192,7 @@ class ReaderDiscoverViewModel @Inject constructor( } private fun dismissAnnouncementCard() { - readerAnnouncementRepository.dismissReaderAnnouncement() + readerAnnouncementHelper.dismissReaderAnnouncement() _uiState.value = (_uiState.value as? DiscoverUiState.ContentUiState)?.let { contentUiState -> contentUiState.copy( cards = contentUiState.cards.filterNot { it is ReaderCardUiState.ReaderAnnouncementCardUiState } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderAnnouncementHelper.kt similarity index 95% rename from WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepository.kt rename to WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderAnnouncementHelper.kt index 0a89094a2ca9..4dbfc19831af 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderAnnouncementHelper.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.reader.repository +package org.wordpress.android.ui.reader.utils import dagger.Reusable import org.wordpress.android.R @@ -11,7 +11,7 @@ import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig import javax.inject.Inject @Reusable -class ReaderAnnouncementRepository @Inject constructor( +class ReaderAnnouncementHelper @Inject constructor( private val readerAnnouncementCardFeatureConfig: ReaderAnnouncementCardFeatureConfig, private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig, private val appPrefsWrapper: AppPrefsWrapper, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt index 338a6d1eea2b..06995bd7a6b1 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModelTest.kt @@ -54,7 +54,7 @@ import org.wordpress.android.ui.reader.discover.ReaderPostCardActionType.LIKE import org.wordpress.android.ui.reader.discover.ReaderPostCardActionType.REBLOG import org.wordpress.android.ui.reader.discover.interests.TagUiState import org.wordpress.android.ui.reader.reblog.ReblogUseCase -import org.wordpress.android.ui.reader.repository.ReaderAnnouncementRepository +import org.wordpress.android.ui.reader.utils.ReaderAnnouncementHelper import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication.Error.NetworkUnavailable import org.wordpress.android.ui.reader.repository.ReaderDiscoverCommunication.Started @@ -139,7 +139,7 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { private lateinit var readerImprovementsFeatureConfig: ReaderImprovementsFeatureConfig @Mock - private lateinit var readerAnnouncementRepository: ReaderAnnouncementRepository + private lateinit var mReaderAnnouncementHelper: ReaderAnnouncementHelper private val fakeDiscoverFeed = ReactiveMutableLiveData() private val fakeCommunicationChannel = MutableLiveData>() @@ -164,7 +164,7 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { displayUtilsWrapper, getFollowedTagsUseCase, readerImprovementsFeatureConfig, - readerAnnouncementRepository, + mReaderAnnouncementHelper, testDispatcher(), testDispatcher() ) @@ -408,7 +408,7 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { @Test fun `if Announcement does not exist then ReaderAnnouncementCardUiState will not be present`() = test { // Arrange - whenever(readerAnnouncementRepository.hasReaderAnnouncement()).thenReturn(false) + whenever(mReaderAnnouncementHelper.hasReaderAnnouncement()).thenReturn(false) val uiStates = init(autoUpdateFeed = false).uiStates // Act fakeDiscoverFeed.value = createDummyReaderCardsList() // mock finished loading @@ -421,8 +421,8 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { @Test fun `if Announcement exists then ReaderAnnouncementCardUiState will be present`() = test { // Arrange - whenever(readerAnnouncementRepository.hasReaderAnnouncement()).thenReturn(true) - whenever(readerAnnouncementRepository.getReaderAnnouncementItems()).thenReturn(mock()) + whenever(mReaderAnnouncementHelper.hasReaderAnnouncement()).thenReturn(true) + whenever(mReaderAnnouncementHelper.getReaderAnnouncementItems()).thenReturn(mock()) val uiStates = init(autoUpdateFeed = false).uiStates // Act fakeDiscoverFeed.value = createDummyReaderCardsList() // mock finished loading @@ -435,8 +435,8 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { @Test fun `clicking done on ReaderAnnouncementCardUiState dismisses and updates the ContentUiState`() = test { // Arrange - whenever(readerAnnouncementRepository.hasReaderAnnouncement()).thenReturn(true) - whenever(readerAnnouncementRepository.getReaderAnnouncementItems()).thenReturn(mock()) + whenever(mReaderAnnouncementHelper.hasReaderAnnouncement()).thenReturn(true) + whenever(mReaderAnnouncementHelper.getReaderAnnouncementItems()).thenReturn(mock()) val uiStates = init(autoUpdateFeed = false).uiStates fakeDiscoverFeed.value = createDummyReaderCardsList() // mock finished loading @@ -447,7 +447,7 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() { announcementCard.onDoneClick() // Assert - verify(readerAnnouncementRepository).dismissReaderAnnouncement() + verify(mReaderAnnouncementHelper).dismissReaderAnnouncement() val newContentUiState = uiStates.last() as ContentUiState assertThat(newContentUiState.cards.first()) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepositoryTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderAnnouncementHelperTest.kt similarity index 96% rename from WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepositoryTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderAnnouncementHelperTest.kt index e6c3b5156833..7a1e99d93069 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderAnnouncementRepositoryTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/utils/ReaderAnnouncementHelperTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.reader.repository +package org.wordpress.android.ui.reader.utils import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -16,7 +16,7 @@ import org.wordpress.android.util.config.ReaderAnnouncementCardFeatureConfig import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig @RunWith(MockitoJUnitRunner::class) -class ReaderAnnouncementRepositoryTest { +class ReaderAnnouncementHelperTest { @Mock private lateinit var readerAnnouncementCardFeatureConfig: ReaderAnnouncementCardFeatureConfig @@ -29,11 +29,11 @@ class ReaderAnnouncementRepositoryTest { @Mock private lateinit var readerTracker: ReaderTracker - private lateinit var repository: ReaderAnnouncementRepository + private lateinit var repository: ReaderAnnouncementHelper @Before fun setUp() { - repository = ReaderAnnouncementRepository( + repository = ReaderAnnouncementHelper( readerAnnouncementCardFeatureConfig, readerTagsFeedFeatureConfig, appPrefsWrapper,