Skip to content

## v4.1.0: Sort Articles By, and real body text for RSS (2026-10-02)

Choose a tag to compare

@VirInvictus VirInvictus released this 09 Oct 22:04
· 13 commits to main since this release

v4.1.0: Sort Articles By, and real body text for RSS (2026-10-02)

The "Sort Articles By" mode set from the October 2 sync's deferred list (NNW 2ccc95721, 70c3ec809, 24688d50e), plus the ingest change it needs to be true for the dominant feed formats: RSS and Atom items now derive a plain-text body at parse time, which also lights up full-text search and BodyContains smart feeds for those feeds. The unread-count single-flight perf port rides in this release too (it landed between releases). 281 tests; clippy -D warnings clean.

  • Added: the timeline header sort menu gains a "Sort by title" section: A to Z / Z to A beside the existing newest/oldest date modes. Upstream rebuilt its sort options as a Mail-style menu over five fields (date, feed, title, unread, starred); ours is the two-field slice this repo's scope ports, rendered as two labeled sections of radio items because GTK menu models are static where AppKit's direction items re-word themselves per field. Untitled articles sort by the start of their body text, porting 70c3ec809's displayed-title key: trimmed title, then a 300-character plain-text excerpt, then summary, compared case-insensitively; title ties break newest-first in both directions, as upstream hardcodes. Per-window persistence (24688d50e) maps to one global setting here (single-window app): the existing timeline-sort-order GSetting enum appends title-ascending / title-descending, so stored values for the original nicks survive the upgrade, and the window's read-fresh-on-every-reload plumbing picks the new modes up unchanged. Known divergences, recorded on SortOrder: COLLATE NOCASE folds ASCII case only (SQLite has no locale-aware collation), and the timeline cap applies in sort order (the first/last N titles), exactly as it always has for oldest-first.
  • Added: ingest-time plain text. parser::html::strip_html_to_text (hand-rolled, no-regex, the same tokenizer rules as the metadata scanner: script/style raw text and comments can neither leak nor forge markup, tag boundaries emit one space so words never fuse across elements, whitespace runs collapse, 64K-character cap) runs over every RSS item body and Atom entry body at parse time, and the result stores as content_text. Two pre-existing gaps close with it: FTS search indexes title, content_text, so body-word search over RSS/Atom previously matched titles only; and BodyContains smart feeds could never match those bodies. Migration cost: the first refresh after upgrading re-diffs every article once (the diff compares the full row and content_text is now populated; status rows are preserved) and the database grows by the stripped text.
  • Changed: the title sort key is identical across views: the SQL ORDER BY expression (single-feed views) and the Rust merge comparator (folder views) trim the same six ASCII whitespace characters and trim before the 300-character cut, so an article orders the same whether its view sorts inside SQLite or across merged chunks. The merge now computes the allocating key once per article (sort_by_cached_key).
  • Changed: unread-count refreshes run one round at a time. SidebarView::refresh_unread_counts is single-flight with a dirty-flag re-run (the Rust twin of upstream's fetchingAllUnreadCounts / needsRefetchAllUnreadCounts flags): a request arriving while a round runs only raises the dirty flag, and each finished round runs at most one follow-up, so a burst of N status mutations (every keyboard navigation marks its article read) collapses from N pooled queries plus N tree walks to one round plus at most one more. The follow-up is honored on the error path too, so a failed round can't strand the badges stale.
  • Tests: 20 new across parser (stripper unit tests; RSS/Atom derivation pins), database (title ordering, excerpt/summary/NULL fallback, the 300-char cap, tie direction, joined-query and cross-feed merge execution, SQL/Rust key parity between feed and folder views), sidebar (UnreadCountFlight burst-collapse semantics), and preferences (every schema nick maps to its SortOrder).