Files
simplex-chat/apps/multiplatform/spec/state.md
2026-02-26 17:54:44 +00:00

30 KiB

State Management

Table of Contents

  1. Overview
  2. ChatModel
  3. ChatsContext
  4. Chat
  5. AppPreferences
  6. Source Files

1. Overview

SimpleX Chat uses a singleton-based, Compose-reactive state model. The primary state holder is ChatModel, a Kotlin object annotated with @Stable. All mutable fields are Compose MutableState, MutableStateFlow, or SnapshotStateList/SnapshotStateMap instances, which trigger Compose recomposition on mutation.

There is no ViewModel layer, no dependency injection framework, and no Redux/MVI pattern. The architecture is:

ChatModel (singleton, global Compose state)
    |
    +-- ChatController (command dispatch + event processing)
    |       |
    |       +-- sendCmd() -> chatSendCmdRetry() [JNI]
    |       +-- recvMsg() -> chatRecvMsgWait() [JNI]
    |       +-- processReceivedMsg() -> mutates ChatModel fields
    |
    +-- AppPreferences (150+ SharedPreferences via multiplatform-settings)
    |
    +-- ChatsContext (primary) -- chat list + current chat items
    +-- ChatsContext? (secondary) -- optional second context for dual-pane/support chat

State mutations originate from two sources:

  1. User actions: Compose UI handlers call api*() suspend functions on ChatController, which send commands to the Haskell core, receive responses, and update ChatModel.
  2. Core events: The receiver coroutine (startReceiver) calls processReceivedMsg(), which updates ChatModel fields on Dispatchers.Main.

2. ChatModel

Defined at ChatModel.kt line 86 as @Stable object ChatModel.

Controller Reference

Field Type Line Purpose
controller ChatController 87 Reference to the ChatController singleton

User State

Field Type Line Purpose
currentUser MutableState<User?> 89 Currently active user profile
users SnapshotStateList<UserInfo> 90 All user profiles (multi-account)
localUserCreated MutableState<Boolean?> 91 Whether a local user has been created (null = unknown during init)
setDeliveryReceipts MutableState<Boolean> 88 Trigger for delivery receipts setup dialog
switchingUsersAndHosts MutableState<Boolean> 100 True while switching active user/remote host
changingActiveUserMutex Mutex 193 Prevents concurrent user switches

Chat Runtime State

Field Type Line Purpose
chatRunning MutableState<Boolean?> 92 null = initializing, true = running, false = stopped
chatDbChanged MutableState<Boolean> 93 Database was changed externally (needs restart)
chatDbEncrypted MutableState<Boolean?> 94 Whether database is encrypted
chatDbStatus MutableState<DBMigrationResult?> 95 Result of database migration attempt
ctrlInitInProgress MutableState<Boolean> 96 Controller initialization in progress
dbMigrationInProgress MutableState<Boolean> 97 Database migration in progress
incompleteInitializedDbRemoved MutableState<Boolean> 98 Tracks if incomplete DB files were removed (prevents infinite retry)

Current Chat State

Field Type Line Purpose
chatId MutableState<String?> 103 ID of the currently open chat (null = chat list shown)
chatAgentConnId MutableState<String?> 104 Agent connection ID for current chat
chatSubStatus MutableState<SubscriptionStatus?> 105 Subscription status for current chat
openAroundItemId MutableState<Long?> 106 Item ID to scroll to when opening chat
chatsContext ChatsContext 107 Primary chat context (see ChatsContext)
secondaryChatsContext MutableState<ChatsContext?> 108 Optional secondary context for dual-pane views
chats State<List<Chat>> 110 Derived from chatsContext.chats
deletedChats MutableState<List<Pair<Long?, String>>> 112 Recently deleted chats (rhId, chatId)

Group Members

Field Type Line Purpose
groupMembers MutableState<List<GroupMember>> 113 Members of currently viewed group
groupMembersIndexes MutableState<Map<Long, Int>> 114 Index lookup by groupMemberId
membersLoaded MutableState<Boolean> 115 Whether group members have been loaded

Chat Tags and Filters

Field Type Line Purpose
userTags MutableState<List<ChatTag>> 118 User-defined chat tags
activeChatTagFilter MutableState<ActiveFilter?> 119 Currently active filter in chat list
presetTags SnapshotStateMap<PresetTagKind, Int> 120 Counts for preset tag categories (favorites, groups, contacts, etc.)
unreadTags SnapshotStateMap<Long, Int> 121 Unread counts per user-defined tag

Terminal and Developer

Field Type Line Purpose
terminalsVisible Set<Boolean> 125 Tracks which terminal views are visible (default vs floating)
terminalItems MutableState<List<TerminalItem>> 126 Command/response log for developer terminal

Calls (WebRTC)

Field Type Line Purpose
callManager CallManager 161 WebRTC call lifecycle manager
callInvitations SnapshotStateMap<String, RcvCallInvitation> 162 Pending incoming call invitations keyed by chatId
activeCallInvitation MutableState<RcvCallInvitation?> 163 Currently displayed incoming call invitation
activeCall MutableState<Call?> 164 Currently active call
activeCallViewIsVisible MutableState<Boolean> 165 Whether call UI is showing
activeCallViewIsCollapsed MutableState<Boolean> 166 Whether call UI is in PiP/collapsed mode
callCommand SnapshotStateList<WCallCommand> 167 Pending WebRTC commands
showCallView MutableState<Boolean> 168 Call view visibility toggle
switchingCall MutableState<Boolean> 169 True during call switching

Compose Draft and Sharing

Field Type Line Purpose
draft MutableState<ComposeState?> 176 Saved compose draft for current chat
draftChatId MutableState<String?> 177 Chat ID the draft belongs to
sharedContent MutableState<SharedContent?> 180 Content received via share intent or internal forwarding

Remote Hosts

Field Type Line Purpose
remoteHosts SnapshotStateList<RemoteHostInfo> 199 Connected remote hosts (for desktop-mobile pairing)
currentRemoteHost MutableState<RemoteHostInfo?> 200 Currently selected remote host
remoteHostPairing MutableState<Pair<RemoteHostInfo?, RemoteHostSessionState>?> 203 Remote host pairing state
remoteCtrlSession MutableState<RemoteCtrlSession?> 204 Remote controller session

Miscellaneous UI State

Field Type Line Purpose
userAddress MutableState<UserContactLinkRec?> 127 User's public contact address
chatItemTTL MutableState<ChatItemTTL> 128 Chat item time-to-live setting
clearOverlays MutableState<Boolean> 131 Signal to close all overlays/modals
appOpenUrl MutableState<Pair<Long?, String>?> 137 URL opened via deep link (rhId, uri)
appOpenUrlConnecting MutableState<Boolean> 138 Whether a deep link connection is in progress
newChatSheetVisible MutableState<Boolean> 141 Whether new chat bottom sheet is visible
fullscreenGalleryVisible MutableState<Boolean> 144 Fullscreen gallery mode
notificationPreviewMode MutableState<NotificationPreviewMode> 147 Notification content preview level
showAuthScreen MutableState<Boolean> 156 Whether to show authentication screen
showChatPreviews MutableState<Boolean> 158 Whether to show chat preview text in list
clipboardHasText MutableState<Boolean> 185 System clipboard has text content
networkInfo MutableState<UserNetworkInfo> 186 Network type and online status
conditions MutableState<ServerOperatorConditionsDetail> 188 Server operator terms/conditions
updatingProgress MutableState<Float?> 190 Progress indicator for app updates
simplexLinkMode MutableState<SimplexLinkMode> 183 How SimpleX links are displayed
migrationState MutableState<MigrationToState?> 174 Database migration to new device state
showingInvitation MutableState<ShowingInvitation?> 172 Currently displayed invitation
desktopOnboardingRandomPassword MutableState<Boolean> 134 Desktop: user skipped password setup
filesToDelete MutableSet<File> 182 Temporary files pending cleanup

3. ChatsContext

Defined as inner class at ChatModel.kt line 339:

class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?)

ChatsContext holds the chat list and current chat items for a given context. The ChatModel maintains a primary context (chatsContext at line 107) and an optional secondary context (secondaryChatsContext at line 108).

The secondary context is used for:

  • Group support chat scope (SecondaryContextFilter.GroupChatScopeContext) -- viewing member support threads alongside the main group chat
  • Message content tag filtering (SecondaryContextFilter.MsgContentTagContext) -- filtering messages by content type

Fields

Field Type Line Purpose
secondaryContextFilter SecondaryContextFilter? 339 Filter type: null = primary, GroupChatScope or MsgContentTag
chats MutableState<SnapshotStateList<Chat>> 340 List of all chats in this context
chatItems MutableState<SnapshotStateList<ChatItem>> 345 Items for the currently open chat in this context
chatState ActiveChatState 347 Tracks unread counts, splits, scroll state

Derived Properties

Property Line Purpose
contentTag 353 MsgContentTag? -- content filter tag if context is MsgContentTag
groupScopeInfo 360 GroupChatScopeInfo? -- group scope if context is GroupChatScope
isUserSupportChat 367 True when viewing own support chat (no specific member)

Key Operations

  • addChat(chat) -- adds chat at index 0, triggers pop animation
  • reorderChat(chat, toIndex) -- reorders chat list (e.g., when a chat receives a new message)
  • updateChatInfo(rhId, cInfo) -- updates chat metadata while preserving connection stats
  • hasChat(rhId, id) / getChat(id) -- lookup methods

ActiveChatState

Defined at ChatItemsMerger.kt line 196:

data class ActiveChatState(
    val splits: MutableStateFlow<List<Long>> = MutableStateFlow(emptyList()),
    val unreadAfterItemId: MutableStateFlow<Long> = MutableStateFlow(-1L),
    val totalAfter: MutableStateFlow<Int> = MutableStateFlow(0),
    val unreadTotal: MutableStateFlow<Int> = MutableStateFlow(0),
    val unreadAfter: MutableStateFlow<Int> = MutableStateFlow(0),
    val unreadAfterNewestLoaded: MutableStateFlow<Int> = MutableStateFlow(0)
)

This tracks the scroll position and unread item accounting for the lazy-loaded chat item list:

Field Purpose
splits List of item IDs where pagination gaps exist (items not yet loaded)
unreadAfterItemId The item ID that marks the boundary of "read" vs "unread after"
totalAfter Total items after the unread boundary
unreadTotal Total unread items in the chat
unreadAfter Unread items after the boundary (exclusive)
unreadAfterNewestLoaded Unread items after the newest loaded batch

4. Chat

Defined at ChatModel.kt line 1328:

@Serializable @Stable
data class Chat(
    val remoteHostId: Long?,
    val chatInfo: ChatInfo,
    val chatItems: List<ChatItem>,
    val chatStats: ChatStats = ChatStats()
)

Fields

Field Type Purpose
remoteHostId Long? Remote host ID (null = local)
chatInfo ChatInfo Sealed class: Direct, Group, Local, ContactRequest, ContactConnection, InvalidJSON
chatItems List<ChatItem> Latest chat items (summary; full list is in ChatsContext.chatItems)
chatStats ChatStats Unread counts and stats

ChatStats

Defined at ChatModel.kt line 1370:

data class ChatStats(
    val unreadCount: Int = 0,
    val unreadMentions: Int = 0,
    val reportsCount: Int = 0,
    val minUnreadItemId: Long = 0,
    val unreadChat: Boolean = false
)

Derived Properties

Property Line Purpose
id 1349 Chat ID derived from chatInfo.id
unreadTag 1343 Whether chat counts as "unread" for tag filtering (considers notification settings)
supportUnreadCount 1351 Unread count in support/moderation context
nextSendGrpInv 1337 Whether next message should send group invitation

ChatInfo Variants

ChatInfo is a sealed class at ChatModel.kt line 1391:

Variant SerialName Key Data
ChatInfo.Direct "direct" contact: Contact
ChatInfo.Group "group" groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?
ChatInfo.Local "local" noteFolder: NoteFolder
ChatInfo.ContactRequest "contactRequest" contactRequest: UserContactRequest
ChatInfo.ContactConnection "contactConnection" contactConnection: PendingContactConnection
ChatInfo.InvalidJSON "invalidJSON" json: String

5. AppPreferences

Defined at SimpleXAPI.kt line 94 as class AppPreferences.

Uses the multiplatform-settings library (com.russhwolf.settings.Settings) for cross-platform key-value storage (Android SharedPreferences / Desktop java.util.prefs.Preferences).

The AppPreferences instance is created lazily in ChatController at SimpleXAPI.kt line 496:

val appPrefs: AppPreferences by lazy { AppPreferences() }

Preference Categories

Notifications (lines 96-103)

Key Type Default Purpose
notificationsMode NotificationsMode SERVICE (if previously enabled) OFF / SERVICE / PERIODIC
notificationPreviewMode String "message" message / contact / hidden
canAskToEnableNotifications Boolean true Whether to show notification enable prompt
backgroundServiceNoticeShown Boolean false Background service notice already shown
backgroundServiceBatteryNoticeShown Boolean false Battery notice already shown
autoRestartWorkerVersion Int 0 Worker version for periodic restart

Calls (lines 105-111)

Key Type Default Purpose
webrtcPolicyRelay Boolean true Use TURN relay for WebRTC
callOnLockScreen CallOnLockScreen SHOW DISABLE / SHOW / ACCEPT
webrtcIceServers String? null Custom ICE servers
experimentalCalls Boolean false Enable experimental call features

Authentication (lines 107-110)

Key Type Default Purpose
performLA Boolean false Enable local authentication
laMode LAMode default Authentication mode
laLockDelay Int 30 Seconds before re-auth required
laNoticeShown Boolean false LA notice shown

Privacy (lines 112-128)

Key Type Default Purpose
privacyProtectScreen Boolean true FLAG_SECURE on Android
privacyAcceptImages Boolean true Auto-accept images
privacyLinkPreviews Boolean true Generate link previews
privacySanitizeLinks Boolean false Remove tracking params from links
simplexLinkMode SimplexLinkMode DESCRIPTION DESCRIPTION / FULL / BROWSER
privacyShowChatPreviews Boolean true Show chat previews in list
privacySaveLastDraft Boolean true Save compose draft
privacyDeliveryReceiptsSet Boolean false Delivery receipts configured
privacyEncryptLocalFiles Boolean true Encrypt local files
privacyAskToApproveRelays Boolean true Ask before using relays
privacyMediaBlurRadius Int 0 Blur radius for media

Network (lines 140-175)

Key Type Default Purpose
networkUseSocksProxy Boolean false Enable SOCKS proxy
networkProxy NetworkProxy localhost:9050 Proxy host/port
networkSessionMode TransportSessionMode default Session mode
networkSMPProxyMode SMPProxyMode default SMP proxy mode
networkSMPProxyFallback SMPProxyFallback default Proxy fallback policy
networkHostMode HostMode default Host mode (onion routing)
networkRequiredHostMode Boolean false Enforce host mode
networkSMPWebPortServers SMPWebPortServers default Web port server config
networkShowSubscriptionPercentage Boolean false Show subscription stats
networkTCPConnectTimeout* Long varies TCP connect timeouts (background/interactive)
networkTCPTimeout* Long varies TCP operation timeouts
networkTCPTimeoutPerKb Long varies Per-KB timeout
networkRcvConcurrency Int default Receive concurrency
networkSMPPingInterval Long default SMP ping interval
networkSMPPingCount Int default SMP ping count
networkEnableKeepAlive Boolean default TCP keep-alive
networkTCPKeepIdle Int default Keep-alive idle time
networkTCPKeepIntvl Int default Keep-alive interval
networkTCPKeepCnt Int default Keep-alive count

Appearance (lines 213-233)

Key Type Default Purpose
currentTheme String "SYSTEM" Active theme name
systemDarkTheme String "SIMPLEX" Theme for system dark mode
currentThemeIds Map<String, String> empty Theme ID per base theme
themeOverrides List<ThemeOverrides> empty Custom theme overrides
profileImageCornerRadius Float 22.5f Avatar corner radius
chatItemRoundness Float 0.75f Message bubble roundness
chatItemTail Boolean true Show bubble tail
fontScale Float 1f Font scale factor
densityScale Float 1f UI density scale
inAppBarsAlpha Float varies Bar transparency
appearanceBarsBlurRadius Int 50 or 0 Bar blur radius (device-dependent)

Developer (lines 135-139)

Key Type Default Purpose
developerTools Boolean false Enable developer tools
logLevel LogLevel WARNING Log level
showInternalErrors Boolean false Show internal errors to user
showSlowApiCalls Boolean false Alert on slow API calls
terminalAlwaysVisible Boolean false Floating terminal window (desktop)

Database (lines 188-208)

Key Type Default Purpose
onboardingStage OnboardingStage OnboardingComplete Current onboarding step
storeDBPassphrase Boolean true Store DB passphrase in keystore
initialRandomDBPassphrase Boolean false DB was created with random passphrase
encryptedDBPassphrase String? null Encrypted DB passphrase
confirmDBUpgrades Boolean false Confirm DB migrations
chatStopped Boolean false Chat was explicitly stopped
chatLastStart Instant? null Last chat start timestamp
newDatabaseInitialized Boolean false DB successfully initialized at least once
shouldImportAppSettings Boolean false Import settings after DB import
selfDestruct Boolean false Self-destruct enabled
selfDestructDisplayName String? null Display name for self-destruct profile

UI Preferences (lines 255-257)

Key Type Default Purpose
oneHandUI Boolean true One-hand mode
chatBottomBar Boolean true Bottom bar in chat

Remote Access (lines 238-243)

Key Type Default Purpose
deviceNameForRemoteAccess String device model Device name shown to paired devices
confirmRemoteSessions Boolean false Confirm remote sessions
connectRemoteViaMulticast Boolean false Use multicast for discovery
connectRemoteViaMulticastAuto Boolean true Auto-connect via multicast
offerRemoteMulticast Boolean true Offer multicast connection

Migration (lines 189-190)

Key Type Default Purpose
migrationToStage String? null Migration-to-device progress
migrationFromStage String? null Migration-from-device progress

Updates and Versioning (lines 184-186, 235-237)

Key Type Default Purpose
appUpdateChannel AppUpdatesChannel DISABLED DISABLED / STABLE / BETA
appSkippedUpdate String "" Skipped update version
appUpdateNoticeShown Boolean false Update notice shown
whatsNewVersion String? null Last "What's New" version seen
lastMigratedVersionCode Int 0 Last app version code for data migrations
customDisappearingMessageTime Int 300 Custom disappearing message time (seconds)

Preference Utility Types

The SharedPreference<T> wrapper (defined in SimpleXAPI.kt) provides:

  • get(): T -- read current value
  • set(value: T) -- write value
  • state: MutableState<T> -- Compose-observable state (derived lazily)

Factory methods: mkBoolPreference, mkIntPreference, mkLongPreference, mkFloatPreference, mkStrPreference, mkEnumPreference, mkSafeEnumPreference, mkDatePreference, mkMapPreference, mkTimeoutPreference.


6. Source Files

File Path Key Contents
ChatModel.kt common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt ChatModel singleton (line 86), ChatsContext (line 339), Chat (line 1328), ChatInfo (line 1391), ChatStats (line 1370), helper methods
SimpleXAPI.kt common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt AppPreferences (line 94), ChatController (line 493), startReceiver (line 660), sendCmd (line 804), recvMsg (line 829), processReceivedMsg (line 2568)
ChatItemsMerger.kt common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt ActiveChatState (line 196), chat item merge/diff logic
Core.kt common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt initChatController (line 62), state initialization flow
App.kt common/src/commonMain/kotlin/chat/simplex/common/App.kt AppScreen (line 47), MainScreen (line 84), top-level UI state reads