ui: design system, app shell and messenger refactor (plan P1-P3) #198

Merged
sirius merged 7 commits from ui/refactor-shell-messenger into main 2026-10-29 05:13:59 +00:00
Owner

Implements P1–P3 of the Android UI/UX refactor plan on one branch: design tokens and a shared component set (P1), the app shell and navigation standard (P2), and the messenger + invite refresh (P3).

View layer only. No Rust core, no FFI/JNI surface, no Tor lifecycle, no docs/PROTOCOL.md / docs/THREAT_MODEL.md, no .forgejo/workflows/** changes — git diff --stat origin/main...HEAD -- ':!android' is empty.

P1 — design system

  • ui/Theme.kt is now the single source of tokens: OwColors (canvas #0A0C0E, surfaces, mint accent #2EE59D, text ramp, danger/warning/info tints), OwRadius (card 16dp, control 12dp squircles, pill only for status badges and address chips), OwSize, and one MonoStyle.
  • New ui/components/: OwButton (primary/secondary/outline/destructive — the alpha build's 999dp pill made every action look identical), OwButtonRow, OwSegmentedControl, OwAlertBanner, OwCard, OwTopBar, OwBottomTabBar, OwConfirmDialog, OwIcon, OwIconButton, OwStatusDot, OwDivider, OwOverflowMenu, Identicon, plus Label/Mono type helpers and a single-colour icon set (res/drawable/ic_ow_*.xml).
  • Monospace is reserved for pubkeys, truncated onion locators, seeds, host:port and block heights — never a sentence.

P2 — app shell and navigation standard

  • MainActivity is a Scaffold whose only job is the persistent bottom tab bar: Chats (default), Wallet, Node & Settings. Sub-screens keep a chevron back button and hide the bar. Tab selection pops to the start destination and restores that tab's saved state.
  • Inset ownership is explicit: the shell owns the tab bar's height, each screen's Scaffold zeroes its own contentWindowInsets, and Modifier.screenContentInsets() applies systemBars ∪ ime — unioned per-side, never summed — so the composer is not drawn behind the keyboard under enableEdgeToEdge(). LocalBottomBarPresent stops a screen padding for the bar twice.
  • Rare and destructive actions moved off the home screen into the ⋮ overflow: rotate identity and delete message history both land behind a typed confirmation (DELETE), destructive styling confined to the confirm button. Saved wallets and profile live there too.

P3 — messenger and invite refresh

  • The roster's expandable identity card is gone: one compact identity bar (identicon, label, truncated pubkey, copy-invite and share actions), then the people. The version line, key-type footnote and keep-alive paragraph moved to Node & Settings — a home screen should not explain the transport before it shows the people.
  • Five text-link actions replaced by two first-class actions (Add contact, Share my invite); the empty state names the next step and carries the same two buttons. Stale-locator rows get a warning dot and label instead of a paragraph.
  • Invite screen: capped payload card, segmented text/QR, OwAlertBanner for the "not discoverable" honesty note. Chat, profile and rotate screens now use the shared top bar and chevron icon instead of a < text glyph.

Verification

Run in the worktree /home/lancelot/Projects/onionwire/.worktrees/t_718859b4/android (the earlier "213 tests pass" evidence was collected in the main checkout and did not include this branch's files — discarded):

  • ./gradlew :app:compileDebugKotlin — BUILD SUCCESSFUL, 33/33 tasks executed, 0 warnings (the one KT-73255 annotation-target warning was fixed in 7d86fd6).
  • ./gradlew :app:testDebugUnitTest --rerun-tasks — BUILD SUCCESSFUL, 16 classes / 221 tests / 0 failures / 0 errors, including the two new JVM tests RosterOnionTest (a v3 locator keeps its .onion suffix; the suffix is never the part cut) and components/IdenticonTest.

This is local verification only — CI builds the APK and publishes. Nothing here claims a shipped APK.

Notes for review

  • WalletScreen.kt and WalletManagerScreen.kt keep their text Back buttons: they are the apk-dev child card's scope and are deliberately untouched here.
  • android/README.md documents the shell, the token locations and the new file layout.

Follow-up commits on the branch

  • 7d86fd6 — dropped the leftover ed25519 pubkey phrase from a ChatScreen KDoc so the Phase-3 acceptance grep is empty across ui/, and pinned @DrawableRes to @param: (KT-73255). Compile is warning-free.
  • 5a47019 — empty state now matches §3.3 exactly: the shield is muted (the accent stays on the primary CTA) and the heading is 18sp/600 rather than 16sp.
  • dc45d9c — the last raw Material buttons (chat send, profile save, both rotate actions) now go through OwButton; it gained an optional RowScope content slot so chat-send keeps its spinner. Rotate's confirm is the Destructive tier and the post-rotation action is "Share the new invite" (the top bar already owns the chevron back).

Re-verified after the last commit: :app:compileDebugKotlin and :app:testDebugUnitTest on dc45d9c — 221 tests / 0 failures / 0 errors (16 classes). CI (android-ci / android) reported pending / "Waiting to run" on this head when the card was handed off; no APK is claimed from this PR.

Implements P1–P3 of the Android UI/UX refactor plan on one branch: design tokens and a shared component set (P1), the app shell and navigation standard (P2), and the messenger + invite refresh (P3). View layer only. No Rust core, no FFI/JNI surface, no Tor lifecycle, no `docs/PROTOCOL.md` / `docs/THREAT_MODEL.md`, no `.forgejo/workflows/**` changes — `git diff --stat origin/main...HEAD -- ':!android'` is empty. ## P1 — design system - `ui/Theme.kt` is now the single source of tokens: `OwColors` (canvas `#0A0C0E`, surfaces, mint accent `#2EE59D`, text ramp, danger/warning/info tints), `OwRadius` (card 16dp, control 12dp squircles, pill only for status badges and address chips), `OwSize`, and one `MonoStyle`. - New `ui/components/`: `OwButton` (primary/secondary/outline/destructive — the alpha build's 999dp pill made every action look identical), `OwButtonRow`, `OwSegmentedControl`, `OwAlertBanner`, `OwCard`, `OwTopBar`, `OwBottomTabBar`, `OwConfirmDialog`, `OwIcon`, `OwIconButton`, `OwStatusDot`, `OwDivider`, `OwOverflowMenu`, `Identicon`, plus `Label`/`Mono` type helpers and a single-colour icon set (`res/drawable/ic_ow_*.xml`). - Monospace is reserved for pubkeys, truncated onion locators, seeds, `host:port` and block heights — never a sentence. ## P2 — app shell and navigation standard - `MainActivity` is a `Scaffold` whose only job is the persistent bottom tab bar: **Chats** (default), **Wallet**, **Node & Settings**. Sub-screens keep a chevron back button and hide the bar. Tab selection pops to the start destination and restores that tab's saved state. - Inset ownership is explicit: the shell owns the tab bar's height, each screen's `Scaffold` zeroes its own `contentWindowInsets`, and `Modifier.screenContentInsets()` applies `systemBars ∪ ime` — unioned per-side, never summed — so the composer is not drawn behind the keyboard under `enableEdgeToEdge()`. `LocalBottomBarPresent` stops a screen padding for the bar twice. - Rare and destructive actions moved off the home screen into the `⋮` overflow: rotate identity and delete message history both land behind a typed confirmation (`DELETE`), destructive styling confined to the confirm button. Saved wallets and profile live there too. ## P3 — messenger and invite refresh - The roster's expandable identity card is gone: one compact identity bar (identicon, label, truncated pubkey, copy-invite and share actions), then the people. The version line, key-type footnote and keep-alive paragraph moved to Node & Settings — a home screen should not explain the transport before it shows the people. - Five text-link actions replaced by two first-class actions (`Add contact`, `Share my invite`); the empty state names the next step and carries the same two buttons. Stale-locator rows get a warning dot and label instead of a paragraph. - Invite screen: capped payload card, segmented text/QR, `OwAlertBanner` for the "not discoverable" honesty note. Chat, profile and rotate screens now use the shared top bar and chevron icon instead of a `<` text glyph. ## Verification Run in the worktree `/home/lancelot/Projects/onionwire/.worktrees/t_718859b4/android` (the earlier "213 tests pass" evidence was collected in the main checkout and did not include this branch's files — discarded): - `./gradlew :app:compileDebugKotlin` — `BUILD SUCCESSFUL`, 33/33 tasks executed, **0 warnings** (the one KT-73255 annotation-target warning was fixed in `7d86fd6`). - `./gradlew :app:testDebugUnitTest --rerun-tasks` — `BUILD SUCCESSFUL`, **16 classes / 221 tests / 0 failures / 0 errors**, including the two new JVM tests `RosterOnionTest` (a v3 locator keeps its `.onion` suffix; the suffix is never the part cut) and `components/IdenticonTest`. This is local verification only — CI builds the APK and publishes. Nothing here claims a shipped APK. ## Notes for review - `WalletScreen.kt` and `WalletManagerScreen.kt` keep their text `Back` buttons: they are the apk-dev child card's scope and are deliberately untouched here. - `android/README.md` documents the shell, the token locations and the new file layout. ## Follow-up commits on the branch - `7d86fd6` — dropped the leftover `ed25519 pubkey` phrase from a `ChatScreen` KDoc so the Phase-3 acceptance grep is empty across `ui/`, and pinned `@DrawableRes` to `@param:` (KT-73255). Compile is warning-free. - `5a47019` — empty state now matches §3.3 exactly: the shield is muted (the accent stays on the primary CTA) and the heading is 18sp/600 rather than 16sp. - `dc45d9c` — the last raw Material buttons (chat send, profile save, both rotate actions) now go through `OwButton`; it gained an optional `RowScope` content slot so chat-send keeps its spinner. Rotate's confirm is the Destructive tier and the post-rotation action is "Share the new invite" (the top bar already owns the chevron back). Re-verified after the last commit: `:app:compileDebugKotlin` and `:app:testDebugUnitTest` on `dc45d9c` — **221 tests / 0 failures / 0 errors** (16 classes). CI (`android-ci / android`) reported `pending` / "Waiting to run" on this head when the card was handed off; no APK is claimed from this PR.
One palette, one geometry, one button. The alpha build had a four-colour
darkColorScheme and every screen inventing its own pill, which is why the app
read as a debug harness; `ui/Theme.kt` now carries the tokens and new UI takes
them from `OwColors` / `OwRadius` / `OwSize` instead of Material defaults.

Tokens (§1.1, §1.2): canvas #0A0C0E, surface #12161A, interactive #1C2228,
border #262E36, mint accent #2EE59D, text #F3F5F7 / #9BA3AE / #5E6773, and the
amber/crimson status triples. Cards are 16dp; buttons and inputs are 12dp
squircles; 999dp survives only for status pills. Typography is system sans for
chrome, and `MonoStyle` is the single mono style, reserved for pubkeys,
truncated locators, seeds, host:port and block heights.

Primitives (§1.3), in `ui/components/`:
- OwButton — primary / secondary / outline / destructive, exact heights and
  radii, no elevation;
- OwSegmentedControl — one track, accent selected segment, animated;
- OwAlertBanner — warning / error / info with a status icon and a tinted
  surface;
- OwCard, OwDivider, OwPill, OwStatusDot, TorStatusPill, OwSectionTitle,
  OwIconButton (36dp), Identicon, OwConfirmDialog (typed confirmation),
  OwQrCode (zxing, light plate for scanning).

Fourteen single-colour vector icons, so no `material-icons-extended` (a
multi-MB dependency) and no icon font. `Label`/`Mono` moved here from the
roster so the design system is not owned by one screen.

Behaviour: none. These are pure view helpers.
Plans P2 and P3 are one change: the shell decides who calls the roster, so the
roster cannot land before it. The tree is green at this commit, which is why
they are not split.

Shell (P2)
- The horizontal text-link bar is gone. Three top-level destinations hang off a
  persistent 64dp bottom bar — Chats (default), Wallet, Node and Settings — and
  everything else is a sub-screen behind a chevron.
- MainActivity owns the shell: a Scaffold with the tab bar around a NavHost of
  chats / wallet / settings plus share, paste, chat, profile, wallets and
  rotate. Tab selection pops back to the start destination and restores saved
  state, so a chat left open behind the Wallet tab survives a trip to Settings.
- The node keeps living on the wallet screen, where its card already is, so the
  third tab is the security surface: profile, saved wallets, rotate identity,
  delete message history, the keep-alive status row, and — disclosed here
  instead of under the roster — the identity/SDK lines that made the home screen
  read as a test build.
- OwTopBar: 20sp bold title, optional "Tor active" pill beneath it, a 36dp
  padlock surface for Lock, and the overflow. Rotate identity, saved wallets and
  delete history live in the overflow; the destructive two go through a typed
  confirmation dialog or the typed ROTATE screen, never a single tap.
- Insets: the tab bar consumes the navigation-bar inset and
  LocalBottomBarPresent tells screens inside the shell not to pad for it again,
  so there is no double gap above the tabs and the IME still wins when the
  keyboard is up.
- window.isNavigationBarContrastEnforced = false on Q+, so the system's
  translucent scrim does not draw a second band over the canvas.

Messenger (P3)
- The identity block is one bar: identicon, "Your identity", the truncated
  fingerprint in mono, a copy-invite icon and a QR icon. Gone from here: the
  YOU (stable forever) card, the "sdk … · identity = ed25519 pubkey" line and
  the keep-alive paragraph. The full fingerprint and locator stay on the profile
  and invite screens.
- The empty state says what to do and offers both ways out ("Add contact",
  "Share my invite"), and keeps the honest sentence: invites are not
  discoverable.
- Friend rows: petname, short mono fingerprint, and a locator shortened as
  head…tail.onion — the suffix is never the part that is cut, and a v3 onion is
  never soft-wrapped.
- Invite sharing shows a QR of the payload beside the copyable text; the clip is
  marked sensitive on API 33+.

No ViewModel, SDK, Tor or wallet call changed. Every callback is the call the
old screens made; the wallet screens are untouched for the next card.
Two things the emulator run surfaced:

- The invite payload is about a kilobyte, so the plain Text card grew past the
  screen and pushed Copy invite / Done below the fold on a phone. Four lines
  with an ellipsis: the copy button carries the whole string, and the QR is the
  primary path anyway.
- "Node & Settings" as a 10sp label in a third of the bottom bar is one OEM font
  scale away from wrapping. The tab says "Settings"; the destination screen keeps
  its "Node & Settings" title.
The SDK/key-type footnote moved to Node and Settings, so it should not survive
as a quoted string in ui/ either — the About card now shows IDENTITY ed25519 and
SDK <version> as label plus mono value, which is the design system's idiom for a
machine fact. Same reasoning for the KDoc: it describes the change instead of
pasting the strings that were removed.
ui: drop the stale key-type phrase and pin the DrawableRes param target
All checks were successful
android-ci / android (pull_request) Successful in 14m54s
7d86fd6a03
Two nits the plan's own greps would have flagged:

- `ChatScreen`'s `peer` KDoc still spelled out the key type. It is an
  internal comment, but the acceptance grep for the removed identity copy
  (`ed25519 pubkey`) should come back empty across `ui/`, not empty-except-
  a-comment.
- `OwTab`'s `@DrawableRes` is now `@param:`-targeted, which silences Kotlin's
  KT-73255 warning about the annotation moving to the backing field.
ui: mute the empty-state shield and set its heading to the plan's 18sp
All checks were successful
android-ci / android (pull_request) Successful in 14m56s
5a4701992e
The plan asks for a *muted* mint/grey icon inside the subtle surface circle and
an 18px/600 heading. The first pass drew the shield in full accent at 16sp
SemiBold, which made the empty state read as a warning and competed with the
primary CTA under it. The accent now stays on the button only.
ui: route the last raw buttons through OwButton, with a content slot
All checks were successful
android-ci / android (pull_request) Successful in 14m48s
dc45d9cb81
Chat's send, Profile's save, and both Rotate actions were still material
Buttons — the 50%-rounded pill the design pass exists to remove. They now
take the design-system shape and height.

OwButton gains an optional RowScope content slot so a caller that needs a
spinner beside its label (chat send while a message is pending) keeps that
behaviour without dropping back to a raw Button. The slot is additive: the
existing text/leading-icon path is unchanged for every other caller.

Rotate's confirm is the Destructive tier — it destroys the current onion —
and the post-rotation action is labelled "Share the new invite" instead of
"Back — share the new invite": the top bar already owns the chevron back.
sirius merged commit 890620763f into main 2026-10-29 05:13:59 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sirius/onionwire!198
No description provided.