Display tags chip in subscriptions feed based on tags feed feature flag

> [!WARNING]
> ~Should be merged after https://github.com/wordpress-mobile/WordPress-Android/pull/20596~

Fixes #20594

-----

## To Test:

- Install JP and sign in.
- Enable remote feature `reader_tags_feed`.
- Open "Reader".
- Select "Subscriptions" feed.
- 🔍 The "Tags" chip should not be displayed..
- Disable remote feature `reader_tags_feed`.
- Restart the app.
- Open "Reader".
- Select "Subscriptions" feed.
- 🔍 The "Tags" chip should be displayed.

-----

## Regression Notes

1. Potential unintended areas of impact

    - None

2. What I did to test those areas of impact (or what existing automated tests I relied on)

    - Manual testing.

3. What automated tests I added (or what prevented me from doing so)

    --

-----

## PR Submission Checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## Testing Checklist (strike-out the not-applying and unnecessary ones):

- [ ] WordPress.com sites and self-hosted Jetpack sites.
- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

[Reader] Hide "Tags" chip from "Subscriptions" feed (behind FF)

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..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,
@@ -60,6 +61,7 @@ fun ReaderFilterChipGroup(
     showBlogsFilter: Boolean = blogsFilterCount > 0,
     showTagsFilter: Boolean = tagsFilterCount > 0,
     chipHeight: Dp = 36.dp,
+    showTagsChip: Boolean,
 ) {
     Row(
         modifier = modifier,
@@ -97,17 +99,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 +246,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))


[Stats] Add year to date picker for weeks.

Fixes #20488

Add year to the week granularity label in datepicker. The screenshot below is on a small screen width.

Note: this affects both the stats that is currently out to everyone and the traffic tab under the week granularity.

![Screenshot_20240403_172130](https://github.com/wordpress-mobile/WordPress-Android/assets/1571223/e3e926d8-8bc0-4d79-b4ff-799965ddb634)

-----

## To Test:

- [ ] Turn on `stats_traffic_tab` feature flag in debug settings.
- [ ] Go to stats and make sure you are on the traffic tab
- [ ] Select the "By Week" granularity.
- [ ] You should see a year on the from/to dates.
- [ ] Turn off `stats_traffic_tab`.
- [ ] Go back into stats and go to the weeks tab.
- [ ] The date picker there should also have the year.

-----

## Regression Notes

1. Potential unintended areas of impact

    This would only affect adding the year to the date picker label on the old stats week granularity.

2. What I did to test those areas of impact (or what existing automated tests I relied on)

    Manual test.

3. What automated tests I added (or what prevented me from doing so)

    n/a

-----

## PR Submission Checklist:

- [ ] I have completed the Regression Notes.
- [ ] I have considered adding accessibility improvements for my changes.
- [ ] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

Stats Traffic tab dates should include year
### Expected behavior
The date shown on the Stats Traffic tab when "By week" is selected should include the year to avoid confusion about which year is being displayed. (e.g. "Feb 19 - Feb 25, 2024")

Discussed in:
- p1709586750284709/1709585366.152709-slack-C06BR07TJHK
- p1709042259067229/1709042158.468369-slack-C06BR07TJHK

### Actual behavior
The dates on "By week" granularity" are shown without a year (e.g. "Feb 19 - Feb 25")

### Steps to reproduce the behavior
1. Open Stats
2. Go to the Traffic tab
3. Switch "By day" to different periods and notice the dates are displayed without a year.

##### Tested on Pixel 8 Pro, Android 14, JPAndroid 24.4

Thanks for reporting! 👍 <!-- generated_by_dangermattic -->

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatter.kt
index b262f70dfc04..4ae0d0607848 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatter.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatter.kt
@@ -9,6 +9,7 @@ import org.wordpress.android.fluxc.network.utils.StatsGranularity.WEEKS
 import org.wordpress.android.fluxc.network.utils.StatsGranularity.YEARS
 import org.wordpress.android.fluxc.utils.SiteUtils
 import org.wordpress.android.util.LocaleManagerWrapper
+import org.wordpress.android.util.config.StatsTrafficTabFeatureConfig
 import org.wordpress.android.viewmodel.ResourceProvider
 import java.text.DateFormat
 import java.text.ParseException
@@ -30,7 +31,11 @@ private const val YEARS_FORMAT = "MMM"
 private const val REMOVE_YEAR = "([^\\p{Alpha}']|('[\\p{Alpha}]+'))*y+([^\\p{Alpha}']|('[\\p{Alpha}]+'))*"

 class StatsDateFormatter
-@Inject constructor(private val localeManagerWrapper: LocaleManagerWrapper, val resourceProvider: ResourceProvider) {
+@Inject constructor(
+    private val localeManagerWrapper: LocaleManagerWrapper,
+    val resourceProvider: ResourceProvider,
+    val statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig
+) {
     private val inputFormat: SimpleDateFormat
         get() {
             return SimpleDateFormat(STATS_INPUT_FORMAT, localeManagerWrapper.getLocale())
@@ -110,7 +115,7 @@ class StatsDateFormatter
                 val startCalendar = Calendar.getInstance()
                 startCalendar.time = endCalendar.time
                 startCalendar.add(Calendar.DAY_OF_WEEK, -6)
-                return printWeek(startCalendar, endCalendar)
+                return printWeek(startCalendar, endCalendar, statsTrafficTabFeatureConfig.isEnabled())
             }
             MONTHS -> outputMonthFormat.format(date)
                 .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatterTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatterTest.kt
index f06c4b71e7cc..000d42ebf933 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatterTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatterTest.kt
@@ -17,6 +17,7 @@ import org.wordpress.android.fluxc.network.utils.StatsGranularity.MONTHS
 import org.wordpress.android.fluxc.network.utils.StatsGranularity.WEEKS
 import org.wordpress.android.fluxc.network.utils.StatsGranularity.YEARS
 import org.wordpress.android.util.LocaleManagerWrapper
+import org.wordpress.android.util.config.StatsTrafficTabFeatureConfig
 import org.wordpress.android.viewmodel.ResourceProvider
 import java.util.Calendar
 import java.util.Locale
@@ -27,6 +28,9 @@ class StatsDateFormatterTest : BaseUnitTest() {
     @Mock
     lateinit var localeManagerWrapper: LocaleManagerWrapper

+    @Mock
+    lateinit var statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig
+
     @Mock
     lateinit var resourceProvider: ResourceProvider
     private lateinit var statsDateFormatter: StatsDateFormatter
@@ -34,7 +38,8 @@ class StatsDateFormatterTest : BaseUnitTest() {
     @Before
     fun setUp() {
         whenever(localeManagerWrapper.getLocale()).thenReturn(Locale.US)
-        statsDateFormatter = StatsDateFormatter(localeManagerWrapper, resourceProvider)
+        whenever(statsTrafficTabFeatureConfig.isEnabled()).thenReturn(false)
+        statsDateFormatter = StatsDateFormatter(localeManagerWrapper, resourceProvider, statsTrafficTabFeatureConfig)
     }

     @Test
@@ -72,6 +77,42 @@ class StatsDateFormatterTest : BaseUnitTest() {
         assertThat(parsedDate).isEqualTo("Dec 17 - Dec 23")
     }

+    @Test
+    fun `prints a week date in the same year in string format with stats traffic tab enabled`() {
+        whenever(statsTrafficTabFeatureConfig.isEnabled()).thenReturn(true)
+        val unparsedDate = "2018W12W19"
+        val result = "Dec 17 - Dec 23, 2018"
+        whenever(
+            resourceProvider.getString(
+                R.string.stats_from_to_dates_in_week_label,
+                "Dec 17",
+                "Dec 23, 2018"
+            )
+        ).thenReturn(result)
+
+        val parsedDate = statsDateFormatter.printGranularDate(unparsedDate, WEEKS)
+
+        assertThat(parsedDate).isEqualTo("Dec 17 - Dec 23, 2018")
+    }
+
+    @Test
+    fun `prints a week date in two different years in string format with traffic tab enabled`() {
+        whenever(statsTrafficTabFeatureConfig.isEnabled()).thenReturn(true)
+        val unparsedDate = "2018W12W31"
+        val result = "Dec 31, 2018 - Jan 6, 2019"
+        whenever(
+            resourceProvider.getString(
+                R.string.stats_from_to_dates_in_week_label,
+                "Dec 31, 2018",
+                "Jan 6, 2019"
+            )
+        ).thenReturn(result)
+
+        val parsedDate = statsDateFormatter.printGranularDate(unparsedDate, WEEKS)
+
+        assertThat(parsedDate).isEqualTo("Dec 31, 2018 - Jan 6, 2019")
+    }
+
     @Test
     fun `prints a week date`() {
         val calendar = Calendar.getInstance()


Handles site successfully fetched but has not been found in the local db

Fixes #20530

**Depends on:** https://github.com/wordpress-mobile/WordPress-Android/pull/20571

## Description
This PR adds a new error UI state to gracefully handle an unexpected error in the site creation preview screen were the site successfully fetched but has not been found in the local db.

-----

## To Test:
Since I was not able to reproduce the error in real conditions I added a test and faked the state with an [error.patch](https://github.com/wordpress-mobile/WordPress-Android/files/14838068/error.patch)

1. Start the site creation (e.g. Site Picker and ➕)
2. Proceed to the end (e.g. with a free wordpress.com domain and plan) and expect a success state
3. Apply the patch above
4. Start the site creation
5. Proceed to the end and expect the new error state

|New Error|Success|
|---|---|
|![error](https://github.com/wordpress-mobile/WordPress-Android/assets/304044/93cfbc6d-65aa-44e2-b636-4605a1827560)|![Screenshot_20240402_160522](https://github.com/wordpress-mobile/WordPress-Android/assets/304044/5c744760-76b1-4a6a-a12d-e0fd966b8eaa)|

-----

## Regression Notes

1. Potential unintended areas of impact

    - Site creation

2. What I did to test those areas of impact (or what existing automated tests I relied on)

    - Manual testing

3. What automated tests I added (or what prevented me from doing so)

    - Added a new test case

-----

## PR Submission Checklist:

- [ ] I have completed the Regression Notes.
- [ ] I have considered adding accessibility improvements for my changes.
- [ ] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## Testing Checklist (strike-out the not-applying and unnecessary ones):

- [ ] WordPress.com sites and self-hosted Jetpack sites.
- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

IllegalArgumentException: Site successfully fetched but has not been found in the local db.
Sentry Issue: [JETPACK-ANDROID-DYR](https://a8c.sentry.io/issues/4584154681/?referrer=github_integration)

```
IllegalArgumentException: Site successfully fetched but has not been found in the local db.
    at org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.fetchNewlyCreatedSiteModel(SitePreviewViewModel.kt:135)
    at org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.access$fetchNewlyCreatedSiteModel(SitePreviewViewModel.kt:41)
    at org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel$fetchNewlyCreatedSiteModel$1.invokeSuspend(SitePreviewViewModel.kt:0)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.CoroutineContextKt.withContinuationContext
...
(6 additional frame(s) were not displayed)
```

Thanks for reporting! 👍 <!-- generated_by_dangermattic -->

Possibly related to this older fixed bug https://github.com/wordpress-mobile/WordPress-Android/issues/11394
Looking at the breadcrumbs the error usually follows a DB error _[Can't insert WP.com site [some site], missing user account](https://github.com/wordpress-mobile/WordPress-FluxC-Android/blob/trunk/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/SiteSqlUtils.kt#L127)_

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt
index 8383d1625288..84f96ab53e6c 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt
@@ -24,6 +24,7 @@ import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SiteP
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewLoadingShimmerState
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewWebErrorUiState
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SiteNotCreatedErrorUiState
+import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SiteNotFoundInDbUiState
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.UrlData
 import org.wordpress.android.ui.sitecreation.services.FetchWpComSiteUseCase
 import org.wordpress.android.ui.sitecreation.usecases.isWordPressComSubDomain
@@ -136,9 +137,13 @@ class SitePreviewViewModel @Inject constructor(
     private suspend fun fetchNewlyCreatedSiteModel(remoteSiteId: Long): SiteModel? {
         val onSiteFetched = fetchWpComSiteUseCase.fetchSiteWithRetry(remoteSiteId)
         return if (!onSiteFetched.isError) {
-            return requireNotNull(siteStore.getSiteBySiteId(remoteSiteId)) {
-                "Site successfully fetched but has not been found in the local db."
+            val site = siteStore.getSiteBySiteId(remoteSiteId)
+            if (site == null) {
+                withContext(mainDispatcher) {
+                    updateUiState(SiteNotFoundInDbUiState)
+                }
             }
+            site
         } else {
             null
         }
@@ -224,6 +229,14 @@ class SitePreviewViewModel @Inject constructor(
             errorTitle = UiStringRes(R.string.error),
         )

+        data object SiteNotFoundInDbUiState : SitePreviewUiState(
+            urlData = UrlData("", "", 0 to 0, 0 to 0),
+            webViewVisibility = false,
+            webViewErrorVisibility = true,
+            subtitle = UiStringRes(R.string.site_creation_error_generic_title),
+            caption = UiStringRes(R.string.site_creation_error_generic_subtitle),
+        )
+
         data class SitePreviewLoadingShimmerState(
             val isFree: Boolean,
             override val urlData: UrlData,

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt
index e5a0051b219f..b1ed67de828e 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt
@@ -40,6 +40,7 @@ import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SiteP
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewContentUiState
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewWebErrorUiState
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SiteNotCreatedErrorUiState
+import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SiteNotFoundInDbUiState
 import org.wordpress.android.ui.sitecreation.progress.LOADING_STATE_TEXT_ANIMATION_DELAY
 import org.wordpress.android.ui.sitecreation.services.FetchWpComSiteUseCase
 import org.wordpress.android.util.UrlUtilsWrapper
@@ -100,6 +101,13 @@ class SitePreviewViewModelTest : BaseUnitTest() {
         verify(fetchWpComSiteUseCase).fetchSiteWithRetry(SITE_REMOTE_ID)
     }

+    @Test
+    fun `on start if site is created but cannot be retrieved from fb fails show error`() = testWith(FETCH_SUCCESS) {
+        whenever(siteStore.getSiteBySiteId(SITE_REMOTE_ID)).thenReturn(null)
+        startViewModel(SITE_CREATION_STATE.copy(result = RESULT_NOT_IN_LOCAL_DB))
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteNotFoundInDbUiState::class.java)
+    }
+
     @Test
     fun `on start does not show preview when fetching fails`() = testWith(FETCH_ERROR) {
         startViewModel()


Show error when the site is not created correctly

Fixes #19791

## Description
This PR adds a new error UI state to gracefully handle an unexpected error in the site creation preview screen were the site appears not created.

-----

## To Test:
Since I was not able to reproduce the error in real conditions I added a test and faked the state with an
[error.patch](https://github.com/wordpress-mobile/WordPress-Android/files/14837002/error.patch).
1. Start the site creation (e.g. Site Picker and ➕)
2. Proceed to the end (e.g. with a free wordpress.com domain and plan) and expect a success state
3. Apply the patch above
4. Start the site creation
5. Proceed to the end and expect the new error state

|New Error|Success|
|---|---|
|![Screenshot_20240402_160301](https://github.com/wordpress-mobile/WordPress-Android/assets/304044/651e9cc1-b9a2-4524-8bcc-2671562bb9f1)|![Screenshot_20240402_160522](https://github.com/wordpress-mobile/WordPress-Android/assets/304044/5c744760-76b1-4a6a-a12d-e0fd966b8eaa)|

-----

## Regression Notes

1. Potential unintended areas of impact

    - Site creation

2. What I did to test those areas of impact (or what existing automated tests I relied on)

    - Manual testing

3. What automated tests I added (or what prevented me from doing so)

    - Added a new test case

-----

## PR Submission Checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## Testing Checklist (strike-out the not-applying and unnecessary ones):

- [ ] WordPress.com sites and self-hosted Jetpack sites.
- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

IllegalArgumentException: Failed requirement.
Sentry Issue: [JETPACK-ANDROID-ETT](https://a8c.sentry.io/issues/4644782022/?referrer=github_integration)

```
IllegalArgumentException: Failed requirement.
    at org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.start(SitePreviewViewModel.kt:84)
    at org.wordpress.android.ui.sitecreation.previews.SiteCreationPreviewFragment.init(SiteCreationPreviewFragment.kt:64)
    at org.wordpress.android.ui.sitecreation.previews.SiteCreationPreviewFragment.init(SiteCreationPreviewFragment.kt:64)
    at org.wordpress.android.ui.sitecreation.previews.SiteCreationPreviewFragment.onViewCreated(SiteCreationPreviewFragment.kt:58)
    at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3137)
...
(13 additional frame(s) were not displayed)
```

Might want to investigate replacing `require` with a conditional check and gracefully exit rather than exception.
- This crash started to occur when we re-enabled the domain selection step in the site creation flow.
- It occurs mostly on low-memory devices.
- It occurs when the app is in the background. See `{"phase":"IDLE","is_foreground":false}` in logs.
I don't want to blindly prevent the crash, as it might lead to a worse state in the flow. I haven't been able to reproduce the crash, but I believe this crash is not highly critical since the app will be opened with the newly created site after the crash.

If we can find why `siteCreationState.result` is not `Created`, we can fix that root cause. I'm setting the priority to low.

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt
index 0ce6f7a2830f..8ab86f82a9d1 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt
@@ -94,6 +94,10 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(),
                     uiHelpers.updateVisibility(sitePreviewWebError, ui.webViewErrorVisibility)
                     uiHelpers.updateVisibility(sitePreviewWebViewShimmerLayout, ui.shimmerVisibility)
                 }
+                ui.errorTitle?.let { error ->
+                    siteCreationPreviewHeaderItem.sitePreviewTitle.text =
+                        uiHelpers.getTextOfUiString(requireContext(), error)
+                }
             }
         }
     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt
index bd7061edf214..8383d1625288 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt
@@ -23,6 +23,7 @@ import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewContentUiState
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewLoadingShimmerState
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewWebErrorUiState
+import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SiteNotCreatedErrorUiState
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.UrlData
 import org.wordpress.android.ui.sitecreation.services.FetchWpComSiteUseCase
 import org.wordpress.android.ui.sitecreation.usecases.isWordPressComSubDomain
@@ -81,7 +82,10 @@ class SitePreviewViewModel @Inject constructor(

     fun start(siteCreationState: SiteCreationState) {
         if (isStarted) return else isStarted = true
-        require(siteCreationState.result is Created)
+        if (siteCreationState.result !is Created) {
+            updateUiState(SiteNotCreatedErrorUiState)
+            return
+        }
         siteDesign = siteCreationState.siteDesign
         result = siteCreationState.result
         isFree = requireNotNull(siteCreationState.domain).isFree
@@ -187,6 +191,7 @@ class SitePreviewViewModel @Inject constructor(
         val shimmerVisibility: Boolean = false,
         val subtitle: UiString,
         val caption: UiString?,
+        val errorTitle: UiString? = null,
     ) {
         data class SitePreviewContentUiState(
             val isFree: Boolean,
@@ -210,6 +215,15 @@ class SitePreviewViewModel @Inject constructor(
             caption = getCaption(isFree),
         )

+        data object SiteNotCreatedErrorUiState : SitePreviewUiState(
+            urlData = UrlData("", "", 0 to 0, 0 to 0),
+            webViewVisibility = false,
+            webViewErrorVisibility = true,
+            subtitle = UiStringRes(R.string.site_creation_error_generic_title),
+            caption = UiStringRes(R.string.site_creation_error_generic_subtitle),
+            errorTitle = UiStringRes(R.string.error),
+        )
+
         data class SitePreviewLoadingShimmerState(
             val isFree: Boolean,
             override val urlData: UrlData,

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt
index 8d1d17845f05..e5a0051b219f 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt
@@ -31,6 +31,7 @@ import org.wordpress.android.ui.sitecreation.SITE_CREATION_STATE
 import org.wordpress.android.ui.sitecreation.SITE_MODEL
 import org.wordpress.android.ui.sitecreation.SITE_REMOTE_ID
 import org.wordpress.android.ui.sitecreation.SUB_DOMAIN
+import org.wordpress.android.ui.sitecreation.SiteCreationResult
 import org.wordpress.android.ui.sitecreation.SiteCreationResult.Created
 import org.wordpress.android.ui.sitecreation.SiteCreationState
 import org.wordpress.android.ui.sitecreation.URL
@@ -38,6 +39,7 @@ import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewContentUiState
 import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewWebErrorUiState
+import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SiteNotCreatedErrorUiState
 import org.wordpress.android.ui.sitecreation.progress.LOADING_STATE_TEXT_ANIMATION_DELAY
 import org.wordpress.android.ui.sitecreation.services.FetchWpComSiteUseCase
 import org.wordpress.android.util.UrlUtilsWrapper
@@ -86,6 +88,12 @@ class SitePreviewViewModelTest : BaseUnitTest() {
         whenever(siteStore.getSiteBySiteId(SITE_REMOTE_ID)).thenReturn(SITE_MODEL)
     }

+    @Test
+    fun `on start show error when result is not created`() = testWith(FETCH_SUCCESS) {
+        startViewModel(SITE_CREATION_STATE.copy(result = SiteCreationResult.NotCreated))
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteNotCreatedErrorUiState::class.java)
+    }
+
     @Test
     fun `on start fetches site by remote id when result is created`() = testWith(FETCH_SUCCESS) {
         startViewModel(SITE_CREATION_STATE.copy(result = RESULT_NOT_IN_LOCAL_DB))


[Reader IA] Show onboarding for users with no tags or dailyprompt tag only

Fixes #20091

Ignores the `dailyprompt` tag when checking for the subscribed tags to show the Discover Onboarding / Choose your interests (onboarding) screens.

-----

## To Test:

### Requirements
Create a new account to run the test (`dailyprompt` tag auto-injected by default) or prepare an existing account by unsubscribing from all tags and subscribing only to `dailyprompt` tag.

### Steps
1. Install Jetpack and log in/sign up
2. Go to Reader
3. Go to Discover
4. **Verify** the "Welcome" message with a "Get Started" button is shown
5. Tap "Get Started"
6. **Verify** the "Choose your interests" screen with the tag cloud shows up

Also, test that having no tags at all shows the Welcome screen and that having other tags doesn't show it.

-----

## Regression Notes

1. Potential unintended areas of impact

    - N/A

2. What I did to test those areas of impact (or what existing automated tests I relied on)

    - N/A

3. What automated tests I added (or what prevented me from doing so)

    - Added unit tests for the new scenario.

-----

## PR Submission Checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## UI Changes Testing Checklist:

- [x] Portrait and landscape orientations.
- [x] Light and dark modes.
- [x] Fonts: Larger, smaller and bold text.
- [x] High contrast.
- [x] Talkback.
- [x] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [x] Large and small screen sizes. (Tablet and smaller phones)
- [x] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

[Reader IA] Bring back the Discover Onboarding for new users
When a new user entered the Reader for the first time we would show the `Discover` feed with a "Welcome" message and a button leading to a "Choose your topics" screen, which essentially followed those tags and made Discover be populated with stuff based on those tags.

All the code for that is still present in the app and is still shown if the user unsubscribes from ALL their tags (when the user has 0 subscribed tags), here's an example:

https://github.com/wordpress-mobile/WordPress-Android/assets/5091503/7b77b04a-fa3d-4e6a-852c-95ac587b97cd

This (0 tags subscribed) used to be the state of a new account as well, but recently the `dailyprompt` tag is being subscribed to by default on new accounts, which causes the Welcome screen to be skipped and the Reader Discover feed to be populated, which makes it hard for the user to configure their tags and see interesting stuff in that feed for the first time accessing the Reader in the app.

We want to bring back this simple Onboarding Flow to provide a better user experience, especially on the first time using the Reader.

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 c9b02681ea84..2e7c7b954b95 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
@@ -16,6 +16,7 @@ import org.wordpress.android.models.discover.ReaderDiscoverCard.ReaderRecommende
 import org.wordpress.android.models.discover.ReaderDiscoverCards
 import org.wordpress.android.modules.IO_THREAD
 import org.wordpress.android.modules.UI_THREAD
+import org.wordpress.android.ui.bloggingprompts.BloggingPromptsPostTagProvider.Companion.BLOGGING_PROMPT_TAG
 import org.wordpress.android.ui.pages.SnackbarMessageHolder
 import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType.TAG_FOLLOWED
 import org.wordpress.android.ui.reader.discover.ReaderCardUiState.ReaderPostNewUiState
@@ -149,7 +150,10 @@ class ReaderDiscoverViewModel @Inject constructor(
         _uiState.addSource(readerDiscoverDataProvider.discoverFeed) { posts ->
             launch {
                 val userTags = getFollowedTagsUseCase.get()
-                if (userTags.isEmpty()) {
+
+                // 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()) {
                     _uiState.value = DiscoverUiState.EmptyUiState.ShowNoFollowedTagsUiState {
                         parentViewModel.onShowReaderInterests()
                     }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModel.kt
index fc06dca994c3..67be5e9cd0ec 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModel.kt
@@ -11,6 +11,7 @@ import org.wordpress.android.R
 import org.wordpress.android.analytics.AnalyticsTracker
 import org.wordpress.android.models.ReaderTag
 import org.wordpress.android.models.ReaderTagList
+import org.wordpress.android.ui.bloggingprompts.BloggingPromptsPostTagProvider.Companion.BLOGGING_PROMPT_TAG
 import org.wordpress.android.ui.pages.SnackbarMessageHolder
 import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsFragment.EntryPoint
 import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel.DoneButtonUiState.DoneButtonDisabledUiState
@@ -92,7 +93,9 @@ class ReaderInterestsViewModel @Inject constructor(
     }

     private fun checkAndLoadInterests(userTags: ReaderTagList) {
-        if (userTags.isEmpty()) {
+        // 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()) {
             loadInterests(userTags)
         } else {
             parentViewModel?.onCloseReaderInterests()
diff --git a/WordPress/src/main/res/layout/reader_interests_fragment_layout.xml b/WordPress/src/main/res/layout/reader_interests_fragment_layout.xml
index 22d0129f84ce..519613821ede 100644
--- a/WordPress/src/main/res/layout/reader_interests_fragment_layout.xml
+++ b/WordPress/src/main/res/layout/reader_interests_fragment_layout.xml
@@ -66,7 +66,7 @@
             <com.google.android.material.textview.MaterialTextView
                 style="@style/ReaderTextView.Interests.Title"
                 android:id="@+id/title"
-                android:text="@string/reader_label_choose_your_interests"
+                android:text="@string/reader_label_choose_interests"
                 android:visibility="gone"
                 app:layout_constraintHorizontal_bias="0.5"
                 app:layout_constraintStart_toStartOf="@id/guideline_beginning"
diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml
index 837a460c84d7..8b3f2559f36c 100644
--- a/WordPress/src/main/res/values/strings.xml
+++ b/WordPress/src/main/res/values/strings.xml
@@ -2162,7 +2162,7 @@
     <string name="reader_label_local_related_posts">More from %s</string>
     <string name="reader_label_global_related_posts">More on WordPress.com</string>
     <string name="reader_label_visit">Visit site</string>
-    <string name="reader_label_choose_your_interests">Choose your topics</string>
+    <string name="reader_label_choose_interests">Choose your interests</string>
     <string name="reader_view_comments">View comments</string>
     <string name="reader_welcome_banner">Welcome to Reader. Discover millions of blogs at your fingertips.</string>
     <string name="reader_label_toolbar_back">Back</string>

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 1db7219634ec..064409261b3c 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
@@ -24,6 +24,7 @@ import org.wordpress.android.models.ReaderBlog
 import org.wordpress.android.models.ReaderPost
 import org.wordpress.android.models.ReaderTag
 import org.wordpress.android.models.ReaderTagList
+import org.wordpress.android.models.ReaderTagType
 import org.wordpress.android.models.discover.ReaderDiscoverCard.InterestsYouMayLikeCard
 import org.wordpress.android.models.discover.ReaderDiscoverCard.ReaderPostCard
 import org.wordpress.android.models.discover.ReaderDiscoverCard.ReaderRecommendedBlogsCard
@@ -292,6 +293,29 @@ class ReaderDiscoverViewModelTest : BaseUnitTest() {
         assertThat(uiStates[1]).isInstanceOf(ShowNoFollowedTagsUiState::class.java)
     }

+    @Test
+    fun `ShowFollowInterestsEmptyUiState is shown when the user follows only the daily prompt tag`() = test {
+        // Arrange
+        val tagsWithDailyPrompt = ReaderTagList().apply {
+            add(
+                ReaderTag(
+                    "dailyprompt",
+                    "dailyprompt",
+                    "dailyprompt",
+                    "https://public-api.wordpress.com/rest/v1.2/read/tags/dailyprompt/posts",
+                    ReaderTagType.DEFAULT
+                )
+            )
+        }
+        whenever(getFollowedTagsUseCase.get()).thenReturn(tagsWithDailyPrompt)
+        val uiStates = init().uiStates
+        // Act
+        viewModel.start(parentViewModel)
+        // Assert
+        assertThat(uiStates.size).isEqualTo(2)
+        assertThat(uiStates[1]).isInstanceOf(ShowNoFollowedTagsUiState::class.java)
+    }
+
     @Test
     fun `ShowNoPostsUiState is shown when the discoverFeed does not contain any posts`() = test {
         // Arrange
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModelTest.kt
index 4b3451b7141c..1943a6a2f15a 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModelTest.kt
@@ -87,6 +87,20 @@ class ReaderInterestsViewModelTest : BaseUnitTest() {
             verify(readerTagRepository, times(1)).getInterests()
         }

+    @Test
+    fun `getInterests invoked if only daily prompt user tag received from repo`() =
+        testWithDailyPromptUserTag {
+            // Given
+            val interests = getInterests()
+            whenever(readerTagRepository.getInterests()).thenReturn(SuccessWithData(interests))
+
+            // When
+            initViewModel()
+
+            // Then
+            verify(readerTagRepository, times(1)).getInterests()
+        }
+
     @Test
     fun `discover close reader screen triggered if non empty user tags are received from repo`() =
         testWithNonEmptyUserTags {
@@ -602,6 +616,25 @@ class ReaderInterestsViewModelTest : BaseUnitTest() {
         }
     }

+    private fun <T> testWithDailyPromptUserTag(block: suspend CoroutineScope.() -> T) {
+        test {
+            val tagsWithDailyPrompt = ReaderTagList().apply {
+                add(
+                    ReaderTag(
+                        "dailyprompt",
+                        "dailyprompt",
+                        "dailyprompt",
+                        "https://public-api.wordpress.com/rest/v1.2/read/tags/dailyprompt/posts",
+                        ReaderTagType.DEFAULT
+                    )
+                )
+            }
+
+            whenever(readerTagRepository.getUserTags()).thenReturn(SuccessWithData(tagsWithDailyPrompt))
+            block()
+        }
+    }
+
     private fun <T> testWithFailedUserTags(block: suspend CoroutineScope.() -> T) {
         test {
             whenever(readerTagRepository.getUserTags()).thenReturn(NetworkUnavailable)


Implements: Pull to refresh in Site Monitor Tabs

## Closes
#20088

## Description
This PR implements pull to refresh functionality in Site monitor Screen

## Screenshots
[Screen_recording_20240201_124924.webm](https://github.com/wordpress-mobile/WordPress-Android/assets/17463767/a3946842-df17-423e-b585-d932dccfc5ab)

## To Test:
#### Prerequisite
◐ Toggle the site_monitoring flag

1. Go to `Me` → `App Settings` → `Debug settings` → `Remote Feature Flags`
2. Tap on the item corresponding to the flag - site_monitoring
3. Tap `RESTART THE APP` button

###  Site monitor pull to refresh works as expected
1. Go to More → Site Monitoring
2. i✅ Verify that the webview is loaded
3. 🖐🏼  Refresh the webview by swipe to refresh
4. ✅ Verify that the loading state is shown again
5. ✅ Verify that only the current tab is refreshed(navigate to the other tabs, before refreshing and check if they still show webview)
6. ✅ Verify that the reload happens as expected

## Merge Instructions
- Make sure that the PR - https://github.com/wordpress-mobile/WordPress-Android/pull/20092 is merged
- Remove the Do not merge label
- Confirm that the base branch is shifted to `trunk` and CI is successful
- Merge as normal

## Regression Notes

1. Potential unintended areas of impact
- Swipe to refresh doesn't work as expected
- First time load of the

4. What I did to test those areas of impact (or what existing automated tests I relied on)
- TODO

5. What automated tests I added (or what prevented me from doing so)
- TODO

PR submission checklist:
- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

UI Changes testing checklist:
- [x] Portrait and landscape orientations.
- [x] Light and dark modes.
- [x] Fonts: Larger, smaller and bold text.
- [x] High contrast.
- [x] Talkback.
- [x] Languages with large words or with letters/accents not frequently used in English.
- [x] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [x] Large and small screen sizes. (Tablet and smaller phones)
- [x] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

Site Monitor: Add pull-to-refresh

Site Monitor: Add pull-to-refresh
Add Pull-to-refresh to the Webview pages

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt
index 3c015c61d3be..97278d29fc62 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt
@@ -19,9 +19,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.Button
+import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Scaffold
 import androidx.compose.material.Text
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Tab
@@ -292,21 +296,30 @@ class SiteMonitorParentActivity : AppCompatActivity(), SiteMonitorWebViewClient.
                 LoadingState()
             }
             is SiteMonitorUiState.Loaded -> {
-                SiteMonitorWebView(webView, modifier)
+                SiteMonitorWebView(webView, tabType, modifier)
             }
             else -> {}
         }
     }

+    @OptIn(ExperimentalMaterialApi::class)
     @Composable
-    private fun SiteMonitorWebView(tabWebView: WebView, modifier: Modifier = Modifier) {
+    private fun SiteMonitorWebView(tabWebView: WebView, tabType: SiteMonitorType, modifier: Modifier = Modifier) {
         // the webview is retrieved from the activity, so we need to use a mutable variable
         // to assign to android view
-
         var webView = tabWebView
+
+        val refreshState = siteMonitorParentViewModel.getRefreshState(tabType)
+
+        val pullRefreshState = rememberPullRefreshState(
+            refreshing = refreshState.value,
+            onRefresh = { siteMonitorParentViewModel.refreshData(tabType) }
+        )
+
         Box(
             modifier = modifier
                 .fillMaxSize()
+                .pullRefresh(pullRefreshState)
         ) {
             LazyColumn(modifier = Modifier.fillMaxHeight()) {
                 item {
@@ -317,6 +330,12 @@ class SiteMonitorParentActivity : AppCompatActivity(), SiteMonitorWebViewClient.
                     )
                 }
             }
+            PullRefreshIndicator(
+                refreshing = refreshState.value,
+                state = pullRefreshState,
+                modifier = Modifier.align(Alignment.TopCenter),
+                contentColor = MaterialTheme.colors.primaryVariant,
+            )
         }
     }

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt
index 2a18f07e1fb2..104800214320 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt
@@ -92,4 +92,36 @@ class SiteMonitorParentViewModel @Inject constructor(
         phpLogViewModel.onCleared()
         webServerViewModel.onCleared()
     }
+
+    fun getRefreshState(siteMonitorType: SiteMonitorType): State<Boolean> {
+        return when (siteMonitorType) {
+            SiteMonitorType.METRICS -> {
+                metricsViewModel.isRefreshing
+            }
+
+            SiteMonitorType.PHP_LOGS -> {
+                phpLogViewModel.isRefreshing
+            }
+
+            SiteMonitorType.WEB_SERVER_LOGS -> {
+                webServerViewModel.isRefreshing
+            }
+        }
+    }
+
+    fun refreshData(siteMonitorType: SiteMonitorType) {
+        when(siteMonitorType) {
+            SiteMonitorType.METRICS -> {
+                metricsViewModel.refreshData()
+            }
+
+            SiteMonitorType.PHP_LOGS -> {
+                phpLogViewModel.refreshData()
+            }
+
+            SiteMonitorType.WEB_SERVER_LOGS -> {
+                webServerViewModel.refreshData()
+            }
+        }
+    }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt
index 45ec9a56d76f..91dea1294b59 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt
@@ -3,22 +3,29 @@ package org.wordpress.android.ui.sitemonitor
 import android.text.TextUtils
 import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateOf
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import org.wordpress.android.fluxc.model.SiteModel
 import org.wordpress.android.fluxc.store.AccountStore
 import org.wordpress.android.fluxc.store.SiteStore
+import org.wordpress.android.modules.BG_THREAD
 import org.wordpress.android.util.NetworkUtilsWrapper
 import javax.inject.Inject
+import javax.inject.Named
+
+const val REFRESH_DELAY = 500L

 class SiteMonitorTabViewModelSlice @Inject constructor(
+    @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
     private val networkUtilsWrapper: NetworkUtilsWrapper,
     private val accountStore: AccountStore,
     private val mapper: SiteMonitorMapper,
     private val siteMonitorUtils: SiteMonitorUtils,
     private val siteStore: SiteStore,
-){
+) {
     private lateinit var scope: CoroutineScope

     private lateinit var site: SiteModel
@@ -28,6 +35,9 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
     private val _uiState = mutableStateOf<SiteMonitorUiState>(SiteMonitorUiState.Preparing)
     val uiState: State<SiteMonitorUiState> = _uiState

+    private val _isRefreshing = mutableStateOf(false)
+    val isRefreshing: State<Boolean> = _isRefreshing
+
     fun initialize(scope: CoroutineScope) {
         this.scope = scope
     }
@@ -50,7 +60,19 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
         assembleAndShowSiteMonitor()
     }

-    private fun checkForInternetConnectivityAndPostErrorIfNeeded() : Boolean {
+    fun refreshData() {
+        scope.launch {
+            _isRefreshing.value = true
+            // this delay is to prevent the refresh from being too fast
+            // so that the user can see the refresh animation
+            // also this would fix the unit tests
+            delay(REFRESH_DELAY)
+            loadView()
+            _isRefreshing.value = false
+        }
+    }
+
+    private fun checkForInternetConnectivityAndPostErrorIfNeeded(): Boolean {
         if (networkUtilsWrapper.isNetworkAvailable()) return true
         postUiState(mapper.toNoNetworkError(::loadView))
         return false
@@ -68,8 +90,10 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
         val sanitizedUrl = siteMonitorUtils.sanitizeSiteUrl(site.url)
         val url = urlTemplate.replace("{blog}", sanitizedUrl)

-        val addressToLoad = prepareAddressToLoad(url)
-        postUiState(mapper.toPrepared(url, addressToLoad, siteMonitorType))
+        scope.launch(bgDispatcher) {
+            val addressToLoad = prepareAddressToLoad(url)
+            postUiState(mapper.toPrepared(url, addressToLoad, siteMonitorType))
+        }
     }

     private fun prepareAddressToLoad(url: String): String {
@@ -95,7 +119,7 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
             addressToLoad,
             username,
             "",
-            accessToken?:""
+            accessToken ?: ""
         )
     }

@@ -106,7 +130,7 @@ class SiteMonitorTabViewModelSlice @Inject constructor(
     }

     fun onUrlLoaded() {
-        if (uiState.value is SiteMonitorUiState.Prepared){
+        if (uiState.value is SiteMonitorUiState.Prepared) {
             postUiState(SiteMonitorUiState.Loaded)
         }
     }

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt
index 411f72a9a442..98d3ddf0e3a1 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt
@@ -167,4 +167,25 @@ class SiteMonitorParentViewModelTest: BaseUnitTest(){

         verify(webServerViewModel).onWebViewError()
     }
+
+    @Test
+    fun `given metrics, when refresh is invoked, then metric vm slice refresh is invoked`() {
+        viewModel.refreshData(SiteMonitorType.METRICS)
+
+        verify(metricsViewModel).refreshData()
+    }
+
+    @Test
+    fun `given php logs, when refresh is invoked, then php logs vm slice refresh is invoked`() {
+        viewModel.refreshData(SiteMonitorType.PHP_LOGS)
+
+        verify(phpLogViewModel).refreshData()
+    }
+
+    @Test
+    fun `given webserver logs, when refresh is invoked, then webserver vm slice refresh is invoked`() {
+        viewModel.refreshData(SiteMonitorType.WEB_SERVER_LOGS)
+
+        verify(webServerViewModel).refreshData()
+    }
 }
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt
index 3fe113c67fb7..02eb59f3a157 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt
@@ -31,9 +31,12 @@ class SiteMonitorTabViewModelSliceTest : BaseUnitTest() {

     val site = mock<SiteModel>()

+    val refreshStates = mutableListOf<Boolean>()
+
     @Before
     fun setUp() = test {
         viewModel = SiteMonitorTabViewModelSlice(
+            testDispatcher(),
             networkUtilsWrapper,
             accountStore,
             mapper,
@@ -119,6 +122,19 @@ class SiteMonitorTabViewModelSliceTest : BaseUnitTest() {
         assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.GenericError::class.java)
     }

+    @Test
+    fun `given loaded  state, when refresh is invoked, then uiState loaded is posted`() = test {
+        viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site)
+        advanceUntilIdle()
+        viewModel.onUrlLoaded()
+        viewModel.refreshData()
+
+        assertThat(viewModel.isRefreshing.value).isTrue()
+        advanceUntilIdle()
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.Prepared::class.java)
+        assertThat(viewModel.isRefreshing.value).isFalse()
+    }
+
     companion object {
         const val USER_NAME = "user_name"
         const val ACCESS_TOKEN = "access_token"


[Reader IA] Add "Followed P2s" item to dropdown menu

Fixes #19898

-----

## To Test:

- Install JP and sign in with an account that has followed P2s and is not associated with A8C
- Open the Reader: you should see the `Followed P2s` item in the dropdown menu

- Tap the `Followed P2s` item: you should see the post list and blogs filter. You can compare it with web to check the expected behavior.

- Verify that the following events are tracked:
```
🔵 Tracked: reader_dropdown_menu_item_tapped, Properties: {"id":"p2"}
🔵 Tracked: reader_p2_shown
```

- Verify that filter blog also works as expected.

-----

## Regression Notes

1. Potential unintended areas of impact

    - None

2. What I did to test those areas of impact (or what existing automated tests I relied on)

    - Manual testing and updated unit tests

3. What automated tests I added (or what prevented me from doing so)

    --

-----

## PR Submission Checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## UI Changes Testing Checklist:

- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

[Reader IA - Bug] Followed P2s feed not showing in drop-down
![image](https://github.com/wordpress-mobile/WordPress-Android/assets/47797566/e0725f07-6802-477c-9f39-d6bbe0baa8ea)

>While comparing the viewpager tabs and feeds content for before / after, I see a missing menu item for "Followed P2S" (that is a filterable with only Sites and no Tags elements, I think similarly to the Automattic item)

What I was trying to say here is (using the same user for the before/after):
- the "Followed P2s" item is missing in the after

About how to get the Followed P2s on your side, what follows is my best guess
>I think that menu is related to P2 the product sites that I think I created in that account from https://p2help.wordpress.com/getting-started/creating-a-new-workspace/ if it's useful to create the test scenario.

but I can share my test account with you if it's quicker (just dm me in slack 👍 )

_Originally posted by @develric in https://github.com/wordpress-mobile/WordPress-Android/issues/19856#issuecomment-1881059568_

We should also consider adding the analytics event for when this menu item is clicked.

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java
index 504d71f1de01..c0798c5824ca 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java
@@ -67,7 +67,7 @@ public class ReaderConstants {
     static final String KEY_FIRST_LOAD = "first_load";
     static final String KEY_ACTIVITY_TITLE = "activity_title";
     static final String KEY_TRACKED_POSITIONS = "tracked_positions";
-    static final String KEY_IS_REFRESHING = "is_refreshing";
+    static final String KEY_CURRENT_UPDATE_ACTIONS = "current_update_actions";
     static final String KEY_ACTIVE_SEARCH_TAB = "active_search_tab";
     static final String KEY_SITE_ID = "site_id";

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 445ac315c255..763d944c130c 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
@@ -144,8 +144,10 @@
 import org.wordpress.android.widgets.RecyclerItemDecoration;
 import org.wordpress.android.widgets.WPSnackbar;

+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Stack;

@@ -210,9 +212,9 @@ public class ReaderPostListFragment extends ViewPagerFragment
     private int mPostSearchAdapterPos;
     private int mSiteSearchAdapterPos;
     private int mSearchTabsPos = NO_POSITION;
-    private boolean mIsUpdating;
     private boolean mIsFilterableScreen;
     private boolean mIsFiltered = false;
+    @NonNull private HashSet<UpdateAction> mCurrentUpdateActions = new HashSet<>();
     /*
      * called by post adapter to load older posts when user scrolls to the last post
      */
@@ -957,8 +959,8 @@ public void onSaveInstanceState(@NonNull Bundle outState) {
         outState.putBoolean(ReaderConstants.KEY_ALREADY_REQUESTED, mHasRequestedPosts);
         outState.putBoolean(ReaderConstants.KEY_ALREADY_UPDATED, mHasUpdatedPosts);
         outState.putBoolean(ReaderConstants.KEY_FIRST_LOAD, mFirstLoad);
+        outState.putSerializable(ReaderConstants.KEY_CURRENT_UPDATE_ACTIONS, mCurrentUpdateActions);
         if (mRecyclerView != null) {
-            outState.putBoolean(ReaderConstants.KEY_IS_REFRESHING, mRecyclerView.isRefreshing());
             outState.putInt(ReaderConstants.KEY_RESTORE_POSITION, getCurrentPosition());
         }
         outState.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, getPostListType());
@@ -1180,9 +1182,12 @@ public void onShowCustomEmptyView(EmptyViewMessageType emptyViewMsgType) {
             }
         }

-        if (savedInstanceState != null && savedInstanceState.getBoolean(ReaderConstants.KEY_IS_REFRESHING)) {
-            mIsUpdating = true;
-            mRecyclerView.setRefreshing(true);
+        if (savedInstanceState != null) {
+            Serializable actions = savedInstanceState.getSerializable(ReaderConstants.KEY_CURRENT_UPDATE_ACTIONS);
+            if (actions instanceof HashSet<?>) {
+                mCurrentUpdateActions = (HashSet<UpdateAction>) actions;
+                updateProgressIndicators();
+            }
         }

         return rootView;
@@ -1699,7 +1704,7 @@ private void setEmptyTitleDescriptionAndButton(boolean requestFailed) {
             setEmptyTitleAndDescriptionForBookmarksList();
             return;
         } else if (!NetworkUtils.isNetworkAvailable(getActivity())) {
-            mIsUpdating = false;
+            clearCurrentUpdateActions();
             title = getString(R.string.reader_empty_posts_no_connection);
         } else if (requestFailed) {
             if (isSearching()) {
@@ -2304,7 +2309,7 @@ public void run() {
     }

     private boolean isUpdating() {
-        return mIsUpdating;
+        return mCurrentUpdateActions.size() > 0;
     }

     /*
@@ -2321,27 +2326,45 @@ private void showLoadingProgress(boolean showProgress) {
         }
     }

-    private void setIsUpdating(boolean isUpdating, UpdateAction updateAction) {
-        if (!isAdded() || mIsUpdating == isUpdating) {
-            return;
-        }
+    private void clearCurrentUpdateActions() {
+        if (!isAdded() || !isUpdating()) return;

-        if (updateAction == UpdateAction.REQUEST_OLDER) {
-            // show/hide progress bar at bottom if these are older posts
-            showLoadingProgress(isUpdating);
-        } else if (isUpdating) {
-            // show swipe-to-refresh if update started
-            mRecyclerView.setRefreshing(true);
+        mCurrentUpdateActions.clear();
+        updateProgressIndicators();
+    }
+
+    private void setIsUpdating(boolean isUpdating, @NonNull UpdateAction updateAction) {
+        if (!isAdded()) return;
+
+        boolean isUiUpdateNeeded;
+        if (isUpdating) {
+            isUiUpdateNeeded = mCurrentUpdateActions.add(updateAction);
         } else {
-            // hide swipe-to-refresh progress if update is complete
+            isUiUpdateNeeded = mCurrentUpdateActions.remove(updateAction);
+        }
+
+        if (isUiUpdateNeeded) updateProgressIndicators();
+    }
+
+    private void updateProgressIndicators() {
+        if (!isUpdating()) {
+            // when there's no update in progress, hide the bottom and swipe-to-refresh progress bars
+            showLoadingProgress(false);
             mRecyclerView.setRefreshing(false);
+        } else if (mCurrentUpdateActions.size() == 1 && mCurrentUpdateActions.contains(UpdateAction.REQUEST_OLDER)) {
+            // if only older posts are being updated, show only the bottom progress bar
+            showLoadingProgress(true);
+            mRecyclerView.setRefreshing(false);
+        } else {
+            // if anything else is being updated, show only the swipe-to-refresh progress bar
+            showLoadingProgress(false);
+            mRecyclerView.setRefreshing(true);
         }
-        mIsUpdating = isUpdating;

         // if swipe-to-refresh isn't active, keep it disabled during an update - this prevents
         // doing a refresh while another update is already in progress
-        if (mRecyclerView != null && !mRecyclerView.isRefreshing()) {
-            mRecyclerView.setSwipeToRefreshEnabled(!isUpdating && isSwipeToRefreshSupported());
+        if (!mRecyclerView.isRefreshing()) {
+            mRecyclerView.setSwipeToRefreshEnabled(!isUpdating() && isSwipeToRefreshSupported());
         }
     }

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 15822295422c..b40dfc0380ee 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
@@ -388,6 +388,7 @@ class ReaderTracker @Inject constructor(
             readerTag.isPostsILike -> "liked"
             readerTag.isA8C -> "a8c"
             readerTag.isListTopic -> "list"
+            readerTag.isP2 -> "p2"
             else -> null
         }?.let { trackingId ->
             analyticsTrackerWrapper.track(
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 05f6f28d8c1b..26e2f876a83c 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
@@ -31,6 +31,12 @@ class ReaderTopBarMenuHelper @Inject constructor() {
             readerTagsList.indexOrNull { it.isA8C }?.let { a8cIndex ->
                 add(createAutomatticItem(getMenuItemIdFromReaderTagIndex(a8cIndex)))
             }
+            readerTagsList.indexOrNull { it.isP2 }?.let { followedP2sIndex ->
+                add(createFollowedP2sItem(
+                    id = getMenuItemIdFromReaderTagIndex(followedP2sIndex),
+                    text = readerTagsList[followedP2sIndex].tagTitle,
+                ))
+            }
             readerTagsList
                 .foldIndexed(SparseArrayCompat<ReaderTag>()) { index, sparseArray, readerTag ->
                     if (readerTag.tagType == ReaderTagType.CUSTOM_LIST) {
@@ -85,6 +91,13 @@ class ReaderTopBarMenuHelper @Inject constructor() {
         )
     }

+    private fun createFollowedP2sItem(id: String, text: String): MenuElementData.Item.Single {
+        return MenuElementData.Item.Single(
+            id = id,
+            text = UiString.UiStringText(text),
+        )
+    }
+
     private fun createCustomListsItem(customLists: SparseArrayCompat<ReaderTag>): MenuElementData.Item.SubMenu {
         val customListsMenuItems = mutableListOf<MenuElementData.Item.Single>()
         customLists.forEach { index, readerTag ->

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 b8a4180dff9d..0279f25bee2d 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
@@ -23,9 +23,10 @@ class ReaderTopBarMenuHelperTest {
             add(mockSavedTag()) // item 2
             add(mockLikedTag()) // item 3
             add(mockA8CTag()) // item 4
-            add(createCustomListTag("custom-list-1")) // item 5
-            add(createCustomListTag("custom-list-2")) // item 6
-            add(createCustomListTag("custom-list-3")) // item 7
+            add(mockFollowedP2sTag()) // item 5
+            add(createCustomListTag("custom-list-1")) // item 6
+            add(createCustomListTag("custom-list-2")) // item 7
+            add(createCustomListTag("custom-list-3")) // item 8
         }

         val menu = helper.createMenu(tags)
@@ -46,18 +47,21 @@ class ReaderTopBarMenuHelperTest {
         val a8cItem = menu.findSingleItem { it.id == "4" }!!
         assertThat(a8cItem.text).isEqualTo(UiStringRes(R.string.reader_dropdown_menu_automattic))

+        val followedP2sItem = menu.findSingleItem { it.id == "5" }!!
+        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 == "5" }!!
+        val customList1Item = customListsItem.children.findSingleItem { it.id == "6" }!!
         assertThat(customList1Item.text).isEqualTo(UiStringText("custom-list-1"))

-        val customList2Item = customListsItem.children.findSingleItem { it.id == "6" }!!
+        val customList2Item = customListsItem.children.findSingleItem { it.id == "7" }!!
         assertThat(customList2Item.text).isEqualTo(UiStringText("custom-list-2"))

-        val customList3Item = customListsItem.children.findSingleItem { it.id == "7" }!!
+        val customList3Item = customListsItem.children.findSingleItem { it.id == "8" }!!
         assertThat(customList3Item.text).isEqualTo(UiStringText("custom-list-3"))
     }

@@ -264,6 +268,13 @@ class ReaderTopBarMenuHelperTest {
         }
     }

+    private fun mockFollowedP2sTag(): ReaderTag {
+        return mock {
+            on { isP2 } doReturn true
+            on { tagTitle } doReturn "Followed P2s"
+        }
+    }
+
     private fun createCustomListTag(title: String): ReaderTag {
         return ReaderTag(
             title,


🤖 [Dynamic Cards] Makes the whole card actionable

Fixes #19833

## Description
Makes the whole card actionable when there is a valid action url.

-----

## To Test:
### Domains management card
* **Verify** that the button is actionable and the `dynamic_dashboard_card_cta_tapped` event is emmitted.
* **Verify** that the whole card is actionable and the `dynamic_dashboard_card_tapped` event is emmitted.
### Mocked data
* Apply the patch [cardsactions.patch](https://github.com/wordpress-mobile/WordPress-Android/files/13742797/cardsactions.patch)
* **Verify** that both the card and the button is actionable on the first card
* **Verify** that the card is actionable on the second card
* **Verify** that the third card is not actionable

-----

## Regression Notes

1. Potential unintended areas of impact

    Dynamic dashboard cards action

2. What I did to test those areas of impact (or what existing automated tests I relied on)

    Manual testing

3. What automated tests I added (or what prevented me from doing so)

    Updated the existing tests

-----

## PR Submission Checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

-----

## UI Changes Testing Checklist:

- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

🤖 [Dynamic Cards] Make the whole card actionable
The whole card should be actionable when there is a valid action url

Slack conversation: p1703156111412259/1702977446.551109-slack-C067H36F011

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt
index 981eb1d64c54..81026f4db9a4 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt
@@ -400,16 +400,17 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte

             sealed class ActionSource {
                 abstract val url: String
-                abstract val onClick: ListItemInteraction
+                abstract val onCardClick: ListItemInteraction

                 data class Card(
                     override val url: String,
-                    override val onClick: ListItemInteraction
+                    override val onCardClick: ListItemInteraction
                 ) : ActionSource()

-                data class Button(
+                data class CardOrButton(
                     override val url: String,
-                    override val onClick: ListItemInteraction,
+                    override val onCardClick: ListItemInteraction,
+                    val onButtonClick: ListItemInteraction,
                     val title: String
                 ) : ActionSource()
             }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt
index 58449c70ca24..caa92fd07216 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardComposable.kt
@@ -22,13 +22,13 @@ fun DynamicDashboardCard(
     UnelevatedCard(
         modifier = modifier
             .run {
-                if (card.action is ActionSource.Card) clickable { card.action.onClick.click() } else this
+                if (card.action != null) clickable { card.action.onCardClick.click() } else this
             },
         content = {
             Column(
                 Modifier.padding(bottom = 8.dp)
             ) {
-                val isCtaInvisible = card.action !is ActionSource.Button
+                val isCtaInvisible = card.action !is ActionSource.CardOrButton
                 DynamicCardHeader(
                     title = card.title,
                     onHideMenuClicked = { card.onHideMenuItemClick.click() }
@@ -47,8 +47,8 @@ fun DynamicDashboardCard(
                         modifier = Modifier.run { if (isCtaInvisible) padding(bottom = 8.dp) else this }
                     )
                 }
-                (card.action as? ActionSource.Button)?.title?.let { title ->
-                    DynamicCardCallToActionButton(text = title, onClicked = { card.action.onClick.click() })
+                (card.action as? ActionSource.CardOrButton)?.title?.let { title ->
+                    DynamicCardCallToActionButton(text = title, onClicked = { card.action.onButtonClick.click() })
                 }
             }
         }
@@ -65,9 +65,10 @@ fun DynamicDashboardCardPreview() {
                 id = "id",
                 title = "Card Title",
                 image = "https://picsum.photos/200/300",
-                action = ActionSource.Button(
+                action = ActionSource.CardOrButton(
                     title = "Call to Action", url = "",
-                    onClick = ListItemInteraction.create {},
+                    onCardClick = ListItemInteraction.create {},
+                    onButtonClick = ListItemInteraction.create {},
                 ),
                 rows = listOf(
                     MySiteCardAndItem.Card.Dynamic.Row(
@@ -96,10 +97,11 @@ fun DynamicDashboardCardWithFeatureAndDescriptionPreview() {
                 id = "id",
                 title = null,
                 image = "https://picsum.photos/200/300",
-                action = ActionSource.Button(
+                action = ActionSource.CardOrButton(
                     title = "See yours now",
                     url = "",
-                    onClick = ListItemInteraction.create {},
+                    onCardClick = ListItemInteraction.create {},
+                    onButtonClick = ListItemInteraction.create {},
                 ),
                 rows = listOf(
                     MySiteCardAndItem.Card.Dynamic.Row(
@@ -124,10 +126,11 @@ fun DynamicDashboardCardWithFeatureAndSubtitleAndDescriptionPreview() {
                 id = "id",
                 title = null,
                 image = "https://picsum.photos/200/300",
-                action = ActionSource.Button(
+                action = ActionSource.CardOrButton(
                     title = "Find out more",
                     url = "",
-                    onClick = ListItemInteraction.create {},
+                    onCardClick = ListItemInteraction.create {},
+                    onButtonClick = ListItemInteraction.create {},
                 ),
                 rows = listOf(
                     MySiteCardAndItem.Card.Dynamic.Row(
@@ -154,7 +157,7 @@ fun DynamicDashboardCardWithNoCta() {
                 image = "https://picsum.photos/200/300",
                 action = ActionSource.Card(
                     url = "",
-                    onClick = ListItemInteraction.create {},
+                    onCardClick = ListItemInteraction.create {},
                 ),
                 rows = listOf(
                     MySiteCardAndItem.Card.Dynamic.Row(
@@ -181,7 +184,7 @@ fun DynamicDashboardWithFeatureImageOnly() {
                 image = "https://picsum.photos/200/300",
                 action = ActionSource.Card(
                     url = "",
-                    onClick = ListItemInteraction.create {},
+                    onCardClick = ListItemInteraction.create {},
                 ),
                 rows = listOf(),
                 onHideMenuItemClick = ListItemInteraction.create {},
@@ -199,9 +202,10 @@ fun DynamicDashboardCardWithTitleAndCompleteRowsPreview() {
                 id = "id",
                 title = "What's New in Jetpack",
                 image = null,
-                action = ActionSource.Button(
+                action = ActionSource.CardOrButton(
                     title = "Find out more", url = "",
-                    onClick = ListItemInteraction.create {},
+                    onCardClick = ListItemInteraction.create {},
+                    onButtonClick = ListItemInteraction.create {},
                 ),
                 rows = listOf(
                     MySiteCardAndItem.Card.Dynamic.Row(
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilder.kt
index a99ad5d9c057..646052113207 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilder.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilder.kt
@@ -60,17 +60,21 @@ class DynamicCardsBuilder @Inject constructor(
         params: DynamicCardsBuilderParams,
         card: DynamicCardModel
     ): Dynamic.ActionSource? = when {
-        isValidUrlOrDeeplink(card.url) && isValidActionTitle(card.action) -> Dynamic.ActionSource.Button(
+        isValidUrlOrDeeplink(card.url) && isValidActionTitle(card.action) -> Dynamic.ActionSource.CardOrButton(
             url = requireNotNull(card.url),
             title = requireNotNull(card.action),
-            onClick = ListItemInteraction.create(
+            onCardClick = ListItemInteraction.create(
+                data = DynamicCardsBuilderParams.ClickParams(id = card.id, actionUrl = requireNotNull(card.url)),
+                action = params.onCardClick
+            ),
+            onButtonClick = ListItemInteraction.create(
                 data = DynamicCardsBuilderParams.ClickParams(id = card.id, actionUrl = requireNotNull(card.url)),
                 action = params.onCtaClick
             )
         )
         isValidUrlOrDeeplink(card.url) -> Dynamic.ActionSource.Card(
             url = requireNotNull(card.url),
-            onClick = ListItemInteraction.create(
+            onCardClick = ListItemInteraction.create(
                 data = DynamicCardsBuilderParams.ClickParams(id = card.id, actionUrl = requireNotNull(card.url)),
                 action = params.onCardClick
             )

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilderTest.kt
index a4284372ad73..23f7dc822998 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilderTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dynamiccard/DynamicCardsBuilderTest.kt
@@ -158,9 +158,10 @@ class DynamicCardsBuilderTest : BaseUnitTest() {
                 ),
                 title = DYNAMIC_CARD_TITLE,
                 image = DYNAMIC_CARD_FEATURED_IMAGE,
-                action = MySiteCardAndItem.Card.Dynamic.ActionSource.Button(
+                action = MySiteCardAndItem.Card.Dynamic.ActionSource.CardOrButton(
                     url = DYNAMIC_CARD_URL,
-                    onClick = mock(),
+                    onCardClick = mock(),
+                    onButtonClick = mock(),
                     title = DYNAMIC_CARD_ACTION
                 ),
                 onHideMenuItemClick = mock(),
@@ -181,9 +182,10 @@ class DynamicCardsBuilderTest : BaseUnitTest() {
         assertEquals(expectedCards[0].image, dynamicCards[0].image)
         assertEquals(expectedCards[0].rows.size, 1)
         assertEquals(expectedCards[0].rows, dynamicCards[0].rows)
-        assertThat(dynamicCards[0].action).isInstanceOf(MySiteCardAndItem.Card.Dynamic.ActionSource.Button::class.java)
-        val expected = expectedCards[0].action as? MySiteCardAndItem.Card.Dynamic.ActionSource.Button
-        val actual = dynamicCards[0].action as? MySiteCardAndItem.Card.Dynamic.ActionSource.Button
+        assertThat(dynamicCards[0].action)
+            .isInstanceOf(MySiteCardAndItem.Card.Dynamic.ActionSource.CardOrButton::class.java)
+        val expected = expectedCards[0].action as? MySiteCardAndItem.Card.Dynamic.ActionSource.CardOrButton
+        val actual = dynamicCards[0].action as? MySiteCardAndItem.Card.Dynamic.ActionSource.CardOrButton
         assertEquals(expected?.title, actual?.title)
         assertEquals(expected?.url, actual?.url)
     }