From fc25307e7f258b1043e1367a44a8800d03085c66 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 13 Nov 2026 08:40:44 +0000 Subject: Make an image zoomable even when the feed didn't spell out where it lives Tapping an image in a Ftrain article did nothing. The zoom affordance was there, and the rendering path was the ordinary one -- the feed's own HTML, with reader mode off -- but a tap went nowhere at all. Tap detection works by wrapping every article image in a link to a scheme nothing can navigate to, which the WebView then hands to shouldOverrideUrlLoading. That href has to carry the complete image URL on its own, so an image whose src was relative got no wrapper and no tap: better an image that can't be zoomed, the reasoning went, than a link that goes nowhere. Ftrain's is entirely site-relative -- every image in it is src="/art/uploads/..." -- so not one of them was zoomable. That reasoning had an unexamined premise. The page itself resolves those srcs perfectly well, against the base URL loadReaderHtml already hands loadDataWithBaseURL: the article's own link. Resolving them the same way at HTML-assembly time produces exactly the URL the WebView went and fetched, so the wrapper can be built after all. Both reader surfaces now pass that base URL down to the zoom pass. Where there genuinely is no base URL to resolve against -- nothing else in the app loads reader HTML without one today -- the old behavior stands and the image stays un-zoomable rather than acquiring a dead link, and a data: or javascript: src is still refused however it was reached, since the viewer could never open one. This is why the report could only have come from a feed with reader mode off. Reader mode never hits it: Readability rewrites every src to an absolute URL before the extracted HTML reaches us, which is also why the same images zoom fine the moment the feed's reader toggle is on. TDD: site-relative and path-relative resolution, and the whole trip through wrapReaderHtml, were written first against the unused parameter and went red on assertion mismatches (3 of 38 in ImageZoomTest), green on the resolution itself. The data:-src case was written at the same time and passed immediately -- it pins existing behavior against the new code path rather than having driven it. Verified on the emulator against the reported article itself, subscribed live from ftrain.com/feed.xml with reader mode off: on the previous build, a tap on the header image left the screen byte-identical; on this one the same tap at the same point opens the full-screen viewer. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B --- .../java/org/terst/feedreader/ui/ImageZoomTest.kt | 49 ++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) (limited to 'app/src/test/java/org/terst/feedreader/ui/ImageZoomTest.kt') diff --git a/app/src/test/java/org/terst/feedreader/ui/ImageZoomTest.kt b/app/src/test/java/org/terst/feedreader/ui/ImageZoomTest.kt index 3f7078d..aead885 100644 --- a/app/src/test/java/org/terst/feedreader/ui/ImageZoomTest.kt +++ b/app/src/test/java/org/terst/feedreader/ui/ImageZoomTest.kt @@ -38,13 +38,45 @@ class ImageZoomTest { } @Test - fun `a relative image source is left alone`() { - // Relative srcs resolve against the WebView's base URL, which a custom-scheme href - // can't reproduce -- better to leave the image un-zoomable than to build a dead link. + fun `a relative image source is left alone when nothing says where it came from`() { + // Relative srcs resolve against the WebView's base URL -- with no base URL to resolve + // against, better to leave the image un-zoomable than to build a dead link. val original = """""" assertEquals(original, linkifyImagesForZoom(original)) } + @Test + fun `a site-relative image source resolves against the page it came from`() { + // Verbatim shape of the Ftrain feed's own markup, which is where this surfaced: + // every image in is site-relative, so none of them were zoomable. + val html = linkifyImagesForZoom( + """x""", + baseUrl = "https://ftrain.com/aboard-let-s-have-a-mouse-party" + ) + assertEquals( + """""" + + """x""", + html + ) + } + + @Test + fun `a path-relative image source resolves against the article's own directory`() { + val html = linkifyImagesForZoom( + """""", + baseUrl = "https://ex.com/posts/2026/hello" + ) + assertTrue(html.contains("""""")) + } + + @Test + fun `a data uri image is still left alone when there is a page url`() { + // Resolving a data: src against a base URL just gives the data: URI back -- it is + // not something the viewer could ever open, so it must not become a zoom target. + val original = """""" + assertEquals(original, linkifyImagesForZoom(original, baseUrl = "https://ex.com/post")) + } + @Test fun `a data uri image is left alone`() { val original = """""" @@ -86,6 +118,17 @@ class ImageZoomTest { assertTrue(html.contains("""""")) } + @Test + fun `reader html makes a feed article's relative images zoomable`() { + // The non-reader path renders the feed's own HTML, which routinely keeps relative + // srcs -- the reader path never hits this because Readability absolutizes them. + val html = wrapReaderHtml( + """

x

""", + baseUrl = "https://ftrain.com/aboard-let-s-have-a-mouse-party" + ) + assertTrue(html.contains("""""")) + } + @Test fun `reader html makes the synthesized hero image zoomable too`() { val html = wrapReaderHtml("

no image of its own

", imageUrl = "https://ex.com/hero.jpg") -- cgit v1.2.3