<feed xmlns='http://www.w3.org/2005/Atom'>
<title>feedreader.git/app/src/main/java/org/terst/feedreader/data/FeedParser.kt, branch master</title>
<subtitle>FeeDreader — personal Android RSS reader
</subtitle>
<id>https://git.terst.org/feedreader.git/atom?h=master</id>
<link rel='self' href='https://git.terst.org/feedreader.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.terst.org/feedreader.git/'/>
<updated>2026-10-22T22:13:32+00:00</updated>
<entry>
<title>Name a title-less feed item by its own opening words</title>
<updated>2026-10-22T22:13:32+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-10-22T22:13:32+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/feedreader.git/commit/?id=d4538340fdbe7016a7b3a2eb040c7b133df19efe'/>
<id>urn:sha1:d4538340fdbe7016a7b3a2eb040c7b133df19efe</id>
<content type='text'>
RSS requires an item to carry a title or a description, not both, and
plenty of feeds take the second option. Mastodon's tag feeds are the
case that surfaced it: every one of the 20 items in
mastodon.social/tags/giftlink.rss has a description and no title at
all, so subscribing to one produced a list of blank rows with nothing
to read or tap.

A post says what it is, it just says it in the body, so the item is
now named by its own first sentence -- falling back to its opening
words when the sentence runs long, and to its link when there are no
words at all.

Two details that the live feed taught rather than the fixture:

Inline tags are removed without leaving a gap, and only block
boundaries become a space. Mastodon splits a hashtag as
`#&lt;span&gt;giftlink&lt;/span&gt;` and a URL as `&lt;span class="invisible"&gt;https://
&lt;/span&gt;&lt;span&gt;wapo.st/x&lt;/span&gt;`, so turning every tag into a space tore
both apart -- first pass produced "# HumaneRights # USPolitics" and
"the loophole https:// wapo.".

A full stop only ends a sentence when whitespace or the end of the
text follows it, or the dot in "wapo.st/abc" ends it instead -- which
is how that same title got cut at "https://wapo.".

It also skips an opening fragment shorter than 20 characters: naming
an article "Wow." helps nobody.

Deliberately free of android.text.Html, unlike the parser's existing
entity decoding -- that pulls in the framework, which is why
FeedParser has no unit tests. This part does, 22 of them.

Verified on a device against the live feed: 20 items, 0 blank titles,
URLs and hashtags intact.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Fix Atom feeds crashing the whole parse on xhtml content/summary</title>
<updated>2026-08-22T00:54:33+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-08-22T00:54:33+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/feedreader.git/commit/?id=5c5ba596a50274aa4d99dc682fb34d43bb7f7326'/>
<id>urn:sha1:5c5ba596a50274aa4d99dc682fb34d43bb7f7326</id>
<content type='text'>
&lt;content type="xhtml"&gt;/&lt;summary type="xhtml"&gt; wrap real nested markup,
which XmlPullParser's nextText() can't handle (it requires a text-only
element) -- it threw and aborted parsing of the entire feed over one
xhtml-typed field. Walks and reconstructs the inner markup manually
instead, tracking depth against the parser's own getDepth().
</content>
</entry>
<entry>
<title>Fix crash on blank-guid articles; simplify browser back; backlog notes</title>
<updated>2026-07-23T01:59:14+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-23T01:59:14+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/feedreader.git/commit/?id=3914ba17608884a6fa8614b9058752e0c1c0d94f'/>
<id>urn:sha1:3914ba17608884a6fa8614b9058752e0c1c0d94f</id>
<content type='text'>
- FeedParser: a feed with a literal empty &lt;guid&gt;/&lt;id&gt; element (confirmed
  live: a Mother Jones entry) produced an ArticleEntity with id = "" --
  the fallback chain only checked for null, not blank, so an empty-but-
  present tag passed straight through. An empty-string primary key broke
  Navigation Compose's route matching and crashed on tap ("tapping this
  article crashes the app"). Fixed both RSS and Atom paths to treat blank
  the same as null before falling back to a link-based id. Schema v5
  migration deletes any row already on-device with the bad id; it'll
  come back with a real, hash-based id on the next fetch of that feed.
  Verified live: zero blank-id rows after the migration and a fresh
  fetch, no crash.

- BrowserScreen: back (both the system gesture and the in-app arrow) now
  always closes the screen directly, rather than stepping through
  in-page WebView history first. Reported live as not making sense for
  multi-page browsing (search results -&gt; article -&gt; another link, etc.)
  -- you'd have to tap back that many times just to leave. There's no
  separate in-page-back affordance now, matching how every other screen
  in this app behaves.

- Tried moving swipe-to-next-article detection off Compose's gesture
  system entirely (a passive native OnTouchListener on the WebView, never
  consuming) on the theory that Compose's gesture arbitration was the
  source of reported scroll jerkiness. Reverted after confirming live
  that swipe stopped working at all with that approach -- a worse
  regression than unproven jerkiness. Back to the known-working
  Compose-based detector; the jerkiness complaint itself is still open.

- BACKLOG.md: added bug-report-button, smaller-title-text, and
  bottom-action-strip-with-auto-hide notes.
</content>
</entry>
<entry>
<title>Split save/Stash actions, follow links in-app via reader pipeline, tune scroll, fix title entities</title>
<updated>2026-07-22T18:26:14+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-22T18:26:14+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/feedreader.git/commit/?id=91edd1f5f29a3336fa9e3bd1762c70ea74e3e48d'/>
<id>urn:sha1:91edd1f5f29a3336fa9e3bd1762c70ea74e3e48d</id>
<content type='text'>
- ReaderScreen: bookmark icon is now a purely local save (just flips isSaved,
  no network) -- Stash archiving is a separate explicit action (new cloud
  icon), since it's a heavier networked action the user should trigger on
  purpose rather than have bundled into "save." Verified via direct DB
  queries and (once I stopped misreading small screenshots) the icon
  updates immediately in-session for both actions.
- New ReaderWebView.kt: shared WebView factory for the dark reader palette
  and link handling, used by both ReaderScreen and the new LinkReaderScreen.
  Links tapped inside an article's WebView now open in a lightweight
  in-app reader (extracted via the same Readability pipeline, no DB
  persistence) instead of falling through to the system browser --
  recursive, so a chain of links stays in-app. Verified live: tapping a
  link mid-article navigated to a real extracted page with a working back
  stack.
- WebView scroll tuning: overscroll glow and scrollbars off, on both the
  article reader and the new link reader -- aimed at the reported "still a
  little choppy" scrolling on top of the previous reload-on-recompose fix.
- FeedParser: some feeds (kottke.org among them) double-encode titles --
  the XML parser resolves outer XML escaping, but leftover HTML entities
  like "&amp;#8220;" were showing up literally in the title text. Titles are
  now run through a second HTML-entity decode pass. Verified: a kottke
  title that previously showed a literal &amp;#8220; now renders the actual
  curly quote.

All changes verified live on the emulator against the real 107-feed dataset.
</content>
</entry>
<entry>
<title>First round of testing feedback: images, unread landing, dark, delete</title>
<updated>2026-07-22T16:26:45+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-22T16:26:45+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/feedreader.git/commit/?id=52d7877fccd60a1dae33d877ba6aa765dc098a52'/>
<id>urn:sha1:52d7877fccd60a1dae33d877ba6aa765dc098a52</id>
<content type='text'>
- Fix OPML import status message reporting the raw pre-dedup count
  (144) instead of the actual unique feed count after Room's upsert
  collapses duplicates (108) -- cosmetic bug, the DB was already
  correct.
- Article thumbnails: extract media:thumbnail/enclosure or the first
  &lt;img&gt; in feed content, show via Coil in the article list.
- App now lands on Unread instead of the feed list; a top-bar icon
  gets from there to the feed list (forward nav, not back-stack pop,
  since Unread is now the root).
- Dark color scheme by default.
- Delete-feed action (with confirmation) on each feed row -- was
  entirely missing. Deletes its non-saved articles too, so they don't
  linger as orphaned rows in the cross-feed Unread view; saved articles
  are kept regardless.
- Room migration 1-&gt;2 for the new imageUrl column (real migration, not
  destructive fallback, since there's now real on-device subscription
  data from testing).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
</content>
</entry>
<entry>
<title>Initial commit: Feeds, an Android RSS reader</title>
<updated>2026-07-22T08:08:58+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-07-22T08:08:58+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/feedreader.git/commit/?id=d8ce4b3c0f383e844b125d2d3abd618715bb3c89'/>
<id>urn:sha1:d8ce4b3c0f383e844b125d2d3abd618715bb3c89</id>
<content type='text'>
Feed subscriptions (RSS 2.0 + Atom, hand-rolled parser), OPML
import/export, WorkManager periodic fetch with conditional GET,
Room-backed local storage (feeds/articles/filter rules), keyword
mute/highlight filtering.

Reader mode: on-device extraction via Mozilla's Readability.js
(vendored from github.com/mozilla/readability, pinned to release
0.6.0) run in a hidden WebView, for feed articles whose provided
content is too thin to be the real article body.

Save: explicit action (from a feed article, or the Android share-sheet
target for any URL) archives to Stash via POST /artifacts?sync=true,
while a local on-device copy (Readability-extracted) is always kept so
saved articles read offline regardless of Stash's reachability.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
</content>
</entry>
</feed>
