# Navigation Specification Source: `common/src/commonMain/kotlin/chat/simplex/common/App.kt` (470 lines) --- ## Table of Contents 1. [Overview](#1-overview) 2. [AppScreen Composable](#2-appscreen-composable) 3. [MainScreen](#3-mainscreen) 4. [Android Layout](#4-android-layout) 5. [Desktop Layout](#5-desktop-layout) 6. [ModalManager](#6-modalmanager) 7. [Authentication Gate](#7-authentication-gate) 8. [Onboarding Flow](#8-onboarding-flow) 9. [Source Files](#9-source-files) --- ## Executive Summary SimpleX Chat navigation is a platform-adaptive system implemented in `App.kt`. The root `AppScreen` composable applies theming and safe-area insets, delegating to `MainScreen` which acts as a state machine routing between onboarding, authentication, database error, and the main chat interface. Android uses a 2-column sliding layout (`AndroidScreen`), while desktop uses a fixed 3-column layout (`DesktopScreen`). Modal presentation is managed by `ModalManager`, which provides named zones (start, center, end, fullscreen) for layered content. Authentication is gated by `AppLock`, and onboarding follows a linear `OnboardingStage` enum. --- ## 1. Overview ``` AppScreen (line 46) +-- SimpleXTheme +-- Surface +-- MainScreen (line 82) |-- [Migration in progress] -> DefaultProgressView |-- [Database opening] -> DefaultProgressView |-- [Database error] -> DatabaseErrorView |-- [Encryption check pending] -> SplashView |-- [Onboarding incomplete] -> AnimatedContent { OnboardingStage views } |-- [Onboarding complete] | |-- [Android] | | +-- AndroidWrapInCallLayout | | +-- AndroidScreen (line 293) | | |-- StartPartOfScreen (ChatListView) | | +-- ChatView (slide-in panel) | +-- [Desktop] | +-- DesktopScreen (line 406) | |-- StartPartOfScreen + UserPicker (left column) | |-- ModalManager.start (overlay on left) | |-- CenterPartOfScreen / ChatView (center column) | +-- ModalManager.end (right column) |-- [Unauthorized] -> AuthView / SplashView / PasscodeView |-- [Active call] -> ActiveCallView (desktop) / startCallActivity (Android) +-- [Incoming call] -> IncomingCallAlertView ``` --- ## 2. AppScreen Composable **Location:** [`App.kt#L47`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L47) ```kotlin @Composable fun AppScreen() ``` ### Responsibilities 1. **Theme application:** Wraps content in `SimpleXTheme` with `Surface` using `MaterialTheme.colors.background`. 2. **Window insets:** Computes safe padding for landscape mode, accounting for display cutouts on both sides. Uses `WindowInsets.safeDrawing` and `WindowInsets.displayCutout` to calculate symmetric padding. 3. **Fullscreen gallery overlay:** When `chatModel.fullscreenGalleryVisible` is true, draws a black rectangle behind content extending into the cutout areas to provide an immersive gallery background. 4. **Delegates to `MainScreen()`.** --- ## 3. MainScreen **Location:** [`App.kt#L84`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L84) ```kotlin @Composable fun MainScreen() ``` ### State Machine `MainScreen` evaluates a series of conditions in priority order: | Priority | Condition | View | |---|---|---| | 1 | `onboarding == Step1_SimpleXInfo && migrationState != null` | `SimpleXInfo` (migration in progress) | | 2 | `dbMigrationInProgress` | `DefaultProgressView("Database migration...")` | | 3 | `chatDbStatus == null && showInitializationView` | `DefaultProgressView("Opening database...")` | | 4 | `showChatDatabaseError` | `DatabaseErrorView` | | 5 | `chatDbEncrypted == null \|\| localUserCreated == null` | `SplashView` | | 6 | `onboarding == OnboardingComplete` | Platform-specific main screen | | 7 | Other onboarding stages | `AnimatedContent` with stage-specific views | ### Onboarding Complete Branch (line ~156) When onboarding is complete: 1. Shows "advertise lock" alert if conditions met (not shown before, LA not enabled, >3 chats, no active call). 2. Sets up clipboard listener. 3. Routes to `AndroidScreen` or `DesktopScreen` based on platform. ### Overlay Layers (bottom of MainScreen) | Layer | Condition | Content | |---|---|---| | `ModalManager.fullscreen` | Android + migration/onboarding | Fullscreen modals | | `SwitchingUsersView` | User switch in progress | Loading overlay | | Auth gate | `userAuthorized != true` | `AuthView` or `SplashView` + passcode | | Active call | `showCallView == true` | `ActiveCallView` (desktop) or call activity (Android) | | One-time passcode | Always | `ModalManager.fullscreen.showOneTimePasscodeInView` | | Privacy alerts | Always | `AlertManager.privacySensitive` | | Incoming call | `activeCallInvitation != null` | `IncomingCallAlertView` | | Shared alerts | Always | `AlertManager.shared` | --- ## 4. Android Layout **Location:** [`App.kt#L296`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L296) ```kotlin @Composable fun AndroidScreen(userPickerState: MutableStateFlow) ``` ### 2-Column Slide Animation Uses `BoxWithConstraints` to get `maxWidth`, then two `Box` containers: 1. **Left panel (StartPartOfScreen):** Chat list, positioned at `translationX = -offset`. 2. **Right panel (ChatView):** Chat view, positioned at `translationX = maxWidth - offset`. The `offset` is an `Animatable`: - `0f` when no chat is selected (chat list visible). - `maxWidth.value` when a chat is open (chat view visible). ### Animation Flow 1. `snapshotFlow { chatModel.chatId.value }` detects chat ID changes. 2. When `chatId` becomes null, `onComposed(null)` animates offset to 0. 3. When `ChatView` finishes composing (calls `onComposed(chatId)`), offset animates to `maxWidth`. 4. Animation uses `chatListAnimationSpec()` (standard spring or tween). ### Display Cutout Handling If the device has a display cutout on horizontal sides (detected via `WindowInsets.displayCutout`), the panels are clipped with `RectangleShape` to prevent the chat list from showing through during transition. ### Call Layout Wrapper `AndroidWrapInCallLayout` (line ~279) adds a 40dp top padding when an active call is in progress (not in `WaitCapabilities` or `InvitationAccepted` state), with an `ActiveCallInteractiveArea` banner above. --- ## 5. Desktop Layout **Location:** [`App.kt#L410`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L410) ```kotlin @Composable fun DesktopScreen(userPickerState: MutableStateFlow) ``` ### 3-Column Layout | Column | Width | Content | |---|---|---| | **Left** | `DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier` (fixed) | `StartPartOfScreen` (ChatListView) + `UserPicker` overlay | | **Left overlay** | Same as left column | `ModalManager.start` modals + `SwitchingUsersView` | | **Center** | `min = DEFAULT_MIN_CENTER_MODAL_WIDTH`, `weight = 1f` (flexible) | `CenterPartOfScreen` (ChatView or "no selected chat" placeholder, or `ModalManager.center`) | | **Right** | `max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier` (flexible, 0 when empty) | `ModalManager.end` (ChatInfoView, GroupChatInfoView, ChatItemInfoView, etc.) | ### Column Separators - `VerticalDivider` between left and center columns (always visible). - `VerticalDivider` between center and right columns (visible when `ModalManager.end.hasModalsOpen()`). ### Click-to-Dismiss Overlay When the UserPicker is visible or a start modal is open (but no center modal), a full-size clickable overlay covers the center+right area (line ~428). Clicking it closes start modals and hides the UserPicker. ### CenterPartOfScreen **Location:** [`App.kt#L373`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L373) - When `chatId` is null and no center modals: shows "No selected chat" placeholder. - When `chatId` is null and center modals open: shows `ModalManager.center`. - When `chatId` is set: shows `ChatView`. - Automatically closes center modals when a chat is selected. ### StartPartOfScreen **Location:** [`App.kt#L352`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L352) Routes between: - `SetDeliveryReceiptsView` (if `chatModel.setDeliveryReceipts` is true) - `ChatListView` (normal operation) - `ShareListView` (when `chatModel.sharedContent` is non-null, i.e., forwarding) --- ## 6. ModalManager **Location:** `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt` (line 92) ```kotlin class ModalManager(private val placement: ModalPlacement?) ``` ### Zones | Zone | Android Behavior | Desktop Behavior | |---|---|---| | `start` | Shared (same as all others) | Left column overlay, slides from start | | `center` | Shared | Center column overlay, replaces ChatView | | `end` | Shared | Right column, slides from end | | `fullscreen` | Shared | Fullscreen overlay | On Android, all four zones point to the same `shared` instance, meaning modals stack in a single overlay. On desktop, each zone is independent with its own `ModalPlacement`. ```kotlin companion object { val start = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.START) val center = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.CENTER) val end = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.END) val fullscreen = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.FULLSCREEN) } ``` ### Modal Stack Each `ModalManager` maintains a stack of `ModalViewHolder` objects with: - `id: ModalViewId?` -- optional identifier for deduplication - `animated: Boolean` -- whether to use enter/exit transitions - `data: ModalData` -- scoped data for the modal - `modal: @Composable ModalData.(close: () -> Unit) -> Unit` -- the modal content ### Key Methods | Method | Description | |---|---| | `showModal` | Push a simple modal onto the stack | | `showModalCloseable` | Push a modal with a close callback | | `showCustomModal` | Push a modal with full control over `ModalView` wrapper | | `closeModals` | Pop all modals from the stack | | `closeModalsExceptFirst` | Pop all but the bottom modal | | `hasModalsOpen()` | Check if any modals are on the stack | | `showInView` | Render the current modal stack into the composable tree | ### Usage Pattern | Action | Zone Used | |---|---| | Settings, New Chat, User Address | `ModalManager.start` | | Onboarding conditions, What's New | `ModalManager.center` | | ChatInfoView, GroupChatInfoView, ChatItemInfoView, GroupMemberInfoView | `ModalManager.end` | | Passcode entry, Call view, Migration | `ModalManager.fullscreen` | --- ## 7. Authentication Gate **Location:** [`AppLock.kt#L17`](../../common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt#L17) ```kotlin object AppLock { val userAuthorized = mutableStateOf(null) val enteredBackground = mutableStateOf(null) val laFailed = mutableStateOf(false) } ``` ### State | Field | Type | Description | |---|---|---| | `userAuthorized` | `MutableState` | `null` = not yet determined, `true` = authenticated, `false` = locked | | `enteredBackground` | `MutableState` | Timestamp when app entered background (for lock delay) | | `laFailed` | `MutableState` | True if last authentication attempt failed | ### Authentication Flow 1. **MainScreen** checks `unauthorized` (derived: `userAuthorized.value != true`) at line ~135. 2. If unauthorized and not in an active call: - Launches `AppLock.runAuthenticate()` which triggers platform-specific biometric/passcode prompt. - On Android with system auth finishing during activity destruction, authentication is skipped. 3. If `performLA` preference is set and `laFailed` is true: shows `AuthView` with "Unlock" button. 4. If `performLA` is set and `laFailed` is false: shows `SplashView` with passcode overlay. ### Lock Delay The `laLockDelay` preference controls how long after backgrounding the app requires re-authentication. When `laLockDelay == 0`, screen rotation triggers a 3-second grace period (line ~270) to prevent unnecessary re-auth. ### Lock Modes - `LAMode.SYSTEM`: Uses Android biometric/system lock screen. - `LAMode.PASSCODE`: Uses in-app passcode (`SetAppPasscodeView`). ### First-Time Lock Notice `showLANotice` (line ~33 in `AppLock.kt`) prompts users to enable SimpleX Lock when they have more than 3 chats, have not yet been shown the notice, and have not enabled lock. On Android, it offers a choice between system auth and passcode. --- ## 8. Onboarding Flow **Location:** `common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt` (line 3) ```kotlin enum class OnboardingStage { Step1_SimpleXInfo, Step2_CreateProfile, LinkAMobile, Step2_5_SetupDatabasePassphrase, Step3_ChooseServerOperators, Step3_CreateSimpleXAddress, Step4_SetNotificationsMode, OnboardingComplete } ``` ### Stage Progression | Stage | View | Next Stage | |---|---|---| | `Step1_SimpleXInfo` | `SimpleXInfo` -- app introduction, privacy features | `Step2_CreateProfile` or `LinkAMobile` (desktop) | | `Step2_CreateProfile` | `CreateFirstProfile` -- display name, optional image | `Step2_5_SetupDatabasePassphrase` or `Step3_ChooseServerOperators` | | `LinkAMobile` | `LinkAMobile` -- desktop linking to mobile device | `Step2_CreateProfile` | | `Step2_5_SetupDatabasePassphrase` | `SetupDatabasePassphrase` -- optional DB encryption | `Step3_ChooseServerOperators` | | `Step3_ChooseServerOperators` | `OnboardingConditionsView` -- server operator selection, T&C | `Step3_CreateSimpleXAddress` or `Step4_SetNotificationsMode` | | `Step3_CreateSimpleXAddress` | `SetNotificationsMode` (legacy backcompat) | `Step4_SetNotificationsMode` | | `Step4_SetNotificationsMode` | `SetNotificationsMode` -- notification permission setup | `OnboardingComplete` | | `OnboardingComplete` | Main app screen | -- | ### Animated Transitions Onboarding uses `AnimatedContent` with directional transitions: - Forward: `fromEndToStartTransition` (slide left). - Backward: `fromStartToEndTransition` (slide right). The stage value is stored in `appPrefs.onboardingStage` and persisted across app restarts. --- ## 9. Source Files | File | Description | |---|---| | `App.kt` | AppScreen, MainScreen, AndroidScreen, DesktopScreen, StartPartOfScreen, CenterPartOfScreen, EndPartOfScreen | | `AppLock.kt` | AppLock object, authentication state, lock notice, LA mode selection | | `views/helpers/ModalView.kt` | ModalManager class, ModalPlacement enum, modal stack management | | `views/onboarding/OnboardingView.kt` | OnboardingStage enum | | `views/onboarding/SimpleXInfo.kt` | Step 1: App introduction | | `views/WelcomeView.kt` | Step 2: Profile creation (CreateFirstProfile) | | `views/onboarding/LinkAMobileView.kt` | Desktop: Link a mobile device | | `views/onboarding/SetupDatabasePassphrase.kt` | Step 2.5: Database passphrase | | `views/onboarding/ChooseServerOperators.kt` | Step 3: Server operators and conditions | | `views/onboarding/SetNotificationsMode.kt` | Step 4: Notification setup | | `views/chatlist/ChatListView.kt` | Chat list (StartPartOfScreen content) | | `views/chatlist/UserPicker.kt` | User switching panel | | `views/chat/ChatView.kt` | Chat view (CenterPartOfScreen content) | | `views/database/DatabaseErrorView.kt` | Database error recovery | | `views/SplashView.kt` | Splash / loading screen | | `views/call/CallView.kt` | In-call fullscreen view (ActiveCallView) | | `views/localauth/PasswordEntry.kt` | Column divider utility (contains VerticalDivider) |