Skip to content

Add batch moderation to the rs comments list (Phase 2b)#23062

Merged
nbradbury merged 29 commits into
trunkfrom
feature/rs-comments-list-batch
Jul 7, 2026
Merged

Add batch moderation to the rs comments list (Phase 2b)#23062
nbradbury merged 29 commits into
trunkfrom
feature/rs-comments-list-batch

Conversation

@nbradbury

@nbradbury nbradbury commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds batch moderation for the RS comments list, matching the legacy list's action mode. Long-press a comment to enter selection mode and tap rows to build the selection.

It also includes small selection-mode polish: an animated selection bar with a distinct color, avatar-tap to toggle selection, back press dismissing the selection, and re-tapping the active tab to scroll to the top.

Testing instructions

First make sure to enable the experimental feature flag, then long press a comment to start selection mode. Verify the the approve/unapprove icons appear in the toolbar and work as expected, then verify the Spam, Trash, and Delete items in the menu work as expected.

Also verify that tapping an avatar toggles selection, pressing back clears the selection, and re-tapping the active tab scrolls the list to the top.

Add a Compose comments list backed by wordpress-rs, following the
postsrs/pagesrs pattern. There's no rs mobile-cache support for comments
yet, so each tab pages directly against /wp/v2/comments using the
response's nextPageParams as the cursor.

- 5 filter tabs (All/Pending/Approved/Spam/Trashed). The All and
  Approved tabs use CommentStatus.Custom("all"/"approve") because
  WP_Comment_Query doesn't recognise the "approved" value the rs enum
  serialises to
- Rows show avatar, bold "author on post" title (post titles resolved
  via a batched sparse-field request), snippet, date, pending indicator
- Pull-to-refresh, load-more, retry snackbars, shimmer/empty/error states
- Tapping a row opens the rs comment detail; the list refreshes on return
- Gated in ActivityLauncher.viewUnifiedComments by RS_UNIFIED_COMMENTS +
  site capability (WP.com REST or application password), falling back to
  the legacy list; the now-dead per-tap rs gate inside the legacy list
  fragment is removed

Batch moderation and search follow in stacked PRs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dangermattic

dangermattic commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator
2 Warnings
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@nbradbury nbradbury changed the base branch from feature/rs-comments-list to trunk July 2, 2026 16:33
@wpmobilebot

wpmobilebot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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
Versionpr23062-25c279b
Build Number1498
Application IDorg.wordpress.android.prealpha
Commit25c279b
Installation URL6cqtnl81g0dsg
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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
Versionpr23062-25c279b
Build Number1498
Application IDcom.jetpack.android.prealpha
Commit25c279b
Installation URL4t3ff862jnv9o
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 275 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.37%. Comparing base (28a3af8) to head (25c279b).

Files with missing lines Patch % Lines
...roid/ui/commentsrs/screens/CommentsRsListScreen.kt 0.00% 121 Missing ⚠️
...ess/android/ui/commentsrs/CommentsRsListUiState.kt 0.00% 64 Missing ⚠️
...s/android/ui/commentsrs/CommentsRsListViewModel.kt 0.00% 55 Missing ⚠️
...ndroid/ui/commentsrs/screens/CommentsRsListItem.kt 0.00% 30 Missing ⚠️
...d/ui/commentsrs/screens/CommentsRsTabListScreen.kt 0.00% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #23062      +/-   ##
==========================================
- Coverage   37.44%   37.37%   -0.08%     
==========================================
  Files        2337     2337              
  Lines      126454   126703     +249     
  Branches    17422    17453      +31     
==========================================
  Hits        47354    47354              
- Misses      75223    75472     +249     
  Partials     3877     3877              

☔ View full report in Codecov by Harness.
📢 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 8 commits July 2, 2026 13:27
…rience

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g and SnackbarMessage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Danger requires immutable string keys: changed copy gets a new key rather
than editing the old one in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Key the post-title cache by (site, post id) so titles can't leak across
  sites, and negative-cache unresolvable ids so they aren't re-requested
  on every page load
- Set perPage explicitly on title batches (server default of 10 silently
  truncated larger batches) and fall back to the pages endpoint for
  comments left unresolved by the posts endpoint
- Discard load-more results whose fetch predates the latest applied first
  page, preventing stale pages/cursors after a silent refresh
- Offer a retry action on load-more failures and auto-advance past pages
  that dedupe away entirely, so pagination can't stall at the list bottom

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Chunk post-title batches to the server's per_page maximum of 100;
  larger values are rejected outright with rest_invalid_param
- Always attempt the pages endpoint for ids the posts endpoint didn't
  resolve, and only negative-cache when both endpoints succeeded
- Clear negative-cached titles on user-initiated refresh so posts
  published after first sight can resolve
- Cap unattended load-more auto-advance at 3 pages and let it handle
  empty pages, not just fully-deduplicated ones
- Don't cancel/re-fire an identical in-flight title-resolve batch
- Add CommentsRsDataSourceTest covering the cache, fallback, and
  chunking behavior, plus ViewModel tests for the new paging rules

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Cancel a tab's in-flight title-resolve job on user refresh so a job
  holding pre-clear 'not found' results can't re-apply them via the
  identical-ids guard
- Generation-stamp the negative cache so a fetch already in flight when
  the user cleared can't re-poison entries with pre-clear results
- Make the data source tests execute each request's builder lambda
  against a mocked uniffi client, asserting the endpoint, include count,
  and perPage actually sent — the chunking test now fails if chunking is
  reverted, and the posts-before-pages order is asserted, not assumed

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the Android Lint code-scanning alerts on CommentsRsDataSourceTest:
SparseAnyPostWithViewContext and PostsRequestFilterListWithViewContextResponse
are pure data classes, so the tests now build real instances.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Detail opened from the rs list backfills the FluxC cache (WP.com) and
  resolves the post title via rs, so title and like state are correct
- Detail reports RESULT_OK only when the comment changed; the list only
  refreshes then, keeping paged lists and scroll position intact
- Guard against duplicate first-page fetches; skip the retry snackbar
  when a silent refresh fails on a tab that already shows comments
- Clear isLoadingMore when a refresh replaces the list so paging can't
  stall behind the busy guard
- Stop the load-more trigger firing on the empty pre-layout list and
  re-arm it when the list size changes
- Evict all cached post titles for the site on user refresh so renamed
  posts pick up their new title
- Track COMMENT_FILTER_CHANGED on tab changes like the legacy list
- Restore the singleTop launch mode the legacy comments activity has
- Drop the dead initializingTabs bookkeeping
- Merge RsCommentListItem into RsComment (one shared type and mapper)
- Move the new unit tests to a follow-up PR to reduce this PR's size

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nbradbury nbradbury force-pushed the feature/rs-comments-list-batch branch from 7ce128b to 26aaa08 Compare July 3, 2026 12:20
nbradbury and others added 4 commits July 3, 2026 08:42
- Delete the now-unused ActivityLauncher.viewUnifiedCommentsDetails
- Track title-resolve jobs as a plain per-tab Job: cancel-and-relaunch
  is cheap because the data source caches resolved titles
- Drop the dead tab parameter from onCommentClick and the unused
  snackbarMessages default argument

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Long-press a comment to enter selection mode, then tap rows to build the
selection. The top bar swaps to a contextual action bar offering the same
per-tab actions as the legacy list's action mode (approve/unapprove/spam/
not-spam/trash/restore/delete), with confirmation dialogs for trash and
delete-permanently and COMMENT_BATCH_* analytics.

Moderation runs the rs writes in parallel, aggregates any failures into a
single snackbar, then refreshes all initialized tabs, since moderated
comments move between them. Selection clears on tab change and after each
batch action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nbradbury nbradbury force-pushed the feature/rs-comments-list-batch branch from 26aaa08 to 8a944bc Compare July 3, 2026 13:43
nbradbury and others added 7 commits July 3, 2026 15:01
…list-batch

# Conflicts:
#	WordPress/src/main/java/org/wordpress/android/ui/commentsrs/CommentsRsListActivity.kt
#	WordPress/src/main/java/org/wordpress/android/ui/commentsrs/CommentsRsListUiState.kt
#	WordPress/src/main/java/org/wordpress/android/ui/commentsrs/CommentsRsListViewModel.kt
#	WordPress/src/main/java/org/wordpress/android/ui/commentsrs/screens/CommentsRsListItem.kt
#	WordPress/src/main/java/org/wordpress/android/ui/commentsrs/screens/CommentsRsListScreen.kt
#	WordPress/src/main/java/org/wordpress/android/ui/commentsrs/screens/CommentsRsTabListScreen.kt
The confirm button is wired directly to onConfirmPendingAction in the
screen, so this field was never set or read.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drop the single-use ConfirmationDialogState wrapper; pass pending
  confirmation and dismiss callback directly to the screen
- Extract the confirmation dialogs into a private BatchConfirmationDialogs
  composable, removing the LongMethod suppression
- Remove a redundant trailing semicolon in CommentsRsBatchAction

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Text labels ("Approve", etc.) crowded the contextual action bar and
wrapped the "N selected" title. Render each batch action as an icon
button reusing the legacy comments list's action-mode drawables
(thumbs up/down, spam, trash), keeping the string label as the icon's
content description for accessibility.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the legacy comment list's action mode: keep approve/unapprove as
always-visible icon buttons and move the remaining actions (spam,
not-spam, trash, restore, delete) into an overflow menu, so the
"N selected" title no longer wraps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the PendingConfirmation sealed interface into a single data
class that carries the CommentsRsBatchAction, so onBatchAction routes on
isDestructive and onConfirmPendingAction reuses action.targetStatus
instead of re-mapping the confirmation type back to a CommentStatus.
Also use MutableStateFlow.update in toggleSelection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Approve and unapprove stay visible in the batch action bar but are
disabled (dimmed) unless the selection actually contains a comment they
can act on: approve needs an unapproved comment, unapprove needs an
approved one. The active tab's selected statuses drive the enablement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nbradbury nbradbury marked this pull request as ready for review July 6, 2026 14:50
@nbradbury nbradbury requested a review from adalpari July 6, 2026 14:50
nbradbury and others added 3 commits July 6, 2026 12:07
Gate the contextual action bar on the active tab actually containing
selected comments, so it no longer briefly flashes the next tab's
actions (with approve/unapprove dimmed) mid-swipe before the selection
clears.

Drive the confirmation dialog off ConfirmationCopy carried by each
destructive action instead of a non-exhaustive when: onBatchAction now
routes to confirmation only when the action has copy, so a destructive
action can no longer strand the selection with no dialog. This also
replaces the isDestructive flag and drops the duplicated trash/delete
dialog branches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Animate the top-bar swap between the normal bar and the selection CAB
- Make the avatar tappable to toggle selection, the discoverable path
  into selection mode (long-press still works)
- Add Select All to the CAB overflow menu
- Re-tapping the active tab scrolls its list back to the top (per-tab
  LazyListStates hoisted to the screen)

Long-press haptics were considered but need no code: Compose Foundation
1.11.4's combinedClickable already performs them by default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nbradbury and others added 3 commits July 6, 2026 14:31
On a paginated list "Select all" only selects the comments loaded so
far, which reads as selecting everything on the tab. Drop it rather
than ship a misleading affordance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Back now dismisses the selection instead of leaving the screen,
  matching the legacy action mode
- The avatar crossfades to the checkmark when a row is selected
- The selection bar uses surfaceContainerHigh so entering selection
  mode visibly reads as a mode change

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

adalpari commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Haven't tested yet, but some notes form Claude before doing it:

1. Batch action moderates comments the user can't see — CommentsRsListScreen.kt:123 (PLAUSIBLE)
selectedCount = selectedIds.size (global) and onBatchAction operate on the full _selectedIds, but action-gating uses only visible comments. A refresh in selection mode drops a selected comment off the page → bar still reads "2 selected" → Trash moderates a comment the user can't see and never re-confirmed.

2. Taps toggle selection instead of opening detail — CommentsRsListViewModel.kt:194 (CONFIRMED, also line 244)
onCommentClick gates on global _selectedIds being non-empty, but the selection bar hides based on the tab-filtered subset. After a refresh drops the selected comment, the bar vanishes (looks cleared) but _selectedIds still holds the id — so the next tap silently re-enters selection mode instead of opening the comment.

3. Back button appears stuck — CommentsRsListScreen.kt:96 (CONFIRMED, also line 101)
The selection bar is gated on selectedStatuses (visible comments) while BackHandler(enabled = selectedIds.isNotEmpty()) is gated on the global set. Invisible-but-active selection means the first back press only clears the hidden selection and appears to do nothing — user must press back twice to exit.

4. Avatar tap hijacks the detail-open gesture — CommentsRsListItem.kt:85 (CONFIRMED)
CommentAvatar now wires onClick = onLongClick, so tapping an avatar (not in selection mode) enters multi-select instead of opening the comment as it did before.

5. Un-spam / un-trash logged as approvals — CommentsRsListViewModel.kt:279 (CONFIRMED, also line 277)
trackBatchModeration keys off targetStatus. NOT_SPAM and UNTRASH both map to APPROVED, so they record COMMENT_BATCH_APPROVED — inflating approve metrics and hiding un-spam/restore usage.

6. Failed batch strips retry ability — CommentsRsListViewModel.kt:251 (CONFIRMED, also line 266)
Selection is cleared optimistically before the network calls. If the batch fails, the user sees "X of 10 couldn't be updated" but the selection is gone — they must manually re-select every failed comment.

7. Scroll position lost after rotation — CommentsRsListScreen.kt:85 (CONFIRMED)
Hoisted state uses plain remember { LazyListState() } instead of the rememberSaveable-backed rememberLazyListState() that was removed. Rotation/process recreation jumps every tab's list back to the top.

8. Refresh spinner tied to unrelated job — CommentsRsListViewModel.kt:252 (PLAUSIBLE)
performBatchModeration sets isRefreshing=true, but if a first-page job is already active, refreshTab early-returns without ever resetting the flag — the spinner lingers until the pre-existing load happens to finish.

Cleanup

9. Over-broad tab refetch — CommentsRsListViewModel.kt:273 (CONFIRMED)
refreshAllTabs() re-fetches the first page of all 5 tabs after a batch action, though only the source + destination tabs can change — up to 5 parallel requests where 2–3 suffice.

10. Hardcoded plural string — strings.xml:437 (CONFIRMED)
comments_rs_moderation_failed hardcodes "comments"; a single failure reads "1 of 1 comments couldn't be updated" and can't be pluralized by translators. Use a <plurals> 

nbradbury and others added 2 commits July 7, 2026 07:22
- Clear the selection on user refresh: a refresh can remove selected
  comments from the list, leaving the global selection out of sync with
  the visible checkmarks (batch actions on off-screen comments, back
  presses that appear stuck, taps that toggle instead of opening)
- Re-select the failed comments after a partial batch failure so the
  user can retry without hunting them down again
- Use per-tab rememberLazyListState so scroll positions survive
  rotation again (lost when the states were hoisted for scroll-to-top)
- Split the moderation-failure snackbar into single/multiple string
  keys so a lone failure no longer reads "1 of 1 comments"

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

Copy link
Copy Markdown
Contributor Author

@adalpari I'm surprised your Claude review found a number of issues because my Claude review prior to submitting this PR found none of them. I asked Claude to examine your comment and saved the response as a PDF. I fixed all the "Valid — worth fixing" items, so ready for another round.

review-assessment-pr23062-2026-07-07.pdf

@adalpari

adalpari commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@adalpari I'm surprised your Claude review found a number of issues because my Claude review prior to submitting this PR found none of them.

I'm having a hard time trusting some Claude's reviews. Sometimes it keeps finding and finding problems if you keep running /review in a fresh context. Sometimes I even experienced a loop, suggesting a B scenario from A scenario, and then finding an extra problem and reverting the PR back to scenario A. So...

@adalpari adalpari left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good! :shipit:

@nbradbury

Copy link
Copy Markdown
Contributor Author

Sometimes it keeps finding and finding problems if you keep running /review in a fresh context.

I've experienced the same thing.

@nbradbury nbradbury merged commit 3a5f579 into trunk Jul 7, 2026
23 checks passed
@nbradbury nbradbury deleted the feature/rs-comments-list-batch branch July 7, 2026 12:23
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