Skip to content

Reader: Add Discover sub-tabs (Freshly Pressed, Recommended, Latest)#22793

Draft
nbradbury wants to merge 28 commits intotrunkfrom
issue/reader-discover
Draft

Reader: Add Discover sub-tabs (Freshly Pressed, Recommended, Latest)#22793
nbradbury wants to merge 28 commits intotrunkfrom
issue/reader-discover

Conversation

@nbradbury
Copy link
Copy Markdown
Contributor

@nbradbury nbradbury commented Apr 14, 2026

Description

Replaces the single Reader Discover feed with a tabbed experience containing three sub-tabs: Freshly Pressed, Recommended, and Latest, matching the web (except for Tags which are handled separately by the app). While working on this, I had Claude examine the web reader codebase and make sure we're using the same endpoints and parameters.

Testing instructions

  1. Open the Reader and tap Discover in the top-bar dropdown
  2. Verify three sub-tabs appear: Freshly Pressed, Recommended, Latest
  • Each tab loads its own stream of posts
  • Swiping between tabs works
  • Tapping a tab switches to it
  1. Scroll down in any tab and pull to refresh
  • Posts refresh correctly
  1. Scroll to the bottom of Recommended or Latest
  • "Load more" pagination works
  1. Switch to a different Reader feed (e.g. Subscriptions), then return to Discover
  • The last-selected sub-tab is restored
  1. Kill and relaunch the app, navigate to Discover
  • The last-selected sub-tab is still restored
Screen_recording_20260414_105039.mp4

nbradbury and others added 13 commits April 10, 2026 10:57
Apply a new WordPress.TabLayout.Material3 style to the Discover sub-tab
strip so it visually matches the Compose PrimaryTabRow used in the new
Stats and RS post list screens: text-width indicator with rounded top,
elastic animation, MD3 title-small typography, primary-colored selected
text, and a surface background flush with the app bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Route the Recommended and Latest Discover sub-tabs through the v2
/read/streams/discover endpoint to match iOS ReaderCardService, instead
of the legacy /read/tags/{slug}/posts endpoint. Both sub-tabs hit the
same endpoint and are distinguished by the tag slug — Latest adds
sort=date while Recommended uses the server's default editorial order.

Pagination is cursor-based via an opaque page_handle stored per-stream
in AppPrefs, and first-page requests include the shared reader refresh
counter so the server rotates the visible shard on pull-to-refresh.
The tags request param uses the user's followed tag slugs, falling
back to "dailyprompt,wordpress" when the user isn't following anything
(mirroring the existing ReaderDiscoverLogic behavior).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Apply post-review cleanups to the Recommended/Latest Discover streams:
drop the RECOMMENDED_PATH/LATEST_PATH aliases in favor of
DISCOVER_STREAMS_PATH, remove the single-use isDiscoverStream() helper,
inline the get/setPageHandleForStream helpers at their one-call-site
each, and rewire handleDiscoverStreamResponse to run on the existing
coroutine scope instead of spawning a raw Thread per response.

Also give the Recommended and Latest sub-tab ReaderTags proper display
titles (TAG_TITLE_RECOMMENDED / TAG_TITLE_LATEST) instead of reusing
their lowercase slugs, and make the tabTitleResFor when-expression
fail fast on unknown positions instead of silently falling back to
Freshly Pressed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Match the unselected tab text color to the selected color so the
Discover sub-tab strip is visually consistent with the New Stats and
new RS Post List tabbed screens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migrate the Freshly Pressed sub-tab from the legacy v1.2
/freshly-pressed endpoint to the v2 /read/streams/freshly-pressed
endpoint, matching iOS ReaderPostServiceRemote.fetchStreamCards and
reusing the same pipeline already in place for the Recommended and
Latest sub-tabs. Freshly Pressed now shares the cards parser, opaque
page_handle pagination, and shared refresh counter with the other two
Discover streams — only the per-stream page_handle is stored in its
own AppPrefs key so the three cursors don't collide.

Parameterise requestPostsForDiscoverStream on the tag's endpoint
instead of the hardcoded DISCOVER_STREAMS_PATH constant, and extract
get/setDiscoverStreamPageHandle helpers to keep the per-stream prefs
selection in one place. isFreshlyPressed() now checks by tag slug
(matching isRecommended / isLatest) instead of an endpoint suffix,
which is more robust now that the endpoint carries the v2 path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Collapse duplication that built up while wiring the Discover sub-tabs:
dedupe the three near-identical tag factories in ReaderDiscoverTabsFragment,
inline the tab title res lookup, fold the three per-stream page-handle
prefs into a single keyed accessor, drop the single-use isRecommended/
isLatest predicates in favor of slug comparisons, and replace the ad-hoc
SupervisorJob scope in requestPostsForDiscoverStream with the injected
application scope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The detailed streams-pipeline comment duplicated the KDoc already
present on ReaderPostRepository.requestPostsForDiscoverStream.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Freshly Pressed now uses the legacy v1.2 /freshly-pressed endpoint
(matching web and iOS) instead of the v2 /read/streams/freshly-pressed
path which returned 404. Latest matches iOS using read/streams/discover
with sort=date.

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

Remove the duplicate member `formatRelativeEndpointForTag` in
ReaderPostRepository (the companion version is sufficient), and
simplify ReaderDiscoverTabsFragment to extend Fragment directly
since it opted out of ViewPagerFragment behavior by returning null.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract card-parsing logic into parsePostCards/parsePostCard helpers
to fix NestedBlockDepth, LoopWithTooManyJumpStatements, and
TooGenericExceptionCaught detekt violations. Narrows catch blocks
from Exception to JSONException.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove redundant tabSelectedTextColor (identical to tabTextColor)
- Make FRESHLY_PRESSED_PATH and DISCOVER_STREAMS_PATH private
- Use idiomatic mutableMapOf instead of HashMap constructor

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

1 Warning
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Apr 14, 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
Versionpr22793-73bfa1f
Build Number1488
Application IDcom.jetpack.android.prealpha
Commit73bfa1f
Installation URL25pun4stp0nr8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Apr 14, 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
Versionpr22793-73bfa1f
Build Number1488
Application IDorg.wordpress.android.prealpha
Commit73bfa1f
Installation URL70etbdecc4bn8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 11.56463% with 130 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.26%. Comparing base (2c55cc4) to head (73bfa1f).

Files with missing lines Patch % Lines
...droid/ui/reader/repository/ReaderPostRepository.kt 12.90% 99 Missing and 9 partials ⚠️
.../java/org/wordpress/android/ui/prefs/AppPrefs.java 0.00% 10 Missing ⚠️
...rg/wordpress/android/datasets/ReaderPostTable.java 0.00% 6 Missing ⚠️
.../org/wordpress/android/ui/prefs/AppPrefsWrapper.kt 0.00% 4 Missing ⚠️
...n/java/org/wordpress/android/models/ReaderTag.java 50.00% 1 Missing ⚠️
...ui/reader/services/discover/ReaderDiscoverLogic.kt 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22793      +/-   ##
==========================================
- Coverage   37.29%   37.26%   -0.04%     
==========================================
  Files        2326     2326              
  Lines      124859   124987     +128     
  Branches    16961    16990      +29     
==========================================
+ Hits        46563    46573      +10     
- Misses      74524    74633     +109     
- Partials     3772     3781       +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

nbradbury and others added 7 commits April 14, 2026 10:51
…states

Set tabSelectedTextColor to colorPrimary so both states use the same
color, matching the Compose PrimaryTabRow behavior in Stats and RS
post list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The OnPageChangeCallback was registered but never unregistered,
causing a potential memory leak when the fragment's view was destroyed.

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

Wraps the coroutine body in requestPostsForDiscoverStream so that
uncaught exceptions (e.g. from getFollowedTagsUseCase) report FAILED
to the result listener instead of propagating to the application scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix AppPrefsWrapper import ordering in ReaderDiscoverTabsFragment
- Replace filterNot().isEmpty() with none() in Discover stream tags check

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wpmobilebot
Copy link
Copy Markdown
Contributor

🤖 Build Failure Analysis

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

nbradbury and others added 3 commits April 16, 2026 15:31
- Extract resolveInitialTabIndex in ReaderDiscoverTabsFragment as a pure
  companion function and cover it with unit tests
- Add ReaderPostRepositoryTest covering the REQUEST_OLDER short-circuit
  for Recommended and Latest streams
- Clarify that Freshly Pressed falls through to the regular tag-based
  flow, and note why REQUEST_OLDER_THAN_GAP is folded into first-page

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

Pagination on the Recommended and Latest sub-tabs dropped posts into the
middle of the list because the local sort was by date_published — which
doesn't match the server's editorial (Recommended) or cursor-paginated
(Latest) ordering. Route these tags to the date_tagged sort column and
stamp each post with a monotonically decreasing date_tagged in server
order so REQUEST_OLDER pages land at the end of the list.

- ReaderTag.isDiscoverStream() identifies Recommended/Latest tags
- ReaderPostTable.getSortColumnForTag() uses date_tagged for them
- ReaderPostRepository.stampServerOrderOnPosts() assigns insertion-order
  timestamps before saveUpdatedPosts, with a null-safe oldest-date lookup
  and a getNumPostsWithTag fallback for stale rows from pre-fix builds

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is a new feature, so the date_tagged fallback that compensated for
posts stored before stamp logic existed isn't needed. Keep the null-safe
parse around DateTimeUtils.dateFromIso8601 and let REQUEST_NEWER flow
through saveUpdatedPosts unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nbradbury and others added 5 commits April 17, 2026 06:57
getOldestDateWithTag queries "SELECT datetime(date_tagged)", and SQLite's
datetime() normalizes to "YYYY-MM-DD HH:MM:SS" without timezone — which
DateTimeUtils.dateFromIso8601 can't parse. Every REQUEST_OLDER call was
silently falling back to now, so paginated posts landed at the top of
the list instead of the end.

Add getOldestDateTaggedForTag that selects the raw date_tagged column so
the ISO8601-with-timezone value round-trips intact, and use it from
stampServerOrderOnPosts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the web Reader: Recommended stays on /read/streams/discover with
editorial curation and page_handle pagination, but Latest now hits
/read/tags/posts with orderBy=date and the user's followed tags, using
before=<oldestDate> pagination (same mechanism as Freshly Pressed).
This gives Latest a true date-descending stream that appends cleanly on
scroll, instead of a curated stream with opaque cursor pagination that
interleaved posts on the client.

- ReaderDiscoverTabsFragment: new LATEST_PATH = "read/tags/posts"
- ReaderTag.isDiscoverStream() now only matches Recommended (Latest uses
  the standard date_published sort)
- ReaderPostRepository.requestPostsForLatestStream: new handler that
  seeds tags from getFollowedTagsUseCase and paginates by date
- Obsolete DISCOVER_STREAM_TAG_SLUGS set removed; Recommended/Latest are
  now routed via a direct when-branch on tagSlug

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull the duplicated "user followed tags → query param" block out of
both requestPostsForDiscoverStream and requestPostsForLatestStream.
Also drop the stale Latest reference from the handleDiscoverStreamResponse
comment (Latest no longer flows through that method).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
requestPostsWithTag had three returns after the Latest branch was added;
pull the sub-tab routing into a helper that returns Boolean so the
dispatch logic has a clear name and requestPostsWithTag stays within
the ReturnCount limit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

3 participants