Skip to content

Browse stories by classifier (tag/author/title/url/text) - #2102

Open
samuelclay wants to merge 22 commits into
mainfrom
classifier-view
Open

Browse stories by classifier (tag/author/title/url/text)#2102
samuelclay wants to merge 22 commits into
mainfrom
classifier-view

Conversation

@samuelclay

@samuelclay samuelclay commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • New classifier filter banner: click a tag/author/url pill on a story (or the "view matching stories" icon in the trainer) to browse every unread story in the current feed/folder/river that matches that single classifier value. Inline training controls on the banner pill write through save_classifier and recompute story scores on the fly.
  • Filter state lives in the URL as ?classifier_tag=foo / ?classifier_author=foo / ?classifier_title=foo / ?classifier_url=foo / ?classifier_text=foo (plus optional classifier_scope / classifier_folder), so the view is bookmarkable and survives back/forward. Router auto-restores the filter on reload; popstate syncs when only the query string changes.
  • Backend filtering is intentionally "dumb": stories flow through the normal feed/usersub/river loading paths and get narrowed in Python via filter_stories_by_classifier, with a 20x batch multiplier so a single page scan catches every match the unread counts promise. No new indexes required.

Test plan

  • Click a tag pill on a story → "View matching stories" tooltip → banner appears with the clicked value, story list narrows to matching stories
  • Click a second classifier (different type) — URL updates from classifier_tag to classifier_author, no stale keys left behind
  • Reload the page on a filtered URL — banner + filtered story list restore correctly
  • Browser back/forward toggles the banner on and off without a full reload
  • Train the pill to like/dislike/super-dislike — matching stories' sentiment indicators flip immediately, sidebar unread counts refresh, rows stay visible even when hidden
  • Navigate to another feed via the sidebar — banner disappears and the next classifier click on a different feed shows a fresh banner
  • Scope toggles (feed/folder/global) re-issue the filter for Premium Archive users; non-archive users see the "denied" shake
  • "Browse stories by classifier" works for river (all site stories), folder, and single feed views

Local validation after syncing main

Validated on a local, unstaged merge of current origin/main into this branch. The PR head has not been changed.

  • Exercised populated tag, author, title, URL, and text filters against real local feed data.
  • Verified filtered URLs restore after a full reload and remain active when changing oldest/newest order.
  • Checked both light and dark themes.
  • Added focused regressions for preserving active filters during reloads, title word-boundary matching, and accessible trainer matching-story controls. These fixes are local and unstaged, not present in the PR head yet.

Tag filter, light theme

Tag classifier filter showing matching comics stories

Author filter, light theme

Author classifier filter showing stories by John Voorhees

Title filter, light theme

Title classifier filter showing Apple title matches

URL filter, dark theme

URL classifier filter showing linked URL matches

Text filter after changing sort order, dark theme

Text classifier filter preserved after changing story order


Generated with Claude Code

samuelclay and others added 10 commits April 10, 2026 19:00
…rl/text

Lets users click a pill in a story or the trainer and jump to a filtered
view of every unread story in the current feed/folder/river that matches
that single classifier value. Filter state is reflected in the URL as
?classifier_tag=... / ?classifier_author=... etc., so it's bookmarkable
and survives back/forward.

Training from the inline banner saves through the same save_classifier
endpoint the trainer uses, recomputes story scores on the fly via
recalculate_story_scores + render:intelligence, and fires feed_unread_count
so sidebar badges update in place. Stories trained to dislike/super-dislike
stay visible in the filter view (threshold forced to -2) so the user can
see exactly what they just trained without rows vanishing out from under
them.

Backend filtering is intentionally "dumb": stories load via the normal
feed/usersub/river paths and are narrowed in Python by
filter_stories_by_classifier. No dedicated ES index — a 20x batch
multiplier on the effective limit gives post-filtering enough raw stories
to catch every match the unread counts promise.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* main:
  Require Android version bump commits to be tagged
  Fix Android auto mark-read so stories flip while still visible
  Bump Android version to 14.2.2 (269)
  Apply black and isort formatting across apps and newsblur_mcp
  Shrink Reading fragment args to avoid TransactionTooLargeException
  Bounds-check WidgetRemoteViewsFactory.getViewAt/getItemId
  Fix ReadingStoryMenuPopup coerceIn crash on narrow windows
  Guard Hilt injection in SyncService and SubscriptionSyncService
  Fix APIResponse crash when widget HTTP call is interrupted
  Fix FolderListFragment ClassCastException on long-press
  Fix NotSerializableException writing Story to activity state Bundle
* main:
  Bump iOS version to 14.5 (build 326)
  Stabilize river story hash test fixtures
  Bump Android version to 14.3
  Add Android story clustering badges and controls
  Add iOS story clustering controls
  Enable IP rate limiting at 500 req/5min to block abusive API clients
  Add Title vs Title+Related cluster mode with per-sibling tier badges
Dark mode selectors used .NB-theme-dark/.NB-theme-black which don't
exist — the actual body class is .NB-dark, so no dark styles applied.

Tags with special characters (e.g. "ai & ml") were double HTML-encoded
because .html() was used instead of .text() to extract tag pill values,
causing the filter to find zero matching stories.

Also simplifies comments and removes redundant code across the
classifier filter view, banner, and backend filter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The scope toggles on the classifier filter banner only shook silently
when non-archive users clicked folder/global scope. The trainer and
notification bell both show a "Requires Premium Archive" tooltip and
flash the toggle amber. Match that behavior on the banner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* main:
  Cap iOS UI smoke test runtime
  Fix CI timeouts and iOS custom-domain test
The bell on the classifier filter banner only bound click, so hover
didn't open the popover. Once opened, the channel indicators vanished
after ~1s because toggling a channel only updated bell data locally,
never persisted to the server, and the mouseleave handler then called
render() which rebuilt the bell from empty server state.

Add mouseenter/mouseleave handlers with the same timer/hover pattern
the trainer uses, make the popover sticky via a _popover_hovered
flag, and pass an on_change callback to ClassifierNotificationPopover
that saves via set_classifier_notification immediately. Drop the
destructive render() on close since the popover already updates the
bell indicators in place.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* main: (63 commits)
  Avoid unread cache rebuilds on all-story rivers
  Fix daily briefing ignoring super-disliked classifiers
  Fix unread status on all-story river pages
  Add sticky story side actions preference
  Add iOS 14.5.4 release notes
  Track MCP usage metrics
  Bump iOS version to 14.5.4
  Handle partial failures in OPML imports
  Fix iOS activity interactions loading
  Bump Android version to 14.3.3
  Log Android reader restore mismatches
  Fix Android reader rotation after mark-read
  Return MCP upstream HTTP failures as tool errors
  Return MCP archive requirement as tool error
  Accept singular feed ID in MCP stories
  Fix MCP resource serialization
  Fix trial users skipped during paid premium upgrade
  Fix Android scroll position lost when leaving and returning to app
  Fix grace period email suppressed across annual expirations
  Fix iOS story detail classifier cycling
  ...
* origin/main: (295 commits)
  chore(ios): bump version to 14.7.2
  chore(android): bump version to 14.5.8
  feat(ios): add Good Reads to feed list
  feat(android): add Good Reads to feed list
  feat(android): model Good Reads trending feed
  Stop rewriting Open RSS first-party feeds to /feed/ preview paths
  Route legacy haiku briefing key to Luna instead of Sonnet-class anthropic tier
  Switch briefing and webfeed AI to Luna
  Infer RSS image media from untyped URLs
  Add Feedler and Web Subscriber to the Goodies dialog after auditing prod OAuth apps
  Replace the Tips & Tutorial dialog with a Features & Tips showcase modal
  Redesign the Goodies & Extras dialog with platform tabs and audited, screenshot-rich app listings
  Move Common Questions below the testimonials on the welcome page
  Redesign the welcome feature groups as reader-style folder rows
  Move Android Ask AI to Claude Sonnet 5 and bump to 14.5.7 (282)
  Switch Ask AI Anthropic thinking to the adaptive mode Sonnet 5 requires
  Move iOS Ask AI to Claude Sonnet 5 and bump to 14.7.1 (335)
  Move Ask AI and Daily Briefing to Claude Sonnet 5 via a new chat model tier
  Fall back to the first feed when opening Statistics on a river or folder
  Send remaining premium upsells to the upgrade modal instead of the feedchooser
  ...

# Conflicts:
#	apps/reader/views.py
@samuelclay

Copy link
Copy Markdown
Owner Author

Visual polish preview (local, unstaged)

These are the requested follow-up visual changes running from the local worktree. The PR branch has not been pushed: the screenshots show the compact banner hierarchy, live result context, clearer controls, match highlighting, empty-state recovery, and the trainer entry point that are currently left unstaged.

Author filter in dark mode

Dark theme author filter with matching authors highlighted

Tag filter in light mode

Light theme tag filter with matching stories

Title match highlighting

Title filter with MacStories highlighted in matching story titles

URL match context

URL filter showing the matching URL fragment beneath each story

Text match highlighting

Text filter with matching words highlighted in story previews

Empty state and wider search action

No-match state with Search All Site Stories and Clear filter actions

Trainer matching-story affordance

Classifier trainer rows with magnifying-glass buttons for viewing matching stories

@samuelclay

Copy link
Copy Markdown
Owner Author

Segmented-control label follow-up (local, unstaged)

Added concise text beside every segmented-control icon: Site / Folder / All for scope and Like / Dislike / Hide for training. The banner clear action is now the X alone, while retaining its accessible Clear classifier filter label.

Light theme, narrow split pane

Classifier filter controls with visible segment labels in the light theme

Dark theme, narrow split pane

Classifier filter controls with visible segment labels in the dark theme

Validated locally in the real reader with Computer Use. These follow-up changes are intentionally uncommitted and unstaged.

@samuelclay

Copy link
Copy Markdown
Owner Author

Inline notification controls (local, unstaged)

Replaced the standalone notification bell/popover in the filter banner with a third aligned NOTIFY ON row for Email, Web, iOS, and Android. Hide keeps its clearer label but now uses NewsBlur's double-thumbs-down Super Dislike icon.

Premium Archive

Channels are directly toggleable in the segmented control; the active Email channel is shown here.

Classifier filter banner with inline notification channels for a Premium Archive account

Upgrade state

For accounts below Premium Archive, the same channels remain visible but disabled, with an inline upgrade action that opens the Premium modal focused on Notifications.

Classifier filter banner with disabled notification channels and an Upgrade to Premium Archive action

Validated against the local reader in both plan states. The Computer Use helper was unavailable during this pass, so these screenshots were captured with a disposable local Chromium render; no changes were committed or staged.

@samuelclay

Copy link
Copy Markdown
Owner Author

Responsive story-pane pass: the classifier controls now stay at the full, hover-equivalent opacity at rest. Rows stack below 380px, icons simplify below 240px, and controls become a compact widen cue below 200px so nothing clips, including at the app's 68px minimum.

Classifier filter responsive states from 420px down to the 68px minimum

@samuelclay

Copy link
Copy Markdown
Owner Author

Resolved the split-pane resize behavior from the recording. The story carousel now uses percentage positioning during live resize and cancels any in-flight tab animation; an automated 120px drag against the running worktree sampled both edges 13 times with a maximum alignment error of 0px on the left and right.

Before: the story detail contracts from both sides during the drag

Before: story detail pane pulls inward on the right while its left edge follows the divider

After: the story detail remains attached to both pane boundaries throughout the drag

After: story detail pane remains aligned to both boundaries during live resize

@samuelclay

samuelclay commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

Proposed hierarchy pass, currently unstaged and not pushed: “Filter this view” is a standalone temporary scope, while “Save to account” is separated and branches into Train and Notify on. The distinction comes from section structure, indentation, and connector lines rather than another background treatment.

Dark theme and responsive states

Classifier banner hierarchy in dark theme from 420px to the 68px minimum

Light theme and responsive states

Classifier banner hierarchy in light theme from 420px to the 68px minimum

@samuelclay

Copy link
Copy Markdown
Owner Author

Updated hierarchy proposal, still unstaged and not pushed: the temporary control now says Filter stories, while Train and Notify on share a subtly darker inset surface. I removed both the “Save to account” heading and the left connector/tree, and tightened the responsive rules so all notification channels remain contained through the 230px pane.

Dark theme classifier banner at six story-pane widths

Light theme classifier banner at six story-pane widths

@samuelclay

Copy link
Copy Markdown
Owner Author

Latest follow-up, still unstaged and not pushed: fixed the Notify on row crossing the inset border at the ~345px pane width, and removed the matching-story summary line entirely so its count no longer churns while pages load. These matrices focus on the 300–390px breakpoint range where the overlap could occur.

Dark theme classifier banner from 300 to 390 pixels with notification controls contained

Light theme classifier banner from 300 to 390 pixels with notification controls contained

@samuelclay

Copy link
Copy Markdown
Owner Author

Icon migration preview, still unstaged and not pushed: the Story Titles banner now reuses the Training dialog’s existing noun thumb silhouettes. Like uses the established green, Dislike the red, and Hide the darker red with the layered double-thumb icon; the Training dialog pills themselves are unchanged. Each matrix cycles through all three active states.

Dark theme banner showing Like, Dislike, and Hide with migrated Training dialog icons

Light theme banner showing Like, Dislike, and Hide with migrated Training dialog icons

@samuelclay

Copy link
Copy Markdown
Owner Author

Neutral-state follow-up, still unstaged and not pushed: the three Train icons now retain their sentiment colors even when no training choice is selected. Labels stay neutral; the underline remains the selected-state indicator.

Dark theme neutral classifier banner with green Like, red Dislike, and dark-red Hide icons

Light theme neutral classifier banner with green Like, red Dislike, and dark-red Hide icons

@samuelclay

Copy link
Copy Markdown
Owner Author

Notification-control proposal, still unstaged and not pushed: reused the banner’s Email/Web/iOS/Android icons in the feed-options popover, classifier hover popover, and both feed/classifier rows in the Notifications dialog. Enabled channels keep the existing flat fill and add a thin blue underline; the controls remain gradient-free and borderless.

Light theme notification popovers and dialog with channel icons and enabled underlines

Dark theme notification popovers and dialog with channel icons and enabled underlines

@samuelclay

Copy link
Copy Markdown
Owner Author

Android-width follow-up, still unstaged and not pushed: widened only the four-channel notification controls from 232px to 248px, leaving the Unread/Focus segmented control unchanged. Android now has comfortable trailing space in both popover and dialog rows.

Notification channel controls with corrected Android width in light mode

Notification channel controls with corrected Android width in dark mode

@samuelclay

Copy link
Copy Markdown
Owner Author

House-style classifier banner follow-up, still unstaged and not pushed: all three segmented controls now use NewsBlur’s borderless rail and gray selected surface while retaining the custom icons, colors, and enabled underline. The narrow example also shows the 340px containment fix: the decorative classifier badge drops out before the notification channels can cross the account panel.

House-style classifier banner with active Site, Like, and Email controls in light mode

House-style classifier banner with active Site, Like, and Email controls in dark mode

House-style classifier banner contained at a 340px story pane in dark mode

@samuelclay

Copy link
Copy Markdown
Owner Author

Verified the tag matching flow end to end from All Site Stories. The default Folder scope stays in the current river, Site switches to the originating feed, and All returns to the global river; each loaded result is an exact frontpage deals tag match, the clicked story remains present even after being marked read, and the temporary All · Newest display does not overwrite the saved Unread preference.

Folder — stays in All Site Stories

Folder scope stays in All Site Stories and shows exact tag matches

Site — uses the story’s original feed

Site scope switches to the originating Slickdeals feed and retains the source story

All — searches across all subscribed sites

All scope searches the global river for exact tag matches

@samuelclay

Copy link
Copy Markdown
Owner Author

Fixed the stationary-pointer hover bug from the screen recording. The lazy classifier-pill tooltip was being initialized by layout-generated mouseenter events and then forcibly shown even after the moving story header had passed under the pointer; it now initializes only after real pointer movement.

Before — selecting a lower story opens a tooltip without moving the pointer

A View matching stories tooltip remains open even though the pointer did not intentionally hover the tag

After — the same stationary-pointer interaction stays quiet

Selecting the tagged story with a stationary pointer does not open a tooltip

Intentional hover still works and dismisses normally

Moving the pointer onto the frontpage deals tag opens one View matching stories tooltip

Regression coverage is included; the full frontend suite passes 39/39.

@samuelclay

samuelclay commented Sep 3, 2026

Copy link
Copy Markdown
Owner Author

Narrow List follow-up

Extra-narrow List rows now stack the feed, headline, byline, summary, and date instead of squeezing desktop columns into the narrow pane. At this width thumbnails are hidden to preserve readable copy, while stories still expand inline.

Extra-narrow List rows with readable stacked metadata

The matching-stories control now uses the stacked-cards icon, keeps its hover state inside the author/tag pill, and lazily shows the View matching stories tooltip without interrupting story rendering.

Expanded story with the View matching stories tooltip

samuelclay and others added 2 commits September 2, 2026 22:35
Folder and global tag filters previously loaded a river window and
post-filtered it, so a tag that only appears on a handful of stories
could come back empty or miss most of its matches. Premium Archive
users now page through SearchStory.query_tag, which matches one exact
tag in tags.raw across the selected feeds, and the story that opened
the filter is pinned onto page one even when the index has not caught
up with it yet. Non-tag filters keep the bounded scan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mzAvw4kxMeCwwpAk7XTBs
The filter banner's Site/Folder/All toggle now only changes which
stories are listed. Training and notification writes use their own
classifier_scope and classifier_folder_name, so browsing a tag across
a folder no longer silently retargets the classifier itself, and
non-archive users can widen the view without the denied-scope shake.

Opening a filter now records where it came from: the source feed, the
source story hash, and the river or folder the user was reading. The
banner uses that to switch scopes by navigating to the right feed or
river instead of reloading the current list in place, the URL carries
the full context so a reload or shared link restores the same view,
and story pills and the trainer dialog build their filter options
through classifier_filter_context_for_matching_view.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mzAvw4kxMeCwwpAk7XTBs
samuelclay and others added 8 commits September 2, 2026 22:37
A classifier filter is meant to surface every story a classifier would
touch, so the request now sends read_filter=all whenever a filter is
active instead of honoring the feed's saved unread setting. The feed
title, folder header, and feed options popover display that temporary
"all" so the UI matches what was actually loaded, while the saved view
preference is left untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mzAvw4kxMeCwwpAk7XTBs
Author, tag, and URL classifiers in the story detail used a tippy
popup that appeared on hover and carried the browse action inside it.
That was easy to miss and impossible to reach from the keyboard. Each
classifier now renders a small always-present button with its own
tooltip, the pills switch to inline-flex so the icon sits beside the
text, and the URL pill wraps its highlighted segments so the button
does not get clipped by the ellipsis. Inline training still toggles on
the classifier text; the button is stripped before that handler reads
the value.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mzAvw4kxMeCwwpAk7XTBs
The banner re-rendered on every story collection change to keep a
"12 matching stories in ..." line current, which flickered while
pages loaded and repeated what the story list already shows. Remove
the subtext, its listeners, and the format_result_summary helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mzAvw4kxMeCwwpAk7XTBs
Split the banner tools into two labeled sections: "Filter stories"
holds the temporary Site/Folder/All view toggle, and a bordered
account section holds the training and notification controls that
actually write to the account. The segmented controls drop their
bordered pill look for the flat house style with a hover well and an
active underline, the training buttons reuse the trainer's thumbs-up
and thumbs-down mask icons and sentiment colors instead of inline
SVGs, and the container breakpoints tighten so the notification
buttons compact before they overflow the account section.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mzAvw4kxMeCwwpAk7XTBs
The feed notification dialog, the classifier notification popover, and
the inline classifier notification rows each rendered plain Email,
Web, iOS, and Android labels with their own segmented styling. They
now share make_channel_content and the CHANNEL_ICONS set from the
popover, so every channel shows the same icon beside its label with a
flat active underline, and the four-channel rows widen slightly so
the Android icon and label fit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mzAvw4kxMeCwwpAk7XTBs
The desktop river list positions the feed name, date, and optional
image in fixed columns. When the story titles pane is dragged to its
minimum width those reservations can leave the headline only a few
characters wide. Under NB-extra-narrow-content the feed line and date
become stacked rows, the image column is hidden, and the title gets
the full width.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mzAvw4kxMeCwwpAk7XTBs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mzAvw4kxMeCwwpAk7XTBs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant