Repository navigation
feat(nav): open on the Chats tab on cold launch - #1529
Merged
Merged
Conversation
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.
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.
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.
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.
A signed-in cold launch now opens on the Chats tab instead of Wallet.
buildNavGraphForLaunchuses a newlaunchRoutefor a launch with no deeplink, and for one the router resolves to nothing.launchRouteisNavBarButton.Chats.destinationRoute(), so launching lands on the same route a tap on the Chats tab does.Two paths stay on Wallet (
homeRoute):OpenTokenandOpenCashLinkopen as the wallet's expanded card, so they need the wallet as the base.OnboardingFlow.newAccountmarks 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.BuildNavGraphForLaunchTestnow expects Chats for the two plain-launch cases, andHomeRouteTestchecks thatlaunchRoutemaps to the Chats tab.Launching into Chats exposed an empty-state flash:
feed()emitted the state holder's initial empty list beforeobserveFeedFromDbhad read Room, andTipFlowViewModeltreats any emission as loaded, so "No Chats Yet" appeared over chats that were about to load.ChatState.feedis now null until the first database read, andfeed()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, sofeed()holds an empty list untilfeedSyncStateisSyncedorError. Chats already on disk emit as soon as they are read.FeedDbHydrationTestcovers 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.
RealChatCoordinatornow waits for the first database read before starting the sync (bounded at 3s), so the two don't compete for CPU, andTipFlowViewModelbuilds its rows on the Default dispatcher instead of the main thread. On a Seeker with the non-debuggablebenchmarkbuild, 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 onbenchmark.The remaining blank is gone. The feed builder read each chat's latest visible message with its own query (115-165ms for 65 chats);
getLatestVisibleForAllChatsreads them in one (13-38ms), and both queries now break a timestamp tie onmessage_id DESC, as the paged message list does. That puts the feed ahead ofTipFlowViewModel, which now seeds its first rows ininitfromChatCoordinator.currentFeed()andTokenCoordinator.cachedTokens().currentFeed()sharesfeed()'s gate, so it is null wheneverfeed()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.