* desktop: add bounded animated image decoder Skia's Codec is already on the desktop classpath through skiko and decodes both GIF and animated WebP. The frames come from a file somebody else composed, so the decoder is bounded before it allocates: the raster is measured in bytes with the sides multiplied as Long, each side is capped separately so an extreme aspect ratio cannot slip under the byte budget, and the encoded size is checked before the bytes are copied into native memory. Anything outside the bounds, or any failure, keeps the still image the chat already renders. Nothing calls this yet. * desktop: animate GIFs in chat items and full screen Both views drew the first frame only. The full screen view also decoded its still on every recomposition, which an animation recomposes once per frame, so that decode is remembered against the data it comes from. The chat list preview stays a still image: it is a 36dp box that the desktop layout keeps on screen the whole time, so animating it would hold a raster and spend a frame of work per listed chat, without pause. Removes the two markers left for this work. * desktop: don't decode animation frames that cannot be seen With media blur on, a blurred image is only revealed while the mouse is over it, so every frame was decoded, uploaded and then blurred away again for nobody - and the blur is a render effect re-run per frame. Frames now decode only while the image can be seen, which also stops motion showing through a blur that is there to hide it. Passing the blur state to the view is why the shared signature changes; coil drives its own animation on Android, so there is nothing to pause there. * docs: move animated images plan to plans/ * docs: drop file path references from animated images plan * docs: correct animated images plan against the code * desktop: correct animated image comments * desktop: reduce animated image comments * desktop: correct and bound animated image decoding * docs: correct animated images plan against measurements * desktop: fuse the animation prior frame decision * docs: cover desktop animated images in spec and product * desktop: drop the unused animated image component * desktop: return the animation frame instead of its state * docs: correct the animated images documentation * desktop: don't decode animations under the full screen viewer * desktop: bound the frames an animation rebuilds * desktop: pause animations under any full screen modal * desktop: stop animations that alternate expensive frames * desktop: read what playing a frame needs only once * desktop: close the codec of an animation outside the bounds * desktop: wait out what an animation frame cost to decode * docs: correct animated images claims against the code * desktop: bound the frame count where the others are bounded * desktop: don't wait out a stall an animation frame did not spend * desktop: say what the slow frame constants stand for * desktop: don't decode animations behind a minimised window * desktop: make the animation frame wait testable * desktop: bound the file size where the others are bounded * desktop: pin the frame wait clamp in its test * desktop: keep the frame wait clamp private * desktop: reduce animated image comments --------- Co-authored-by: sh <github.shum@liber.li>
SimpleX Chat -- Kotlin Multiplatform Specification
Table of Contents
Executive Summary
SimpleX Chat is a Kotlin Multiplatform application targeting Android and Desktop (JVM) platforms. The UI layer is built entirely with Jetpack Compose. The application communicates with a Haskell-based cryptographic core (simplex-chat) through a JNI bridge -- native functions declared in Kotlin and linked at runtime to a shared library (libapp-lib). Platform-specific behavior (notifications, file system paths, services, audio/video) is abstracted using the expect/actual pattern and a runtime-assignable PlatformInterface callback object.
The Gradle project is structured as three modules:
| Module | Purpose |
|---|---|
:common |
Shared Compose UI, models, platform abstractions (commonMain, androidMain, desktopMain) |
:android |
Android application entry point (SimplexApp, MainActivity) |
:desktop |
Desktop application entry point (Main.kt, showApp()) |
All meaningful application logic resides in :common/commonMain. Platform source sets (androidMain, desktopMain) provide actual implementations for expect declarations and host platform-specific integration code.
Dependency Graph
App Entry Points
+-- Android: SimplexApp.onCreate -> initHaskell -> initMultiplatform -> initChatControllerOnStart
| MainActivity.onCreate -> setContent { AppScreen() }
+-- Desktop: main() -> initHaskell -> runMigrations -> initApp -> showApp -> AppWindow -> AppScreen()
|
v
Common Module (commonMain)
+-- ChatModel (Compose state singleton) <-> ChatController/SimpleXAPI (JNI bridge) <-> Haskell Core (chat_ctrl)
+-- Views (Compose)
| +-- App.kt: AppScreen -> MainScreen
| +-- ChatListView -> ChatView -> ComposeView -> SendMsgView
| +-- ChatItemView (message rendering: text, image, video, voice, file, call, events)
| +-- Settings: SettingsView, UserProfileView, UserProfilesView
| +-- Onboarding: OnboardingView, WhatsNewView, CreateFirstProfile
| +-- Call: CallView, IncomingCallAlertView
| +-- Database: DatabaseView, DatabaseEncryptionView, DatabaseErrorView
| +-- Groups: GroupChatInfoView, AddGroupMembersView, GroupMemberInfoView
| +-- Contacts: ContactListNavView
| +-- Remote: ConnectDesktopView, ConnectMobileView
| +-- Terminal: TerminalView
+-- Models
| +-- ChatModel -- global app state (Compose MutableState singleton)
| +-- ChatsContext -- per-context chat list state (primary + optional secondary)
| +-- Chat -- per-conversation state (chatInfo, chatItems, chatStats)
| +-- ChatController -- API command dispatch, event receiver, preferences
| +-- AppPreferences -- 150+ SharedPreferences keys
+-- Services
| +-- NtfManager -- abstract notification coordinator (Android/Desktop implementations)
| +-- SimplexService -- Android foreground service for background messaging
| +-- ThemeManager -- theme resolution (system/light/dark/simplex/black + per-user overrides)
| +-- CallManager -- WebRTC call lifecycle
+-- Platform (expect/actual)
+-- Core.kt -- JNI declarations (external fun), initChatController, chatInitTemporaryDatabase
+-- AppCommon.kt -- runMigrations, AppPlatform enum
+-- Files.kt -- dataDir, tmpDir, filesDir, dbAbsolutePrefixPath (expect)
+-- Share.kt -- shareText, shareFile, openFile (expect)
+-- VideoPlayer.kt -- VideoPlayerInterface, VideoPlayer (expect class)
+-- RecAndPlay.kt -- RecorderInterface, AudioPlayerInterface (expect)
+-- UI.kt -- showToast, hideKeyboard, getKeyboardState (expect)
+-- Notifications.kt -- allowedToShowNotification (expect)
+-- NtfManager.kt -- abstract NtfManager class
+-- Platform.kt -- PlatformInterface (runtime callback object)
+-- Cryptor.kt -- CryptorInterface (expect)
+-- Images.kt -- bitmap utilities (expect)
+-- SimplexService.kt-- getWakeLock (expect)
+-- Log.kt, Modifier.kt, Back.kt, ScrollableColumn.kt, PlatformTextField.kt, Resources.kt
Specification Documents
| Document | Path | Description |
|---|---|---|
| Architecture | spec/architecture.md | System layers, module structure, JNI bridge, app lifecycle, event streaming, platform abstraction |
| State Management | spec/state.md | ChatModel singleton, ChatsContext, Chat data class, AppPreferences, ActiveChatState |
| API | spec/api.md | ChatController command dispatch, ~150 API functions in 11 categories, CC/CR/API types |
| Database | spec/database.md | SQLite database files, migrations, encryption, backup/restore |
| Impact | spec/impact.md | Source file → product concept mapping for change impact analysis |
| Chat View | spec/client/chat-view.md | ChatView, ChatItemView, message rendering, item interactions |
| Chat List | spec/client/chat-list.md | ChatListView, ChatPreviewView, filtering, search, tags |
| Compose | spec/client/compose.md | ComposeView, SendMsgView, ComposeState, attachments, mentions |
| Navigation | spec/client/navigation.md | App screen routing, onboarding, settings, new chat flows |
| Calls | spec/services/calls.md | WebRTC call lifecycle, signaling, platform-specific call views |
| Files | spec/services/files.md | File transfer (SMP inline / XFTP), CryptoFile encryption, platform file paths |
| Notifications | spec/services/notifications.md | NtfManager, SimplexService, notification channels, background delivery |
| Theme | spec/services/theme.md | ThemeManager, color system, wallpapers, per-user overrides |
Product Documents
| Category | Path | Topic |
|---|---|---|
| Overview | product/README.md | Product overview, capability map, navigation map |
| Concepts | product/concepts.md | 30 product concepts (PC1-PC30) mapped to docs + source |
| Glossary | product/glossary.md | Domain term definitions (9 sections) |
| Rules | product/rules.md | 18 business rules in 6 categories |
| Gaps | product/gaps.md | 7 known gaps with recommendations |
| Flows | product/flows/ | onboarding, messaging, connection, calling, file-transfer, group-lifecycle |
| Views | product/views/ | chat-list, chat, settings, onboarding, call, new-chat, contact-info, group-info, user-profiles |
Source Entry Points
| Component | File | Key Symbol | Line |
|---|---|---|---|
| Android Application | SimplexApp.kt |
class SimplexApp |
41 |
| Android Activity | MainActivity.kt |
class MainActivity |
27 |
| Desktop Entry | Main.kt |
fun main() |
21 |
| Desktop App Window | DesktopApp.kt |
fun showApp() |
33 |
| Desktop Init | AppCommon.desktop.kt |
fun initApp() |
21 |
| Common App Screen | App.kt |
fun AppScreen() |
47 |
| JNI Bridge | Core.kt |
external fun initHS() |
18 |
| Chat Controller | SimpleXAPI.kt |
object ChatController |
493 |
| Chat Model | ChatModel.kt |
object ChatModel |
86 |
| App Preferences | SimpleXAPI.kt |
class AppPreferences |
94 |
| Platform Interface | Platform.kt |
interface PlatformInterface |
15 |
| Notification Manager | NtfManager.kt |
abstract class NtfManager |
19 |
| Theme Manager | ThemeManager.kt |
object ThemeManager |
18 |
| Android Haskell Init | AppCommon.android.kt |
fun initHaskell(packageName: String) |
33 |
| Common Migrations | AppCommon.kt |
fun runMigrations() |
41 |
| Android Service | SimplexService.kt |
class SimplexService |
41 |
| Gradle Root | settings.gradle.kts |
include(":android", ":desktop", ":common") |
22 |
| Common Build | build.gradle.kts |
kotlin { androidTarget(); jvm("desktop") } |
14 |