Files
simplex-chat/apps/multiplatform/spec/state.md
T
spacedandy-agents-simplex 7627e2d4eb android, desktop: refresh stale line references in spec documents
CODE.md asks for every line reference in an affected spec document to be
updated. Most of these were already stale on master. Numbers only: no
wording changes. Kept apart from the feature documentation so it can be
reviewed or dropped on its own.

Run: 003-kotlin-directory-search, slice 7
2026-10-14 14:08:04 +00:00

32 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 155 as @Stable object ChatModel.

Controller Reference

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

User State

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

Chat Runtime State

Field Type Line Purpose
chatRunning MutableState<Boolean?> 161 null = initializing, true = running, false = stopped
chatDbChanged MutableState<Boolean> 162 Database was changed externally (needs restart)
chatDbEncrypted MutableState<Boolean?> 163 Whether database is encrypted
chatDbStatus MutableState<DBMigrationResult?> 164 Result of database migration attempt
ctrlInitInProgress MutableState<Boolean> 165 Controller initialization in progress
dbMigrationInProgress MutableState<Boolean> 166 Database migration in progress
incompleteInitializedDbRemoved MutableState<Boolean> 167 Tracks if incomplete DB files were removed (prevents infinite retry)

Current Chat State

Field Type Line Purpose
chatId MutableState<String?> 172 ID of the currently open chat (null = chat list shown)
chatAgentConnId MutableState<String?> 173 Agent connection ID for current chat
chatSubStatus MutableState<SubscriptionStatus?> 174 Subscription status for current chat
openAroundItemId MutableState<Long?> 175 Item ID to scroll to when opening chat
chatsContext ChatsContext 176 Primary chat context (see ChatsContext)
secondaryChatsContext MutableState<ChatsContext?> 177 Optional secondary context for dual-pane views
chats State<List<Chat>> 179 Derived from chatsContext.chats
deletedChats MutableState<List<Pair<Long?, String>>> 181 Recently deleted chats (rhId, chatId)

Group Members

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

Chat Tags and Filters

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

Terminal and Developer

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

Calls (WebRTC)

Field Type Line Purpose
callManager CallManager 240 WebRTC call lifecycle manager
callInvitations SnapshotStateMap<String, RcvCallInvitation> 241 Pending incoming call invitations keyed by chatId
activeCallInvitation MutableState<RcvCallInvitation?> 242 Currently displayed incoming call invitation
activeCall MutableState<Call?> 243 Currently active call
activeCallViewIsVisible MutableState<Boolean> 244 Whether call UI is showing
activeCallViewIsCollapsed MutableState<Boolean> 245 Whether call UI is in PiP/collapsed mode
callCommand SnapshotStateList<WCallCommand> 246 Pending WebRTC commands
showCallView MutableState<Boolean> 247 Call view visibility toggle
switchingCall MutableState<Boolean> 248 True during call switching

Compose Draft and Sharing

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

Remote Hosts

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

Miscellaneous UI State

Field Type Line Purpose
userAddress MutableState<UserContactLinkRec?> 200 User's public contact address
chatItemTTL MutableState<ChatItemTTL> 201 Chat item time-to-live setting
clearOverlays MutableState<Boolean> 204 Signal to close all overlays/modals
appOpenUrl MutableState<Pair<Long?, String>?> 210 URL opened via deep link (rhId, uri)
appOpenUrlConnecting MutableState<Boolean> 211 Whether a deep link connection is in progress
newChatSheetVisible MutableState<Boolean> 214 Whether new chat bottom sheet is visible
fullscreenGalleryVisible MutableState<Boolean> 217 Fullscreen gallery mode
chatListBanner ChatListBanner? (plain var) 221 The banner kind the chat list showed this app session; bannerSlotFree(banner) tells whether a kind may take the slot (see chat-list.md)
notificationPreviewMode MutableState<NotificationPreviewMode> 226 Notification content preview level
showAuthScreen MutableState<Boolean> 235 Whether to show authentication screen
showChatPreviews MutableState<Boolean> 237 Whether to show chat preview text in list
networkInfo MutableState<UserNetworkInfo> 265 Network type and online status
conditions MutableState<ServerOperatorConditionsDetail> 267 Server operator terms/conditions
updatingProgress MutableState<Float?> 269 Progress indicator for app updates
simplexLinkMode MutableState<SimplexLinkMode> 263 How SimpleX links are displayed
migrationState MutableState<MigrationToState?> 253 Database migration to new device state
showingInvitation MutableState<ShowingInvitation?> 251 Currently displayed invitation
desktopOnboardingRandomPassword MutableState<Boolean> 207 Desktop: user skipped password setup
filesToDelete MutableSet<File> 262 Temporary files pending cleanup

3. ChatsContext

Defined as inner class at ChatModel.kt line 418:

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 176) and an optional secondary context (secondaryChatsContext at line 177).

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? 418 Filter type: null = primary, GroupChatScope or MsgContentTag
chats MutableState<SnapshotStateList<Chat>> 419 List of all chats in this context
chatItems MutableState<SnapshotStateList<ChatItem>> 424 Items for the currently open chat in this context
chatState ActiveChatState 426 Tracks unread counts, splits, scroll state

Derived Properties

Property Line Purpose
contentTag 432 MsgContentTag? -- content filter tag if context is MsgContentTag
groupScopeInfo 439 GroupChatScopeInfo? -- group scope if context is GroupChatScope
isUserSupportChat 446 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 1460:

@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 1502:

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 1523:

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

RelayStatus (Channels)

RelayStatus is an enum class at ChatModel.kt line 2752 modelling a relay's lifecycle for a channel on the owner's side. Serialized as a lowercase string via @SerialName.

Case SerialName Meaning
RsNew "new" Allocated locally; not yet sent
RsInvited "invited" XGrpRelayInv sent, awaiting XGrpRelayAcpt
RsAccepted "accepted" Accepted, link-data update pending
RsActive "active" Listed in channel link data; forwarding
RsInactive "inactive" No longer in link data or backend reports it removed
RsRejected "rejected" Relay sent XGrpRelayReject for the channel link; final on the owner side. Clearable only by the relay operator running /group allow #<channel>. The owner-side GroupMember.memberStatus is also set to MemLeft so the relay renders identically to one that explicitly left (MemRejected is reserved for the knocking-admission flow).

The text extension on the enum returns the localized status string (resource key relay_status_*, with relay_status_rejected = "rejected").


5. AppPreferences

Defined at SimpleXAPI.kt line 103 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 540:

val appPrefs: AppPreferences by lazy { AppPreferences() }

Preference Categories

Notifications (lines 105-113)

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 115-121)

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 117-120)

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 122-141)

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 153-188)

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 233-253)

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 148-152)

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 207-228)

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 275-277)

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

Hints

Preferences listed in hintPreferences are restored to their defaults by "Reset all hints" (resetHintPreferences() in views/usersettings/SettingsView.kt).

Key Type Default Purpose
directorySearchAlertShown ("DirectorySearchAlertShown") Boolean false The user chose "Search and don't show again" in the online search alert; in hintPreferences (see Chat List, Online Search)

Remote Access (lines 258-263)

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 208-209)

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

Updates and Versioning (lines 203-205, 255-257)

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 155), ChatsContext (line 418), Chat (line 1460), ChatInfo (line 1523), ChatStats (line 1502), helper methods
SimpleXAPI.kt common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt AppPreferences (line 103), ChatController (line 537), startReceiver (line 785), sendCmd (line 939), recvMsg (line 964), processReceivedMsg (line 2920)
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 70), state initialization flow
App.kt common/src/commonMain/kotlin/chat/simplex/common/App.kt AppScreen (line 48), MainScreen (line 85), top-level UI state reads