Skip to content

feat(app): reach the blog from the Account hub via a themed Custom Tab (#88) - #121

Merged
Adron merged 1 commit into
parity/queuefrom
issue/88-blog-reach
Sep 16, 2026
Merged

Adron merged 1 commit into
parity/queuefrom
issue/88-blog-reach

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

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

  • A Blog row in the Account hub menu, between "Search users" and "Sign out" — the bottom of the
    informational group, clear of the account actions. It carries an opens-outside-the-app affordance
    (OpenInNew + "Opens in a browser" content description).
  • The Custom Tab's toolbar / secondary / navigation-bar colours come from the live
    MaterialTheme.colorScheme, and light/dark is chosen from the resolved surface colour. Because
    InterlinedListTheme has already resolved System/Light/Dark from ThemeSettingsStore, the tab
    follows the user's setting with zero new plumbing:core:datastore was read but not
    modified, so there is no collision with Settings: theme preference synced to the account (theme) #36.
  • Fallback chain: Custom Tab → plain ACTION_VIEWBlogLaunchResult.UNAVAILABLE. It is
    deliberately free of Android types, so all three outcomes are covered by plain JVM tests.
  • androidx.browser 1.8.0 added via the version catalog.

To carry the external-link affordance, the private AccountMenuRow gained a defaulted
trailingIcon/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 own
interlinedlist://blog[/<slug>] scheme is registered. Three reasons:

  1. With no in-app renderer, claiming the link means intercepting a browser-bound URL only to hand it
    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.
  2. The no-Custom-Tabs fallback is a plain ACTION_VIEW on that same URL. If the app were a
    registered 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.
  3. autoVerify on /blog/* also ties the path to assetlinks.json upkeep for no gain.

BlogLink.webUrlFor still recognises and normalises the web form, so the manifest claim can be
switched 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, 0
failures
(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
/blogroll and /blog-archive prefix traps, interlinedlist.com.evil.example, ../.
traversal, non-blog in-app links, garbage, empty and null. BlogLauncherTest (6) covers Custom Tab
used / browser untouched, fallback when no provider, no browser at all reporting failure instead of
crashing
, a contained SecurityException, and dark/light selection against the real
ILSurfaceDark/ILSurfaceLight.

The merged manifest was checked to contain the interlinedlist://blog filter and not an https
blog 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/blog endpoint that would make real in-app rendering possible is
posted on epic #87.

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
@Adron
Adron merged commit 9cb1ef3 into parity/queue Sep 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant