Skip to content

feat(nav): open on the Chats tab on cold launch - #1529

Merged
bmc08gt merged 7 commits into
code/cashfrom
feat/launch-chats-tab
Sep 23, 2026
Merged

bmc08gt merged 7 commits into
code/cashfrom
feat/launch-chats-tab

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

A signed-in cold launch now opens on the Chats tab instead of Wallet.

buildNavGraphForLaunch uses a new launchRoute for a launch with no deeplink, and for one the router resolves to nothing. launchRoute is NavBarButton.Chats.destinationRoute(), so launching lands on the same route a tap on the Chats tab does.

Two paths stay on Wallet (homeRoute):

  • Deeplink launches. OpenToken and OpenCashLink open as the wallet's expanded card, so they need the wallet as the base.
  • Onboarding that creates an account. A new account has no chats yet, so it lands on the balance. OnboardingFlow.newAccount marks these paths: finishing the access key or purchase step, and resuming at display-name entry. A restore or an app resume lands on Chats, which matches iOS. A restored account that still owes the purchase counts as new.

BuildNavGraphForLaunchTest now expects Chats for the two plain-launch cases, and HomeRouteTest checks that launchRoute maps to the Chats tab.

Launching into Chats exposed an empty-state flash: feed() emitted the state holder's initial empty list before observeFeedFromDb had read Room, and TipFlowViewModel treats any emission as loaded, so "No Chats Yet" appeared over chats that were about to load. ChatState.feed is now null until the first database read, and feed() skips the null. An empty database is only final once the first sync answers, since on a fresh sign-in the sync is about to write the account's chats, so feed() holds an empty list until feedSyncState is Synced or Error. Chats already on disk emit as soon as they are read. FeedDbHydrationTest covers no emission before the read, no emission for an empty database mid-sync, and an empty feed once the sync succeeds or fails.

The Chats tab still drew blank for 200-350ms before its list appeared. RealChatCoordinator now waits for the first database read before starting the sync (bounded at 3s), so the two don't compete for CPU, and TipFlowViewModel builds its rows on the Default dispatcher instead of the main thread. On a Seeker with the non-debuggable benchmark build, four cold launches each, the blank went from 207-353ms to 122-152ms. Debug builds are far slower here (about 1-1.5s blank), so measure launch timing on benchmark.

The remaining blank is gone. The feed builder read each chat's latest visible message with its own query (115-165ms for 65 chats); getLatestVisibleForAllChats reads them in one (13-38ms), and both queries now break a timestamp tie on message_id DESC, as the paged message list does. That puts the feed ahead of TipFlowViewModel, which now seeds its first rows in init from ChatCoordinator.currentFeed() and TokenCoordinator.cachedTokens(). currentFeed() shares feed()'s gate, so it is null whenever feed() would stay silent. Over five cold launches on the Seeker (benchmark), the seed found the feed every time and the Chats screen's first composition already held the list.

A signed-in launch with no deeplink, or one the router resolves to nothing,
now starts on the Chats tab instead of Wallet. The route comes from the Chats
tab's own destination, so launching lands where a tap on the tab does.

Deeplink launches keep the Wallet base, since token and cash-link actions open
as the wallet's expanded card. Onboarding still releases to Wallet.
@bmc08gt bmc08gt self-assigned this Sep 22, 2026
The chat list shows "No Chats Yet" as soon as the feed emits an empty list,
and feed() emitted the state holder's initial empty list before
observeFeedFromDb had read Room. Opening on the Chats tab at launch put
the empty state up over chats that were about to load from disk.

ChatState now carries feedLoaded, set by the first database read, and
feed() filters on it. An empty database still emits an empty feed.
The Chats tab showed blank for 200-350ms on a cold launch before its list
drew. Two things held it up: the feed sync started alongside the first
database read and competed with it for CPU, and TipFlowViewModel built the
rows on the main thread while it was drawing the app's first screens.

The sync now waits for the first read (bounded at 3s), and the row mapping
runs on the Default dispatcher.

Measured on a Seeker with the non-debuggable benchmark build, four cold
launches each: the blank tab went from 207-353ms to 122-152ms, and the list
now draws about 870ms after process start instead of about 1015ms.
…read

Replaces the separate feedLoaded flag, which allowed a non-empty feed marked
as not loaded. Null now means "not read yet" and an empty list means "no
chats".
An empty database is only known to mean "no chats" once the server has
answered: on a fresh sign-in the first sync is about to write the
account's chats, and until then the Chats tab showed "No Chats Yet".
feed() now emits an empty list only after the sync succeeds or fails.
Chats already on disk still emit as soon as they are read.
Every onboarding ending released to the Wallet tab. Only an account that
onboarding just created should: a restore or an app resume now lands on
the Chats tab, where a cold launch opens. This matches iOS, which passes
the Wallet tab only from completeOnboardingAndLogin.

OnboardingFlow carries a newAccount flag, set on the paths that create
the account: finishing the access key or purchase step, and resuming at
display-name entry. onboardingLandingRoute picks the Wallet tab for it
and launchRoute otherwise.

A restored account that still owes the purchase goes through the
purchase step, so it counts as new and lands on Wallet.
The feed builder ran one getLatestVisible query per chat, which took
115-165ms for 65 chats on a Seeker (benchmark build). It now reads every
chat's latest visible message in one query, which takes 13-38ms. Both
queries now break a timestamp tie on message_id DESC, matching the paged
message list; getLatestVisible previously left ties to SQLite.

With the feed ready sooner, it is built before TipFlowViewModel in
practice, so the view model seeds its first rows in init from the new
ChatCoordinator.currentFeed() and TokenCoordinator.cachedTokens().
currentFeed() shares feed()'s gate, so it is null whenever feed() would
stay silent. Over five cold launches the seed found the feed every time
and the Chats screen's first composition already held the list, at
691-1007ms from process start. Before, the tab composed blank and the
list followed at 826-981ms.
@github-actions github-actions Bot added the area: tokens Token accounts, balances, token info label Sep 23, 2026
@bmc08gt
bmc08gt merged commit b462668 into code/cash Sep 23, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the feat/launch-chats-tab branch September 23, 2026 16:45
bmc08gt added a commit that referenced this pull request Sep 23, 2026
Since #1529 a signed-in launch with no deeplink opens on Chats, but
subflows/login.yaml still waited for wallet_screen. Every flow that logs
in by seed timed out on that check before reaching its own steps. Flows
that need the wallet after login already go through navigate_to_wallet.
bmc08gt added a commit that referenced this pull request Sep 23, 2026
* refactor(nav): move the nav bar homes into AppRoute.Tabs

The Chats tab's home was AppRoute.Sheets.Tips. It is not a sheet and no
longer shows tips, and Wallet and Menu had the same problem under Sheets.
resolveRoutes had to check asNavBarTab() on every Sheets route so that
these three were not wrapped in Main.Sheet.

AppRoute.Tabs now holds Scanner, Wallet, Chats and Menu. Everything left
under Sheets is a real sheet, so resolveRoutes wraps any Sheets route and
the special case is gone. The tab-home checks in navigateAll, AppContent
and NavTabBarInsetEntryDecorator become `is AppRoute.Tabs`.

Chats also drops the tipping flow it was hosted in. TipFlowViewModel only
ever set its steps to listOf(TipStep.Tips), so TipStep.Intro never ran,
TipInfoScreen was never shown, and nothing built Tips(resumed = true), the
only way to reach TipStep.TipCard. The ShareTipCard event was never sent.
Tabs.Chats is a data object that renders ChatsScreen (was TipsScreen)
backed by ChatsViewModel, which keeps only the chat-feed mapping.
TippingFlowScreen, TipCardScreen, TipInfoScreen, TipStep, TipResult and
ic_tipping_hand_large are deleted.

The screen-root tag follows the route name, so tips_screen becomes
chats_screen in the Maestro flows and BaselineProfileGenerator. The other
tabs keep their tags. The back stack is held in remember, not saved
state, so the renamed serializable routes have nothing to restore from.

* test(maestro): wait for the Chats tab after a seed login

Since #1529 a signed-in launch with no deeplink opens on Chats, but
subflows/login.yaml still waited for wallet_screen. Every flow that logs
in by seed timed out on that check before reaching its own steps. Flows
that need the wallet after login already go through navigate_to_wallet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: onboarding area: tokens Token accounts, balances, token info type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant