Skip to content

Add Tags & Categories insights card to new stats screen#22687

Merged
adalpari merged 24 commits intofeat/CMM-1936-create-insights-tabfrom
adalpari/insights-tags-card
Mar 13, 2026
Merged

Add Tags & Categories insights card to new stats screen#22687
adalpari merged 24 commits intofeat/CMM-1936-create-insights-tabfrom
adalpari/insights-tags-card

Conversation

@adalpari
Copy link
Copy Markdown
Contributor

@adalpari adalpari commented Mar 13, 2026

Description

Adds a new "Tags & Categories" card to the Insights tab in the new stats screen. The card displays a ranked list of tag/category groups with view counts, percentage bars, and expandable groups for entries containing multiple tags.

Key changes:

  • New TagsAndCategoriesCard composable with loading shimmer, loaded state with expand/collapse for multi-tag groups, and error handling
  • New TagsAndCategoriesViewModel fetching data from the statsTags wordpress-rs endpoint via StatsRepository
  • New TagsAndCategoriesDetailActivity for the "Show All" view with full list and expand/collapse support
  • Data layer additions: fetchStatsTags in StatsDataSource/StatsDataSourceImpl and fetchTags in StatsRepository
  • Shared composables extracted to TagsAndCategoriesComponents.kt (TagTypeIcon, ExpandedTagsSection)
  • Added modifier parameter to StatsListRowContainer for clickable support

Testing instructions

Card display:

  1. Open the app and navigate to Stats > Insights tab
  2. Scroll to find the "Tags & Categories" card
  • Verify the card shows tags/categories groups with view counts and percentage bars
  • Verify each group shows the correct icon (folder for categories, label for tags)

Expand/collapse multi-tag groups:

  1. Find a tag group that contains multiple tags (shown with a chevron arrow)
  2. Tap the group row
  • Verify the group expands to show individual tags with icons and a connecting vertical line
  1. Tap again
  • Verify the group collapses with animation

Show All detail screen:

  1. Tap "Show All" at the bottom of the card
  • Verify the detail screen shows all tag groups (not just top 7)
  • Verify expand/collapse works in the detail screen as well
  • Verify the back button returns to the Insights tab
Screen_recording_20260313_142851.mp4

adalpari and others added 20 commits March 12, 2026 12:07
Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…emove unnecessary @volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add @volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new top-list card to the Insights tab showing tags and categories
with view counts. Includes expandable multi-tag groups, percentage bars,
folder/tag icons, and a detail screen via Show All. Updates wordpress-rs
to 1230 for the statsTags endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ViewModel, repository, and display type unit tests covering
success/error states, data mapping, refresh, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use StatsCardContainer, StatsCardHeader, StatsListHeader,
  StatsCardEmptyContent, StatsListRowContainer from StatsCardCommon
- Extract TagTypeIcon and ExpandedTagsSection into shared
  TagsAndCategoriesComponents to eliminate duplication between
  Card and DetailActivity
- Add fromTagType() to TagGroupDisplayType to avoid list allocation
  per tag in ExpandedTagsSection
- Add modifier parameter to StatsListRowContainer for clickable rows
- Remove duplicated constants (CardCornerRadius, BAR_BACKGROUND_ALPHA,
  VERTICAL_LINE_ALPHA)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Only set isLoaded on success so loadData() retries after errors
- Guard pull-to-refresh to skip tags refresh when card is hidden
- Move loading state into refresh() so callers don't need showLoading()
- Remove showLoading() public method
- Add test for loadData() retry after error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented Mar 13, 2026

1 Error
🚫 This PR includes a PR version of wordpress-rs (1230-d4c9317af369c7231b414998986fd2e773eebf7d). Please merge the corresponding wordpress-rs PR and update to a trunk version before merging.
1 Warning
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

@adalpari adalpari changed the base branch from trunk to feat/CMM-1936-create-insights-tab March 13, 2026 11:39
@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Mar 13, 2026

Project dependencies changes

list
! Upgraded Dependencies
rs.wordpress.api:android:1230-d4c9317af369c7231b414998986fd2e773eebf7d, (changed from 1219-1de57afce924622700bcc8d3a1f3ce893d8dad5b)
rs.wordpress.api:kotlin:1230-d4c9317af369c7231b414998986fd2e773eebf7d, (changed from 1219-1de57afce924622700bcc8d3a1f3ce893d8dad5b)
tree
 +--- project :libs:fluxc
-|    \--- rs.wordpress.api:android:1219-1de57afce924622700bcc8d3a1f3ce893d8dad5b
-|         +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
-|         +--- com.squareup.okhttp3:okhttp-tls:5.3.2
-|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
-|         |    +--- com.squareup.okio:okio:3.16.4 (*)
-|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.2.21 -> 2.3.10 (*)
-|         +--- net.java.dev.jna:jna:5.18.1
-|         +--- rs.wordpress.api:kotlin:1219-1de57afce924622700bcc8d3a1f3ce893d8dad5b
-|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
-|         |    +--- com.squareup.okhttp3:okhttp-tls:5.3.2 (*)
-|         |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 (*)
-|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.10 (*)
-|         \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.10 (*)
+|    \--- rs.wordpress.api:android:1230-d4c9317af369c7231b414998986fd2e773eebf7d
+|         +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
+|         +--- com.squareup.okhttp3:okhttp-tls:5.3.2
+|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
+|         |    +--- com.squareup.okio:okio:3.16.4 (*)
+|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.2.21 -> 2.3.10 (*)
+|         +--- net.java.dev.jna:jna:5.18.1
+|         +--- rs.wordpress.api:kotlin:1230-d4c9317af369c7231b414998986fd2e773eebf7d
+|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
+|         |    +--- com.squareup.okhttp3:okhttp-tls:5.3.2 (*)
+|         |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 (*)
+|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.10 (*)
+|         \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.10 (*)
-\--- rs.wordpress.api:android:1219-1de57afce924622700bcc8d3a1f3ce893d8dad5b (*)
+\--- rs.wordpress.api:android:1230-d4c9317af369c7231b414998986fd2e773eebf7d (*)

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Mar 13, 2026

Project manifest changes for WordPress

The following changes in the WordPress's merged AndroidManifest.xml file were detected (build variant: wordpressRelease):

--- ./build/reports/diff_manifest/WordPress/wordpressRelease/base_manifest.txt	2026-03-13 14:55:26.237712914 +0000
+++ ./build/reports/diff_manifest/WordPress/wordpressRelease/head_manifest.txt	2026-03-13 14:55:35.567739630 +0000
@@ -207,6 +207,10 @@
         <activity
             android:name="org.wordpress.android.ui.newstats.yearinreview.YearInReviewDetailActivity"
             android:exported="false"
+            android:theme="@style/WordPress.NoActionBar" />
+        <activity
+            android:name="org.wordpress.android.ui.newstats.tagsandcategories.TagsAndCategoriesDetailActivity"
+            android:exported="false"
             android:theme="@style/WordPress.NoActionBar" /> <!-- Account activities -->
         <activity
             android:name="org.wordpress.android.ui.main.MeActivity"

Go to https://buildkite.com/automattic/wordpress-android/builds/25462/canvas?sid=019ce7a2-d577-4e57-82fb-37af305831ec, click on the Artifacts tab and audit the files.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Mar 13, 2026

Project manifest changes for WordPress

The following changes in the WordPress's merged AndroidManifest.xml file were detected (build variant: jetpackRelease):

--- ./build/reports/diff_manifest/WordPress/jetpackRelease/base_manifest.txt	2026-03-13 14:52:32.009217617 +0000
+++ ./build/reports/diff_manifest/WordPress/jetpackRelease/head_manifest.txt	2026-03-13 14:52:40.699242615 +0000
@@ -399,6 +399,10 @@
         <activity
             android:name="org.wordpress.android.ui.newstats.yearinreview.YearInReviewDetailActivity"
             android:exported="false"
+            android:theme="@style/WordPress.NoActionBar" />
+        <activity
+            android:name="org.wordpress.android.ui.newstats.tagsandcategories.TagsAndCategoriesDetailActivity"
+            android:exported="false"
             android:theme="@style/WordPress.NoActionBar" /> <!-- Account activities -->
         <activity
             android:name="org.wordpress.android.ui.main.MeActivity"

Go to https://buildkite.com/automattic/wordpress-android/builds/25462/canvas?sid=019ce7a2-d578-42d7-b122-6a1206bdb910, click on the Artifacts tab and audit the files.

adalpari and others added 2 commits March 13, 2026 12:54
…ts-tags-card

# Conflicts:
#	WordPress/src/main/java/org/wordpress/android/ui/newstats/InsightsCardType.kt
#	WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt
…nk field, fix thread safety and Intent size

- Extract shared TagGroupRow composable into TagsAndCategoriesComponents.kt with optional position parameter, removing duplicate from Card and DetailActivity
- Remove unused TagData.link field from data source, impl, and all tests
- Replace Intent extras with in-memory static holder in DetailActivity to avoid TransactionTooLargeException risk
- Remove unnecessary Parcelable from UI models
- Use AtomicBoolean for isLoaded/isLoading flags in ViewModel for thread safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Mar 13, 2026

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr22687-af05442
Build Number1487
Application IDcom.jetpack.android.prealpha
Commitaf05442
Installation URL41rfeth9i1t7o
Note: Google Login is not supported on these builds.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Mar 13, 2026

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr22687-af05442
Build Number1487
Application IDorg.wordpress.android.prealpha
Commitaf05442
Installation URL3p9aacgd1ma08
Note: Google Login is not supported on these builds.

@adalpari
Copy link
Copy Markdown
Contributor Author

@claude

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 13, 2026

Claude finished @adalpari's task in 2m 28s —— View job


Code Review: Tags & Categories Insights Card

  • Gather context and understand the PR
  • Review data layer changes (StatsDataSource, StatsRepository)
  • Review ViewModel and UI state
  • Review Composables (Card, Components, Detail Activity)
  • Review tests
  • Post final review

Overall this is a well-structured and consistent implementation that follows the patterns already established in the codebase. Test coverage is strong. I found a few issues worth addressing before merging.


Issues

1. Concurrent refresh calls not guarded — TagsAndCategoriesViewModel

loadData() correctly guards against concurrent calls using isLoading.compareAndSet(false, true), but refresh() bypasses this check entirely:

fun refresh() {
    isLoaded.set(false)
    isLoading.set(true)  // Sets flag but doesn't check it first
    _uiState.value = TagsAndCategoriesCardUiState.Loading
    fetchData()          // Called regardless of in-progress state
}

If refresh() is called multiple times in quick succession (e.g., rapid pull-to-refresh), fetchData() will be launched concurrently in multiple coroutines, potentially causing the UI state to be overwritten out of order. The fix is to guard refresh() similarly to other ViewModels in the codebase, or to cancel the previous job before starting a new one.

[Fix this →](https://claude.ai/code?q=In+WordPress%2Fsrc%2Fmain%2Fjava%2Forg%2Fwordpress%2Fandroid%2Fui%2Fnewstats%2Ftag sandcategories%2FTagsAndCategoriesViewModel.kt%2C+the+refresh%28%29+function+on+branch+adalpari%2Finsights-tags-card+does+not+guard+against+concurrent+calls.+loadData%28%29+uses+isLoading.compareAndSet%28false%2C+true%29+correctly%2C+but+refresh%28%29+just+calls+isLoading.set%28true%29+and+fetchData%28%29+without+checking+whether+a+fetch+is+already+in+progress.+Consider+storing+the+Job+returned+by+viewModelScope.launch+and+cancelling+it+before+starting+a+new+one+in+refresh%28%29.)


2. Data loss on process death — TagsAndCategoriesDetailActivity

The detail activity stores items in a companion object static field:

companion object {
    private var detailItems: List<TagGroupUiItem>? = null

    fun start(context: Context, items: List<TagGroupUiItem>) {
        detailItems = items   // written before startActivity
        context.startActivity(intent)
    }
}

If Android kills the app process while the detail screen is in the back stack (background pressure), and the user navigates back to it, detailItems will be null. The activity falls back to emptyList(), showing a blank screen with no error message and no way to recover. The user would need to back-navigate and re-open the detail screen.

This pattern is used for other detail activities in the same codebase (YearInReviewDetailActivity, etc.), so this may be an accepted trade-off. If so, consider at least showing an error state or a "no data" placeholder instead of a silent blank screen when detailItems is null.


3. Duplicated isExpandable logic

The condition item.tags.size > 1 is written independently in two places:

  • TagsAndCategoriesCard.kt (line 194, LoadedContent)
  • TagsAndCategoriesDetailActivity.kt (line 158, TagsAndCategoriesDetailScreen)

If this threshold ever changes, both locations need updating. Consider extracting a TagGroupUiItem.isExpandable computed property or a shared constant.

Fix this →


4. Accessibility: missing content descriptions on meaningful icons

TagTypeIcon (in TagsAndCategoriesComponents.kt) sets contentDescription = null. The icon communicates the type (category folder vs. tag label), which is meaningful to users. Screen readers will not announce anything for these icons.

Similarly, the expand/collapse chevron in TagGroupRow has contentDescription = null.

Consider providing content descriptions such as "Category" / "Tag" for TagTypeIcon and "Expand" / "Collapse" for the chevron.


Minor Observations

  • StatsListHeader uses card title as column header (TagsAndCategoriesCard.kt line 177–179): The left column header shows "Tags & Categories" (same as the card title), rather than a shorter label like "Tag" or "Name". Other cards use the card name for this header too (it's consistent), but worth a UX review.

  • fromTags(emptyList()) returns CATEGORY: Due to Kotlin's vacuous truth in all {}, an empty tag list resolves to CATEGORY. This is correctly tested and unlikely to occur in practice (the server won't return tag groups with zero tags), but the behavior is a bit surprising for an empty input.

  • TagUiItem.tagType is an untyped String: This field is compared against the string literal "category" in multiple places. An enum or sealed class would be safer and prevent future typos, though this may be intentional to avoid over-engineering the mapping from API types.


Testing

Test coverage is solid:

  • TagsAndCategoriesViewModelTest covers loading guards, error/success states, refresh, and detail data retrieval
  • StatsRepositoryTagsTest covers success, error, and data mapping
  • TagGroupDisplayTypeTest covers all display type combinations including edge cases (empty list, unknown types)

The one gap worth noting: there are no tests for the concurrent refresh() behavior described in issue #1 above.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Mar 13, 2026

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

…accessibility

- Cancel in-flight fetch job on refresh() to prevent stale overwrites
- Finish detail activity on process death instead of showing blank screen
- Extract TagGroupUiItem.isExpandable computed property to deduplicate logic
- Add content descriptions for TagTypeIcon and expand/collapse chevron icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari adalpari marked this pull request as ready for review March 13, 2026 13:30
@adalpari adalpari requested a review from a team as a code owner March 13, 2026 13:30
@adalpari adalpari requested review from nbradbury and removed request for a team March 13, 2026 13:30
@nbradbury
Copy link
Copy Markdown
Contributor

@adalpari This looks good overall but there are failing tests in InsightsCardsConfigurationRepositoryTest.

Fixes CI test failures caused by the new TAGS_AND_CATEGORIES card
not being reflected in test fixtures and assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

@nbradbury
Copy link
Copy Markdown
Contributor

@adalpari A local Claude /review turned up a few things, only one of which I think may need to be addressed:

File: TagsAndCategoriesDetailActivity.kt:1163

  companion object {
      private var detailItems: List<TagGroupUiItem>? = null

Storing data in a companion object (static) to pass it between activities is fragile and leaks memory if onDestroy isn't called (e.g., process death). It also won't survive configuration changes correctly. Consider using Parcelable/Serializable extras in the Intent, or a shared ViewModel scoped to the navigation graph.

Summary

The main concern is the static detailItems pattern for passing data to the detail activity — it's a known anti-pattern that can cause issues with process death and memory. The thread safety of allItems and raw error messages shown to users should also be addressed. The rest is solid, well-structured Compose code following existing patterns.

@adalpari
Copy link
Copy Markdown
Contributor Author

@adalpari A local Claude /review turned up a few things, only one of which I think may need to be addressed:

File: TagsAndCategoriesDetailActivity.kt:1163

  companion object {
      private var detailItems: List<TagGroupUiItem>? = null

Storing data in a companion object (static) to pass it between activities is fragile and leaks memory if onDestroy isn't called (e.g., process death). It also won't survive configuration changes correctly. Consider using Parcelable/Serializable extras in the Intent, or a shared ViewModel scoped to the navigation graph.

Summary

The main concern is the static detailItems pattern for passing data to the detail activity — it's a known anti-pattern that can cause issues with process death and memory. The thread safety of allItems and raw error messages shown to users should also be addressed. The rest is solid, well-structured Compose code following existing patterns.

Yes, I saw this one. This is following a practise that, at some point was introduced for another card because of the possibility of passing a huge list using the intent. I've noted it down to be fixed and reviewed for the whole new stats screen. I haven't created a ticket, but here is one to not forget it.

Does it sound good to you?

Copy link
Copy Markdown
Contributor

@nbradbury nbradbury left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, feel free to merge when the wp-rs dependency is updated :shipit:

@adalpari adalpari merged commit a2d0dbf into feat/CMM-1936-create-insights-tab Mar 13, 2026
20 of 24 checks passed
@adalpari adalpari deleted the adalpari/insights-tags-card branch March 13, 2026 15:14
adalpari added a commit that referenced this pull request Mar 20, 2026
* Update wordpress-rs to 1219-9cb722b47c

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Insights tab with Year in Review card to new stats screen

Populate the Insights tab with card management (add, remove, move)
mirroring the Traffic tab architecture. Add Year in Review as the
first Insights card, fetching yearly summaries (posts, words, likes,
comments) via the wordpress-rs statsInsights endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Restyle Year in Review card with 2x2 grid of mini stat cards

Update the card title to show "YEAR in review" instead of a generic
title. Replace the table layout with a 2x2 grid of mini cards, each
displaying an icon, label, and formatted value for Posts, Words,
Likes, and Comments, matching the web design.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Year in Review detail screen with View All functionality

Add a detail screen showing all years with full stats (posts, comments,
avg comments/post, likes, avg likes/post, words, avg words/post). The
card shows the most recent year and a "View all" link when multiple
years are available. Years are sorted newest first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Always show current year in Year in Review card and always display Show All

Ensure the current year is always present in the data, adding it with
zero values if not returned by the API. The Show All footer is now
always visible regardless of the number of years available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Minor cleanup: cache current year and remove redundant variable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix code review issues and add missing tests

- Use resource string for exception errors instead of raw e.message
- Add duplicate guard in addCard()
- Change Error from data class to class to fix lambda equality
- Use Year.now() per-call instead of cached static val
- Fix isValidConfiguration to check for null entries
- Remove 0dp Spacer from detail screen
- Add StatsFormatterTest with 12 tests
- Add repository moveCard and addCard duplicate tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix second round of review issues: stale success flag, race condition, siteId fallback, API type inconsistency

- Reset isLoadedSuccessfully on error/exception so loadDataIfNeeded recovers after failed refresh
- Add Mutex to InsightsCardsConfigurationRepository to prevent concurrent mutation races
- Guard siteId in InsightsViewModel mutations to avoid operating with invalid 0L
- Align fetchStatsInsights parameter from String to Long for interface consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update Year in Review labels to match web app

Card uses short labels (Words, Likes) without "Total" prefix.
Detail screen uses exact web labels: Total posts, Total comments,
Avg comments per post, Total likes, Avg likes per post, Total words,
Avg words per post.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1936: Add All-time Stats and Most Popular Day Insights cards (#22673)

* Update wordpress-rs library hash

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add stats summary data layer with shared caching

Add fetchStatsSummary endpoint and StatsSummaryData model to
StatsDataSource. Implement Mutex-based caching in StatsRepository
so All-time Stats and Most Popular Day cards share a single API call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add All-time Stats Insights card

New card showing Views, Visitors, Posts, and Comments from the
statsSummary endpoint. Uses rememberShimmerBrush for loading state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Most Popular Day Insights card

New card showing the best day for views with date, view count, and
percentage. Shares the statsSummary API call with All-time Stats via
repository caching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Wire All-time Stats and Most Popular Day into Insights tab

Add card types to InsightsCardType enum and default card list.
Wire ViewModels and cards into InsightsTabContent. Add config
migration to automatically show new card types for existing users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: locale, empty best day, config migration, tests

- Create DateTimeFormatter at format time instead of caching in
  companion object to respect locale changes
- Handle empty viewsBestDay by returning loaded state with empty
  values instead of throwing
- Fix config migration by persisting hiddenCards field so new card
  types can be distinguished from user-removed cards
- Add missing tests: empty viewsBestDay, zero views percentage,
  exception path, dayAndMonth assertion, addNewCardTypes migration,
  full config no-migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update RS library and fix insights locale, detekt, and date casing

- Update wordpress-rs to 1de57afce924622700bcc8d3a1f3ce893d8dad5b
- Add locale parameter to StatsInsightsParams matching other endpoints
- Fix detekt MagicNumber and TooGenericExceptionCaught violations
- Capitalize first letter of formatted date in Most Popular Day card

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move stats summary cache from StatsRepository to InsightsViewModel

StatsRepository was not @singleton, so the shared cache for statsSummary
data was broken — each ViewModel got its own instance. Move caching to
InsightsViewModel which is activity-scoped and shared. Child ViewModels
now receive a summary provider function wired from the UI layer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix double-write and concurrency in InsightsCardsConfigurationRepository

addNewCardTypes wrote to prefs directly, then the caller wrote again via
saveConfiguration. Also getConfiguration was not mutex-protected. Make
addNewCardTypes pure, extract loadAndMigrate for atomic read-migrate-save
within the mutex, and make getConfiguration go through the mutex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename hiddenCards() to computeHiddenCards() to avoid naming conflict

InsightsCardsConfiguration had both a hiddenCards property (persisted
list) and a hiddenCards() method (computed from visibleCards). Rename
the method to make the distinction clear.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add NoData state for MostPopularDay and fix percentage precision

When a site has no best day data, show a dedicated NoData state with a
"No data yet" message instead of a blank Loaded card. Also reduce
percentage format from 3 to 1 decimal place for consistency with
typical stats UIs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress TooGenericExceptionThrown detekt warning in test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread-safety, DI design, and Error state in Insights cards

- Add @volatile to isLoading/isLoadedSuccessfully flags in ViewModels
- Extract StatsSummaryUseCase singleton to replace summaryProvider var,
  removing temporal coupling and null-check error paths
- Change Error from class to data class, move onRetry out of state and
  into composable parameters to avoid unnecessary recompositions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Add Most Popular Time card and centralize Insights data fetching (#22684)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card to new stats screen (#22687)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card

Add a new top-list card to the Insights tab showing tags and categories
with view counts. Includes expandable multi-tag groups, percentage bars,
folder/tag icons, and a detail screen via Show All. Updates wordpress-rs
to 1230 for the statsTags endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add expand/collapse for multi-tag groups in detail screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for Tags & Categories feature

ViewModel, repository, and display type unit tests covering
success/error states, data mapping, refresh, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify Tags & Categories by reusing shared components

- Use StatsCardContainer, StatsCardHeader, StatsListHeader,
  StatsCardEmptyContent, StatsListRowContainer from StatsCardCommon
- Extract TagTypeIcon and ExpandedTagsSection into shared
  TagsAndCategoriesComponents to eliminate duplication between
  Card and DetailActivity
- Add fromTagType() to TagGroupDisplayType to avoid list allocation
  per tag in ExpandedTagsSection
- Add modifier parameter to StatsListRowContainer for clickable rows
- Remove duplicated constants (CardCornerRadius, BAR_BACKGROUND_ALPHA,
  VERTICAL_LINE_ALPHA)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused stubUnknownError from ViewModel test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: error recovery, conditional refresh, loading state

- Only set isLoaded on success so loadData() retries after errors
- Guard pull-to-refresh to skip tags refresh when card is hidden
- Move loading state into refresh() so callers don't need showLoading()
- Remove showLoading() public method
- Add test for loadData() retry after error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: deduplicate row composable, remove unused link field, fix thread safety and Intent size

- Extract shared TagGroupRow composable into TagsAndCategoriesComponents.kt with optional position parameter, removing duplicate from Card and DetailActivity
- Remove unused TagData.link field from data source, impl, and all tests
- Replace Intent extras with in-memory static holder in DetailActivity to avoid TransactionTooLargeException risk
- Remove unnecessary Parcelable from UI models
- Use AtomicBoolean for isLoaded/isLoading flags in ViewModel for thread safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix concurrent refresh, process death, isExpandable duplication, and accessibility

- Cancel in-flight fetch job on refresh() to prevent stale overwrites
- Finish detail activity on process death instead of showing blank screen
- Extract TagGroupUiItem.isExpandable computed property to deduplicate logic
- Add content descriptions for TagTypeIcon and expand/collapse chevron icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update configuration tests to include TAGS_AND_CATEGORIES card type

Fixes CI test failures caused by the new TAGS_AND_CATEGORIES card
not being reflected in test fixtures and assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1952: stats insights: tags card and some fixes (#22701)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card

Add a new top-list card to the Insights tab showing tags and categories
with view counts. Includes expandable multi-tag groups, percentage bars,
folder/tag icons, and a detail screen via Show All. Updates wordpress-rs
to 1230 for the statsTags endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add expand/collapse for multi-tag groups in detail screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for Tags & Categories feature

ViewModel, repository, and display type unit tests covering
success/error states, data mapping, refresh, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify Tags & Categories by reusing shared components

- Use StatsCardContainer, StatsCardHeader, StatsListHeader,
  StatsCardEmptyContent, StatsListRowContainer from StatsCardCommon
- Extract TagTypeIcon and ExpandedTagsSection into shared
  TagsAndCategoriesComponents to eliminate duplication between
  Card and DetailActivity
- Add fromTagType() to TagGroupDisplayType to avoid list allocation
  per tag in ExpandedTagsSection
- Add modifier parameter to StatsListRowContainer for clickable rows
- Remove duplicated constants (CardCornerRadius, BAR_BACKGROUND_ALPHA,
  VERTICAL_LINE_ALPHA)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused stubUnknownError from ViewModel test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: error recovery, conditional refresh, loading state

- Only set isLoaded on success so loadData() retries after errors
- Guard pull-to-refresh to skip tags refresh when card is hidden
- Move loading state into refresh() so callers don't need showLoading()
- Remove showLoading() public method
- Add test for loadData() retry after error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: deduplicate row composable, remove unused link field, fix thread safety and Intent size

- Extract shared TagGroupRow composable into TagsAndCategoriesComponents.kt with optional position parameter, removing duplicate from Card and DetailActivity
- Remove unused TagData.link field from data source, impl, and all tests
- Replace Intent extras with in-memory static holder in DetailActivity to avoid TransactionTooLargeException risk
- Remove unnecessary Parcelable from UI models
- Use AtomicBoolean for isLoaded/isLoading flags in ViewModel for thread safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix concurrent refresh, process death, isExpandable duplication, and accessibility

- Cancel in-flight fetch job on refresh() to prevent stale overwrites
- Finish detail activity on process death instead of showing blank screen
- Extract TagGroupUiItem.isExpandable computed property to deduplicate logic
- Add content descriptions for TagTypeIcon and expand/collapse chevron icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update configuration tests to include TAGS_AND_CATEGORIES card type

Fixes CI test failures caused by the new TAGS_AND_CATEGORIES card
not being reflected in test fixtures and assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fetch tags data independently in detail screen instead of using static field

The detail screen now has its own ViewModel that fetches up to 100
items directly from the API, while the card continues to fetch 10.
This removes the static data holder pattern that was prone to data
loss on process death.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract shared mapper, add detail VM tests, guard double calls, show all 10 card items

- Extract TagsAndCategoriesMapper to deduplicate TagGroupData to
  TagGroupUiItem mapping between card and detail ViewModels
- Add unit tests for TagsAndCategoriesDetailViewModel
- Add isLoaded/isLoading guards to detail VM to prevent double fetches
- Remove CARD_MAX_ITEMS limit so card displays all 10 fetched items
- Remove unused import in DetailActivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Skip data fetching for hidden Insights cards

Only call summary/insights endpoints when visible cards need them,
avoiding unnecessary network calls for hidden cards. Track which
endpoint groups have been fetched so re-adding a hidden card
triggers a fetch for its missing data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Early return in fetchData when no endpoints are needed

Prevents setting isDataLoaded=true when no cards require
fetching, which would block future fetches when cards are
re-added to the visible list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address code review findings: reduce duplication and improve tests

- Replace duplicate shimmer animation in YearInReviewCard with
  shared rememberShimmerBrush() utility
- Extract StatsTagsUseCase to centralize token validation and
  repository init, removing duplication between Tags ViewModels
- Add card reordering tests for middle elements in
  InsightsCardsConfigurationRepositoryTest
- Fix locale-dependent assertions in MostPopularDayViewModelTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress LargeClass detekt warning on InsightsViewModelTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix process-death restore and add caching to StatsTagsUseCase

- Call loadData() unconditionally in TagsAndCategoriesDetailActivity
  onCreate to handle process-death restore (loadData guard prevents
  double fetch on rotation)
- Add Mutex-protected in-memory cache to StatsTagsUseCase, consistent
  with StatsSummaryUseCase and StatsInsightsUseCase
- Pass forceRefresh=true on pull-to-refresh in TagsAndCategoriesViewModel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract BaseTagsAndCategoriesViewModel and use localized error messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety and error handling in ViewModels

Use AtomicBoolean with compareAndSet in InsightsViewModel to prevent
race conditions, rethrow CancellationException in base tags ViewModel,
and only mark endpoints as fetched on success results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract isCacheHit method to fix detekt ComplexCondition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Cancel in-flight fetch job before refreshing in InsightsViewModel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix compilation errors after trunk merge

Update wordpress-rs to version with stats tags types and remove
duplicate NoConnectionContent composable and redundant else branches
that caused -Werror failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address code review findings and add base ViewModel tests

- Move network call outside mutex in StatsTagsUseCase to
  avoid blocking concurrent callers during slow requests
- Add main-thread-confinement comments for fetchJob fields
- Document why TAGS_AND_CATEGORIES is absent from
  needsSummary/needsInsights (uses its own fetch path)
- Restore card max items to 7 (was unintentionally changed
  to 10 during refactoring)
- Add tests for BaseTagsAndCategoriesViewModel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix detekt findings: suppress ReturnCount, remove unused composable

- Suppress ReturnCount on StatsTagsUseCase.invoke (3 returns are
  clearer than restructuring with nested conditions)
- Remove unused PlaceholderTabContent composable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix TOCTOU race, config fetch trigger, and detail empty state

- StatsTagsUseCase: use in-flight Deferred to coalesce concurrent
  requests with the same params, eliminating the TOCTOU race where
  two callers could both miss cache and fire duplicate requests
- BaseTagsAndCategoriesViewModel: make isLoaded private since no
  subclass accesses it directly
- InsightsViewModel: trigger loadDataIfNeeded() from
  updateFromConfiguration when new endpoints are required, instead
  of relying on the UI to call it
- TagsAndCategoriesDetailActivity: add empty-state message when
  the loaded items list is empty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Deduplicate detail VM tests, fix Mockito import, add cancellation test

- Remove duplicate tests from TagsAndCategoriesDetailViewModelTest
  that are already covered by BaseTagsAndCategoriesViewModelTest;
  keep only initial-state and maxItems-specific tests
- Replace fully-qualified org.mockito.Mockito.times() with the
  imported mockito-kotlin times() throughout InsightsViewModelTest
- Add test verifying that rapid double-refresh cancels the first
  job so only one forceRefresh call completes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Clear stats caches on screen open to prevent stale data

Add clearCache() to StatsInsightsUseCase, StatsSummaryUseCase, and
StatsTagsUseCase. Call all three from InsightsViewModel init so that
reopening the insights screen always fetches fresh data while still
sharing results between cards within a single session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update wordpress-rs to trunk-262a778ead5f163f3450d62adfac21fb32048714

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify: fix StatsTagsUseCase error handling, deduplicate bottom sheets

- Fix critical bug in StatsTagsUseCase where an exception from fetchTags()
  would leave the CompletableDeferred incomplete, hanging all awaiters
- Extract generic AddCardBottomSheet<T> to replace three duplicate
  implementations (Stats, Insights, Subscribers)
- Merge duplicate LaunchedEffect(cardsToLoad) blocks in InsightsTabContent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix PR #22711 review issues: race conditions, code quality, API compat

- Fix race condition in InsightsViewModel.refreshData() by nulling
  fetchJob before cancel and guarding finally block for current job
- Fix StatsTagsUseCase cancellation propagation: complete deferred
  with error instead of completeExceptionally for CancellationException
- Remove redundant hiddenCards constructor param, make it a computed
  property; use PersistedConfig for backward-compatible JSON migration
- Key expandedGroups on items in TagsAndCategoriesCard and detail
  activity so expansion state resets on data refresh
- Use StatsCardContainer in AllTimeStatsCard and MostPopularDayCard
  instead of duplicating border/clip/background pattern
- Replace Year.now() (API 26+) with Calendar in YearInReviewViewModel
- Move @Suppress off inline catch to function level in StatsTagsUseCase
- Fix fully qualified references in repository test (import never, Gson)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix insights tab bugs: cancellation, race conditions, perf, and quality

- Fix StatsTagsUseCase cancellation propagation (deferred.cancel instead of error)
- Fix InsightsViewModel card-add race by cancelling in-flight fetch on config change
- Release mutex during network calls in StatsSummaryUseCase and StatsInsightsUseCase
- Replace Calendar with java.time.Year in YearInReviewViewModel
- Add NoData state for empty tags response instead of empty Loaded card
- Show period selector only on Traffic tab, not Insights
- Add accessibility onClickLabel to AddCardItem bottom sheet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Suppress ReturnCount detekt warning in use cases

The mutex refactor introduced a third return path (guard clause
pattern) which is idiomatic and readable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix retry, error handling, race condition, and localization bugs

- Reset isLoaded on exception in BaseTagsAndCategoriesViewModel so retry
  is not a no-op after a failed load
- Complete deferred with TagsResult.Error instead of completeExceptionally
  in StatsTagsUseCase so non-owner callers get a proper result
- Replace raw English error strings with localized R.string.stats_error_unknown
  in InsightsViewModel via ResourceProvider
- Fix updateFromConfiguration race window by cancelling job first and calling
  fetchData() directly with isDataLoading already set to true

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Deduplicate error UI, remove raw Context, fix Intent data passing, add logging

- Replace duplicate ErrorContent composables in AllTimeStatsCard and
  MostPopularDayCard with shared StatsCardErrorContent
- Replace raw Context injection in MostPopularTimeViewModel with
  DateFormatWrapper for better testability
- Refactor YearInReviewDetailActivity to fetch data via ViewModel and
  StatsInsightsUseCase instead of passing Parcelable list via Intent
- Add AppLog warning in MostPopularDayViewModel.parseBestDay catch block
  to aid debugging of unparseable date strings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use LazyColumn, shared shimmer, and reduce line wrapping in insights tab

- Convert Column+verticalScroll to LazyColumn in InsightsTabContent for
  lazy composition of off-screen cards
- Add ProvideShimmerBrush/LocalShimmerBrush CompositionLocal so all cards
  share a single synchronized shimmer animation
- Reduce overly aggressive line wrapping across ViewModels and
  InsightsTabContent, keeping lines within 120 chars but no longer
  wrapping well under the limit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Extract shared YearSummary mapping, sync detail screen shimmer

- Move toUiModel/ensureCurrentYear from both YearInReview ViewModels to
  YearSummary.Companion to eliminate duplication
- Wrap detail screen loading state in ProvideShimmerBrush so multiple
  ShimmerBox instances share a single synchronized animation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove unused Box import in YearInReviewDetailActivity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix missing resourceProvider in InsightsViewModelTest constructors

Add resourceProvider parameter to two remaining InsightsViewModel
constructor calls in tests, and stub stats_error_unknown string
resource to fix error-path test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
nbradbury added a commit that referenced this pull request Mar 23, 2026
* Upgrade Gradle 9.1.0 + AGP 9.0.1

Simultaneously upgrade Gradle (8.12.1 → 9.1.0) and AGP (8.10.1 → 9.0.1)
since AGP 8.x does not support Gradle 9.x.

Uses opt-out flags (android.newDsl=false, android.builtInKotlin=false) to
preserve existing kotlin-android/kapt plugins while on AGP 9.

Version bumps:
- Gradle 8.12.1 → 9.1.0
- AGP 8.10.1 → 9.0.1
- Dagger/Hilt 2.58 → 2.59.2
- Fladle 0.19.0 → 0.21.0

AGP 9 migration fixes:
- Remove archivesBaseName from defaultConfig (use base.archivesName)
- Move compileSdk from defaultConfig to android block
- Replace proguard-android.txt with proguard-android-optimize.txt
- Rename lintOptions → lint in root build.gradle
- Replace deprecated buildDir with layout.buildDirectory
- Remove obsolete android.useAndroidX and android.enableJetifier properties
- Fix kotlin {} → java {} for sourceCompatibility in JVM-only modules
- Remove allowBackup from posttypes library manifest (app-level concern)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Survive dialog selection across rotation (#22699)

Use rememberSaveable instead of remember for the selected index
in StatusDialog, FormatDialog, and AuthorDialog so the user's
choice persists through configuration changes.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Remove Login Module (#22564)

* Remove LoginEmailPasswordFragment

* Remove Login Magic Link Fragments

* Remove username/password WP.com flow

* Remove Google Login

* Remove LoginEmailFragment

* Remove Login 2FA fragment

* Remove SignupMagicLinkFragment

* Remove orphaned files

* Remove the “Find your site address” button

* Remove SignupEpilogueFragment

* Remove native account signup code

* Remove `PostSignupInterstitialActivity`

* Remove SmartLock

* Remove `WOO_LOGIN_MODE`

* Don’t show the username/password fields for Application Password sites

* In the Me tab, start WP.com login directly

* Fix strings

* Fix WP.com login UI

* Improve the WP.org login device name

* Add self-hosted site reauthentication

* Display more detailed error messages

* Fix WP.com sharing login flow

* Fix share flow login

* Remove `LoginMode.JETPACK_SELFHOSTED`

* Move resources out of login module into app

* Flatten theme heirarchies

* Modernize login prologue

* Fix edge-to-edge for login

* Automatically load notifications when switching to that tab

* Fix Application Password login focus

* Remove login module from CI

* Remove unused styles

* Add nullable annotation

* Remove deprecated Smart Lock Credentials API and play-services-auth

The only usage of play-services-auth was the deprecated Smart Lock for
Passwords (Credentials API) in AppInitializer, which connected a
GoogleApiClient on startup and called disableAutoSignIn on logout.
play-services-fido was only present as a transitive dependency of
play-services-auth. Neither is needed anymore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove trailing comma in localization.rb array

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix trailing backslash in run-unit-tests.sh

Removing :libs:login:koverXmlReportDebug left a trailing backslash on
the previous line, causing TESTS_EXIT_STATUS=$? to be passed as a
gradle task argument instead of being a variable assignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused imports left over from login lib removal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress detekt LongMethod for application password error handling

These methods are long due to exhaustive error handling across multiple
WpRequestResult variants, which is reasonable to suppress.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Delete unused bg_oval_translucent_stroke_3dp drawable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove references to deleted signup epilogue analytics constants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix constructor mismatches in application password test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused login prologue resources

Delete login prologue drawables, colors, dimens, strings, and menu
resources that became unused after the login library removal. Also
update TrainOfIcons preview to use app_icon instead of deleted drawables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Restore login_prologue_revamped colors used by wordpress source set

These colors are referenced by LoginPrologueRevampedFragment.kt and
Tagline.kt in src/wordpress/, so they must not be removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove remaining unused resources from login lib removal

Delete unused palette colors (purple_10, purple_20, purple_60, pink_10,
orange_10, green_60, celadon_30), login prologue dimens, indicator
drawables, promo illustration PNGs, PromoTitle style, and orphaned
LoginSiteAddressValidatorTest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move login prologue revamped colors to wordpress source set

These colors are only used by WordPress-flavor code in src/wordpress/,
so they belong in src/wordpress/res/ rather than shared src/main/res/.
This fixes the Jetpack lint check which correctly flagged them as unused.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix dark/light mode switching issues

* Add a release note

* Remove unused View import in LoginActivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove stale Google Login warnings from build scripts

Google Login no longer exists in the app, so these warnings
about it not working are no longer relevant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use bodyLarge text style for site address label

The headlineSmall style competes with the TopAppBar title.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Make login form scrollable for landscape support

Flatten the two-column layout into a single scrollable column
so the text field and button don't get crushed when the keyboard
opens on small devices in landscape mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ANR risk and error handling in WP.com OAuth login

- Replace runBlocking with async coroutine scope to avoid blocking
  the main thread during token exchange (Critical #1)
- Use dedicated CoroutineScope instead of Dispatchers.IO so
  dispose() only cancels this helper's coroutines (Critical #2)
- Propagate login errors to the caller via Consumer<Exception>
  instead of swallowing them
- Show error state on the loading screen with a retry button
  instead of silently failing
- Cancel coroutine scope in LoginActivity.onDestroy()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Handle null and unknown values in LoginFlow.fromIntent()

getStringExtra() can return null even when hasExtra() is true,
and valueOf() throws for old enum names that no longer exist.
Fall back to PROLOGUE safely in both cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Connect site comparison logic after OAuth login

After fetching sites post-OAuth login, route back through
loggedInAndFinish() so FINISH_WITH_SITE flows correctly
return the newly added site ID to the calling activity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Persist login flow state in SharedPreferences instead of static fields

Static fields don't survive process death and can cause issues in
multi-window mode. Move sPendingLoginFlow and sIsShareFlowPending to
AppPrefs with dedicated getter/setter methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add system bar transparency to login night theme

The light theme had transparent status/navigation bars but the night
theme was missing these attributes, causing opaque system bars in dark
mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add @Singleton to LoginAnalyticsTracker provider method

The class had @Singleton but the Dagger provider didn't, so a new
instance was created on every injection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Call startPostLoginServices and remove dead stub methods

startPostLoginServices was defined but never called, so Reader tags
and notification services weren't started after login. Also removes
unused startOver and gotConnectedSiteInfo methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove leftover debug Log.e in WPMainActivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove plaintext username from debug logs

The hasApiRestUsername boolean check is sufficient for debugging
without exposing the actual credential value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Replace deprecated onActivityCreated with onViewCreated

Move activity title setup into onViewCreated, removing the suppressed
deprecation warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Align ARG_LOGIN_FLOW constant name with its string value

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Unbind CustomTabsServiceConnection on dispose

The service was bound but never unbound, causing a minor resource
leak. Now unbinds when the helper is disposed in onDestroy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use Channel instead of SharedFlow for discovery URL event

Channel guarantees the event is buffered and consumed exactly once,
even if the collector starts slightly after emission. Safer than
SharedFlow(replay=0) for one-shot navigation events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress overdraw lint warning on login loading layout

The window background from LoginTheme triggers a false positive
overdraw warning since the ConstraintLayout itself has no explicit
background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tools:keep for flavor-specific login prologue colors

Android Lint can't trace cross-flavor resource usage, so it flags
these colors as unused. They're referenced by Compose code in the
wordpress flavor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Redirect WP.com site addresses to OAuth login flow

When a user enters a WordPress.com site address in the site address
field, redirect them to the WP.com OAuth flow instead of proceeding
with application password authorization. This ensures WP.com account
state is properly established so the Me tab shows the user's profile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove old login lib references from e2e tests

The LoginFlow e2e helper imported resources from the deleted login
library and referenced views (login_username_row, login_password_row)
that no longer exist. Remove the dead enterUsernameAndPassword and
enterSiteAddress methods since login forms are now Compose-based and
WP.com login uses OAuth via Custom Tabs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Call onFailure when OAuth callback is missing code parameter

Previously tryLoginWithDataString silently returned if the code
query parameter was absent, leaving the caller on a loading screen
indefinitely. Now reports the error via the onFailure callback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt violations and CI unit test task name

- Remove unused AppLog imports from WPcomLoginHelper
- Replace generic Exception catch with Result.fold to satisfy detekt
- Fix kover task name to match trunk's flavor rename (no more Wasabi)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Persist post-OAuth loading state across configuration changes

mIsWaitingForSitesToLoad and mOldSitesIdsForLoginUpdate were not
saved in onSaveInstanceState, so a device rotation during the
post-OAuth account/sites fetch would reset the flags and leave
the user stuck on the loading screen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix share flow race condition in LoginActivity onResume

Gate the onResume completion check on a new mShareFlowLoginLaunched
flag so LoginActivity doesn't immediately finish with RESULT_OK when
the user already has sites but hasn't completed the share-flow login.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Nick Bradbury <nick.bradbury@gmail.com>

* Remove AGP 9 opt-out flags and adopt built-in Kotlin

Remove android.newDsl=false and android.builtInKotlin=false to fully
adopt AGP 9's new DSL and built-in Kotlin compilation support.

- Remove kotlin-android plugin from all modules (AGP 9 built-in)
- Migrate kapt → legacy-kapt in fluxc (AGP 9 requirement)
- Migrate applicationVariants API → androidComponents.onVariants
- Remove kotlin-android and kapt entries from version catalog
- Keep kotlin-compose plugin (still required for Compose compiler)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove obsolete AGP 9 gradle.properties flags

Remove android.nonTransitiveRClass and android.nonFinalResIds (now
always enabled in AGP 9) and android.enableR8.fullMode=false to adopt
the new default of full R8 mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix issues introduced by login lib removal (#22703)

* Fix issues introduced by login lib removal

- Use single root layout with loading overlay in LoginActivity to avoid
  multiple setContentView() calls that orphan fragments
- Wrap unbindService() in try-catch for IllegalArgumentException
- Reset cached mLoginFlow in onNewIntent() to prevent stale values
- Remove custom get() on loadingStateFlow to avoid recreating wrapper
- Remove redundant e.printStackTrace() already logged via appLogWrapper
- Require dot in site URL validation to reject single-word inputs
- Make loading dialog dismissible by adding cancel discovery callback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add hideLoadingOverlay() to LoginActivity

Adds the inverse of showLoadingOverlay() so future code paths
can transition back from the loading state to the fragment UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Keep R8 full mode disabled to reduce migration risk

Re-add android.enableR8.fullMode=false to preserve the previous R8
behavior. Enabling full mode can be done as a separate follow-up
after verifying keep rules with a release build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add diagnostic error handling for application password login (#22702)

* Add diagnostic error handling for application password login failures

Surface detailed error messages when application password login fails,
instead of showing a generic toast or silently crashing. Catches
exceptions in SiteStore encryption/decryption paths and threads error
details through to the UI toast.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Send Sentry report on application password login failure

Every error path in the login flow now sends a crash report via
CrashLogging so we get visibility into failures in the wild.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add analytics tracking and crash logging for app password storing failures

Re-applies the reverted changes from #22694 that add trackStoringFailed()
calls with specific reason codes (empty_raw_data, empty_fetch_params,
fetch_sites_exception, site_changed_failed, bad_data, site_not_found)
and CrashLogging reports for better debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address code review: hide internal errors from toast, fix non-null assertion, add tests

- Show only user-friendly message in toast, keep detailed errors in logs/crash reports
- Replace site!! with safe ?: return@launch
- Fix import ordering (com.* before org.*)
- Add TODO comments on broad catch blocks to narrow once root cause identified
- Add CrashLogging mock and 5 new tests for error branches (SiteStore error,
  no rows affected, bad credentials, DB exception, crash report verification)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Refactor onSiteChanged to fix LongMethod detekt finding, remove TODOs

Split onSiteChanged into smaller focused methods to stay under the
60-line detekt limit: handleSiteChangedError, handleSiteChangedSuccess,
validateSiteChanged, and logAndEmitSiteChangedError. Also remove TODO
comments from SiteStore catch blocks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix double Sentry reporting, unreported storeCredentials exception, and error message leaks

- Remove crashLogging from ApplicationPasswordLoginHelper.trackStoringFailed
  so only the ViewModel's emitError sends Sentry reports with full context
- Add trackStoringFailed and crashLogging.sendReportWithTag to the
  storeCredentials catch block so KeyStore failures are tracked
- Replace technical error messages in NavigationActionData.errorMessage
  with opaque error codes (e.g. site_store_error, no_rows_affected)
- Use e.message ?: e.javaClass.simpleName in SiteStore catch blocks
  as fallback for exceptions without a message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Restore Sentry reports in helper for storeCredentials false-return paths

Add crashLogging.sendReportWithTag calls in storeApplicationPasswordCredentialsFrom
for the bad_data and site_not_found paths, which return false without throwing.
These are not duplicates of the ViewModel's emitError reports — they cover failures
where execution continues to fetchSites and the ViewModel never calls emitError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add commented-out forced error for testing Sentry reporting path

TODO: Remove before merging. Uncomment the throw line to verify
that the storeCredentials error path sends analytics and Sentry reports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Refactor helper to fix LongMethod detekt finding

Extract logAndReportBadData and logAndReportSiteNotFound from
storeApplicationPasswordCredentialsFrom to bring it under the
60-line detekt limit. Also extract reportStoringFailedToSentry
to deduplicate Sentry exception construction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove commented-out forced error used for testing Sentry reporting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: adalpari <adalpari@gmail.com>

* Bump io.sentry.android.gradle from 6.1.0 to 6.2.0 (#22708)

Bumps [io.sentry.android.gradle](https://github.com/getsentry/sentry-android-gradle-plugin) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/getsentry/sentry-android-gradle-plugin/releases)
- [Changelog](https://github.com/getsentry/sentry-android-gradle-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-android-gradle-plugin/compare/6.1.0...6.2.0)

---
updated-dependencies:
- dependency-name: io.sentry.android.gradle
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Activity Log: Show MCP agent metadata (#22706)

* Activity Log: Show MCP agent metadata in list and detail views

Display "via {client}" label (e.g. "via Claude") in the Activity Log
when an entry was performed by an MCP agent. Adds isMCPAgent and
mcpClient fields through the full data pipeline: API response parsing,
database persistence with migration, and UI rendering in both the
list and detail views.

Ref: AIINT-290

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt and lint issues in MCP agent metadata changes

Suppress LongParameterList for Actor class and extract hardcoded
dot separator to a string resource.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Refactor MCP metadata: extract shared logic, use uiHelpers, add tests

- Extract duplicated MCP formatting into ActivityActorExtensions
- Remove unnecessary intermediate variable in detail ViewModel
- Pass uiHelpers to EventItemViewHolder for consistent visibility handling
- Add unit tests for MCP metadata in both list and detail ViewModels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: adalpari <adalpari@gmail.com>

* Bump json from 2.18.1 to 2.19.2 (#22715)

Bumps [json](https://github.com/ruby/json) from 2.18.1 to 2.19.2.
- [Release notes](https://github.com/ruby/json/releases)
- [Changelog](https://github.com/ruby/json/blob/master/CHANGES.md)
- [Commits](https://github.com/ruby/json/compare/v2.18.1...v2.19.2)

---
updated-dependencies:
- dependency-name: json
  dependency-version: 2.19.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* RS Post Settings: Add search to author dialog (#22689)

* RS Post Settings: Add polish and error handling

Replace Toast with Compose Snackbar for inline error messages with retry
actions, add pull-to-refresh support, show "Saving..." label alongside
the spinner, display "Not Set" placeholder for empty status, and improve
accessibility with content descriptions for password toggle and featured
image placeholder. Extract shared error utilities from PostRsListViewModel
into PostRsErrorUtils for reuse across both list and settings screens.
Add unit tests for PostRsSettingsViewModel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix review issues from polish PR

- Preserve unsaved edits when refreshing post from server
- Remove unreachable snackbar when site is null (activity finishes immediately)
- Fix PullToRefreshBox content indentation
- Remove redundant offline mocks in refresh tests
- Rename misleading test name for status selection
- Add test for save-online sets isSaving

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix detekt and checkstyle issues

Extract preserveEdits() helper to shorten refreshPost() below the
60-line detekt limit, and remove empty line after opening brace in
test class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix snackbar navbar overlap and center error content

Add navigationBarsPadding to SnackbarHost so snackbars aren't
obscured by the system navigation bar. Wrap ErrorContent in a
centered Box so the error message displays in the center of the
screen instead of at the top.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Center error content on term selection screen

Add fillMaxWidth and TextAlign.Center to the ErrorContent on the
term selection screen so the network error message is properly
centered horizontally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Add retry action to save network error snackbar

The network error snackbar shown when saving in airplane mode was
missing an actionLabel and onAction callback, so no Retry button
appeared. Add both so the user can retry saving after reconnecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Use primary color for snackbar action button

Set the snackbar action color to MaterialTheme.colorScheme.primary
so the Retry text on snackbars matches the primary color used by
the Retry button on the error empty state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Remove pre-flight network check from save to avoid race condition

When disabling airplane mode and immediately tapping Retry, Android may not
have re-established connectivity yet, causing a spurious network error. Now
the save always proceeds to the API call, which handles network errors
naturally via its catch block using PostRsErrorUtils.friendlyErrorMessage().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Simplify code and remove status bar hiding

- Use BackHandler(onBack=) instead of wrapping lambda
- Use ?.let { } ?: Modifier for conditional click modifiers
- Replace PostApiException with RuntimeException
- Inline isAuthError wrapper in PostRsListViewModel
- Remove status bar hiding to fix PTR triggering on system bar pull

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Use named exception to satisfy detekt

Replace RuntimeException with PostApiRequestException to fix
TooGenericExceptionThrown detekt violations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Cache WpApiClient to avoid repeated instantiation

On self-hosted sites, a new WpApiClient was created for every API call
(5 instantiations just to open post settings). Cache self-hosted clients
in WpApiClientProvider (mirroring the existing WP.com pattern), add a
per-site client cache in PostRsRestClient, and use a lazy property in
PostRsSettingsViewModel to reuse the same client across fetch and save.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Remove dead parameters and redundant client cache

Now that WpApiClientProvider caches self-hosted clients, the per-site
client cache in PostRsRestClient is redundant — remove it. Also remove
the now-unused site parameters from fetchPost() and savePost() in the
ViewModel, and replace shadowed locals with simple null guards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix import ordering, replace crash with handled exception, remove unused methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix detekt ThrowsCount and ReturnCount violations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Replace loading spinner with shimmer skeleton

Show hero image shimmer and placeholder rows while loading instead
of a titled app bar with a centered spinner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Simplify code and remove redundant null checks

- Extract shared HeroOverlay composable to deduplicate gradient
  and back button between loading skeleton and hero layout
- Inline statusResId variable
- Remove unreachable site null guards in ViewModel
- Remove extra blank line in WpApiClientProvider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Add author search to author dialog

Add a debounced search field to the author selection dialog, matching
the existing pattern used for category/tag term search. Also fixes
author selection from index-based to ID-based to prevent incorrect
selection when the list changes during search or load-more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Simplify author dialog and deduplicate constant

Replace itemsIndexed with items in AuthorDialog since the index was
unused, and consolidate AUTHORS_PER_PAGE to a single constant in
PostRsRestClient.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Clear author search state when dialog closes

Reset the search query, search flag, and cached author list on
dismiss so reopening the dialog shows a fresh state instead of
stale search results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Remove redundant state clearing in onAuthorClicked

The authorSearchQuery and isSearchingAuthors fields are already
reset by onDismissDialog(), so clearing them again on open is
unnecessary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt issues: remove unused import and extract hideStatusBar()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Author search: reset pagination on dismiss and show error snackbar

Reset nextAuthorPageParams and canLoadMoreAuthors when the author
dialog is dismissed to prevent stale pagination state. Show a
snackbar when author search fails, consistent with other error
handling in the ViewModel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Author search: disable OK when no results and clear search on confirm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Merge release/26.7 into trunk (#22716)

* Bump version number

* Update WordPress metadata translations for 26.7

* Update Jetpack metadata translations for 26.7

* Bump org.mockito.kotlin:mockito-kotlin from 6.2.3 to 6.3.0 (#22718)

Bumps [org.mockito.kotlin:mockito-kotlin](https://github.com/mockito/mockito-kotlin) from 6.2.3 to 6.3.0.
- [Release notes](https://github.com/mockito/mockito-kotlin/releases)
- [Commits](https://github.com/mockito/mockito-kotlin/compare/v6.2.3...v6.3.0)

---
updated-dependencies:
- dependency-name: org.mockito.kotlin:mockito-kotlin
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* RS Post Settings: Use device timezone for date picker (#22704)

* RS Post Settings: Use device timezone for date picker

The date/time picker was using UTC for display and selection,
causing users to see times offset from their local timezone.
Switch to device timezone to match the old post settings behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Move UTC constant to its only usage site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Append timezone label to displayed date

Show the device timezone abbreviation (e.g., EST, PST) after the
formatted date so users know which timezone the date refers to.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1936: Add Insights tab to new stats screen (#22711)

* Update wordpress-rs to 1219-9cb722b47c

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Insights tab with Year in Review card to new stats screen

Populate the Insights tab with card management (add, remove, move)
mirroring the Traffic tab architecture. Add Year in Review as the
first Insights card, fetching yearly summaries (posts, words, likes,
comments) via the wordpress-rs statsInsights endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Restyle Year in Review card with 2x2 grid of mini stat cards

Update the card title to show "YEAR in review" instead of a generic
title. Replace the table layout with a 2x2 grid of mini cards, each
displaying an icon, label, and formatted value for Posts, Words,
Likes, and Comments, matching the web design.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Year in Review detail screen with View All functionality

Add a detail screen showing all years with full stats (posts, comments,
avg comments/post, likes, avg likes/post, words, avg words/post). The
card shows the most recent year and a "View all" link when multiple
years are available. Years are sorted newest first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Always show current year in Year in Review card and always display Show All

Ensure the current year is always present in the data, adding it with
zero values if not returned by the API. The Show All footer is now
always visible regardless of the number of years available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Minor cleanup: cache current year and remove redundant variable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix code review issues and add missing tests

- Use resource string for exception errors instead of raw e.message
- Add duplicate guard in addCard()
- Change Error from data class to class to fix lambda equality
- Use Year.now() per-call instead of cached static val
- Fix isValidConfiguration to check for null entries
- Remove 0dp Spacer from detail screen
- Add StatsFormatterTest with 12 tests
- Add repository moveCard and addCard duplicate tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix second round of review issues: stale success flag, race condition, siteId fallback, API type inconsistency

- Reset isLoadedSuccessfully on error/exception so loadDataIfNeeded recovers after failed refresh
- Add Mutex to InsightsCardsConfigurationRepository to prevent concurrent mutation races
- Guard siteId in InsightsViewModel mutations to avoid operating with invalid 0L
- Align fetchStatsInsights parameter from String to Long for interface consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update Year in Review labels to match web app

Card uses short labels (Words, Likes) without "Total" prefix.
Detail screen uses exact web labels: Total posts, Total comments,
Avg comments per post, Total likes, Avg likes per post, Total words,
Avg words per post.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1936: Add All-time Stats and Most Popular Day Insights cards (#22673)

* Update wordpress-rs library hash

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add stats summary data layer with shared caching

Add fetchStatsSummary endpoint and StatsSummaryData model to
StatsDataSource. Implement Mutex-based caching in StatsRepository
so All-time Stats and Most Popular Day cards share a single API call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add All-time Stats Insights card

New card showing Views, Visitors, Posts, and Comments from the
statsSummary endpoint. Uses rememberShimmerBrush for loading state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Most Popular Day Insights card

New card showing the best day for views with date, view count, and
percentage. Shares the statsSummary API call with All-time Stats via
repository caching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Wire All-time Stats and Most Popular Day into Insights tab

Add card types to InsightsCardType enum and default card list.
Wire ViewModels and cards into InsightsTabContent. Add config
migration to automatically show new card types for existing users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: locale, empty best day, config migration, tests

- Create DateTimeFormatter at format time instead of caching in
  companion object to respect locale changes
- Handle empty viewsBestDay by returning loaded state with empty
  values instead of throwing
- Fix config migration by persisting hiddenCards field so new card
  types can be distinguished from user-removed cards
- Add missing tests: empty viewsBestDay, zero views percentage,
  exception path, dayAndMonth assertion, addNewCardTypes migration,
  full config no-migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update RS library and fix insights locale, detekt, and date casing

- Update wordpress-rs to 1de57afce924622700bcc8d3a1f3ce893d8dad5b
- Add locale parameter to StatsInsightsParams matching other endpoints
- Fix detekt MagicNumber and TooGenericExceptionCaught violations
- Capitalize first letter of formatted date in Most Popular Day card

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move stats summary cache from StatsRepository to InsightsViewModel

StatsRepository was not @Singleton, so the shared cache for statsSummary
data was broken — each ViewModel got its own instance. Move caching to
InsightsViewModel which is activity-scoped and shared. Child ViewModels
now receive a summary provider function wired from the UI layer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix double-write and concurrency in InsightsCardsConfigurationRepository

addNewCardTypes wrote to prefs directly, then the caller wrote again via
saveConfiguration. Also getConfiguration was not mutex-protected. Make
addNewCardTypes pure, extract loadAndMigrate for atomic read-migrate-save
within the mutex, and make getConfiguration go through the mutex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename hiddenCards() to computeHiddenCards() to avoid naming conflict

InsightsCardsConfiguration had both a hiddenCards property (persisted
list) and a hiddenCards() method (computed from visibleCards). Rename
the method to make the distinction clear.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add NoData state for MostPopularDay and fix percentage precision

When a site has no best day data, show a dedicated NoData state with a
"No data yet" message instead of a blank Loaded card. Also reduce
percentage format from 3 to 1 decimal place for consistency with
typical stats UIs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress TooGenericExceptionThrown detekt warning in test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread-safety, DI design, and Error state in Insights cards

- Add @Volatile to isLoading/isLoadedSuccessfully flags in ViewModels
- Extract StatsSummaryUseCase singleton to replace summaryProvider var,
  removing temporal coupling and null-check error paths
- Change Error from class to data class, move onRetry out of state and
  into composable parameters to avoid unnecessary recompositions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Add Most Popular Time card and centralize Insights data fetching (#22684)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @Volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @Volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @Volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card to new stats screen (#22687)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @Volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @Volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @Volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card

Add a new top-list card to the Insights tab showing tags and categories
with view counts. Includes expandable multi-tag groups, percentage bars,
folder/tag icons, and a detail screen via Show All. Updates wordpress-rs
to 1230 for the statsTags endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add expand/collapse for multi-tag groups in detail screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for Tags & Categories feature

ViewModel, repository, and display type unit tests covering
success/error states, data mapping, refresh, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify Tags & Categories by reusing shared components

- Use StatsCardContainer, StatsCardHeader, StatsListHeader,
  StatsCardEmptyContent, StatsListRowContainer from StatsCardCommon
- Extract TagTypeIcon and ExpandedTagsSection into shared
  TagsAndCategoriesComponents to eliminate duplication between
  Card and DetailActivity
- Add fromTagType() to TagGroupDisplayType to avoid list allocation
  per tag in ExpandedTagsSection
- Add modifier parameter to StatsListRowContainer for clickable rows
- Remove duplicated constants (CardCornerRadius, BAR_BACKGROUND_ALPHA,
  VERTICAL_LINE_ALPHA)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused stubUnknownError from ViewModel test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: error recovery, conditional refresh, loading state

- Only set isLoaded on success so loadData() retries after errors
- Guard pull-to-refresh to skip tags refresh when card is hidden
- Move loading state into refresh() so callers don't need showLoading()
- Remove showLoading() public method
- Add test for loadData() retry after error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: deduplicate row composable, remove unused link field, fix thread safety and Intent size

- Extract shared TagGroupRow composable into TagsAndCategoriesComponents.kt with optional position parameter, removing duplicate from Card and DetailActivity
- Remove unused TagData.link field from data source, impl, and all tests
- Replace Intent extras with in-memory static holder in DetailActivity to avoid TransactionTooLargeException risk
- Remove unnecessary Parcelable from UI models
- Use AtomicBoolean for isLoaded/isLoading flags in ViewModel for thread safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix concurrent refresh, process death, isExpandable duplication, and accessibility

- Cancel in-flight fetch job on refresh() to prevent stale overwrites
- Finish detail activity on process death instead of showing blank screen
- Extract TagGroupUiItem.isExpandable computed property to deduplicate logic
- Add content descriptions for TagTypeIcon and expand/collapse chevron icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update configuration tests to include TAGS_AND_CATEGORIES card type

Fixes CI test failures caused by the new TAGS_AND_CATEGORIES card
not being reflected in test fixtures and assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1952: stats insights: tags card and some fixes (#22701)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @Volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @Volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @Volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card

Add a new top-list card to the Insights tab showing tags and categories
with view counts. Includes expandable multi-tag groups, percentage bars,
folder/tag icons, and a detail screen via Show All. Updates wordpress-rs
to 1230 for the statsTags endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add expand/collapse for multi-tag groups in detail screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for Tags & Categories feature

ViewModel, repository, and display type unit tests covering
success/error states, data mapping, refresh, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify Tags & Categories by reusing shared components

- Use StatsCardContainer, StatsCardHeader, StatsListHeader,
  StatsCardEmptyContent, StatsListRowContainer from StatsCardCommon
- Extract TagTypeIcon and ExpandedTagsSection into shared
  TagsAndCategoriesComponents to eliminate duplication between
  Card and DetailActivity
- Add fromTagType() to TagGroupDisplayType to avoid list allocation
  per tag in ExpandedTagsSection
- Add modifier parameter to StatsListRowContainer for clickable rows
- Remove duplicated constants (CardCornerRadius, BAR_BACKGROUND_ALPHA,
  VERTICAL_LINE_ALPHA)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused stubUnknownError from ViewModel test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: error recovery, conditional refresh, loading state

- Only set isLoaded on success so loadData() retries after errors
- Guard pull-to-refresh to skip tags refresh when card is hidden
- Move loading state into refresh() so callers don't need showLoading()
- Remove showLoading() public method
- Add test for loadData() retry after error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: deduplicate row composable, remove unused link field, fix thread safety and Intent size

- Extract shared TagGroupRow composable into TagsAndCategoriesComponents.kt with optional position parameter, removing duplicate from Card and DetailActivity
- Remove unused TagData.link field from data source, impl, and all tests
- Replace Intent extras with in-memory static holder in DetailActivity to avoid TransactionTooLargeException risk
- Remove unnecessary Parcelable from UI models
- Use AtomicBoolean for isLoaded/isLoading flags in ViewModel for thread safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix concurrent refresh, process death, isExpandable duplication, and accessibility

- Cancel in-flight fetch job on refresh() to prevent stale overwrites
- Finish detail activity on process death instead of showing blank screen
- Extract TagGroupUiItem.isExpandable computed property to deduplicate logic
- Add content descriptions for TagTypeIcon and expand/collapse chevron icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update configuration tests to include TAGS_AND_CATEGORIES card type

Fixes CI test failures caused by the new TAGS_AND_CATEGORIES card
not being reflected in test fixtures and assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fetch tags data independently in detail screen instead of using static field

The detail screen now has its own ViewModel that fetches up to 100
items directly from the API, while the card continues to fetch 10.
This removes the static data holder pattern that was prone to data
loss on process death.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract shared mapper, add detail VM tests, guard double calls, show all 10 card items

- Extract TagsAndCategoriesMapper to deduplicate TagGroupData to
  TagGroupUiItem mapping between card and detail ViewModels
- Add unit tests for TagsAndCategoriesDetailViewModel
- Add isLoaded/isLoading guards to detail VM to prevent double fetches
- Remove CARD_MAX_ITEMS limit so card displays all 10 fetched items
- Remove unused import in DetailActivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Skip data fetching for hidden Insights cards

Only call summary/insights endpoints when visible cards need them,
avoiding unnecessary network calls for hidden cards. Track which
endpoint groups have been fetched so re-adding a hidden card
triggers a fetch for its missing data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Early return in fetchData when no endpoints are needed

Prevents setting isDataLoaded=true when no cards require
fetching, which would block future fetches when cards are
re-added to the visible list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address code review findings: reduce duplication and improve tests

- Replace duplicate shimmer animation in YearInReviewCard with
  shared rememberShimmerBrush() utility
- Extract StatsTagsUseCase to centralize token validation and
  repository init, removing duplication between Tags ViewModels
- Add card reordering tests for middle elements in
  InsightsCardsConfigurationRepositoryTest
- Fix locale-dependent assertions in MostPopularDayViewModelTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress LargeClass detekt warning on InsightsViewModelTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix process-death restore and add caching to StatsTagsUseCase

- Call loadData() unconditionally in TagsAndCategoriesDetailActivity
  onCreate to handle process-death restore (loadData guard prevents
  double fetch on rotation)
- Add Mutex-protected in-memory cache to StatsTagsUseCase, consistent
  with StatsSummaryUseCase and StatsInsightsUseCase
- Pass forceRefresh=true on pull-to-refresh in TagsAndCategoriesViewModel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract BaseTagsAndCategoriesViewModel and use localized error messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety and error handling in ViewModels

Use AtomicBoolean with compareAndSet in InsightsViewModel to prevent
race conditions, rethrow CancellationException in base tags ViewModel,
and only mark endpoints as fetched on success results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract isCacheHit method to fix detekt ComplexCondition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Cancel in-flight fetch job before refreshing in InsightsViewModel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix compilation errors after trunk merge

Update wordpress-rs to version with stats tags types and remove
duplicate NoConnectionContent composable and redundant else branches
that caused -Werror failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address code review findings and add base ViewModel tests

- Move network call outside mutex in StatsTagsUseCase to
  avoid blocking concurrent callers during slow requests
- Add main-thread-confinement comments for fetchJob fields
- Document why TAGS_AND_CATEGORIES is absent from
  needsSummary/needsInsights (uses its own fetch path)
- Restore card max items to 7 (was unintentionally changed
  to 10 during refactoring)
- Add tests for BaseTagsAndCategoriesViewModel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix detekt findings: suppress ReturnCount, remove unused composable

- Suppress ReturnCount on StatsTagsUseCase.invoke (3 returns are
  clearer than restructuring with nested conditions)
- Remove unused PlaceholderTabContent composable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix TOCTOU race, config fetch trigger, and detail empty state

- StatsTagsUseCase: use in-flight Deferred to coalesce concurrent
  requests with the same params, eliminating the TOCTOU race where
  two callers could both miss cache and fire duplicate requests
- BaseTagsAndCategoriesViewModel: make isLoaded private since no
  subclass accesses it directly
- InsightsViewModel: trigger loadDataIfNeeded() from
  updateFromConfiguration when new endpoints are required, instead
  of relying on the UI to call it
- TagsAndCategoriesDetailActivity: add empty-state message when
  the loaded items list is empty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Deduplicate detail VM tests, fix Mockito import, add cancellation test

- Remove duplicate tests from TagsAndCategoriesDetailViewModelTest
  that are already covered by BaseTagsAndCategoriesViewModelTest;
  keep only initial-state and maxItems-specific tests
- Replace fully-qualified org.mockito.Mockito.times() with the
  imported mockito-kotlin times() throughout InsightsViewModelTest
- Add test verifying that rapid double-refresh cancels the first
  job so only one forceRefresh call completes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Clear stats caches on screen open to prevent stale data

Add clearCache() to StatsInsightsUseCase, StatsSummaryUseCase, and
StatsTagsUseCase. Call all three from InsightsViewModel init so that
reopening the insights screen always fetches fresh data while still
sharing results between cards within a single session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update wordpress-rs to trunk-262a778ead5f163f3450d62adfac21fb32048714

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify: fix StatsTagsUseCase error handling, deduplicate bottom sheets

- Fix critical bug in StatsTagsUseCase where an exception from fetchTags()
  would leave the CompletableDeferred incomplete, hanging all awaiters
- Extract generic AddCardBottomSheet<T> to replace three duplicate
  implementations (Stats, Insights, Subscribers)
- Merge duplicate LaunchedEffect(cardsToLoad) blocks in InsightsTabContent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix PR #22711 review issues: race conditions, code quality, API compat

- Fix race condition in InsightsViewModel.refreshData() by nulling
  fetchJob before cancel and guarding finally block for current job
- Fix StatsTagsUseCase cancellation propagation: complete deferred
  with error instead of completeExceptionally for CancellationException
- Remove redundant hiddenCards constructor param, make it a computed
  property; use PersistedConfig for backward-compatible JSON migration
- Key expandedGroups on items in TagsAndCategoriesCard and detail
  activity so expansion state resets on data refresh
- Use StatsCardContainer in AllTimeStatsCard and MostPopularDayCard
  instead of duplicating border/clip/background pattern
- Replace Year.now() (API 26+) with Calendar in YearInReviewViewModel
- Move @Suppress off inline catch to function level in StatsTagsUseCase
- Fix fully qualified references in repository test (import never, Gson)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix insights tab bugs: cancellation, race conditions, perf, and quality

- Fix StatsTagsUseCase cancellation propagation (deferred.cancel instead of error)
- Fix InsightsViewModel card-add race by cancelling in-flight fetch on config change
- Release mutex during network calls in StatsSummaryUseCase and StatsInsightsUseCase
- Replace Calendar with java.time.Year in YearInReviewViewModel
- Add NoData state for empty tags response instead of empty Loaded card
- Show period selector only on Traffic tab, not Insights
- Add accessibility onClickLabel to AddCardItem bottom sheet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Suppress ReturnCount detekt warning in use cases

The mutex refactor introduced a third return path (guard clause
pattern) which is idiomatic and readable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix retry, error handling, race condition, and localization bugs

- Reset isLoaded on exception in BaseTagsAndCategoriesViewModel so retry
  is not a no-op after a failed load
- Complete deferred with TagsResult.Error instead of completeExceptionally
  in StatsTagsUseCase so non-owner callers get a proper result
- Replace raw English error strings with localized R.string.stats_error_unknown
  in InsightsViewModel via ResourceProvider
- Fix updateFromConfiguration race window by cancelling job first and calling
  fetchData() directly with isDataLoading already set to true

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Deduplicate error UI, remove raw Context, fix Intent data passing, add logging

- Replace duplicate ErrorContent composables in AllTimeStatsCard and
  MostPopularDayCard with shared StatsCardErrorContent
- Replace raw Context injection in MostPopularTimeViewModel with
  DateFormatWrapper for better testability
- Refactor YearInReviewDetailActivity to fetch data via ViewModel and
  StatsInsightsUseCase instea…
nbradbury added a commit that referenced this pull request Mar 24, 2026
* Upgrade Gradle 9.1.0 + AGP 9.0.1

Simultaneously upgrade Gradle (8.12.1 → 9.1.0) and AGP (8.10.1 → 9.0.1)
since AGP 8.x does not support Gradle 9.x.

Uses opt-out flags (android.newDsl=false, android.builtInKotlin=false) to
preserve existing kotlin-android/kapt plugins while on AGP 9.

Version bumps:
- Gradle 8.12.1 → 9.1.0
- AGP 8.10.1 → 9.0.1
- Dagger/Hilt 2.58 → 2.59.2
- Fladle 0.19.0 → 0.21.0

AGP 9 migration fixes:
- Remove archivesBaseName from defaultConfig (use base.archivesName)
- Move compileSdk from defaultConfig to android block
- Replace proguard-android.txt with proguard-android-optimize.txt
- Rename lintOptions → lint in root build.gradle
- Replace deprecated buildDir with layout.buildDirectory
- Remove obsolete android.useAndroidX and android.enableJetifier properties
- Fix kotlin {} → java {} for sourceCompatibility in JVM-only modules
- Remove allowBackup from posttypes library manifest (app-level concern)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Survive dialog selection across rotation (#22699)

Use rememberSaveable instead of remember for the selected index
in StatusDialog, FormatDialog, and AuthorDialog so the user's
choice persists through configuration changes.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Remove Login Module (#22564)

* Remove LoginEmailPasswordFragment

* Remove Login Magic Link Fragments

* Remove username/password WP.com flow

* Remove Google Login

* Remove LoginEmailFragment

* Remove Login 2FA fragment

* Remove SignupMagicLinkFragment

* Remove orphaned files

* Remove the “Find your site address” button

* Remove SignupEpilogueFragment

* Remove native account signup code

* Remove `PostSignupInterstitialActivity`

* Remove SmartLock

* Remove `WOO_LOGIN_MODE`

* Don’t show the username/password fields for Application Password sites

* In the Me tab, start WP.com login directly

* Fix strings

* Fix WP.com login UI

* Improve the WP.org login device name

* Add self-hosted site reauthentication

* Display more detailed error messages

* Fix WP.com sharing login flow

* Fix share flow login

* Remove `LoginMode.JETPACK_SELFHOSTED`

* Move resources out of login module into app

* Flatten theme heirarchies

* Modernize login prologue

* Fix edge-to-edge for login

* Automatically load notifications when switching to that tab

* Fix Application Password login focus

* Remove login module from CI

* Remove unused styles

* Add nullable annotation

* Remove deprecated Smart Lock Credentials API and play-services-auth

The only usage of play-services-auth was the deprecated Smart Lock for
Passwords (Credentials API) in AppInitializer, which connected a
GoogleApiClient on startup and called disableAutoSignIn on logout.
play-services-fido was only present as a transitive dependency of
play-services-auth. Neither is needed anymore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove trailing comma in localization.rb array

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix trailing backslash in run-unit-tests.sh

Removing :libs:login:koverXmlReportDebug left a trailing backslash on
the previous line, causing TESTS_EXIT_STATUS=$? to be passed as a
gradle task argument instead of being a variable assignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused imports left over from login lib removal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress detekt LongMethod for application password error handling

These methods are long due to exhaustive error handling across multiple
WpRequestResult variants, which is reasonable to suppress.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Delete unused bg_oval_translucent_stroke_3dp drawable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove references to deleted signup epilogue analytics constants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix constructor mismatches in application password test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused login prologue resources

Delete login prologue drawables, colors, dimens, strings, and menu
resources that became unused after the login library removal. Also
update TrainOfIcons preview to use app_icon instead of deleted drawables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Restore login_prologue_revamped colors used by wordpress source set

These colors are referenced by LoginPrologueRevampedFragment.kt and
Tagline.kt in src/wordpress/, so they must not be removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove remaining unused resources from login lib removal

Delete unused palette colors (purple_10, purple_20, purple_60, pink_10,
orange_10, green_60, celadon_30), login prologue dimens, indicator
drawables, promo illustration PNGs, PromoTitle style, and orphaned
LoginSiteAddressValidatorTest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move login prologue revamped colors to wordpress source set

These colors are only used by WordPress-flavor code in src/wordpress/,
so they belong in src/wordpress/res/ rather than shared src/main/res/.
This fixes the Jetpack lint check which correctly flagged them as unused.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix dark/light mode switching issues

* Add a release note

* Remove unused View import in LoginActivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove stale Google Login warnings from build scripts

Google Login no longer exists in the app, so these warnings
about it not working are no longer relevant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use bodyLarge text style for site address label

The headlineSmall style competes with the TopAppBar title.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Make login form scrollable for landscape support

Flatten the two-column layout into a single scrollable column
so the text field and button don't get crushed when the keyboard
opens on small devices in landscape mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ANR risk and error handling in WP.com OAuth login

- Replace runBlocking with async coroutine scope to avoid blocking
  the main thread during token exchange (Critical #1)
- Use dedicated CoroutineScope instead of Dispatchers.IO so
  dispose() only cancels this helper's coroutines (Critical #2)
- Propagate login errors to the caller via Consumer<Exception>
  instead of swallowing them
- Show error state on the loading screen with a retry button
  instead of silently failing
- Cancel coroutine scope in LoginActivity.onDestroy()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Handle null and unknown values in LoginFlow.fromIntent()

getStringExtra() can return null even when hasExtra() is true,
and valueOf() throws for old enum names that no longer exist.
Fall back to PROLOGUE safely in both cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Connect site comparison logic after OAuth login

After fetching sites post-OAuth login, route back through
loggedInAndFinish() so FINISH_WITH_SITE flows correctly
return the newly added site ID to the calling activity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Persist login flow state in SharedPreferences instead of static fields

Static fields don't survive process death and can cause issues in
multi-window mode. Move sPendingLoginFlow and sIsShareFlowPending to
AppPrefs with dedicated getter/setter methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add system bar transparency to login night theme

The light theme had transparent status/navigation bars but the night
theme was missing these attributes, causing opaque system bars in dark
mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add @Singleton to LoginAnalyticsTracker provider method

The class had @Singleton but the Dagger provider didn't, so a new
instance was created on every injection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Call startPostLoginServices and remove dead stub methods

startPostLoginServices was defined but never called, so Reader tags
and notification services weren't started after login. Also removes
unused startOver and gotConnectedSiteInfo methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove leftover debug Log.e in WPMainActivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove plaintext username from debug logs

The hasApiRestUsername boolean check is sufficient for debugging
without exposing the actual credential value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Replace deprecated onActivityCreated with onViewCreated

Move activity title setup into onViewCreated, removing the suppressed
deprecation warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Align ARG_LOGIN_FLOW constant name with its string value

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Unbind CustomTabsServiceConnection on dispose

The service was bound but never unbound, causing a minor resource
leak. Now unbinds when the helper is disposed in onDestroy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use Channel instead of SharedFlow for discovery URL event

Channel guarantees the event is buffered and consumed exactly once,
even if the collector starts slightly after emission. Safer than
SharedFlow(replay=0) for one-shot navigation events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress overdraw lint warning on login loading layout

The window background from LoginTheme triggers a false positive
overdraw warning since the ConstraintLayout itself has no explicit
background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tools:keep for flavor-specific login prologue colors

Android Lint can't trace cross-flavor resource usage, so it flags
these colors as unused. They're referenced by Compose code in the
wordpress flavor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Redirect WP.com site addresses to OAuth login flow

When a user enters a WordPress.com site address in the site address
field, redirect them to the WP.com OAuth flow instead of proceeding
with application password authorization. This ensures WP.com account
state is properly established so the Me tab shows the user's profile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove old login lib references from e2e tests

The LoginFlow e2e helper imported resources from the deleted login
library and referenced views (login_username_row, login_password_row)
that no longer exist. Remove the dead enterUsernameAndPassword and
enterSiteAddress methods since login forms are now Compose-based and
WP.com login uses OAuth via Custom Tabs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Call onFailure when OAuth callback is missing code parameter

Previously tryLoginWithDataString silently returned if the code
query parameter was absent, leaving the caller on a loading screen
indefinitely. Now reports the error via the onFailure callback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt violations and CI unit test task name

- Remove unused AppLog imports from WPcomLoginHelper
- Replace generic Exception catch with Result.fold to satisfy detekt
- Fix kover task name to match trunk's flavor rename (no more Wasabi)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Persist post-OAuth loading state across configuration changes

mIsWaitingForSitesToLoad and mOldSitesIdsForLoginUpdate were not
saved in onSaveInstanceState, so a device rotation during the
post-OAuth account/sites fetch would reset the flags and leave
the user stuck on the loading screen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix share flow race condition in LoginActivity onResume

Gate the onResume completion check on a new mShareFlowLoginLaunched
flag so LoginActivity doesn't immediately finish with RESULT_OK when
the user already has sites but hasn't completed the share-flow login.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Nick Bradbury <nick.bradbury@gmail.com>

* Remove AGP 9 opt-out flags and adopt built-in Kotlin

Remove android.newDsl=false and android.builtInKotlin=false to fully
adopt AGP 9's new DSL and built-in Kotlin compilation support.

- Remove kotlin-android plugin from all modules (AGP 9 built-in)
- Migrate kapt → legacy-kapt in fluxc (AGP 9 requirement)
- Migrate applicationVariants API → androidComponents.onVariants
- Remove kotlin-android and kapt entries from version catalog
- Keep kotlin-compose plugin (still required for Compose compiler)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove obsolete AGP 9 gradle.properties flags

Remove android.nonTransitiveRClass and android.nonFinalResIds (now
always enabled in AGP 9) and android.enableR8.fullMode=false to adopt
the new default of full R8 mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix issues introduced by login lib removal (#22703)

* Fix issues introduced by login lib removal

- Use single root layout with loading overlay in LoginActivity to avoid
  multiple setContentView() calls that orphan fragments
- Wrap unbindService() in try-catch for IllegalArgumentException
- Reset cached mLoginFlow in onNewIntent() to prevent stale values
- Remove custom get() on loadingStateFlow to avoid recreating wrapper
- Remove redundant e.printStackTrace() already logged via appLogWrapper
- Require dot in site URL validation to reject single-word inputs
- Make loading dialog dismissible by adding cancel discovery callback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add hideLoadingOverlay() to LoginActivity

Adds the inverse of showLoadingOverlay() so future code paths
can transition back from the loading state to the fragment UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Keep R8 full mode disabled to reduce migration risk

Re-add android.enableR8.fullMode=false to preserve the previous R8
behavior. Enabling full mode can be done as a separate follow-up
after verifying keep rules with a release build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add diagnostic error handling for application password login (#22702)

* Add diagnostic error handling for application password login failures

Surface detailed error messages when application password login fails,
instead of showing a generic toast or silently crashing. Catches
exceptions in SiteStore encryption/decryption paths and threads error
details through to the UI toast.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Send Sentry report on application password login failure

Every error path in the login flow now sends a crash report via
CrashLogging so we get visibility into failures in the wild.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add analytics tracking and crash logging for app password storing failures

Re-applies the reverted changes from #22694 that add trackStoringFailed()
calls with specific reason codes (empty_raw_data, empty_fetch_params,
fetch_sites_exception, site_changed_failed, bad_data, site_not_found)
and CrashLogging reports for better debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address code review: hide internal errors from toast, fix non-null assertion, add tests

- Show only user-friendly message in toast, keep detailed errors in logs/crash reports
- Replace site!! with safe ?: return@launch
- Fix import ordering (com.* before org.*)
- Add TODO comments on broad catch blocks to narrow once root cause identified
- Add CrashLogging mock and 5 new tests for error branches (SiteStore error,
  no rows affected, bad credentials, DB exception, crash report verification)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Refactor onSiteChanged to fix LongMethod detekt finding, remove TODOs

Split onSiteChanged into smaller focused methods to stay under the
60-line detekt limit: handleSiteChangedError, handleSiteChangedSuccess,
validateSiteChanged, and logAndEmitSiteChangedError. Also remove TODO
comments from SiteStore catch blocks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix double Sentry reporting, unreported storeCredentials exception, and error message leaks

- Remove crashLogging from ApplicationPasswordLoginHelper.trackStoringFailed
  so only the ViewModel's emitError sends Sentry reports with full context
- Add trackStoringFailed and crashLogging.sendReportWithTag to the
  storeCredentials catch block so KeyStore failures are tracked
- Replace technical error messages in NavigationActionData.errorMessage
  with opaque error codes (e.g. site_store_error, no_rows_affected)
- Use e.message ?: e.javaClass.simpleName in SiteStore catch blocks
  as fallback for exceptions without a message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Restore Sentry reports in helper for storeCredentials false-return paths

Add crashLogging.sendReportWithTag calls in storeApplicationPasswordCredentialsFrom
for the bad_data and site_not_found paths, which return false without throwing.
These are not duplicates of the ViewModel's emitError reports — they cover failures
where execution continues to fetchSites and the ViewModel never calls emitError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add commented-out forced error for testing Sentry reporting path

TODO: Remove before merging. Uncomment the throw line to verify
that the storeCredentials error path sends analytics and Sentry reports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Refactor helper to fix LongMethod detekt finding

Extract logAndReportBadData and logAndReportSiteNotFound from
storeApplicationPasswordCredentialsFrom to bring it under the
60-line detekt limit. Also extract reportStoringFailedToSentry
to deduplicate Sentry exception construction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove commented-out forced error used for testing Sentry reporting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: adalpari <adalpari@gmail.com>

* Bump io.sentry.android.gradle from 6.1.0 to 6.2.0 (#22708)

Bumps [io.sentry.android.gradle](https://github.com/getsentry/sentry-android-gradle-plugin) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/getsentry/sentry-android-gradle-plugin/releases)
- [Changelog](https://github.com/getsentry/sentry-android-gradle-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-android-gradle-plugin/compare/6.1.0...6.2.0)

---
updated-dependencies:
- dependency-name: io.sentry.android.gradle
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Activity Log: Show MCP agent metadata (#22706)

* Activity Log: Show MCP agent metadata in list and detail views

Display "via {client}" label (e.g. "via Claude") in the Activity Log
when an entry was performed by an MCP agent. Adds isMCPAgent and
mcpClient fields through the full data pipeline: API response parsing,
database persistence with migration, and UI rendering in both the
list and detail views.

Ref: AIINT-290

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt and lint issues in MCP agent metadata changes

Suppress LongParameterList for Actor class and extract hardcoded
dot separator to a string resource.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Refactor MCP metadata: extract shared logic, use uiHelpers, add tests

- Extract duplicated MCP formatting into ActivityActorExtensions
- Remove unnecessary intermediate variable in detail ViewModel
- Pass uiHelpers to EventItemViewHolder for consistent visibility handling
- Add unit tests for MCP metadata in both list and detail ViewModels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: adalpari <adalpari@gmail.com>

* Bump json from 2.18.1 to 2.19.2 (#22715)

Bumps [json](https://github.com/ruby/json) from 2.18.1 to 2.19.2.
- [Release notes](https://github.com/ruby/json/releases)
- [Changelog](https://github.com/ruby/json/blob/master/CHANGES.md)
- [Commits](https://github.com/ruby/json/compare/v2.18.1...v2.19.2)

---
updated-dependencies:
- dependency-name: json
  dependency-version: 2.19.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* RS Post Settings: Add search to author dialog (#22689)

* RS Post Settings: Add polish and error handling

Replace Toast with Compose Snackbar for inline error messages with retry
actions, add pull-to-refresh support, show "Saving..." label alongside
the spinner, display "Not Set" placeholder for empty status, and improve
accessibility with content descriptions for password toggle and featured
image placeholder. Extract shared error utilities from PostRsListViewModel
into PostRsErrorUtils for reuse across both list and settings screens.
Add unit tests for PostRsSettingsViewModel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix review issues from polish PR

- Preserve unsaved edits when refreshing post from server
- Remove unreachable snackbar when site is null (activity finishes immediately)
- Fix PullToRefreshBox content indentation
- Remove redundant offline mocks in refresh tests
- Rename misleading test name for status selection
- Add test for save-online sets isSaving

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix detekt and checkstyle issues

Extract preserveEdits() helper to shorten refreshPost() below the
60-line detekt limit, and remove empty line after opening brace in
test class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix snackbar navbar overlap and center error content

Add navigationBarsPadding to SnackbarHost so snackbars aren't
obscured by the system navigation bar. Wrap ErrorContent in a
centered Box so the error message displays in the center of the
screen instead of at the top.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Center error content on term selection screen

Add fillMaxWidth and TextAlign.Center to the ErrorContent on the
term selection screen so the network error message is properly
centered horizontally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Add retry action to save network error snackbar

The network error snackbar shown when saving in airplane mode was
missing an actionLabel and onAction callback, so no Retry button
appeared. Add both so the user can retry saving after reconnecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Use primary color for snackbar action button

Set the snackbar action color to MaterialTheme.colorScheme.primary
so the Retry text on snackbars matches the primary color used by
the Retry button on the error empty state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Remove pre-flight network check from save to avoid race condition

When disabling airplane mode and immediately tapping Retry, Android may not
have re-established connectivity yet, causing a spurious network error. Now
the save always proceeds to the API call, which handles network errors
naturally via its catch block using PostRsErrorUtils.friendlyErrorMessage().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Simplify code and remove status bar hiding

- Use BackHandler(onBack=) instead of wrapping lambda
- Use ?.let { } ?: Modifier for conditional click modifiers
- Replace PostApiException with RuntimeException
- Inline isAuthError wrapper in PostRsListViewModel
- Remove status bar hiding to fix PTR triggering on system bar pull

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Use named exception to satisfy detekt

Replace RuntimeException with PostApiRequestException to fix
TooGenericExceptionThrown detekt violations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Cache WpApiClient to avoid repeated instantiation

On self-hosted sites, a new WpApiClient was created for every API call
(5 instantiations just to open post settings). Cache self-hosted clients
in WpApiClientProvider (mirroring the existing WP.com pattern), add a
per-site client cache in PostRsRestClient, and use a lazy property in
PostRsSettingsViewModel to reuse the same client across fetch and save.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Remove dead parameters and redundant client cache

Now that WpApiClientProvider caches self-hosted clients, the per-site
client cache in PostRsRestClient is redundant — remove it. Also remove
the now-unused site parameters from fetchPost() and savePost() in the
ViewModel, and replace shadowed locals with simple null guards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix import ordering, replace crash with handled exception, remove unused methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix detekt ThrowsCount and ReturnCount violations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Replace loading spinner with shimmer skeleton

Show hero image shimmer and placeholder rows while loading instead
of a titled app bar with a centered spinner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Simplify code and remove redundant null checks

- Extract shared HeroOverlay composable to deduplicate gradient
  and back button between loading skeleton and hero layout
- Inline statusResId variable
- Remove unreachable site null guards in ViewModel
- Remove extra blank line in WpApiClientProvider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Add author search to author dialog

Add a debounced search field to the author selection dialog, matching
the existing pattern used for category/tag term search. Also fixes
author selection from index-based to ID-based to prevent incorrect
selection when the list changes during search or load-more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Simplify author dialog and deduplicate constant

Replace itemsIndexed with items in AuthorDialog since the index was
unused, and consolidate AUTHORS_PER_PAGE to a single constant in
PostRsRestClient.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Clear author search state when dialog closes

Reset the search query, search flag, and cached author list on
dismiss so reopening the dialog shows a fresh state instead of
stale search results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Remove redundant state clearing in onAuthorClicked

The authorSearchQuery and isSearchingAuthors fields are already
reset by onDismissDialog(), so clearing them again on open is
unnecessary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt issues: remove unused import and extract hideStatusBar()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Author search: reset pagination on dismiss and show error snackbar

Reset nextAuthorPageParams and canLoadMoreAuthors when the author
dialog is dismissed to prevent stale pagination state. Show a
snackbar when author search fails, consistent with other error
handling in the ViewModel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Author search: disable OK when no results and clear search on confirm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Merge release/26.7 into trunk (#22716)

* Bump version number

* Update WordPress metadata translations for 26.7

* Update Jetpack metadata translations for 26.7

* Bump org.mockito.kotlin:mockito-kotlin from 6.2.3 to 6.3.0 (#22718)

Bumps [org.mockito.kotlin:mockito-kotlin](https://github.com/mockito/mockito-kotlin) from 6.2.3 to 6.3.0.
- [Release notes](https://github.com/mockito/mockito-kotlin/releases)
- [Commits](https://github.com/mockito/mockito-kotlin/compare/v6.2.3...v6.3.0)

---
updated-dependencies:
- dependency-name: org.mockito.kotlin:mockito-kotlin
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* RS Post Settings: Use device timezone for date picker (#22704)

* RS Post Settings: Use device timezone for date picker

The date/time picker was using UTC for display and selection,
causing users to see times offset from their local timezone.
Switch to device timezone to match the old post settings behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Move UTC constant to its only usage site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Append timezone label to displayed date

Show the device timezone abbreviation (e.g., EST, PST) after the
formatted date so users know which timezone the date refers to.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1936: Add Insights tab to new stats screen (#22711)

* Update wordpress-rs to 1219-9cb722b47c

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Insights tab with Year in Review card to new stats screen

Populate the Insights tab with card management (add, remove, move)
mirroring the Traffic tab architecture. Add Year in Review as the
first Insights card, fetching yearly summaries (posts, words, likes,
comments) via the wordpress-rs statsInsights endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Restyle Year in Review card with 2x2 grid of mini stat cards

Update the card title to show "YEAR in review" instead of a generic
title. Replace the table layout with a 2x2 grid of mini cards, each
displaying an icon, label, and formatted value for Posts, Words,
Likes, and Comments, matching the web design.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Year in Review detail screen with View All functionality

Add a detail screen showing all years with full stats (posts, comments,
avg comments/post, likes, avg likes/post, words, avg words/post). The
card shows the most recent year and a "View all" link when multiple
years are available. Years are sorted newest first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Always show current year in Year in Review card and always display Show All

Ensure the current year is always present in the data, adding it with
zero values if not returned by the API. The Show All footer is now
always visible regardless of the number of years available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Minor cleanup: cache current year and remove redundant variable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix code review issues and add missing tests

- Use resource string for exception errors instead of raw e.message
- Add duplicate guard in addCard()
- Change Error from data class to class to fix lambda equality
- Use Year.now() per-call instead of cached static val
- Fix isValidConfiguration to check for null entries
- Remove 0dp Spacer from detail screen
- Add StatsFormatterTest with 12 tests
- Add repository moveCard and addCard duplicate tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix second round of review issues: stale success flag, race condition, siteId fallback, API type inconsistency

- Reset isLoadedSuccessfully on error/exception so loadDataIfNeeded recovers after failed refresh
- Add Mutex to InsightsCardsConfigurationRepository to prevent concurrent mutation races
- Guard siteId in InsightsViewModel mutations to avoid operating with invalid 0L
- Align fetchStatsInsights parameter from String to Long for interface consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update Year in Review labels to match web app

Card uses short labels (Words, Likes) without "Total" prefix.
Detail screen uses exact web labels: Total posts, Total comments,
Avg comments per post, Total likes, Avg likes per post, Total words,
Avg words per post.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1936: Add All-time Stats and Most Popular Day Insights cards (#22673)

* Update wordpress-rs library hash

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add stats summary data layer with shared caching

Add fetchStatsSummary endpoint and StatsSummaryData model to
StatsDataSource. Implement Mutex-based caching in StatsRepository
so All-time Stats and Most Popular Day cards share a single API call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add All-time Stats Insights card

New card showing Views, Visitors, Posts, and Comments from the
statsSummary endpoint. Uses rememberShimmerBrush for loading state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Most Popular Day Insights card

New card showing the best day for views with date, view count, and
percentage. Shares the statsSummary API call with All-time Stats via
repository caching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Wire All-time Stats and Most Popular Day into Insights tab

Add card types to InsightsCardType enum and default card list.
Wire ViewModels and cards into InsightsTabContent. Add config
migration to automatically show new card types for existing users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: locale, empty best day, config migration, tests

- Create DateTimeFormatter at format time instead of caching in
  companion object to respect locale changes
- Handle empty viewsBestDay by returning loaded state with empty
  values instead of throwing
- Fix config migration by persisting hiddenCards field so new card
  types can be distinguished from user-removed cards
- Add missing tests: empty viewsBestDay, zero views percentage,
  exception path, dayAndMonth assertion, addNewCardTypes migration,
  full config no-migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update RS library and fix insights locale, detekt, and date casing

- Update wordpress-rs to 1de57afce924622700bcc8d3a1f3ce893d8dad5b
- Add locale parameter to StatsInsightsParams matching other endpoints
- Fix detekt MagicNumber and TooGenericExceptionCaught violations
- Capitalize first letter of formatted date in Most Popular Day card

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move stats summary cache from StatsRepository to InsightsViewModel

StatsRepository was not @Singleton, so the shared cache for statsSummary
data was broken — each ViewModel got its own instance. Move caching to
InsightsViewModel which is activity-scoped and shared. Child ViewModels
now receive a summary provider function wired from the UI layer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix double-write and concurrency in InsightsCardsConfigurationRepository

addNewCardTypes wrote to prefs directly, then the caller wrote again via
saveConfiguration. Also getConfiguration was not mutex-protected. Make
addNewCardTypes pure, extract loadAndMigrate for atomic read-migrate-save
within the mutex, and make getConfiguration go through the mutex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename hiddenCards() to computeHiddenCards() to avoid naming conflict

InsightsCardsConfiguration had both a hiddenCards property (persisted
list) and a hiddenCards() method (computed from visibleCards). Rename
the method to make the distinction clear.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add NoData state for MostPopularDay and fix percentage precision

When a site has no best day data, show a dedicated NoData state with a
"No data yet" message instead of a blank Loaded card. Also reduce
percentage format from 3 to 1 decimal place for consistency with
typical stats UIs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress TooGenericExceptionThrown detekt warning in test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread-safety, DI design, and Error state in Insights cards

- Add @Volatile to isLoading/isLoadedSuccessfully flags in ViewModels
- Extract StatsSummaryUseCase singleton to replace summaryProvider var,
  removing temporal coupling and null-check error paths
- Change Error from class to data class, move onRetry out of state and
  into composable parameters to avoid unnecessary recompositions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Add Most Popular Time card and centralize Insights data fetching (#22684)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @Volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @Volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @Volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card to new stats screen (#22687)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @Volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @Volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @Volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card

Add a new top-list card to the Insights tab showing tags and categories
with view counts. Includes expandable multi-tag groups, percentage bars,
folder/tag icons, and a detail screen via Show All. Updates wordpress-rs
to 1230 for the statsTags endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add expand/collapse for multi-tag groups in detail screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for Tags & Categories feature

ViewModel, repository, and display type unit tests covering
success/error states, data mapping, refresh, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify Tags & Categories by reusing shared components

- Use StatsCardContainer, StatsCardHeader, StatsListHeader,
  StatsCardEmptyContent, StatsListRowContainer from StatsCardCommon
- Extract TagTypeIcon and ExpandedTagsSection into shared
  TagsAndCategoriesComponents to eliminate duplication between
  Card and DetailActivity
- Add fromTagType() to TagGroupDisplayType to avoid list allocation
  per tag in ExpandedTagsSection
- Add modifier parameter to StatsListRowContainer for clickable rows
- Remove duplicated constants (CardCornerRadius, BAR_BACKGROUND_ALPHA,
  VERTICAL_LINE_ALPHA)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused stubUnknownError from ViewModel test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: error recovery, conditional refresh, loading state

- Only set isLoaded on success so loadData() retries after errors
- Guard pull-to-refresh to skip tags refresh when card is hidden
- Move loading state into refresh() so callers don't need showLoading()
- Remove showLoading() public method
- Add test for loadData() retry after error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: deduplicate row composable, remove unused link field, fix thread safety and Intent size

- Extract shared TagGroupRow composable into TagsAndCategoriesComponents.kt with optional position parameter, removing duplicate from Card and DetailActivity
- Remove unused TagData.link field from data source, impl, and all tests
- Replace Intent extras with in-memory static holder in DetailActivity to avoid TransactionTooLargeException risk
- Remove unnecessary Parcelable from UI models
- Use AtomicBoolean for isLoaded/isLoading flags in ViewModel for thread safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix concurrent refresh, process death, isExpandable duplication, and accessibility

- Cancel in-flight fetch job on refresh() to prevent stale overwrites
- Finish detail activity on process death instead of showing blank screen
- Extract TagGroupUiItem.isExpandable computed property to deduplicate logic
- Add content descriptions for TagTypeIcon and expand/collapse chevron icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update configuration tests to include TAGS_AND_CATEGORIES card type

Fixes CI test failures caused by the new TAGS_AND_CATEGORIES card
not being reflected in test fixtures and assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1952: stats insights: tags card and some fixes (#22701)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @Volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @Volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @Volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card

Add a new top-list card to the Insights tab showing tags and categories
with view counts. Includes expandable multi-tag groups, percentage bars,
folder/tag icons, and a detail screen via Show All. Updates wordpress-rs
to 1230 for the statsTags endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add expand/collapse for multi-tag groups in detail screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for Tags & Categories feature

ViewModel, repository, and display type unit tests covering
success/error states, data mapping, refresh, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify Tags & Categories by reusing shared components

- Use StatsCardContainer, StatsCardHeader, StatsListHeader,
  StatsCardEmptyContent, StatsListRowContainer from StatsCardCommon
- Extract TagTypeIcon and ExpandedTagsSection into shared
  TagsAndCategoriesComponents to eliminate duplication between
  Card and DetailActivity
- Add fromTagType() to TagGroupDisplayType to avoid list allocation
  per tag in ExpandedTagsSection
- Add modifier parameter to StatsListRowContainer for clickable rows
- Remove duplicated constants (CardCornerRadius, BAR_BACKGROUND_ALPHA,
  VERTICAL_LINE_ALPHA)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused stubUnknownError from ViewModel test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: error recovery, conditional refresh, loading state

- Only set isLoaded on success so loadData() retries after errors
- Guard pull-to-refresh to skip tags refresh when card is hidden
- Move loading state into refresh() so callers don't need showLoading()
- Remove showLoading() public method
- Add test for loadData() retry after error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: deduplicate row composable, remove unused link field, fix thread safety and Intent size

- Extract shared TagGroupRow composable into TagsAndCategoriesComponents.kt with optional position parameter, removing duplicate from Card and DetailActivity
- Remove unused TagData.link field from data source, impl, and all tests
- Replace Intent extras with in-memory static holder in DetailActivity to avoid TransactionTooLargeException risk
- Remove unnecessary Parcelable from UI models
- Use AtomicBoolean for isLoaded/isLoading flags in ViewModel for thread safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix concurrent refresh, process death, isExpandable duplication, and accessibility

- Cancel in-flight fetch job on refresh() to prevent stale overwrites
- Finish detail activity on process death instead of showing blank screen
- Extract TagGroupUiItem.isExpandable computed property to deduplicate logic
- Add content descriptions for TagTypeIcon and expand/collapse chevron icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update configuration tests to include TAGS_AND_CATEGORIES card type

Fixes CI test failures caused by the new TAGS_AND_CATEGORIES card
not being reflected in test fixtures and assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fetch tags data independently in detail screen instead of using static field

The detail screen now has its own ViewModel that fetches up to 100
items directly from the API, while the card continues to fetch 10.
This removes the static data holder pattern that was prone to data
loss on process death.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract shared mapper, add detail VM tests, guard double calls, show all 10 card items

- Extract TagsAndCategoriesMapper to deduplicate TagGroupData to
  TagGroupUiItem mapping between card and detail ViewModels
- Add unit tests for TagsAndCategoriesDetailViewModel
- Add isLoaded/isLoading guards to detail VM to prevent double fetches
- Remove CARD_MAX_ITEMS limit so card displays all 10 fetched items
- Remove unused import in DetailActivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Skip data fetching for hidden Insights cards

Only call summary/insights endpoints when visible cards need them,
avoiding unnecessary network calls for hidden cards. Track which
endpoint groups have been fetched so re-adding a hidden card
triggers a fetch for its missing data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Early return in fetchData when no endpoints are needed

Prevents setting isDataLoaded=true when no cards require
fetching, which would block future fetches when cards are
re-added to the visible list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address code review findings: reduce duplication and improve tests

- Replace duplicate shimmer animation in YearInReviewCard with
  shared rememberShimmerBrush() utility
- Extract StatsTagsUseCase to centralize token validation and
  repository init, removing duplication between Tags ViewModels
- Add card reordering tests for middle elements in
  InsightsCardsConfigurationRepositoryTest
- Fix locale-dependent assertions in MostPopularDayViewModelTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress LargeClass detekt warning on InsightsViewModelTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix process-death restore and add caching to StatsTagsUseCase

- Call loadData() unconditionally in TagsAndCategoriesDetailActivity
  onCreate to handle process-death restore (loadData guard prevents
  double fetch on rotation)
- Add Mutex-protected in-memory cache to StatsTagsUseCase, consistent
  with StatsSummaryUseCase and StatsInsightsUseCase
- Pass forceRefresh=true on pull-to-refresh in TagsAndCategoriesViewModel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract BaseTagsAndCategoriesViewModel and use localized error messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety and error handling in ViewModels

Use AtomicBoolean with compareAndSet in InsightsViewModel to prevent
race conditions, rethrow CancellationException in base tags ViewModel,
and only mark endpoints as fetched on success results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract isCacheHit method to fix detekt ComplexCondition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Cancel in-flight fetch job before refreshing in InsightsViewModel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix compilation errors after trunk merge

Update wordpress-rs to version with stats tags types and remove
duplicate NoConnectionContent composable and redundant else branches
that caused -Werror failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address code review findings and add base ViewModel tests

- Move network call outside mutex in StatsTagsUseCase to
  avoid blocking concurrent callers during slow requests
- Add main-thread-confinement comments for fetchJob fields
- Document why TAGS_AND_CATEGORIES is absent from
  needsSummary/needsInsights (uses its own fetch path)
- Restore card max items to 7 (was unintentionally changed
  to 10 during refactoring)
- Add tests for BaseTagsAndCategoriesViewModel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix detekt findings: suppress ReturnCount, remove unused composable

- Suppress ReturnCount on StatsTagsUseCase.invoke (3 returns are
  clearer than restructuring with nested conditions)
- Remove unused PlaceholderTabContent composable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix TOCTOU race, config fetch trigger, and detail empty state

- StatsTagsUseCase: use in-flight Deferred to coalesce concurrent
  requests with the same params, eliminating the TOCTOU race where
  two callers could both miss cache and fire duplicate requests
- BaseTagsAndCategoriesViewModel: make isLoaded private since no
  subclass accesses it directly
- InsightsViewModel: trigger loadDataIfNeeded() from
  updateFromConfiguration when new endpoints are required, instead
  of relying on the UI to call it
- TagsAndCategoriesDetailActivity: add empty-state message when
  the loaded items list is empty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Deduplicate detail VM tests, fix Mockito import, add cancellation test

- Remove duplicate tests from TagsAndCategoriesDetailViewModelTest
  that are already covered by BaseTagsAndCategoriesViewModelTest;
  keep only initial-state and maxItems-specific tests
- Replace fully-qualified org.mockito.Mockito.times() with the
  imported mockito-kotlin times() throughout InsightsViewModelTest
- Add test verifying that rapid double-refresh cancels the first
  job so only one forceRefresh call completes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Clear stats caches on screen open to prevent stale data

Add clearCache() to StatsInsightsUseCase, StatsSummaryUseCase, and
StatsTagsUseCase. Call all three from InsightsViewModel init so that
reopening the insights screen always fetches fresh data while still
sharing results between cards within a single session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update wordpress-rs to trunk-262a778ead5f163f3450d62adfac21fb32048714

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify: fix StatsTagsUseCase error handling, deduplicate bottom sheets

- Fix critical bug in StatsTagsUseCase where an exception from fetchTags()
  would leave the CompletableDeferred incomplete, hanging all awaiters
- Extract generic AddCardBottomSheet<T> to replace three duplicate
  implementations (Stats, Insights, Subscribers)
- Merge duplicate LaunchedEffect(cardsToLoad) blocks in InsightsTabContent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix PR #22711 review issues: race conditions, code quality, API compat

- Fix race condition in InsightsViewModel.refreshData() by nulling
  fetchJob before cancel and guarding finally block for current job
- Fix StatsTagsUseCase cancellation propagation: complete deferred
  with error instead of completeExceptionally for CancellationException
- Remove redundant hiddenCards constructor param, make it a computed
  property; use PersistedConfig for backward-compatible JSON migration
- Key expandedGroups on items in TagsAndCategoriesCard and detail
  activity so expansion state resets on data refresh
- Use StatsCardContainer in AllTimeStatsCard and MostPopularDayCard
  instead of duplicating border/clip/background pattern
- Replace Year.now() (API 26+) with Calendar in YearInReviewViewModel
- Move @Suppress off inline catch to function level in StatsTagsUseCase
- Fix fully qualified references in repository test (import never, Gson)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix insights tab bugs: cancellation, race conditions, perf, and quality

- Fix StatsTagsUseCase cancellation propagation (deferred.cancel instead of error)
- Fix InsightsViewModel card-add race by cancelling in-flight fetch on config change
- Release mutex during network calls in StatsSummaryUseCase and StatsInsightsUseCase
- Replace Calendar with java.time.Year in YearInReviewViewModel
- Add NoData state for empty tags response instead of empty Loaded card
- Show period selector only on Traffic tab, not Insights
- Add accessibility onClickLabel to AddCardItem bottom sheet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Suppress ReturnCount detekt warning in use cases

The mutex refactor introduced a third return path (guard clause
pattern) which is idiomatic and readable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix retry, error handling, race condition, and localization bugs

- Reset isLoaded on exception in BaseTagsAndCategoriesViewModel so retry
  is not a no-op after a failed load
- Complete deferred with TagsResult.Error instead of completeExceptionally
  in StatsTagsUseCase so non-owner callers get a proper result
- Replace raw English error strings with localized R.string.stats_error_unknown
  in InsightsViewModel via ResourceProvider
- Fix updateFromConfiguration race window by cancelling job first and calling
  fetchData() directly with isDataLoading already set to true

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Deduplicate error UI, remove raw Context, fix Intent data passing, add logging

- Replace duplicate ErrorContent composables in AllTimeStatsCard and
  MostPopularDayCard with shared StatsCardErrorContent
- Replace raw Context injection in MostPopularTimeViewModel with
  DateFormatWrapper for better testability
- Refactor YearInReviewDetailActivity to fetch data via ViewModel and
  StatsInsightsUseCase instea…
nbradbury added a commit that referenced this pull request Mar 24, 2026
* AGP 9 Phase 2-3: Upgrade Gradle/AGP + adopt built-in Kotlin (#22705)

* Upgrade Gradle 9.1.0 + AGP 9.0.1

Simultaneously upgrade Gradle (8.12.1 → 9.1.0) and AGP (8.10.1 → 9.0.1)
since AGP 8.x does not support Gradle 9.x.

Uses opt-out flags (android.newDsl=false, android.builtInKotlin=false) to
preserve existing kotlin-android/kapt plugins while on AGP 9.

Version bumps:
- Gradle 8.12.1 → 9.1.0
- AGP 8.10.1 → 9.0.1
- Dagger/Hilt 2.58 → 2.59.2
- Fladle 0.19.0 → 0.21.0

AGP 9 migration fixes:
- Remove archivesBaseName from defaultConfig (use base.archivesName)
- Move compileSdk from defaultConfig to android block
- Replace proguard-android.txt with proguard-android-optimize.txt
- Rename lintOptions → lint in root build.gradle
- Replace deprecated buildDir with layout.buildDirectory
- Remove obsolete android.useAndroidX and android.enableJetifier properties
- Fix kotlin {} → java {} for sourceCompatibility in JVM-only modules
- Remove allowBackup from posttypes library manifest (app-level concern)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove AGP 9 opt-out flags and adopt built-in Kotlin

Remove android.newDsl=false and android.builtInKotlin=false to fully
adopt AGP 9's new DSL and built-in Kotlin compilation support.

- Remove kotlin-android plugin from all modules (AGP 9 built-in)
- Migrate kapt → legacy-kapt in fluxc (AGP 9 requirement)
- Migrate applicationVariants API → androidComponents.onVariants
- Remove kotlin-android and kapt entries from version catalog
- Keep kotlin-compose plugin (still required for Compose compiler)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* AGP9 Phase 4-5 (#22714)

* Upgrade Gradle 9.1.0 + AGP 9.0.1

Simultaneously upgrade Gradle (8.12.1 → 9.1.0) and AGP (8.10.1 → 9.0.1)
since AGP 8.x does not support Gradle 9.x.

Uses opt-out flags (android.newDsl=false, android.builtInKotlin=false) to
preserve existing kotlin-android/kapt plugins while on AGP 9.

Version bumps:
- Gradle 8.12.1 → 9.1.0
- AGP 8.10.1 → 9.0.1
- Dagger/Hilt 2.58 → 2.59.2
- Fladle 0.19.0 → 0.21.0

AGP 9 migration fixes:
- Remove archivesBaseName from defaultConfig (use base.archivesName)
- Move compileSdk from defaultConfig to android block
- Replace proguard-android.txt with proguard-android-optimize.txt
- Rename lintOptions → lint in root build.gradle
- Replace deprecated buildDir with layout.buildDirectory
- Remove obsolete android.useAndroidX and android.enableJetifier properties
- Fix kotlin {} → java {} for sourceCompatibility in JVM-only modules
- Remove allowBackup from posttypes library manifest (app-level concern)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Survive dialog selection across rotation (#22699)

Use rememberSaveable instead of remember for the selected index
in StatusDialog, FormatDialog, and AuthorDialog so the user's
choice persists through configuration changes.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Remove Login Module (#22564)

* Remove LoginEmailPasswordFragment

* Remove Login Magic Link Fragments

* Remove username/password WP.com flow

* Remove Google Login

* Remove LoginEmailFragment

* Remove Login 2FA fragment

* Remove SignupMagicLinkFragment

* Remove orphaned files

* Remove the “Find your site address” button

* Remove SignupEpilogueFragment

* Remove native account signup code

* Remove `PostSignupInterstitialActivity`

* Remove SmartLock

* Remove `WOO_LOGIN_MODE`

* Don’t show the username/password fields for Application Password sites

* In the Me tab, start WP.com login directly

* Fix strings

* Fix WP.com login UI

* Improve the WP.org login device name

* Add self-hosted site reauthentication

* Display more detailed error messages

* Fix WP.com sharing login flow

* Fix share flow login

* Remove `LoginMode.JETPACK_SELFHOSTED`

* Move resources out of login module into app

* Flatten theme heirarchies

* Modernize login prologue

* Fix edge-to-edge for login

* Automatically load notifications when switching to that tab

* Fix Application Password login focus

* Remove login module from CI

* Remove unused styles

* Add nullable annotation

* Remove deprecated Smart Lock Credentials API and play-services-auth

The only usage of play-services-auth was the deprecated Smart Lock for
Passwords (Credentials API) in AppInitializer, which connected a
GoogleApiClient on startup and called disableAutoSignIn on logout.
play-services-fido was only present as a transitive dependency of
play-services-auth. Neither is needed anymore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove trailing comma in localization.rb array

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix trailing backslash in run-unit-tests.sh

Removing :libs:login:koverXmlReportDebug left a trailing backslash on
the previous line, causing TESTS_EXIT_STATUS=$? to be passed as a
gradle task argument instead of being a variable assignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused imports left over from login lib removal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress detekt LongMethod for application password error handling

These methods are long due to exhaustive error handling across multiple
WpRequestResult variants, which is reasonable to suppress.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Delete unused bg_oval_translucent_stroke_3dp drawable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove references to deleted signup epilogue analytics constants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix constructor mismatches in application password test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused login prologue resources

Delete login prologue drawables, colors, dimens, strings, and menu
resources that became unused after the login library removal. Also
update TrainOfIcons preview to use app_icon instead of deleted drawables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Restore login_prologue_revamped colors used by wordpress source set

These colors are referenced by LoginPrologueRevampedFragment.kt and
Tagline.kt in src/wordpress/, so they must not be removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove remaining unused resources from login lib removal

Delete unused palette colors (purple_10, purple_20, purple_60, pink_10,
orange_10, green_60, celadon_30), login prologue dimens, indicator
drawables, promo illustration PNGs, PromoTitle style, and orphaned
LoginSiteAddressValidatorTest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move login prologue revamped colors to wordpress source set

These colors are only used by WordPress-flavor code in src/wordpress/,
so they belong in src/wordpress/res/ rather than shared src/main/res/.
This fixes the Jetpack lint check which correctly flagged them as unused.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix dark/light mode switching issues

* Add a release note

* Remove unused View import in LoginActivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove stale Google Login warnings from build scripts

Google Login no longer exists in the app, so these warnings
about it not working are no longer relevant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use bodyLarge text style for site address label

The headlineSmall style competes with the TopAppBar title.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Make login form scrollable for landscape support

Flatten the two-column layout into a single scrollable column
so the text field and button don't get crushed when the keyboard
opens on small devices in landscape mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ANR risk and error handling in WP.com OAuth login

- Replace runBlocking with async coroutine scope to avoid blocking
  the main thread during token exchange (Critical #1)
- Use dedicated CoroutineScope instead of Dispatchers.IO so
  dispose() only cancels this helper's coroutines (Critical #2)
- Propagate login errors to the caller via Consumer<Exception>
  instead of swallowing them
- Show error state on the loading screen with a retry button
  instead of silently failing
- Cancel coroutine scope in LoginActivity.onDestroy()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Handle null and unknown values in LoginFlow.fromIntent()

getStringExtra() can return null even when hasExtra() is true,
and valueOf() throws for old enum names that no longer exist.
Fall back to PROLOGUE safely in both cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Connect site comparison logic after OAuth login

After fetching sites post-OAuth login, route back through
loggedInAndFinish() so FINISH_WITH_SITE flows correctly
return the newly added site ID to the calling activity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Persist login flow state in SharedPreferences instead of static fields

Static fields don't survive process death and can cause issues in
multi-window mode. Move sPendingLoginFlow and sIsShareFlowPending to
AppPrefs with dedicated getter/setter methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add system bar transparency to login night theme

The light theme had transparent status/navigation bars but the night
theme was missing these attributes, causing opaque system bars in dark
mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add @Singleton to LoginAnalyticsTracker provider method

The class had @Singleton but the Dagger provider didn't, so a new
instance was created on every injection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Call startPostLoginServices and remove dead stub methods

startPostLoginServices was defined but never called, so Reader tags
and notification services weren't started after login. Also removes
unused startOver and gotConnectedSiteInfo methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove leftover debug Log.e in WPMainActivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove plaintext username from debug logs

The hasApiRestUsername boolean check is sufficient for debugging
without exposing the actual credential value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Replace deprecated onActivityCreated with onViewCreated

Move activity title setup into onViewCreated, removing the suppressed
deprecation warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Align ARG_LOGIN_FLOW constant name with its string value

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Unbind CustomTabsServiceConnection on dispose

The service was bound but never unbound, causing a minor resource
leak. Now unbinds when the helper is disposed in onDestroy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use Channel instead of SharedFlow for discovery URL event

Channel guarantees the event is buffered and consumed exactly once,
even if the collector starts slightly after emission. Safer than
SharedFlow(replay=0) for one-shot navigation events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress overdraw lint warning on login loading layout

The window background from LoginTheme triggers a false positive
overdraw warning since the ConstraintLayout itself has no explicit
background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tools:keep for flavor-specific login prologue colors

Android Lint can't trace cross-flavor resource usage, so it flags
these colors as unused. They're referenced by Compose code in the
wordpress flavor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Redirect WP.com site addresses to OAuth login flow

When a user enters a WordPress.com site address in the site address
field, redirect them to the WP.com OAuth flow instead of proceeding
with application password authorization. This ensures WP.com account
state is properly established so the Me tab shows the user's profile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove old login lib references from e2e tests

The LoginFlow e2e helper imported resources from the deleted login
library and referenced views (login_username_row, login_password_row)
that no longer exist. Remove the dead enterUsernameAndPassword and
enterSiteAddress methods since login forms are now Compose-based and
WP.com login uses OAuth via Custom Tabs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Call onFailure when OAuth callback is missing code parameter

Previously tryLoginWithDataString silently returned if the code
query parameter was absent, leaving the caller on a loading screen
indefinitely. Now reports the error via the onFailure callback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt violations and CI unit test task name

- Remove unused AppLog imports from WPcomLoginHelper
- Replace generic Exception catch with Result.fold to satisfy detekt
- Fix kover task name to match trunk's flavor rename (no more Wasabi)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Persist post-OAuth loading state across configuration changes

mIsWaitingForSitesToLoad and mOldSitesIdsForLoginUpdate were not
saved in onSaveInstanceState, so a device rotation during the
post-OAuth account/sites fetch would reset the flags and leave
the user stuck on the loading screen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix share flow race condition in LoginActivity onResume

Gate the onResume completion check on a new mShareFlowLoginLaunched
flag so LoginActivity doesn't immediately finish with RESULT_OK when
the user already has sites but hasn't completed the share-flow login.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Nick Bradbury <nick.bradbury@gmail.com>

* Remove AGP 9 opt-out flags and adopt built-in Kotlin

Remove android.newDsl=false and android.builtInKotlin=false to fully
adopt AGP 9's new DSL and built-in Kotlin compilation support.

- Remove kotlin-android plugin from all modules (AGP 9 built-in)
- Migrate kapt → legacy-kapt in fluxc (AGP 9 requirement)
- Migrate applicationVariants API → androidComponents.onVariants
- Remove kotlin-android and kapt entries from version catalog
- Keep kotlin-compose plugin (still required for Compose compiler)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove obsolete AGP 9 gradle.properties flags

Remove android.nonTransitiveRClass and android.nonFinalResIds (now
always enabled in AGP 9) and android.enableR8.fullMode=false to adopt
the new default of full R8 mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix issues introduced by login lib removal (#22703)

* Fix issues introduced by login lib removal

- Use single root layout with loading overlay in LoginActivity to avoid
  multiple setContentView() calls that orphan fragments
- Wrap unbindService() in try-catch for IllegalArgumentException
- Reset cached mLoginFlow in onNewIntent() to prevent stale values
- Remove custom get() on loadingStateFlow to avoid recreating wrapper
- Remove redundant e.printStackTrace() already logged via appLogWrapper
- Require dot in site URL validation to reject single-word inputs
- Make loading dialog dismissible by adding cancel discovery callback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add hideLoadingOverlay() to LoginActivity

Adds the inverse of showLoadingOverlay() so future code paths
can transition back from the loading state to the fragment UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Keep R8 full mode disabled to reduce migration risk

Re-add android.enableR8.fullMode=false to preserve the previous R8
behavior. Enabling full mode can be done as a separate follow-up
after verifying keep rules with a release build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add diagnostic error handling for application password login (#22702)

* Add diagnostic error handling for application password login failures

Surface detailed error messages when application password login fails,
instead of showing a generic toast or silently crashing. Catches
exceptions in SiteStore encryption/decryption paths and threads error
details through to the UI toast.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Send Sentry report on application password login failure

Every error path in the login flow now sends a crash report via
CrashLogging so we get visibility into failures in the wild.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add analytics tracking and crash logging for app password storing failures

Re-applies the reverted changes from #22694 that add trackStoringFailed()
calls with specific reason codes (empty_raw_data, empty_fetch_params,
fetch_sites_exception, site_changed_failed, bad_data, site_not_found)
and CrashLogging reports for better debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address code review: hide internal errors from toast, fix non-null assertion, add tests

- Show only user-friendly message in toast, keep detailed errors in logs/crash reports
- Replace site!! with safe ?: return@launch
- Fix import ordering (com.* before org.*)
- Add TODO comments on broad catch blocks to narrow once root cause identified
- Add CrashLogging mock and 5 new tests for error branches (SiteStore error,
  no rows affected, bad credentials, DB exception, crash report verification)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Refactor onSiteChanged to fix LongMethod detekt finding, remove TODOs

Split onSiteChanged into smaller focused methods to stay under the
60-line detekt limit: handleSiteChangedError, handleSiteChangedSuccess,
validateSiteChanged, and logAndEmitSiteChangedError. Also remove TODO
comments from SiteStore catch blocks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix double Sentry reporting, unreported storeCredentials exception, and error message leaks

- Remove crashLogging from ApplicationPasswordLoginHelper.trackStoringFailed
  so only the ViewModel's emitError sends Sentry reports with full context
- Add trackStoringFailed and crashLogging.sendReportWithTag to the
  storeCredentials catch block so KeyStore failures are tracked
- Replace technical error messages in NavigationActionData.errorMessage
  with opaque error codes (e.g. site_store_error, no_rows_affected)
- Use e.message ?: e.javaClass.simpleName in SiteStore catch blocks
  as fallback for exceptions without a message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Restore Sentry reports in helper for storeCredentials false-return paths

Add crashLogging.sendReportWithTag calls in storeApplicationPasswordCredentialsFrom
for the bad_data and site_not_found paths, which return false without throwing.
These are not duplicates of the ViewModel's emitError reports — they cover failures
where execution continues to fetchSites and the ViewModel never calls emitError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add commented-out forced error for testing Sentry reporting path

TODO: Remove before merging. Uncomment the throw line to verify
that the storeCredentials error path sends analytics and Sentry reports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Refactor helper to fix LongMethod detekt finding

Extract logAndReportBadData and logAndReportSiteNotFound from
storeApplicationPasswordCredentialsFrom to bring it under the
60-line detekt limit. Also extract reportStoringFailedToSentry
to deduplicate Sentry exception construction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove commented-out forced error used for testing Sentry reporting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: adalpari <adalpari@gmail.com>

* Bump io.sentry.android.gradle from 6.1.0 to 6.2.0 (#22708)

Bumps [io.sentry.android.gradle](https://github.com/getsentry/sentry-android-gradle-plugin) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/getsentry/sentry-android-gradle-plugin/releases)
- [Changelog](https://github.com/getsentry/sentry-android-gradle-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-android-gradle-plugin/compare/6.1.0...6.2.0)

---
updated-dependencies:
- dependency-name: io.sentry.android.gradle
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Activity Log: Show MCP agent metadata (#22706)

* Activity Log: Show MCP agent metadata in list and detail views

Display "via {client}" label (e.g. "via Claude") in the Activity Log
when an entry was performed by an MCP agent. Adds isMCPAgent and
mcpClient fields through the full data pipeline: API response parsing,
database persistence with migration, and UI rendering in both the
list and detail views.

Ref: AIINT-290

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt and lint issues in MCP agent metadata changes

Suppress LongParameterList for Actor class and extract hardcoded
dot separator to a string resource.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Refactor MCP metadata: extract shared logic, use uiHelpers, add tests

- Extract duplicated MCP formatting into ActivityActorExtensions
- Remove unnecessary intermediate variable in detail ViewModel
- Pass uiHelpers to EventItemViewHolder for consistent visibility handling
- Add unit tests for MCP metadata in both list and detail ViewModels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: adalpari <adalpari@gmail.com>

* Bump json from 2.18.1 to 2.19.2 (#22715)

Bumps [json](https://github.com/ruby/json) from 2.18.1 to 2.19.2.
- [Release notes](https://github.com/ruby/json/releases)
- [Changelog](https://github.com/ruby/json/blob/master/CHANGES.md)
- [Commits](https://github.com/ruby/json/compare/v2.18.1...v2.19.2)

---
updated-dependencies:
- dependency-name: json
  dependency-version: 2.19.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* RS Post Settings: Add search to author dialog (#22689)

* RS Post Settings: Add polish and error handling

Replace Toast with Compose Snackbar for inline error messages with retry
actions, add pull-to-refresh support, show "Saving..." label alongside
the spinner, display "Not Set" placeholder for empty status, and improve
accessibility with content descriptions for password toggle and featured
image placeholder. Extract shared error utilities from PostRsListViewModel
into PostRsErrorUtils for reuse across both list and settings screens.
Add unit tests for PostRsSettingsViewModel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix review issues from polish PR

- Preserve unsaved edits when refreshing post from server
- Remove unreachable snackbar when site is null (activity finishes immediately)
- Fix PullToRefreshBox content indentation
- Remove redundant offline mocks in refresh tests
- Rename misleading test name for status selection
- Add test for save-online sets isSaving

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix detekt and checkstyle issues

Extract preserveEdits() helper to shorten refreshPost() below the
60-line detekt limit, and remove empty line after opening brace in
test class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix snackbar navbar overlap and center error content

Add navigationBarsPadding to SnackbarHost so snackbars aren't
obscured by the system navigation bar. Wrap ErrorContent in a
centered Box so the error message displays in the center of the
screen instead of at the top.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Center error content on term selection screen

Add fillMaxWidth and TextAlign.Center to the ErrorContent on the
term selection screen so the network error message is properly
centered horizontally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Add retry action to save network error snackbar

The network error snackbar shown when saving in airplane mode was
missing an actionLabel and onAction callback, so no Retry button
appeared. Add both so the user can retry saving after reconnecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Use primary color for snackbar action button

Set the snackbar action color to MaterialTheme.colorScheme.primary
so the Retry text on snackbars matches the primary color used by
the Retry button on the error empty state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Remove pre-flight network check from save to avoid race condition

When disabling airplane mode and immediately tapping Retry, Android may not
have re-established connectivity yet, causing a spurious network error. Now
the save always proceeds to the API call, which handles network errors
naturally via its catch block using PostRsErrorUtils.friendlyErrorMessage().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Simplify code and remove status bar hiding

- Use BackHandler(onBack=) instead of wrapping lambda
- Use ?.let { } ?: Modifier for conditional click modifiers
- Replace PostApiException with RuntimeException
- Inline isAuthError wrapper in PostRsListViewModel
- Remove status bar hiding to fix PTR triggering on system bar pull

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Use named exception to satisfy detekt

Replace RuntimeException with PostApiRequestException to fix
TooGenericExceptionThrown detekt violations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Cache WpApiClient to avoid repeated instantiation

On self-hosted sites, a new WpApiClient was created for every API call
(5 instantiations just to open post settings). Cache self-hosted clients
in WpApiClientProvider (mirroring the existing WP.com pattern), add a
per-site client cache in PostRsRestClient, and use a lazy property in
PostRsSettingsViewModel to reuse the same client across fetch and save.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Remove dead parameters and redundant client cache

Now that WpApiClientProvider caches self-hosted clients, the per-site
client cache in PostRsRestClient is redundant — remove it. Also remove
the now-unused site parameters from fetchPost() and savePost() in the
ViewModel, and replace shadowed locals with simple null guards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix import ordering, replace crash with handled exception, remove unused methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Fix detekt ThrowsCount and ReturnCount violations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Replace loading spinner with shimmer skeleton

Show hero image shimmer and placeholder rows while loading instead
of a titled app bar with a centered spinner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Simplify code and remove redundant null checks

- Extract shared HeroOverlay composable to deduplicate gradient
  and back button between loading skeleton and hero layout
- Inline statusResId variable
- Remove unreachable site null guards in ViewModel
- Remove extra blank line in WpApiClientProvider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Add author search to author dialog

Add a debounced search field to the author selection dialog, matching
the existing pattern used for category/tag term search. Also fixes
author selection from index-based to ID-based to prevent incorrect
selection when the list changes during search or load-more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Simplify author dialog and deduplicate constant

Replace itemsIndexed with items in AuthorDialog since the index was
unused, and consolidate AUTHORS_PER_PAGE to a single constant in
PostRsRestClient.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Clear author search state when dialog closes

Reset the search query, search flag, and cached author list on
dismiss so reopening the dialog shows a fresh state instead of
stale search results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Remove redundant state clearing in onAuthorClicked

The authorSearchQuery and isSearchingAuthors fields are already
reset by onDismissDialog(), so clearing them again on open is
unnecessary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt issues: remove unused import and extract hideStatusBar()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Author search: reset pagination on dismiss and show error snackbar

Reset nextAuthorPageParams and canLoadMoreAuthors when the author
dialog is dismissed to prevent stale pagination state. Show a
snackbar when author search fails, consistent with other error
handling in the ViewModel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Author search: disable OK when no results and clear search on confirm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Merge release/26.7 into trunk (#22716)

* Bump version number

* Update WordPress metadata translations for 26.7

* Update Jetpack metadata translations for 26.7

* Bump org.mockito.kotlin:mockito-kotlin from 6.2.3 to 6.3.0 (#22718)

Bumps [org.mockito.kotlin:mockito-kotlin](https://github.com/mockito/mockito-kotlin) from 6.2.3 to 6.3.0.
- [Release notes](https://github.com/mockito/mockito-kotlin/releases)
- [Commits](https://github.com/mockito/mockito-kotlin/compare/v6.2.3...v6.3.0)

---
updated-dependencies:
- dependency-name: org.mockito.kotlin:mockito-kotlin
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* RS Post Settings: Use device timezone for date picker (#22704)

* RS Post Settings: Use device timezone for date picker

The date/time picker was using UTC for display and selection,
causing users to see times offset from their local timezone.
Switch to device timezone to match the old post settings behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Move UTC constant to its only usage site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* RS Post Settings: Append timezone label to displayed date

Show the device timezone abbreviation (e.g., EST, PST) after the
formatted date so users know which timezone the date refers to.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1936: Add Insights tab to new stats screen (#22711)

* Update wordpress-rs to 1219-9cb722b47c

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Insights tab with Year in Review card to new stats screen

Populate the Insights tab with card management (add, remove, move)
mirroring the Traffic tab architecture. Add Year in Review as the
first Insights card, fetching yearly summaries (posts, words, likes,
comments) via the wordpress-rs statsInsights endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Restyle Year in Review card with 2x2 grid of mini stat cards

Update the card title to show "YEAR in review" instead of a generic
title. Replace the table layout with a 2x2 grid of mini cards, each
displaying an icon, label, and formatted value for Posts, Words,
Likes, and Comments, matching the web design.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Year in Review detail screen with View All functionality

Add a detail screen showing all years with full stats (posts, comments,
avg comments/post, likes, avg likes/post, words, avg words/post). The
card shows the most recent year and a "View all" link when multiple
years are available. Years are sorted newest first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Always show current year in Year in Review card and always display Show All

Ensure the current year is always present in the data, adding it with
zero values if not returned by the API. The Show All footer is now
always visible regardless of the number of years available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Minor cleanup: cache current year and remove redundant variable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix code review issues and add missing tests

- Use resource string for exception errors instead of raw e.message
- Add duplicate guard in addCard()
- Change Error from data class to class to fix lambda equality
- Use Year.now() per-call instead of cached static val
- Fix isValidConfiguration to check for null entries
- Remove 0dp Spacer from detail screen
- Add StatsFormatterTest with 12 tests
- Add repository moveCard and addCard duplicate tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix second round of review issues: stale success flag, race condition, siteId fallback, API type inconsistency

- Reset isLoadedSuccessfully on error/exception so loadDataIfNeeded recovers after failed refresh
- Add Mutex to InsightsCardsConfigurationRepository to prevent concurrent mutation races
- Guard siteId in InsightsViewModel mutations to avoid operating with invalid 0L
- Align fetchStatsInsights parameter from String to Long for interface consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update Year in Review labels to match web app

Card uses short labels (Words, Likes) without "Total" prefix.
Detail screen uses exact web labels: Total posts, Total comments,
Avg comments per post, Total likes, Avg likes per post, Total words,
Avg words per post.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1936: Add All-time Stats and Most Popular Day Insights cards (#22673)

* Update wordpress-rs library hash

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add stats summary data layer with shared caching

Add fetchStatsSummary endpoint and StatsSummaryData model to
StatsDataSource. Implement Mutex-based caching in StatsRepository
so All-time Stats and Most Popular Day cards share a single API call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add All-time Stats Insights card

New card showing Views, Visitors, Posts, and Comments from the
statsSummary endpoint. Uses rememberShimmerBrush for loading state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Most Popular Day Insights card

New card showing the best day for views with date, view count, and
percentage. Shares the statsSummary API call with All-time Stats via
repository caching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Wire All-time Stats and Most Popular Day into Insights tab

Add card types to InsightsCardType enum and default card list.
Wire ViewModels and cards into InsightsTabContent. Add config
migration to automatically show new card types for existing users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: locale, empty best day, config migration, tests

- Create DateTimeFormatter at format time instead of caching in
  companion object to respect locale changes
- Handle empty viewsBestDay by returning loaded state with empty
  values instead of throwing
- Fix config migration by persisting hiddenCards field so new card
  types can be distinguished from user-removed cards
- Add missing tests: empty viewsBestDay, zero views percentage,
  exception path, dayAndMonth assertion, addNewCardTypes migration,
  full config no-migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update RS library and fix insights locale, detekt, and date casing

- Update wordpress-rs to 1de57afce924622700bcc8d3a1f3ce893d8dad5b
- Add locale parameter to StatsInsightsParams matching other endpoints
- Fix detekt MagicNumber and TooGenericExceptionCaught violations
- Capitalize first letter of formatted date in Most Popular Day card

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move stats summary cache from StatsRepository to InsightsViewModel

StatsRepository was not @Singleton, so the shared cache for statsSummary
data was broken — each ViewModel got its own instance. Move caching to
InsightsViewModel which is activity-scoped and shared. Child ViewModels
now receive a summary provider function wired from the UI layer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix double-write and concurrency in InsightsCardsConfigurationRepository

addNewCardTypes wrote to prefs directly, then the caller wrote again via
saveConfiguration. Also getConfiguration was not mutex-protected. Make
addNewCardTypes pure, extract loadAndMigrate for atomic read-migrate-save
within the mutex, and make getConfiguration go through the mutex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename hiddenCards() to computeHiddenCards() to avoid naming conflict

InsightsCardsConfiguration had both a hiddenCards property (persisted
list) and a hiddenCards() method (computed from visibleCards). Rename
the method to make the distinction clear.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add NoData state for MostPopularDay and fix percentage precision

When a site has no best day data, show a dedicated NoData state with a
"No data yet" message instead of a blank Loaded card. Also reduce
percentage format from 3 to 1 decimal place for consistency with
typical stats UIs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress TooGenericExceptionThrown detekt warning in test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread-safety, DI design, and Error state in Insights cards

- Add @Volatile to isLoading/isLoadedSuccessfully flags in ViewModels
- Extract StatsSummaryUseCase singleton to replace summaryProvider var,
  removing temporal coupling and null-check error paths
- Change Error from class to data class, move onRetry out of state and
  into composable parameters to avoid unnecessary recompositions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Add Most Popular Time card and centralize Insights data fetching (#22684)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @Volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @Volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @Volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card to new stats screen (#22687)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @Volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @Volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @Volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card

Add a new top-list card to the Insights tab showing tags and categories
with view counts. Includes expandable multi-tag groups, percentage bars,
folder/tag icons, and a detail screen via Show All. Updates wordpress-rs
to 1230 for the statsTags endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add expand/collapse for multi-tag groups in detail screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for Tags & Categories feature

ViewModel, repository, and display type unit tests covering
success/error states, data mapping, refresh, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify Tags & Categories by reusing shared components

- Use StatsCardContainer, StatsCardHeader, StatsListHeader,
  StatsCardEmptyContent, StatsListRowContainer from StatsCardCommon
- Extract TagTypeIcon and ExpandedTagsSection into shared
  TagsAndCategoriesComponents to eliminate duplication between
  Card and DetailActivity
- Add fromTagType() to TagGroupDisplayType to avoid list allocation
  per tag in ExpandedTagsSection
- Add modifier parameter to StatsListRowContainer for clickable rows
- Remove duplicated constants (CardCornerRadius, BAR_BACKGROUND_ALPHA,
  VERTICAL_LINE_ALPHA)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused stubUnknownError from ViewModel test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: error recovery, conditional refresh, loading state

- Only set isLoaded on success so loadData() retries after errors
- Guard pull-to-refresh to skip tags refresh when card is hidden
- Move loading state into refresh() so callers don't need showLoading()
- Remove showLoading() public method
- Add test for loadData() retry after error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: deduplicate row composable, remove unused link field, fix thread safety and Intent size

- Extract shared TagGroupRow composable into TagsAndCategoriesComponents.kt with optional position parameter, removing duplicate from Card and DetailActivity
- Remove unused TagData.link field from data source, impl, and all tests
- Replace Intent extras with in-memory static holder in DetailActivity to avoid TransactionTooLargeException risk
- Remove unnecessary Parcelable from UI models
- Use AtomicBoolean for isLoaded/isLoading flags in ViewModel for thread safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix concurrent refresh, process death, isExpandable duplication, and accessibility

- Cancel in-flight fetch job on refresh() to prevent stale overwrites
- Finish detail activity on process death instead of showing blank screen
- Extract TagGroupUiItem.isExpandable computed property to deduplicate logic
- Add content descriptions for TagTypeIcon and expand/collapse chevron icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update configuration tests to include TAGS_AND_CATEGORIES card type

Fixes CI test failures caused by the new TAGS_AND_CATEGORIES card
not being reflected in test fixtures and assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* CMM-1952: stats insights: tags card and some fixes (#22701)

* Add Most Popular Time Insights card

Introduce a new "Most popular time" card in the stats insights tab
that shows the best day of week and best hour with their view
percentages. The card reuses the insights API endpoint via a new
shared StatsInsightsUseCase (following the StatsSummaryUseCase
caching pattern), which also refactors YearInReviewViewModel to
use the same use case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up MostPopularTimeViewModel: locale-aware hour formatting and remove unnecessary @Volatile

Use DateTimeFormatter.ofLocalizedTime instead of hardcoded AM/PM to
respect device locale settings. Remove unnecessary @Volatile annotations
since all access is on the main thread via viewModelScope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for MostPopularTimeViewModel and StatsInsightsUseCase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix day-of-week mapping, NoData condition, and add bounds check

- Fix day mapping: WordPress API uses 0=Monday (not Sunday)
- Show NoData when either day or hour percent is zero (not both)
- Add bounds check for invalid day values (returns empty string)
- Update and add tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt: suppress LongMethod and remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Centralize Insights data fetching in InsightsViewModel

Move data fetching from individual card ViewModels to InsightsViewModel
as coordinator, ensuring each API endpoint (stats summary and insights)
is called only once per load. Card ViewModels now receive results via
SharedFlow instead of fetching independently, reducing duplicate
network calls from 4 to 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix race condition, consistent onRetry pattern, and remove unused siteId

- Set isDataLoading in refreshData() to prevent duplicate fetches
- Move onRetry from YearInReviewCardUiState.Error to composable param
- Remove unused siteId property, use resolvedSiteId() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add formatHour bounds check, remove duplicate string, clean up import

- Guard formatHour against invalid hour values (crash prevention)
- Remove duplicate stats_insights_percent_of_views string resource
- Use import for kotlin.math.round instead of fully qualified call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix detekt LongMethod: extract fetchSummary and fetchInsights

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Reduce duplication in MostPopularTimeCard using shared components

Replace manual card container, header, error content, and shimmer
boxes with StatsCardContainer, StatsCardHeader, StatsCardErrorContent,
and ShimmerBox. Extract repeated day/hour section into StatSection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename views percent string resource and add NoData preview

- Rename stats_insights_most_popular_day_percent to
  stats_insights_views_percent for neutral naming
- Add missing NoData preview to MostPopularTimeCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger PR checks

* Use device 24h/12h setting for hour formatting

Use android.text.format.DateFormat.is24HourFormat() to respect
the device time format preference instead of relying on locale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety, CancellationException handling, and lambda allocation

- Add @Volatile to isDataLoaded/isDataLoading flags
- Rethrow CancellationException to preserve structured concurrency
- Wrap onRetryData lambda with remember to avoid recomposition allocations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Tags & Categories insights card

Add a new top-list card to the Insights tab showing tags and categories
with view counts. Includes expandable multi-tag groups, percentage bars,
folder/tag icons, and a detail screen via Show All. Updates wordpress-rs
to 1230 for the statsTags endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add expand/collapse for multi-tag groups in detail screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add tests for Tags & Categories feature

ViewModel, repository, and display type unit tests covering
success/error states, data mapping, refresh, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify Tags & Categories by reusing shared components

- Use StatsCardContainer, StatsCardHeader, StatsListHeader,
  StatsCardEmptyContent, StatsListRowContainer from StatsCardCommon
- Extract TagTypeIcon and ExpandedTagsSection into shared
  TagsAndCategoriesComponents to eliminate duplication between
  Card and DetailActivity
- Add fromTagType() to TagGroupDisplayType to avoid list allocation
  per tag in ExpandedTagsSection
- Add modifier parameter to StatsListRowContainer for clickable rows
- Remove duplicated constants (CardCornerRadius, BAR_BACKGROUND_ALPHA,
  VERTICAL_LINE_ALPHA)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused stubUnknownError from ViewModel test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix review issues: error recovery, conditional refresh, loading state

- Only set isLoaded on success so loadData() retries after errors
- Guard pull-to-refresh to skip tags refresh when card is hidden
- Move loading state into refresh() so callers don't need showLoading()
- Remove showLoading() public method
- Add test for loadData() retry after error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: deduplicate row composable, remove unused link field, fix thread safety and Intent size

- Extract shared TagGroupRow composable into TagsAndCategoriesComponents.kt with optional position parameter, removing duplicate from Card and DetailActivity
- Remove unused TagData.link field from data source, impl, and all tests
- Replace Intent extras with in-memory static holder in DetailActivity to avoid TransactionTooLargeException risk
- Remove unnecessary Parcelable from UI models
- Use AtomicBoolean for isLoaded/isLoading flags in ViewModel for thread safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix concurrent refresh, process death, isExpandable duplication, and accessibility

- Cancel in-flight fetch job on refresh() to prevent stale overwrites
- Finish detail activity on process death instead of showing blank screen
- Extract TagGroupUiItem.isExpandable computed property to deduplicate logic
- Add content descriptions for TagTypeIcon and expand/collapse chevron icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update configuration tests to include TAGS_AND_CATEGORIES card type

Fixes CI test failures caused by the new TAGS_AND_CATEGORIES card
not being reflected in test fixtures and assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fetch tags data independently in detail screen instead of using static field

The detail screen now has its own ViewModel that fetches up to 100
items directly from the API, while the card continues to fetch 10.
This removes the static data holder pattern that was prone to data
loss on process death.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract shared mapper, add detail VM tests, guard double calls, show all 10 card items

- Extract TagsAndCategoriesMapper to deduplicate TagGroupData to
  TagGroupUiItem mapping between card and detail ViewModels
- Add unit tests for TagsAndCategoriesDetailViewModel
- Add isLoaded/isLoading guards to detail VM to prevent double fetches
- Remove CARD_MAX_ITEMS limit so card displays all 10 fetched items
- Remove unused import in DetailActivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Skip data fetching for hidden Insights cards

Only call summary/insights endpoints when visible cards need them,
avoiding unnecessary network calls for hidden cards. Track which
endpoint groups have been fetched so re-adding a hidden card
triggers a fetch for its missing data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Early return in fetchData when no endpoints are needed

Prevents setting isDataLoaded=true when no cards require
fetching, which would block future fetches when cards are
re-added to the visible list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address code review findings: reduce duplication and improve tests

- Replace duplicate shimmer animation in YearInReviewCard with
  shared rememberShimmerBrush() utility
- Extract StatsTagsUseCase to centralize token validation and
  repository init, removing duplication between Tags ViewModels
- Add card reordering tests for middle elements in
  InsightsCardsConfigurationRepositoryTest
- Fix locale-dependent assertions in MostPopularDayViewModelTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Suppress LargeClass detekt warning on InsightsViewModelTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix process-death restore and add caching to StatsTagsUseCase

- Call loadData() unconditionally in TagsAndCategoriesDetailActivity
  onCreate to handle process-death restore (loadData guard prevents
  double fetch on rotation)
- Add Mutex-protected in-memory cache to StatsTagsUseCase, consistent
  with StatsSummaryUseCase and StatsInsightsUseCase
- Pass forceRefresh=true on pull-to-refresh in TagsAndCategoriesViewModel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract BaseTagsAndCategoriesViewModel and use localized error messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix thread safety and error handling in ViewModels

Use AtomicBoolean with compareAndSet in InsightsViewModel to prevent
race conditions, rethrow CancellationException in base tags ViewModel,
and only mark endpoints as fetched on success results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract isCacheHit method to fix detekt ComplexCondition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Cancel in-flight fetch job before refreshing in InsightsViewModel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix compilation errors after trunk merge

Update wordpress-rs to version with stats tags types and remove
duplicate NoConnectionContent composable and redundant else branches
that caused -Werror failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address code review findings and add base ViewModel tests

- Move network call outside mutex in StatsTagsUseCase to
  avoid blocking concurrent callers during slow requests
- Add main-thread-confinement comments for fetchJob fields
- Document why TAGS_AND_CATEGORIES is absent from
  needsSummary/needsInsights (uses its own fetch path)
- Restore card max items to 7 (was unintentionally changed
  to 10 during refactoring)
- Add tests for BaseTagsAndCategoriesViewModel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix detekt findings: suppress ReturnCount, remove unused composable

- Suppress ReturnCount on StatsTagsUseCase.invoke (3 returns are
  clearer than restructuring with nested conditions)
- Remove unused PlaceholderTabContent composable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix TOCTOU race, config fetch trigger, and detail empty state

- StatsTagsUseCase: use in-flight Deferred to coalesce concurrent
  requests with the same params, eliminating the TOCTOU race where
  two callers could both miss cache and fire duplicate requests
- BaseTagsAndCategoriesViewModel: make isLoaded private since no
  subclass accesses it directly
- InsightsViewModel: trigger loadDataIfNeeded() from
  updateFromConfiguration when new endpoints are required, instead
  of relying on the UI to call it
- TagsAndCategoriesDetailActivity: add empty-state message when
  the loaded items list is empty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Deduplicate detail VM tests, fix Mockito import, add cancellation test

- Remove duplicate tests from TagsAndCategoriesDetailViewModelTest
  that are already covered by BaseTagsAndCategoriesViewModelTest;
  keep only initial-state and maxItems-specific tests
- Replace fully-qualified org.mockito.Mockito.times() with the
  imported mockito-kotlin times() throughout InsightsViewModelTest
- Add test verifying that rapid double-refresh cancels the first
  job so only one forceRefresh call completes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Clear stats caches on screen open to prevent stale data

Add clearCache() to StatsInsightsUseCase, StatsSummaryUseCase, and
StatsTagsUseCase. Call all three from InsightsViewModel init so that
reopening the insights screen always fetches fresh data while still
sharing results between cards within a single session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update wordpress-rs to trunk-262a778ead5f163f3450d62adfac21fb32048714

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify: fix StatsTagsUseCase error handling, deduplicate bottom sheets

- Fix critical bug in StatsTagsUseCase where an exception from fetchTags()
  would leave the CompletableDeferred incomplete, hanging all awaiters
- Extract generic AddCardBottomSheet<T> to replace three duplicate
  implementations (Stats, Insights, Subscribers)
- Merge duplicate LaunchedEffect(cardsToLoad) blocks in InsightsTabContent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix PR #22711 review issues: race conditions, code quality, API compat

- Fix race condition in InsightsViewModel.refreshData() by nulling
  fetchJob before cancel and guarding finally block for current job
- Fix StatsTagsUseCase cancellation propagation: complete deferred
  with error instead of completeExceptionally for CancellationException
- Remove redundant hiddenCards constructor param, make it a computed
  property; use PersistedConfig for backward-compatible JSON migration
- Key expandedGroups on items in TagsAndCategoriesCard and detail
  activity so expansion state resets on data refresh
- Use StatsCardContainer in AllTimeStatsCard and MostPopularDayCard
  instead of duplicating border/clip/background pattern
- Replace Year.now() (API 26+) with Calendar in YearInReviewViewModel
- Move @Suppress off inline catch to function level in StatsTagsUseCase
- Fix fully qualified references in repository test (import never, Gson)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix insights tab bugs: cancellation, race conditions, perf, and quality

- Fix StatsTagsUseCase cancellation propagation (deferred.cancel instead of error)
- Fix InsightsViewMode…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants