feat(app): reach the blog from the Account hub via a themed Custom Tab (#88) - #121
Merged
Merged
Conversation
The blog at /blog is server-rendered and the API exposes no listing or post endpoint, so there is nothing to render in-app and scraping HTML is not an option. Reaching it is therefore a routing decision, not a data-layer one — no DTOs, no Room, no repository. - The Account hub's menu gains a Blog row that opens the blog in a Custom Tab coloured from the live MaterialTheme colour scheme, which InterlinedListTheme has already resolved from the user's System/Light/Dark setting, so the tab follows that setting with no extra plumbing. The row carries an "opens outside the app" affordance instead of the forward arrow the in-app rows use; AccountMenuRow gained an optional trailing icon for that, defaulted to the existing arrow so every other row is unchanged. onOpenBlog is defaulted on both ProfileRoute and ProfileScreen, so existing call sites and tests compile untouched. - Falls back to a plain ACTION_VIEW intent when no Custom Tabs provider is installed, and to a no-op (not a crash) when nothing can open a web URL. - Deep links: only the app's own interlinedlist://blog[/<slug>] scheme is claimed. The https://interlinedlist.com/blog URLs are deliberately NOT claimed — with no in-app renderer, intercepting a browser-bound link only to hand it straight back to a browser is worse than letting the browser have it, and the no-Custom-Tabs fallback would resolve back into this activity. The parser still normalises the web form so the claim can be switched on later. - androidx.browser added through gradle/libs.versions.toml. Tests: 20 new JVM unit tests covering the entry point URL, deep-link parsing (including the /blogroll prefix trap, look-alike hosts, traversal segments and garbage input), and all three launch outcomes; plus a case in the existing Account hub Compose test asserting the Blog row sits in the menu and routes out. Closes #88
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.
Closes #88. Part of epic #87.
A routing change only — no DTOs, no Room, no repository, no scraping. There is no public blog
JSON endpoint, and the epic rules scraping out.
What changed
informational group, clear of the account actions. It carries an opens-outside-the-app affordance
(
OpenInNew+ "Opens in a browser" content description).MaterialTheme.colorScheme, and light/dark is chosen from the resolved surface colour. BecauseInterlinedListThemehas already resolved System/Light/Dark fromThemeSettingsStore, the tabfollows the user's setting with zero new plumbing —
:core:datastorewas read but notmodified, so there is no collision with Settings: theme preference synced to the account (
theme) #36.ACTION_VIEW→BlogLaunchResult.UNAVAILABLE. It isdeliberately free of Android types, so all three outcomes are covered by plain JVM tests.
androidx.browser 1.8.0added via the version catalog.To carry the external-link affordance, the private
AccountMenuRowgained a defaultedtrailingIcon/trailingContentDescription; all thirteen other rows are unchanged in behaviour.The deep-link decision, and why it is a non-claim
https://interlinedlist.com/blog/...is deliberately NOT claimed. Only the app's owninterlinedlist://blog[/<slug>]scheme is registered. Three reasons:straight back to a browser — the user pays an app launch and loses their own browser's
session/state to arrive at the same page.
ACTION_VIEWon that same URL. If the app were aregistered handler, that intent would resolve back into
MainActivity— a ping-pong loop.Avoiding it would need an explicit package plus a
<queries>block to enumerate browsers:complexity added solely to undo a claim that bought nothing.
autoVerifyon/blog/*also ties the path toassetlinks.jsonupkeep for no gain.BlogLink.webUrlForstill recognises and normalises the web form, so the manifest claim can beswitched on with no parser change once a real renderer exists. The rationale is recorded as a
manifest comment.
Verification
./gradlew :app:assembleDebug testDebugUnitTest→ BUILD SUCCESSFUL. Repo-wide 1279 tests, 0failures (20 new).
BlogLinkTest(14) covers the canonical index route, custom-scheme and web post links,www/case/trailing-slash/query/fragment tolerance, percent-encoded slugs — and negatively: the
/blogrolland/blog-archiveprefix traps,interlinedlist.com.evil.example,../.traversal, non-blog in-app links, garbage, empty and null.
BlogLauncherTest(6) covers Custom Tabused / browser untouched, fallback when no provider, no browser at all reporting failure instead of
crashing, a contained
SecurityException, and dark/light selection against the realILSurfaceDark/ILSurfaceLight.The merged manifest was checked to contain the
interlinedlist://blogfilter and not an httpsblog filter.
Note
The Compose assertion for the menu row is compile-verified only, never executed (no emulator), so a
tag typo or hidden-node issue would not have been caught. It will be exercised the first time
instrumented tests run on a device.
A spec for the public
GET /api/blogendpoint that would make real in-app rendering possible isposted on epic #87.