Add offline article-image caching behind a setting#5352
Open
davidfwatson wants to merge 2 commits into
Open
Conversation
davidfwatson
force-pushed
the
feature/offline-article-images
branch
3 times, most recently
from
July 2, 2026 11:44
825dad8 to
acba163
Compare
davidfwatson
force-pushed
the
feature/offline-article-images
branch
2 times, most recently
from
July 4, 2026 11:03
c762b1c to
e4a6df7
Compare
Author
|
Rebased onto the latest Since this PR was opened, |
davidfwatson
force-pushed
the
feature/offline-article-images
branch
2 times, most recently
from
July 14, 2026 05:07
bff01e2 to
4748ec9
Compare
Optional, off-by-default caching of the images embedded in article HTML: prefetched during refresh and served from a local cache at render time, so articles can be read with their images while offline. Addresses Ranchero-Software#1873; a step toward the unified image handling in Ranchero-Software#3460. - ArticleImagePrefetcher observes .AccountDidDownloadArticles and, for each new article, extracts its <img> URLs (off the main thread, via the unit-tested ArticleImageURLExtractor) and fetches them through ArticleImageDownloader into a disk cache, throttled rather than in one unbounded burst. - With the setting on, main.js / main_ios.js rewrite http(s) <img src> to nnwimage://cache/…; ArticleImageSchemeHandler resolves those memory → disk → network. The img's srcset — and, inside a <picture>, its <source> candidates — are dropped on rewrite so WebKit can't select an original URL that bypasses the cache offline. The enable flag is passed to main.js as a hidden DOM marker (not an inline <script>) so it still applies when the reader has article JavaScript turned off. - ArticleImageDownloader mirrors ImageDownloader/FaviconDownloader (full-size bytes in a separate ArticleImages folder, ~50MB NSCache, 20MB disk-write cap with image-type sniffing). App-side fetches reuse the web view's content blocker via ArticleImageContentBlocker, so tracker/ad URLs stay blocked — including on cross-host redirects. The folder is swept by the 3-day CacheCleaner and purged outright when the setting is off. - Prefetch only catches future refreshes, so switching the setting on offers to backfill images for the current unread articles right away (a "Cache Images Now? / Not Now" prompt). There's no standalone button — toggle off and on to backfill again later. A progress line and the current cache size appear under the setting, and in the iOS account-stats screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu69KNiXXmKjfmgcnS8RZU
resizeWindow(toFitView:) sized the (non-resizable) Preferences window to each pane's fixed storyboard frame, so a pane whose content outgrew that frame — like General after gaining the offline-images rows — was clipped off the bottom. Measure the pane's real Auto Layout fitting height at the fixed window width instead — before the view is added to the window hierarchy, so its height is free — and size the window to that, letting panes grow to fit rather than squishing or clipping their last control. A <2000pt clamp guards against the old unbounded-width measurement ballooning the multiline label. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu69KNiXXmKjfmgcnS8RZU
davidfwatson
force-pushed
the
feature/offline-article-images
branch
from
July 14, 2026 05:46
4748ec9 to
fbc8c46
Compare
davidfwatson
marked this pull request as ready for review
July 14, 2026 07:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an optional, off-by-default setting that caches article images during refresh and
serves them from a local cache at render time, so articles read with images while offline
(planes, trains, tunnels, metered data). Addresses #1873; a step toward #3460.
Design note: what happens when you turn it on
Prefetch only catches images from future refreshes, so the moment you enable this —
typically right before you lose connectivity — your existing unread articles still have
no cached images. To bridge that, enabling the setting offers to cache images for your
current unread articles right away (a "Cache Images Now? / Not Now" prompt showing the
unread count). There's deliberately no standalone "cache everything" button: if you want
to backfill again later, toggle the setting off and back on.
How it works
Two halves sharing an on-disk cache:
ArticleImagePrefetcherwatches.AccountDidDownloadArticles, extracts<img>URLs off the main thread (a sync can deliver a large batch at once), and fetchesthem via
ArticleImageDownloader— throttled, not one unbounded burst. The pureURL-extraction is a nonisolated, unit-tested
ArticleImageURLExtractor.main.jsrewriteshttp(s)<img src>tonnwimage://cache/?u=…;ArticleImageSchemeHandler(aWKURLSchemeHandler) resolvesthose memory → disk → network. Uncached images load online-only; cached ones load
offline. On rewrite it also drops the img's
srcsetand, inside a<picture>, itssibling
<source>candidates, so WebKit can't select an originalhttpsURL thatbypasses the cache. The enable flag reaches
main.jsas a hidden DOM marker rather thanan inline
<script>, so it applies even when the reader has article JavaScript off (thatsetting gates content JS;
main.jsruns as an injected user script).ArticleImageDownloadermirrors the existingImageDownloader/FaviconDownloaderpattern(same
Downloader,ImageMetadataDatabasebackoff,BinaryDiskCache) but keeps full-sizebytes in a separate
ArticleImagesfolder. Same-URL requests coalesce; the in-memoryNSCacheis bounded (~50 MB, cleared on background/low-memory); only plausible imageresponses persist (an
image/*type or recognized magic bytes, under a 20 MB cap). App-sidefetches reuse the web view's content blocker via
ArticleImageContentBlocker, so tracker/adURLs stay blocked — including on cross-host redirects. The
ArticleImagesfolder is swept bythe existing 3-day
CacheCleanerflush, and purged outright when the setting is off.The setting
cacheImagesForOffline, off by default — iOS: Settings → Articles; Mac: Settings → General.While a backfill runs, the settings line shows live progress ("Caching Images… N of M");
when idle it shows the current cache size ("166 images · 63.5 MB used"). iOS account-stats
gains an Offline Images section with the same figures.
Key limitations
so a few hotlink-protected images may fail when the setting is on.
but the shared
RSWeb.Downloaderstill buffers whole bodies first. A streaming cap belongs inDownloader(it affects every image path) and is left as shared-infra follow-up.Testing
ArticleImageURLExtractorTestscovers absolute / relative / protocol-relative resolution,scheme lowercasing,
data-canonical-srcpreference, entity decoding, non-http rejection,dedup, ordering.
(Full internals live in the three commit messages: core caching, on-enable backfill, and the
Mac prefs-pane sizing fix.)