State Management
Table of Contents
- Overview
- ChatModel
- ChatsContext
- Chat
- AppPreferences
- 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:
State mutations originate from two sources:
- User actions: Compose UI handlers call
api*() suspend functions on ChatController, which send commands to the Haskell core, receive responses, and update ChatModel.
- 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:
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:
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:
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:
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:
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 |