Browse stories by classifier (tag/author/title/url/text) - #2102
Browse stories by classifier (tag/author/title/url/text)#2102samuelclay wants to merge 22 commits into
Conversation
…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
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 modeTag filter in light modeTitle match highlightingURL match contextText match highlightingEmpty state and wider search actionTrainer matching-story affordance |
Segmented-control label follow-up (local, unstaged)Added concise text beside every segmented-control icon: Light theme, narrow split paneDark theme, narrow split paneValidated locally in the real reader with Computer Use. These follow-up changes are intentionally uncommitted and unstaged. |
Inline notification controls (local, unstaged)Replaced the standalone notification bell/popover in the filter banner with a third aligned Premium ArchiveChannels are directly toggleable in the segmented control; the active Email channel is shown here. Upgrade stateFor accounts below Premium Archive, the same channels remain visible but disabled, with an inline upgrade action that opens the Premium modal focused on Notifications. 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. |
|
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 After: the story detail remains attached to both pane boundaries throughout the drag |
|
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 Light theme and responsive states |
|
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. |
|
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. |
|
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. |
|
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. |
|
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. |
|
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 Folder — stays in All Site Stories Site — uses the story’s original feed All — searches across all subscribed sites |
|
Fixed the stationary-pointer hover bug from the screen recording. The lazy classifier-pill tooltip was being initialized by layout-generated Before — selecting a lower story opens a tooltip without moving the pointer After — the same stationary-pointer interaction stays quiet Intentional hover still works and dismisses normally Regression coverage is included; the full frontend suite passes 39/39. |
Narrow List follow-upExtra-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. 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. |
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
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
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017mzAvw4kxMeCwwpAk7XTBs







































Summary
save_classifierand recompute story scores on the fly.?classifier_tag=foo/?classifier_author=foo/?classifier_title=foo/?classifier_url=foo/?classifier_text=foo(plus optionalclassifier_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.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
classifier_tagtoclassifier_author, no stale keys left behindLocal validation after syncing main
Validated on a local, unstaged merge of current
origin/maininto this branch. The PR head has not been changed.Tag filter, light theme
Author filter, light theme
Title filter, light theme
URL filter, dark theme
Text filter after changing sort order, dark theme
Generated with Claude Code