diff --git a/apps/multiplatform/CODE.md b/apps/multiplatform/CODE.md new file mode 100644 index 0000000000..26a36e75bb --- /dev/null +++ b/apps/multiplatform/CODE.md @@ -0,0 +1,309 @@ +# Coding and building + +You are an expert developer for SimpleX Chat, a privacy-first decentralized messaging platform. You MUST navigate and develop this codebase using the three-layer documentation architecture described below. You MUST NOT write code without first loading the relevant product and spec context. + +## Three-Layer Documentation Architecture + +### Why this structure exists + +LLMs start each session with no persistent understanding of the codebase. Navigating thousands of lines of flat source code to reconstruct behavior, constraints, and intent wastes context window and produces unreliable results. + +The `product/`, `spec/`, and source layers form a persistent, structured representation of the system that survives across sessions. Each layer is connected to the next by bidirectional cross-references. This structure enables you to load only the context relevant to a specific change, understand all affected concepts, and maintain coherence as the system evolves. + +### The layers + +| Layer | Contains | Question it answers | +|-------|----------|-------------------| +| `product/` | Capabilities, user flows, views, business rules, glossary | **What** does the system do and why? | +| `spec/` | Technical design, API contracts, database schema, service internals | **How** is it organized technically? | +| `common/src/commonMain/` | Shared Kotlin/Compose code (Android + Desktop) | What does it **execute** on both platforms? | +| `common/src/androidMain/` | Android-specific Kotlin (platform implementations) | What does it execute on **Android**? | +| `common/src/desktopMain/` | Desktop-specific Kotlin (platform implementations) | What does it execute on **Desktop**? | +| `android/src/main/` | Android app module (Application, Activity, Services) | What is the **Android entry point**? | +| `desktop/src/jvmMain/` | Desktop app module (main function) | What is the **Desktop entry point**? | +| `../../src/Simplex/Chat/` | Haskell core (chat logic, protocol, database) | What does the **core** execute? | + +Each layer links to the next: +- `product/concepts.md` links every concept to its spec docs, source files, and tests in a single table — this is the primary navigation entry point +- `product/views/*.md` and `product/flows/*.md` each have a **Related spec:** line linking to their most relevant spec documents +- `product/glossary.md` uses *See: [spec/...]* references and `product/rules.md` uses **Spec:** [spec/...] references to link individual terms and rules down to spec +- `spec/` documents contain **Source:** headers and inline function links pointing down to source. Line references MUST be clickable by embedding the `#Lxx-Lyy` fragment in the link URL: [`functionName()`](common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#Lxx-Lyy). You MUST NOT duplicate line numbers in the display text — the URL fragment is sufficient. Why: redundant line numbers in display text create maintenance burden on every line shift. +- Reverse direction: the Document Map (end of this file) maps source → spec → product + +### Navigation workflow + +When the user requests any change, you MUST follow these steps before writing any code: + +1. **Identify scope.** You MUST read `product/concepts.md` and find which product concepts are affected by the requested change. Each row links to the relevant product docs, spec docs, source files, and tests. Why: concepts.md is the fastest path to identify all affected documents — skipping it risks missing impacted areas. + +2. **Load product context.** You MUST read the relevant `product/views/*.md` or `product/flows/*.md` to understand current user-facing behavior. For business constraints, you MUST read `product/rules.md`. Why: product documents define the intended behavior — changing code without understanding current behavior risks breaking the user contract. + +3. **Load spec context.** You MUST follow the product → spec links to read the relevant `spec/*.md` or `spec/services/*.md`. You MUST understand the technical design, function signatures, and data flows. Why: spec documents reveal technical constraints and invariants that product docs omit — ignoring them leads to implementations that violate existing guarantees. + +4. **Load source context.** You MUST follow the spec → source links (with line numbers) to read the relevant source files. Why: source code is the ground truth — product and spec may lag behind actual behavior. + +5. **Identify full impact.** You MUST read `spec/impact.md` to find all product concepts affected by the source files you plan to change. This determines which documents you MUST update after the code change. Why: without impact analysis, documentation updates will be incomplete, and future sessions will navigate using stale information. + +For internal-only changes that do not map to a product concept (infrastructure, refactoring, non-user-facing fixes), you MUST start at step 3 using the Document Map to find the relevant spec document, then proceed to steps 4–6. + +6. **Implement.** Make the code change in source, then you MUST update all affected documentation as described in the Change Protocol below. + +### Key navigation documents + +| Document | Purpose | When to read | +|----------|---------|-------------| +| `product/concepts.md` | Concept → doc → code → test cross-reference | Starting point for every change | +| `product/rules.md` | Business invariants with enforcement locations and tests | Before modifying any behavior | +| `product/glossary.md` | Domain term definitions | When encountering unfamiliar terms | +| `product/gaps.md` | Known issues and recommendations | Before designing a fix or feature | +| `spec/impact.md` | Source file → affected product concepts | After identifying which files to change | +| Document Map (below) | Source ↔ spec ↔ product mapping | When updating documentation | + +--- + +## Code Security + +When designing code and planning implementations, you MUST: +- Apply adversarial thinking, and consider what may happen if one of the communicating parties is malicious. Why: security vulnerabilities arise from untested assumptions about trust boundaries. +- Formulate an explicit threat model for each change — who can do which undesirable things and under which circumstances. Why: explicit threat models catch attack vectors that implicit reasoning misses. + +--- + +## Code Style + +**Follow existing code patterns — you MUST:** +- Match the style of surrounding code. Why: consistent style reduces cognitive load and prevents unnecessary diff noise. +- Use Kotlin data classes for value types, regular classes for reference types, and sealed classes/interfaces for variants. Why: correct type choices leverage the type system for compile-time correctness. +- Prefer exhaustive `when` expressions over `else` branches. Why: `else` branches bypass compiler checks for new sealed subclasses and hide bugs. + +**Comments policy — you MUST:** +- Only comment on non-obvious design decisions or tricky implementation details. Why: redundant comments create maintenance burden and drift from code. +- Keep function names and type signatures self-documenting. Why: good names eliminate the need for most comments. +- Assume a competent Kotlin reader. Why: over-explaining trivial Kotlin adds noise without value. + +**Diff and refactoring — you MUST:** +- Avoid unnecessary changes and code movements. Why: unnecessary changes increase review burden and hide the meaningful diff. +- Never do refactoring unless it substantially reduces cost of solving the current problem, including the cost of refactoring itself. Why: speculative refactoring has guaranteed present cost with uncertain future benefit. +- Minimize the code changes — do what is minimally required to solve users' problems. Why: smaller diffs are easier to review, less likely to introduce bugs, and faster to revert. + +**Document and code structure — you MUST:** +- **Never move existing code or sections around** — add new content at appropriate locations without reorganizing existing structure. Why: moving code creates large diffs that obscure the actual change and break git blame. +- When adding new sections to documents, continue the existing numbering scheme. Why: consistent numbering preserves document navigability. +- Minimize diff size — prefer small, targeted changes over reorganization. Why: large diffs compound review errors and make rollback difficult. + +**Code analysis and review — you MUST:** +- Trace data flows end-to-end: from origin, through storage/parameters, to consumption. Flag values that are discarded and reconstructed from partial data (e.g. extracted from a URI missing original fields) — this is usually a bug. Why: broken data flows are the most common source of security and correctness bugs. +- Read implementations of called functions, not just signatures — if duplication involves a called function, check whether decomposing it resolves the duplication. Why: function signatures can be misleading about actual behavior. +- Read every function in the data flow even when the interface seems clear. Why: wrong assumptions about internals are the main source of missed bugs. + +--- + +## Plans + +When developing via plans (non-trivial features, multi-step changes, architectural decisions), you MUST store the plan in the `plans/` folder before implementing. Why: plans are the persistent record of design decisions and rationale — without them, future sessions cannot understand why the system was built the way it was. + +### Plan requirements + +1. **File naming.** You MUST use the format `YYYYMMDD_NN.md` (e.g., `20260211_01.md`). Why: chronological ordering makes it easy to trace the evolution of design decisions. + +2. **Plan structure.** Every plan MUST include: (1) Problem statement, (2) Solution summary, (3) Detailed technical design, (4) Detailed implementation steps. Why: incomplete plans lead to ad-hoc implementation that drifts from intent. + +3. **Consistency with product/ and spec/.** The plan MUST be consistent with the current state of `product/` and `spec/`. If the plan introduces new behavior, it MUST describe which product and spec documents will be affected. Why: plans that contradict existing documentation create conflicting sources of truth. + +4. **Adversarial self-review.** After writing the plan, you MUST run the same adversarial self-review as for code changes: verify the plan is internally consistent, consistent with product/ and spec/, and does not introduce contradictions. You MUST repeat until two consecutive passes find zero issues. Why: an incoherent plan produces incoherent implementation. + +--- + +## Change Protocol + +### The rule + +Every code change MUST include corresponding updates to `spec/` and `product/`. A task is NOT complete until all three layers are coherent with each other. Why: these layers are the persistent memory that enables coherent development across sessions — stale documentation creates false confidence and compounds errors in every future change. + +### What to update + +1. **spec/ — on every code change.** You MUST update the corresponding spec document to reflect the change. You MUST add new functions, update changed signatures, and remove deleted ones. Why: spec documents map 1:1 to source files — divergence defeats specification. + +2. **product/ — when user-visible behavior changes.** You MUST update the relevant `product/views/*.md` and any affected `product/flows/*.md`. You MUST update `product/rules.md` when business invariants change. Why: product documents are the contract with users — silent changes create confusion. + +3. **Line number references — on every code change.** You MUST verify and update all `#Lxx-Lyy` references in affected spec documents. Why: stale line numbers make spec documents misleading and destroy navigational value. + +4. **Cross-references — when adding or removing files.** You MUST add corresponding spec documents and update `spec/README.md` document index and reverse index. When adding pages, you MUST add `product/views/` and `spec/client/` documents. You MUST update the Document Map at the end of this file. Why: every source file must be covered for the navigation system to work. + +5. **Impact graph — when adding files or changing what a file affects.** You MUST update `spec/impact.md` to reflect the source file → product concept mapping. Why: the impact graph drives documentation updates for all future changes — an incomplete graph causes future changes to miss required updates. + +6. **Concept index — when adding or changing product concepts.** You MUST add or update the relevant row in `product/concepts.md` with links to product docs, spec docs, source files, and tests. Why: the concept index is the entry point for all future navigation — a missing row means future changes to that concept will miss context. + +7. **[GAP] annotations — when discovering issues.** When encountering missing error handling, dead code, inconsistencies, or incomplete features, you MUST add a `[GAP]` annotation in the relevant spec or product document and add a summary to `product/gaps.md`. Why: this builds institutional knowledge about technical debt. + +8. **[REC] annotations — when identifying improvements.** You MUST add a `[REC]` annotation in the relevant document. Why: capturing improvement ideas at discovery time preserves context that is lost later. + +9. **Preserve document structure.** You MUST follow existing format conventions: spec documents use function-anchored links with line numbers, product documents use interaction descriptions, flow documents use Mermaid diagrams. Why: consistent structure makes documents predictable and navigable. + +### Adversarial self-review + +After completing all changes (code + documentation), you MUST run an adversarial self-review. You MUST check coherence both within each layer and across layers. + +**Within-layer coherence — you MUST verify:** +- spec/ is internally consistent — no contradictory descriptions, state machines have no unreachable states, data model is referentially intact +- product/ is internally consistent — flows match views, rules match behavior descriptions + +**Across-layer coherence — you MUST verify:** +- Every new or changed function in source appears in the corresponding spec/ document +- Every user-visible behavior change in source appears in the relevant product/ document +- All `#Lxx-Lyy` line references in affected spec documents point to the correct lines +- All cross-references resolve — product → spec links, spec → source links +- `spec/impact.md` covers all affected product concepts for the changed source files +- `product/concepts.md` rows are current for any affected concepts + +**Convergence:** You MUST repeat the review-and-fix cycle until two consecutive passes find zero issues. You MUST fix all issues discovered between passes. Why: LLM non-determinism means a single review pass may miss violations — two consecutive clean passes provide confidence that the layers are coherent. + +--- + +## Multiplatform Architecture Notes + +### Kotlin Multiplatform (KMP) + Compose Multiplatform + +The app uses Kotlin Multiplatform with Compose Multiplatform for shared UI. The project has three Gradle modules: + +- **common/** — Shared library containing all models, views, platform abstractions, and theme system +- **android/** — Android app module (Application, Activity, Services) +- **desktop/** — Desktop JVM app module (main entry point) + +### expect/actual Pattern + +Platform-specific code uses Kotlin's `expect`/`actual` mechanism. The `commonMain` source set declares `expect` functions/classes, and `androidMain`/`desktopMain` provide `actual` implementations. Files follow the naming convention: +- `commonMain`: `FileName.kt` (contains `expect` declarations) +- `androidMain`: `FileName.android.kt` (contains `actual` implementations) +- `desktopMain`: `FileName.desktop.kt` (contains `actual` implementations) + +When modifying platform abstractions, you MUST update both `actual` implementations. + +### Source Set Structure + +``` +common/src/ +├── commonMain/kotlin/chat/simplex/common/ -- Shared code (195 files) +│ ├── model/ -- ChatModel, SimpleXAPI, CryptoFile +│ ├── platform/ -- expect/actual platform abstractions +│ ├── ui/theme/ -- Theme system (ThemeManager, colors, types) +│ └── views/ -- Compose UI (chat, chatlist, call, settings, etc.) +├── androidMain/kotlin/chat/simplex/common/ -- Android actuals (55 files) +│ ├── platform/ -- actual implementations +│ └── views/ -- Android-specific view variants +├── desktopMain/kotlin/chat/simplex/common/ -- Desktop actuals (56 files) +│ ├── platform/ -- actual implementations +│ └── views/ -- Desktop-specific view variants +android/src/main/java/chat/simplex/app/ -- Android app (8 files) +desktop/src/jvmMain/kotlin/chat/simplex/desktop/ -- Desktop app (1 file) +``` + +### Platform Differences + +| Aspect | Android | Desktop | +|--------|---------|---------| +| Layout | 2-column (chat list → chat) | 3-column (sidebar → chat list → details) | +| Background messaging | SimplexService (foreground service) + MessagesFetcherWorker (WorkManager) | Continuous (always-on process) | +| Notifications | Android NotificationManager with channels | Desktop system notifications | +| Calls | CallActivity (separate Activity) + CallService | In-window call view | +| Video playback | ExoPlayer | VLC (VLCJ) | +| Authentication | Android BiometricPrompt | Passcode only | +| Auto-update | Play Store / manual APK | Built-in AppUpdater | +| Window management | Standard Activity lifecycle | StoreWindowState persistence | +| Entry point | SimplexApp (Application) + MainActivity | Main.kt → initHaskell() → showApp() | + +--- + +## Document Map + +### Shared Sources (commonMain) + +| Source Location | Spec Document | Product Document | +|----------------|---------------|-----------------| +| common/.../common/App.kt | spec/architecture.md | product/views/chat-list.md | +| common/.../common/AppLock.kt | spec/architecture.md | product/views/settings.md | +| common/.../common/model/ChatModel.kt | spec/state.md | product/concepts.md | +| common/.../common/model/SimpleXAPI.kt | spec/api.md, spec/architecture.md | product/concepts.md | +| common/.../common/model/CryptoFile.kt | spec/services/files.md | product/flows/file-transfer.md | +| common/.../common/platform/Core.kt | spec/architecture.md | product/concepts.md | +| common/.../common/platform/AppCommon.kt | spec/architecture.md | product/flows/onboarding.md | +| common/.../common/platform/Notifications.kt | spec/services/notifications.md | product/flows/messaging.md | +| common/.../common/platform/NtfManager.kt | spec/services/notifications.md | product/flows/messaging.md | +| common/.../common/platform/Files.kt | spec/services/files.md | product/flows/file-transfer.md | +| common/.../common/platform/SimplexService.kt | spec/services/notifications.md | product/flows/messaging.md | +| common/.../common/platform/Share.kt | spec/architecture.md | product/concepts.md | +| common/.../common/platform/VideoPlayer.kt | spec/services/files.md | product/views/chat.md | +| common/.../common/platform/RecAndPlay.kt | spec/services/files.md | product/views/chat.md | +| common/.../common/platform/UI.kt | spec/architecture.md | product/views/chat.md | +| common/.../common/platform/Platform.kt | spec/architecture.md | product/concepts.md | +| common/.../common/ui/theme/ThemeManager.kt | spec/services/theme.md | product/views/settings.md | +| common/.../common/ui/theme/Theme.kt | spec/services/theme.md | product/views/settings.md | +| common/.../common/ui/theme/Color.kt | spec/services/theme.md | product/views/settings.md | +| common/.../common/views/chatlist/ChatListView.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chatlist/ChatListNavLinkView.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chatlist/ChatPreviewView.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chatlist/UserPicker.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chatlist/TagListView.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chat/ChatView.kt | spec/client/chat-view.md | product/views/chat.md | +| common/.../common/views/chat/ComposeView.kt | spec/client/compose.md | product/views/chat.md | +| common/.../common/views/chat/SendMsgView.kt | spec/client/compose.md | product/views/chat.md | +| common/.../common/views/chat/ChatInfoView.kt | spec/client/chat-view.md | product/views/contact-info.md | +| common/.../common/views/chat/group/ | spec/client/chat-view.md | product/views/group-info.md | +| common/.../common/views/chat/item/ | spec/client/chat-view.md | product/views/chat.md | +| common/.../common/views/call/CallView.kt | spec/services/calls.md | product/views/call.md | +| common/.../common/views/call/IncomingCallAlertView.kt | spec/services/calls.md | product/views/call.md | +| common/.../common/views/call/WebRTC.kt | spec/services/calls.md | product/flows/calling.md | +| common/.../common/views/newchat/NewChatView.kt | spec/client/navigation.md | product/views/new-chat.md | +| common/.../common/views/newchat/AddGroupView.kt | spec/client/navigation.md | product/views/new-chat.md | +| common/.../common/views/usersettings/SettingsView.kt | spec/client/navigation.md | product/views/settings.md | +| common/.../common/views/usersettings/Appearance.kt | spec/services/theme.md | product/views/settings.md | +| common/.../common/views/usersettings/PrivacySettings.kt | spec/client/navigation.md | product/views/settings.md | +| common/.../common/views/usersettings/networkAndServers/ | spec/architecture.md | product/views/settings.md | +| common/.../common/views/usersettings/UserProfilesView.kt | spec/client/navigation.md | product/views/user-profiles.md | +| common/.../common/views/onboarding/ | spec/client/navigation.md | product/views/onboarding.md | +| common/.../common/views/localauth/ | spec/architecture.md | product/views/settings.md | +| common/.../common/views/database/ | spec/database.md | product/views/settings.md | +| common/.../common/views/migration/ | spec/database.md | product/flows/onboarding.md | +| common/.../common/views/remote/ | spec/architecture.md | product/views/settings.md | +| common/.../common/views/contacts/ | spec/client/chat-view.md | product/views/contact-info.md | +| common/.../common/views/helpers/ | spec/architecture.md | product/concepts.md | + +### Android-Specific Sources + +| Source Location | Spec Document | Product Document | +|----------------|---------------|-----------------| +| android/.../app/SimplexApp.kt | spec/architecture.md | product/flows/onboarding.md | +| android/.../app/MainActivity.kt | spec/architecture.md | product/views/chat-list.md | +| android/.../app/SimplexService.kt | spec/services/notifications.md | product/flows/messaging.md | +| android/.../app/CallService.kt | spec/services/calls.md | product/flows/calling.md | +| android/.../app/MessagesFetcherWorker.kt | spec/services/notifications.md | product/flows/messaging.md | +| android/.../app/model/NtfManager.android.kt | spec/services/notifications.md | product/flows/messaging.md | +| android/.../app/views/call/CallActivity.kt | spec/services/calls.md | product/views/call.md | + +### Desktop-Specific Sources + +| Source Location | Spec Document | Product Document | +|----------------|---------------|-----------------| +| desktop/.../desktop/Main.kt | spec/architecture.md | product/flows/onboarding.md | +| common/.../common/DesktopApp.kt (desktopMain) | spec/architecture.md | product/views/chat-list.md | +| common/.../common/StoreWindowState.kt (desktopMain) | spec/architecture.md | product/views/settings.md | +| common/.../common/model/NtfManager.desktop.kt (desktopMain) | spec/services/notifications.md | product/flows/messaging.md | +| common/.../common/views/helpers/AppUpdater.kt (desktopMain) | spec/architecture.md | product/views/settings.md | + +### Haskell Core Sources (at `../../src/Simplex/Chat/` relative to `apps/multiplatform/`) + +| Source Location | Spec Document | Product Document | +|----------------|---------------|-----------------| +| ../../src/Simplex/Chat/Controller.hs | spec/api.md | product/concepts.md | +| ../../src/Simplex/Chat/Types.hs | spec/api.md | product/glossary.md | +| ../../src/Simplex/Chat/Core.hs | spec/architecture.md | product/concepts.md | +| ../../src/Simplex/Chat/Protocol.hs | spec/architecture.md | product/concepts.md | +| ../../src/Simplex/Chat/Messages.hs | spec/api.md | product/flows/messaging.md | +| ../../src/Simplex/Chat/Messages/CIContent.hs | spec/api.md | product/flows/messaging.md | +| ../../src/Simplex/Chat/Call.hs | spec/services/calls.md | product/flows/calling.md | +| ../../src/Simplex/Chat/Files.hs | spec/services/files.md | product/flows/file-transfer.md | +| ../../src/Simplex/Chat/Store/Messages.hs | spec/database.md | product/flows/messaging.md | +| ../../src/Simplex/Chat/Store/Groups.hs | spec/database.md | product/flows/group-lifecycle.md | +| ../../src/Simplex/Chat/Store/Direct.hs | spec/database.md | product/flows/connection.md | +| ../../src/Simplex/Chat/Store/Files.hs | spec/database.md | product/flows/file-transfer.md | +| ../../src/Simplex/Chat/Store/Profiles.hs | spec/database.md | product/views/user-profiles.md | diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index 22e53af849..56279a5143 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -108,6 +108,7 @@ class ActiveCallState: Closeable { } +// Spec: spec/services/calls.md#ActiveCallView @SuppressLint("SourceLockedOrientationActivity") @Composable actual fun ActiveCallView() { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 70e0067260..d9439a5474 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -42,6 +42,7 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +// Spec: spec/client/navigation.md#AppScreen @Composable fun AppScreen() { AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() } @@ -78,6 +79,7 @@ fun AppScreen() { } } +// Spec: spec/client/navigation.md#MainScreen @Composable fun MainScreen() { val chatModel = ChatModel @@ -289,6 +291,7 @@ fun AndroidWrapInCallLayout(content: @Composable () -> Unit) { } } +// Spec: spec/client/navigation.md#AndroidScreen @Composable fun AndroidScreen(userPickerState: MutableStateFlow) { BoxWithConstraints { @@ -402,6 +405,7 @@ fun EndPartOfScreen() { ModalManager.end.showInView() } +// Spec: spec/client/navigation.md#DesktopScreen @Composable fun DesktopScreen(userPickerState: MutableStateFlow) { Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt index d6f9640cb9..32a5ce1ef1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt @@ -13,6 +13,7 @@ import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import kotlinx.coroutines.* +// Spec: spec/client/navigation.md#AppLock object AppLock { /** * We don't want these values to be bound to Activity lifecycle since activities are changed often, for example, when a user diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 8db2cc1a76..01f8197beb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -81,6 +81,7 @@ val connectProgressManager = ConnectProgressManager /* * Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it * */ +// Spec: spec/state.md#ChatModel @Stable object ChatModel { val controller: ChatController = ChatController @@ -334,6 +335,7 @@ object ChatModel { } } + // Spec: spec/state.md#ChatsContext class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?) { val chats = mutableStateOf(SnapshotStateList()) /** if you modify the items by adding/removing them, use helpers methods like [addToChatItems], [removeLastChatItems], [removeAllAndNotify], [clearAndNotify] and so on. @@ -1321,6 +1323,7 @@ interface SomeChat { val updatedAt: Instant } +// Spec: spec/state.md#Chat @Serializable @Stable data class Chat( val remoteHostId: Long?, @@ -1362,6 +1365,7 @@ data class Chat( true } + // Spec: spec/state.md#ChatStats @Serializable data class ChatStats( val unreadCount: Int = 0, @@ -1382,6 +1386,7 @@ data class Chat( } } +// Spec: spec/state.md#ChatInfo @Serializable sealed class ChatInfo: SomeChat, NamedChat { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt index 6ef56a9124..60f5c9e2ca 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt @@ -20,6 +20,7 @@ sealed class WriteFileResult { } * */ +// Spec: spec/services/files.md#writeCryptoFile fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs { val ctrl = ChatController.getChatCtrl() ?: throw Exception("Controller is not initialized") val buffer = ByteBuffer.allocateDirect(data.size) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 31edeec55a..388a8064c4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -90,6 +90,7 @@ enum class SimplexLinkMode { } } +// Spec: spec/state.md#AppPreferences class AppPreferences { // deprecated, remove in 2024 private val runServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true) @@ -491,6 +492,7 @@ private const val MESSAGE_TIMEOUT: Int = 300_000_000 object ChatController { private var chatCtrl: ChatCtrl? = -1 + // Spec: spec/state.md#appPrefs val appPrefs: AppPreferences by lazy { AppPreferences() } val messagesChannel: Channel = Channel() @@ -654,6 +656,7 @@ object ChatController { chatModel.updateChatTags(rhId) } + // Spec: spec/api.md#startReceiver private fun startReceiver() { Log.d(TAG, "ChatController startReceiver") if (receiverJob != null || chatCtrl == null) return @@ -797,6 +800,7 @@ object ChatController { return null } + // Spec: spec/api.md#sendCmd suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, retryNum: Int = 0, log: Boolean = true): API { val ctrl = otherCtrl ?: chatCtrl ?: throw Exception("Controller is not initialized") @@ -821,6 +825,7 @@ object ChatController { } } + // Spec: spec/api.md#recvMsg fun recvMsg(ctrl: ChatCtrl): API? { val rStr = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT) return if (rStr == "") { @@ -2559,6 +2564,7 @@ object ChatController { AlertManager.shared.showAlertMsg(title, errMsg) } + // Spec: spec/api.md#processReceivedMsg private suspend fun processReceivedMsg(msg: API) { lastMsgReceivedTimestamp = System.currentTimeMillis() val rhId = msg.rhId @@ -3519,6 +3525,7 @@ class SharedPreference(val get: () -> T, set: (T) -> Unit) { } // ChatCommand +// Spec: spec/api.md#CC sealed class CC { class Console(val cmd: String): CC() class ShowActiveUser: CC() @@ -4150,9 +4157,11 @@ class UpdatedMessage(val msgContent: MsgContent, val mentions: Map @Serializable class ChatTagData(val emoji: String?, val text: String) +// Spec: spec/api.md#ArchiveConfig @Serializable class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null) +// Spec: spec/database.md#DBEncryptionConfig @Serializable class DBEncryptionConfig(val currentKey: String, val newKey: String) @@ -5960,6 +5969,7 @@ val yaml = Yaml(configuration = YamlConfiguration( codePointLimit = 5500000, )) +// Spec: spec/api.md#API @Suppress("SERIALIZER_TYPE_INCOMPATIBLE") @Serializable(with = APISerializer::class) sealed class API { @@ -6099,6 +6109,7 @@ private fun decodeObject(deserializer: DeserializationStrategy, obj: Json runCatching { json.decodeFromJsonElement(deserializer, obj!!) }.getOrNull() // ChatResponse +// Spec: spec/api.md#CR @Serializable sealed class CR { @Serializable @SerialName("activeUser") class ActiveUser(val user: User): CR() @@ -6958,6 +6969,7 @@ data class RemoteFile( val fileSource: CryptoFile ) +// Spec: spec/api.md#ChatError @Serializable sealed class ChatError { val string: String get() = when (this) { @@ -7641,6 +7653,7 @@ sealed class RCErrorType { @Serializable @SerialName("syntax") data class SYNTAX(val syntaxErr: String): RCErrorType() } +// Spec: spec/database.md#ArchiveError @Serializable sealed class ArchiveError { val string: String get() = when (this) { @@ -7722,6 +7735,7 @@ sealed class RemoteCtrlError { @Serializable @SerialName("protocolError") object ProtocolError: RemoteCtrlError() } +// Spec: spec/services/notifications.md#NotificationsMode enum class NotificationsMode() { OFF, PERIODIC, SERVICE, /*INSTANT - for Firebase notifications */; diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index d0ce703033..36a7ae1a80 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -14,12 +14,14 @@ import java.io.File import java.nio.ByteBuffer // ghc's rts +// Spec: spec/architecture.md#initHS external fun initHS() // android-support external fun pipeStdOutToSocket(socketName: String) : Int // SimpleX API typealias ChatCtrl = Long +// Spec: spec/architecture.md#chatMigrateInit external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array external fun chatCloseStore(ctrl: ChatCtrl): String external fun chatSendCmdRetry(ctrl: ChatCtrl, msg: String, retryNum: Int): String @@ -45,6 +47,7 @@ val appPreferences: AppPreferences val chatController: ChatController = ChatController +// Spec: spec/architecture.md#initChatControllerOnStart fun initChatControllerOnStart() { withLongRunningApi { if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { @@ -55,6 +58,7 @@ fun initChatControllerOnStart() { } } +// Spec: spec/architecture.md#initChatController suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred = { CompletableDeferred(true) }) { Log.d(TAG, "initChatController") try { @@ -182,6 +186,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } } +// Spec: spec/architecture.md#chatInitTemporaryDatabase fun chatInitTemporaryDatabase(dbPath: String, key: String? = null, confirmation: MigrationConfirmation = MigrationConfirmation.Error): Pair { val dbKey = key ?: randomDatabasePassword() Log.d(TAG, "chatInitTemporaryDatabase path: $dbPath") @@ -193,6 +198,7 @@ fun chatInitTemporaryDatabase(dbPath: String, key: String? = null, confirmation: return res to migrated[1] as ChatCtrl } +// Spec: spec/architecture.md#chatInitControllerRemovingDatabases fun chatInitControllerRemovingDatabases() { val dbPath = dbAbsolutePrefixPath // Remove previous databases, otherwise, can be .errorNotADatabase with null controller diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index 0a4f670fe0..88d9fbb705 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -14,6 +14,7 @@ import java.net.URLEncoder import java.nio.file.Files import java.nio.file.StandardCopyOption +// Spec: spec/services/files.md#dataDir expect val dataDir: File expect val tmpDir: File expect val filesDir: File diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index d906ef7baf..39fcea3981 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -13,6 +13,7 @@ enum class NotificationAction { ACCEPT_CONTACT_REQUEST } +// Spec: spec/services/notifications.md#ntfManager lateinit var ntfManager: NtfManager abstract class NtfManager { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index df9af7fbf6..1b5a81a819 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -22,6 +22,7 @@ import chat.simplex.res.MR import kotlinx.serialization.Transient import java.util.UUID +// Spec: spec/services/theme.md#DefaultTheme enum class DefaultTheme { LIGHT, DARK, SIMPLEX, BLACK; @@ -47,6 +48,7 @@ enum class DefaultThemeMode { @SerialName("dark") DARK } +// Spec: spec/services/theme.md#AppColors @Stable class AppColors( title: Color, @@ -99,6 +101,7 @@ class AppColors( } } +// Spec: spec/services/theme.md#AppWallpaper @Stable class AppWallpaper( background: Color? = null, @@ -133,6 +136,7 @@ class AppWallpaper( } } +// Spec: spec/services/theme.md#ThemeColor enum class ThemeColor { PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, SENT_QUOTE, RECEIVED_MESSAGE, RECEIVED_QUOTE, PRIMARY_VARIANT2, WALLPAPER_BACKGROUND, WALLPAPER_TINT; @@ -174,6 +178,7 @@ enum class ThemeColor { } } +// Spec: spec/services/theme.md#ThemeColors @Serializable data class ThemeColors( @SerialName("accent") @@ -214,6 +219,7 @@ data class ThemeColors( } } +// Spec: spec/services/theme.md#ThemeWallpaper @Serializable data class ThemeWallpaper ( val preset: String? = null, @@ -293,6 +299,7 @@ data class ThemesFile( val themes: List = emptyList() ) +// Spec: spec/services/theme.md#ThemeOverrides @Serializable data class ThemeOverrides ( val themeId: String = UUID.randomUUID().toString(), @@ -463,6 +470,7 @@ fun List.skipDuplicates(): List { return res } +// Spec: spec/services/theme.md#ThemeModeOverrides @Serializable data class ThemeModeOverrides ( val light: ThemeModeOverride? = null, @@ -474,6 +482,7 @@ data class ThemeModeOverrides ( } } +// Spec: spec/services/theme.md#ThemeModeOverride @Serializable data class ThemeModeOverride ( val mode: DefaultThemeMode = CurrentColors.value.base.mode, @@ -714,6 +723,7 @@ val BlackColorPaletteApp = AppColors( var systemInDarkThemeCurrently: Boolean = isInNightMode() +// Spec: spec/services/theme.md#CurrentColors val CurrentColors: MutableStateFlow = MutableStateFlow(ThemeManager.currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())) @Composable @@ -758,6 +768,7 @@ fun reactOnDarkThemeChanges(isDark: Boolean) { } } +// Spec: spec/services/theme.md#SimpleXTheme @Composable fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { // TODO: Fix preview working with dark/light theme diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index 07f2b678cf..7d8c79b4a8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -53,6 +53,7 @@ object ThemeManager { ?: ThemeWallpaper.from(PresetWallpaper.SCHOOL.toType(CurrentColors.value.base), null, null)) } + // Spec: spec/services/theme.md#currentColors fun currentColors(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?, appSettingsTheme: List): ActiveTheme { val themeName = appPrefs.currentTheme.get()!! val nonSystemThemeName = nonSystemThemeName() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt index 8f5aba138d..7a92bc8c39 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt @@ -6,6 +6,7 @@ import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import kotlinx.coroutines.* +// Spec: spec/services/calls.md#ActiveCallView @Composable expect fun ActiveCallView() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt index 4d8c1fae46..563f4c3b83 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt @@ -22,6 +22,7 @@ import chat.simplex.common.views.usersettings.ProfilePreview import chat.simplex.res.MR import kotlinx.datetime.Clock +// Spec: spec/services/calls.md#IncomingCallAlertView @Composable fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) { val cm = chatModel.callManager diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt index 705fc6a28f..6fa99283d8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt @@ -46,6 +46,7 @@ data class Call( get() = localMediaSources.hasVideo || peerMediaSources.hasVideo } +// Spec: spec/services/calls.md#CallState enum class CallState { WaitCapabilities, InvitationSent, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 7322e3b17d..1344f13c84 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -93,6 +93,7 @@ fun ConnectInProgressView(s: String) { @Composable // staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat // to chat list smooth. Otherwise, chat view will become blank right before the transition starts +// Spec: spec/client/chat-view.md#ChatView fun ChatView( chatsCtx: ChatModel.ChatsContext, staleChatId: State, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index af4baad90f..eebf4a7bf8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -47,6 +47,7 @@ import kotlin.math.min const val MAX_NUMBER_OF_MENTIONS = 3 +// Spec: spec/client/compose.md#ComposePreview @Serializable sealed class ComposePreview { @Serializable object NoPreview: ComposePreview() @@ -92,6 +93,7 @@ object ComposeMessageSerializer : KSerializer { decoder.decodeLong().let { value -> TextRange(unpackInt1(value), unpackInt2(value)) } } +// Spec: spec/client/compose.md#ComposeState @Serializable data class ComposeState( val message: ComposeMessage = ComposeMessage(), @@ -259,6 +261,7 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview { } } +// Spec: spec/client/compose.md#AttachmentSelection @Composable expect fun AttachmentSelection( composeState: MutableState, @@ -341,6 +344,7 @@ suspend fun MutableState.processPickedMedia(uris: List, text: } } +// Spec: spec/client/compose.md#ComposeView @Composable fun ComposeView( rhId: Long?, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 467f1e52af..4de0175457 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -31,6 +31,7 @@ import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.* import java.net.URI +// Spec: spec/client/compose.md#SendMsgView @Composable fun SendMsgView( composeState: MutableState, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 758980059d..633d6c454e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -61,6 +61,7 @@ data class ChatItemReactionMenuItem ( val onClick: (() -> Unit)? ) +// Spec: spec/client/chat-view.md#ChatItemView @Composable fun ChatItemView( chatsCtx: ChatModel.ChatsContext, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 014a180712..293a93b15a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -32,6 +32,7 @@ import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.datetime.Clock +// Spec: spec/client/chat-list.md#ChatListNavLinkView @Composable fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { val showMenu = remember { mutableStateOf(false) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 2109e21bfe..a42f66c6cf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -122,6 +122,7 @@ fun ToggleChatListCard() { } } +// Spec: spec/client/chat-list.md#ChatListView @Composable fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { val oneHandUI = remember { appPrefs.oneHandUI.state } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 4280845867..9248ac6efe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -35,6 +35,7 @@ import chat.simplex.common.views.chat.item.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource +// Spec: spec/client/chat-list.md#ChatPreviewView @Composable fun ChatPreviewView( chat: Chat, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt index 8dfe138da1..c6cc887655 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt @@ -43,6 +43,7 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* +// Spec: spec/client/chat-list.md#TagListView @Composable fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: Boolean) { val userTags = remember { chatModel.userTags } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index ed74e083e7..a02e0dc768 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.* private val USER_PICKER_SECTION_SPACING = 32.dp +// Spec: spec/client/chat-list.md#UserPicker @Composable fun UserPicker( chatModel: ChatModel, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt index 4827e6ae61..300e5f44fe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt @@ -74,6 +74,7 @@ object DatabaseUtils { } } +// Spec: spec/database.md#DBMigrationResult @Serializable sealed class DBMigrationResult { @Serializable @SerialName("ok") object OK: DBMigrationResult() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index db1a0be9da..5c18fa3d47 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -114,6 +114,7 @@ fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedStr expect fun SetupClipboardListener() // maximum image file size to be auto-accepted +// Spec: spec/services/files.md#MAX_IMAGE_SIZE const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt index 9be10a584b..ed2f6e7859 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -23,6 +23,7 @@ private const val SERVER_HOST = "localhost" private const val SERVER_PORT = 50395 val connections = ArrayList() +// Spec: spec/services/calls.md#ActiveCallView @Composable actual fun ActiveCallView() { val scope = rememberCoroutineScope() diff --git a/apps/multiplatform/product/README.md b/apps/multiplatform/product/README.md new file mode 100644 index 0000000000..173def8ae7 --- /dev/null +++ b/apps/multiplatform/product/README.md @@ -0,0 +1,396 @@ +# SimpleX Chat Android & Desktop -- Product Overview + +> SimpleX Chat multiplatform product specification (Android + Desktop). Bidirectional code links: product docs reference source files, source files reference product docs. +> +> **Related spec:** [spec/README.md](../spec/README.md) | [spec/architecture.md](../spec/architecture.md) + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Vision](#vision) +3. [Target Users](#target-users) +4. [Capability Map](#capability-map) +5. [Navigation Map](#navigation-map) +6. [Related Specifications](#related-specifications) + +## Executive Summary + +SimpleX Chat is the first messaging platform with no user identifiers of any kind -- not even random numbers. It provides end-to-end encrypted messaging (with optional post-quantum cryptography), audio/video calls, file sharing, and group communication through a fully decentralized architecture where users control their own SMP relay servers. + +The Android and Desktop apps share a single **Kotlin Multiplatform + Compose Multiplatform** codebase. Common UI and business logic lives in a shared `common/` module, while platform-specific behavior (notifications, audio, video playback, file system access, call management) is abstracted through the Kotlin `expect`/`actual` pattern and a runtime `PlatformInterface` delegate. The Haskell core library is loaded via **JNI** (`external fun` declarations in `Core.kt`), exposing the full SimpleX Chat API (message send/receive, encryption, migration, file handling) through native FFI. + +Key platform differences: + +- **Android** uses a 2-column layout (`AndroidScreen`): chat list slides to chat view. Background messaging is handled by `SimplexService` (foreground service) + `MessagesFetcherWorker` (WorkManager periodic fetch). Calls use a dedicated `CallService` + `CallActivity`. +- **Desktop** uses a 3-column layout (`DesktopScreen`): chat list (start) | chat view (center) | detail panel (`ModalManager.end`). It includes `AppUpdater` for in-app update checking, `StoreWindowState` for window geometry persistence, and VLC-based video playback. Calls use browser-based WebRTC rendered inline. + +--- + +## Vision + +SimpleX Chat is the first messaging platform that has no user identifiers -- not even random numbers. It uses double-ratchet end-to-end encryption with optional post-quantum cryptography. The system is fully decentralized with user-controlled SMP relay servers. + +The protocol design ensures that no server or network observer can determine who communicates with whom. Each conversation uses separate unidirectional messaging queues on potentially different servers, and there is no shared identifier between the sender and receiver queues. + +--- + +## Target Users + +- **Privacy-conscious individuals** wanting secure messaging without phone-number or email-based identity +- **Groups and communities** needing encrypted group communication with role-based access control +- **Users avoiding identity linkage** who want to communicate without any persistent user identifier +- **Organizations** needing self-hosted messaging infrastructure with full control over relay servers +- **Desktop users** wanting a native desktop client with the same privacy guarantees as the mobile app + +--- + +## Capability Map + +All source paths below are relative to `apps/multiplatform/`. The common source root is `common/src/commonMain/kotlin/chat/simplex/common/`. + +### 1. Messaging + +Core message composition, delivery, and interaction features. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Text with markdown | Rich text formatting with SimpleX markdown syntax | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt` | +| Images | Compressed inline images with full-screen gallery | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt` | +| Video | Video message recording and playback | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt` | +| Voice messages | Audio recording and playback (5min / 510KB limit) | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt` | +| File sharing | Files up to 1GB via XFTP protocol | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt` | +| Link previews | OpenGraph metadata extraction and display | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt` | +| Message reactions | Emoji reactions on sent/received messages | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt` | +| Message editing | Edit previously sent messages | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt` | +| Message deletion | Broadcast delete (for recipient) or internal-only delete | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt` | +| Timed messages | Self-destructing messages with configurable TTL | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt` | +| Quoted replies | Reply to specific messages with quote context | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt` | +| Forwarding | Forward messages between chats | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt` | +| Search | Full-text search within conversations | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt` | +| Message reports | Report messages to group moderators | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt` | +| Send message bar | Composable message input with attachments, voice, send button | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt` | + +### 2. Contacts + +Establishing, managing, and verifying contacts. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Add via SimpleX address | Connect using a SimpleX contact address | `common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt` | +| Add via QR code | Scan QR code to establish connection | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt` | +| Contact requests | Accept or reject incoming contact requests | `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt` | +| Local aliases | Set private display names for contacts | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt` | +| Contact verification | Compare security codes out-of-band | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt` | +| Blocking | Block contacts from sending messages | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt` | +| Incognito mode | Per-contact random profile generation | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt` | +| Bot detection | Identify automated/bot contacts | `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` | +| Contact list | Dedicated contact browsing view | `common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt` | + +### 3. Groups + +Multi-party encrypted conversations with role-based management. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Create groups | Create new group with initial members | `common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt` | +| Invite members | Invite by individual contact or link | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt` | +| Member roles | Owner, admin, moderator, member, observer | `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` | +| Member admission | Queue-based admission with review workflow | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt` | +| Group links | Shareable invite links for groups | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt` | +| Business chat mode | Structured business communication groups | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt` | +| Content moderation | Member reports and moderator actions | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt` | +| Group preferences | Configure group-level feature settings | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt` | +| Member direct contacts | Establish direct chats from group membership | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt` | +| Group mentions | @-mention members in group messages | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt` | +| Welcome message | Custom welcome message for new group members | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt` | +| Group profile | Edit group name, image, description | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt` | +| Member support chat | Scoped support threads between members and admins | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt` | + +### 4. Calling + +End-to-end encrypted audio and video communication. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| E2E encrypted calls | Audio/video calls via WebRTC with E2E encryption | `common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt` | +| Call manager | Call state machine and lifecycle management | `common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt` | +| Call history | Call events displayed as chat items | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt` | +| Incoming call view | Dedicated UI for incoming call notifications | `common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt` | +| Android CallService | Foreground service for active calls on Android | `android/src/main/java/chat/simplex/app/CallService.kt` | +| Android CallActivity | Dedicated Activity for call UI on Android | `android/src/main/java/chat/simplex/app/views/call/CallActivity.kt` | +| Desktop inline calls | Browser-based WebRTC rendered inline in desktop window | `common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt` | + +### 5. Privacy & Security + +Encryption, authentication, and privacy controls. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| E2E encryption | Double-ratchet encryption for all messages | `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` | +| Post-quantum encryption | Optional PQ key exchange for direct chats | `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` | +| Local authentication | Biometric (fingerprint/face) or app passcode lock | `common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt` | +| Passcode entry | Custom numeric/alphanumeric passcode UI | `common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt` | +| Hidden profiles | Password-protected profiles invisible in UI | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt` | +| Database encryption | AES encryption of local SQLite database | `common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt` | +| Screen privacy | Blur/hide app content when in app switcher | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt` | +| Encrypted file storage | Local files encrypted at rest | `common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt` | +| Delivery receipts control | Toggle delivery/read receipts per contact/group | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt` | +| App lock | Automatic lock on background/timeout with configurable delay | `common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt` | + +### 6. User Management + +Multiple profiles and identity management. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Multiple profiles | Multiple user profiles within one app | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt` | +| Active user switching | Switch between profiles via user picker | `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt` | +| Incognito contacts | Per-contact random identities | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt` | +| Profile sharing | Share profile via contact address link | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt` | +| User muting | Mute notifications for specific profiles | `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt` | +| User profile editing | Edit display name and profile image | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt` | + +### 7. Network + +Server configuration, proxy support, and connectivity. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Custom SMP servers | Configure personal SMP relay servers | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt` | +| Custom XFTP servers | Configure personal XFTP file servers | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt` | +| Tor/onion support | Route traffic through Tor .onion addresses | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt` | +| SOCKS5 proxy | Route connections through SOCKS5 proxy | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt` | +| Custom ICE servers | Configure WebRTC ICE/TURN servers | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/RTCServers.kt` | +| Network timeouts | Configure connection timeout parameters | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt` | +| Server operators | Configure and manage SMP/XFTP server operators | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt` | +| Server status | View aggregate server connectivity status | `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt` | +| Network & servers hub | Central network configuration entry point | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt` | + +### 8. Customization + +Visual appearance and UI preferences. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Themes | Light, dark, SimpleX, black, and custom themes | `common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt` | +| Wallpapers | Preset and custom chat wallpapers | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt` | +| Chat bubble styling | Customize message bubble appearance | `common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt` | +| One-handed UI mode | Compact layout for single-hand use (Android) | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt` | +| Language selection | In-app language override | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt` | +| Theme mode editor | Interactive theme color and mode customization | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt` | + +### 9. Data Management + +Import, export, encryption, and storage management. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Export/import profiles | Full database export and import | `common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt` | +| Database encryption | Encrypt/decrypt local database with passphrase | `common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt` | +| Local file encryption | Encrypt stored media and attachments | `common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt` | +| Database error handling | Recovery UI for database migration failures | `common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt` | +| Device-to-device migration | Migrate full profile between devices | `common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt` | +| Receive migration | Accept incoming device migration transfer | `common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt` | +| Database utilities | Key storage, password management, helper functions | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` | + +### 10. Desktop Features + +Desktop-specific functionality not present on Android. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| 3-column layout | Start (chat list) / center (chat) / end (detail) panels | `common/src/commonMain/kotlin/chat/simplex/common/App.kt` (`DesktopScreen`) | +| ModalManager.end | Third-column detail panel for settings/info views | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt` (`ModalManager`) | +| App update checker | In-app notification for available updates | `common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt` | +| Window state persistence | Save/restore window position and dimensions | `common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt` | +| VLC video playback | Desktop video playback via VLC native libraries | `common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt` | +| Desktop app entry | Main function, Haskell init, VLC loading | `desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt` | +| Desktop notification manager | Platform-native desktop notifications | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Notifications.desktop.kt` | +| Connect mobile device | Pair desktop with a mobile device for remote access | `common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt` | +| Desktop platform abstraction | Desktop-specific PlatformInterface implementation | `common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt` | +| Desktop app shell | Compose Desktop window, theming, lifecycle | `common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt` | + +--- + +## Navigation Map + +### Android Navigation (2-column slide) + +``` +Onboarding + views/onboarding/SimpleXInfo.kt + -> SimpleXInfo -> CreateFirstProfile -> SetupDatabasePassphrase + -> ChooseServerOperators -> SetNotificationsMode + -> ChatListView (home) + +ChatListView (home) + views/chatlist/ChatListView.kt + -> ChatView .................. (tap conversation row, slides in) + -> NewChatSheet .............. (+ FAB button) + -> SettingsView .............. (gear icon) + -> UserPicker ................ (avatar tap) + -> TagListView ............... (tag filter bar) + -> ServersSummaryView ........ (server status indicator) + -> ShareListView ............. (share intent from external apps) + -> ChatHelpView .............. (empty state help) + +ChatView + views/chat/ChatView.kt + -> ChatInfoView .............. (contact name tap, direct chat) + -> GroupChatInfoView ......... (group name tap, group chat) + -> ActiveCallView ............ (call button, launches CallActivity) + -> ComposeView ............... (message input area) + -> ChatItemInfoView .......... (long press -> info) + -> MemberSupportChatView ..... (member support thread) + -> ScanCodeView .............. (scan QR) + -> CommandsMenuView .......... (/ commands) + +ChatInfoView + views/chat/ChatInfoView.kt + -> ContactPreferences ........ (preferences) + -> VerifyCodeView ............ (verify security code) + +GroupChatInfoView + views/chat/group/GroupChatInfoView.kt + -> GroupProfileView .......... (edit profile) + -> AddGroupMembersView ....... (invite members) + -> GroupLinkView ............. (manage group link) + -> MemberAdmission ........... (admission settings) + -> GroupPreferences .......... (group feature settings) + -> GroupMemberInfoView ....... (tap member) + -> WelcomeMessageView ........ (welcome message) + -> GroupReportsView .......... (view reports) + +NewChatSheet + views/newchat/NewChatSheet.kt + -> NewChatView ............... (QR scanner / paste link) + -> AddGroupView .............. (create group) + -> UserAddressView ........... (create SimpleX address) + +SettingsView + views/usersettings/SettingsView.kt + -> AppearanceView ............ (themes, wallpapers, UI) + -> NetworkAndServers ......... (SMP/XFTP/proxy config) + -> PrivacySettings ........... (privacy toggles) + -> NotificationsSettingsView . (notification mode) + -> DatabaseView .............. (export/import/encrypt) + -> CallSettings .............. (call preferences) + -> VersionInfoView ........... (about/version) + -> DeveloperView ............. (developer options) + -> HelpView .................. (help & support) + +UserPicker + views/chatlist/UserPicker.kt + -> UserProfilesView .......... (manage all profiles) + -> UserAddressView ........... (SimpleX address) + -> Preferences ............... (user preferences) + -> SettingsView .............. (app settings) + -> ConnectDesktopView ........ (pair with desktop) +``` + +### Desktop Navigation (3-column panels) + +``` ++---------------------------+----------------------------------+----------------------------+ +| START PANEL | CENTER PANEL | END PANEL | +| (DEFAULT_START_MODAL_ | (flexible width, min | (DEFAULT_END_MODAL_ | +| WIDTH) | DEFAULT_MIN_CENTER_MODAL_ | WIDTH) | +| | WIDTH) | | ++---------------------------+----------------------------------+----------------------------+ +| | | | +| ChatListView | ChatView | ChatInfoView | +| - chat rows | - message list | GroupChatInfoView | +| - search | - ComposeView | GroupMemberInfoView | +| - tag filters | - media viewer | ContactPreferences | +| - server status | | GroupPreferences | +| | OR (when no chat selected): | GroupProfileView | +| UserPicker (overlay) | "No selected chat" | AddGroupMembersView | +| - profile switcher | | MemberAdmission | +| - quick settings | OR (when modal open): | VerifyCodeView | +| | ModalManager.center content | SettingsView subtabs | +| ModalManager.start | (settings, new chat, etc.) | | +| - secondary modals | | ModalManager.end | +| | | - detail modals | ++---------------------------+----------------------------------+----------------------------+ + +ModalManager Placement (Desktop): + - ModalManager.start -> left panel overlay (settings subviews) + - ModalManager.center -> center panel (replaces chat, used when chatId is null) + - ModalManager.end -> right panel (detail/info views) + - ModalManager.fullscreen -> full window overlay (onboarding, auth, call) + +On Android, all ModalManager instances (start/center/end/fullscreen) collapse to a +single shared ModalManager that presents modals as full-screen overlays. + +Desktop-only navigation targets: + ConnectMobileView ......... (pair with mobile device) + AppUpdater notice ......... (update available notification) + Floating terminal ......... (developer console) + ActiveCallView ............ (inline WebRTC call, not separate Activity) +``` + +--- + +## Platform Abstraction + +The codebase uses two mechanisms for platform-specific behavior: + +### 1. `expect`/`actual` Declarations + +Kotlin Multiplatform `expect` declarations in `common/src/commonMain/kotlin/chat/simplex/common/platform/` with corresponding `actual` implementations in: +- `common/src/androidMain/kotlin/chat/simplex/common/platform/*.android.kt` +- `common/src/desktopMain/kotlin/chat/simplex/common/platform/*.desktop.kt` + +Key `expect`/`actual` abstractions: `appPlatform`, `BackHandler`, `VideoPlayer`, `AudioPlayer`, `RecorderNative`, `NtfManager`, `showToast`, `getKeyboardState`, `PlatformTextField`, image processing, file sharing, and more. + +### 2. Runtime `PlatformInterface` + +Defined in `common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt`, this interface provides platform-specific callbacks that cannot use `expect`/`actual` (because `android/` module code cannot be called from `common/androidMain/`). The `platform` variable is reassigned at app startup: +- **Android:** `SimplexApp` sets `platform` to an implementation with `CallService`, notification channels, orientation locking, status bar theming, and PiP support. +- **Desktop:** `Main.kt` sets `platform` to an implementation with `desktopShowAppUpdateNotice()`. + +### 3. Haskell Core (JNI/FFI) + +Native FFI bindings are declared in `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` as `external fun` declarations. These include: `chatMigrateInit`, `chatSendCmdRetry`, `chatRecvMsg`, `chatParseMarkdown`, `chatPasswordHash`, `chatWriteFile`, `chatReadFile`, `chatEncryptFile`, `chatDecryptFile`, and more. The native library (`libapp-lib`) is loaded at startup from platform-specific resource directories. + +--- + +## Background Messaging (Android) + +Android has no equivalent to iOS NSE (Notification Service Extension). Instead, it uses: + +- **`SimplexService`** (`android/src/main/java/chat/simplex/app/SimplexService.kt`) -- A foreground service that keeps the Haskell core running to receive messages in real-time. Displays a persistent notification while active. +- **`MessagesFetcherWorker`** (`android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt`) -- A WorkManager-based periodic task that wakes the app at configurable intervals to fetch messages when the foreground service is not running (battery-optimized mode). +- **Notification modes:** Instant (foreground service always running), Periodic (WorkManager fetch every N minutes), Off. + +--- + +## Related Specifications + +### Product Layer (this directory) + +- [concepts.md](concepts.md) -- Feature concept index with bidirectional code links +- [glossary.md](glossary.md) -- Terminology definitions +- [rules.md](rules.md) -- Business rules and constraints +- [gaps.md](gaps.md) -- Known documentation gaps +- Views: [chat-list](views/chat-list.md), [chat](views/chat.md), [new-chat](views/new-chat.md), [settings](views/settings.md), [call](views/call.md), [contact-info](views/contact-info.md), [group-info](views/group-info.md), [onboarding](views/onboarding.md), [user-profiles](views/user-profiles.md) +- Flows: [messaging](flows/messaging.md), [calling](flows/calling.md), [onboarding](flows/onboarding.md), [group-lifecycle](flows/group-lifecycle.md), [connection](flows/connection.md), [file-transfer](flows/file-transfer.md) + +### Spec Layer + +- [spec/README.md](../spec/README.md) -- Technical specification overview +- [spec/architecture.md](../spec/architecture.md) -- JNI bridge, startup, lifecycle +- [spec/state.md](../spec/state.md) -- ChatModel, ChatsContext, Chat, AppPreferences +- [spec/api.md](../spec/api.md) -- Command/response protocol (CC, CR, ChatError) +- [spec/database.md](../spec/database.md) -- Migration, encryption, export/import +- Client: [navigation](../spec/client/navigation.md), [chat-list](../spec/client/chat-list.md), [chat-view](../spec/client/chat-view.md), [compose](../spec/client/compose.md) +- Services: [calls](../spec/services/calls.md), [theme](../spec/services/theme.md), [files](../spec/services/files.md), [notifications](../spec/services/notifications.md) + +### Source Entry Points + +- Haskell core: `../../src/Simplex/Chat/Controller.hs`, `../../src/Simplex/Chat/Types.hs` +- Kotlin model: `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` +- Kotlin API bridge: `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` +- Kotlin FFI: `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` +- Android entry: `android/src/main/java/chat/simplex/app/SimplexApp.kt`, `MainActivity.kt` +- Desktop entry: `desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt` diff --git a/apps/multiplatform/product/concepts.md b/apps/multiplatform/product/concepts.md new file mode 100644 index 0000000000..da33bf11d7 --- /dev/null +++ b/apps/multiplatform/product/concepts.md @@ -0,0 +1,120 @@ +# SimpleX Chat Android & Desktop -- Concept Index + +> SimpleX Chat multiplatform concept index. Maps every product concept to its documentation and source code with bidirectional links. +> +> **Related spec:** [spec/README.md](../spec/README.md) | [spec/architecture.md](../spec/architecture.md) + +## Table of Contents + +1. [Feature Concepts](#section-1-feature-concepts) +2. [Entity Index](#section-2-entity-index) + +## Executive Summary + +This document provides a structured mapping between product-level concepts, their documentation, and their implementation in both the Kotlin multiplatform layer and the Haskell core library. All Kotlin source paths are relative to `apps/multiplatform/`. Haskell paths use `../../src/` prefix (relative to `apps/multiplatform/`). The common source root abbreviation used below is `common/src/commonMain/kotlin/chat/simplex/common/`. + +--- + +## Section 1: Feature Concepts + +| # | Concept | Product Docs | Spec Docs | Source Files (Kotlin) | Source Files (Haskell) | +|---|---------|-------------|-----------|----------------------|----------------------| +| PC1 | Chat List | [README.md](README.md) (Navigation Map) | [spec/client/chat-list.md](../spec/client/chat-list.md) | `common/.../views/chatlist/ChatListView.kt`, `ChatListNavLinkView.kt`, `ChatPreviewView.kt` | `Controller.hs` (`APIGetChats`) | +| PC2 | Direct Chat | [README.md](README.md) (Messaging) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `common/.../views/chat/ChatView.kt`, `ChatInfoView.kt` | `Types.hs` (`Contact`), `Messages.hs` | +| PC3 | Group Chat | [README.md](README.md) (Groups) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `common/.../views/chat/ChatView.kt`, `group/GroupChatInfoView.kt` | `Types.hs` (`GroupInfo`, `GroupMember`) | +| PC4 | Message Composition | [README.md](README.md) (Messaging) | [spec/client/compose.md](../spec/client/compose.md) | `common/.../views/chat/ComposeView.kt`, `SendMsgView.kt`, `ComposeVoiceView.kt`, `ComposeImageView.kt`, `ComposeFileView.kt` | `Controller.hs` (`APISendMessages`) | +| PC5 | Message Reactions | [README.md](README.md) (Messaging) | [spec/api.md](../spec/api.md) | `common/.../views/chat/ChatItemView.kt` (ChatItemReactions composable) | `Controller.hs` (`APIChatItemReaction`) | +| PC6 | Message Editing | [README.md](README.md) (Messaging) | [spec/client/compose.md](../spec/client/compose.md) | `common/.../views/chat/ComposeView.kt`, `ChatItemInfoView.kt` | `Controller.hs` (`APIUpdateChatItem`) | +| PC7 | Message Deletion | [README.md](README.md) (Messaging) | [spec/api.md](../spec/api.md) | `common/.../views/chat/item/MarkedDeletedItemView.kt`, `DeletedItemView.kt` | `Controller.hs` (`APIDeleteChatItem`) | +| PC8 | Timed Messages | [README.md](README.md) (Messaging) | [spec/api.md](../spec/api.md) | `common/.../views/chat/item/CIChatFeatureView.kt` | `Types/Preferences.hs` (`TimedMessagesPreference`) | +| PC9 | Voice Messages | [README.md](README.md) (Messaging) | [spec/client/compose.md](../spec/client/compose.md) | `common/.../views/chat/item/CIVoiceView.kt`, `ComposeVoiceView.kt`, `platform/RecAndPlay.kt` | `Protocol.hs` (`MCVoice`) | +| PC10 | File Transfer | [README.md](README.md) (Messaging, Data Management) | [spec/services/files.md](../spec/services/files.md) | `common/.../views/chat/item/CIFileView.kt`, `platform/Files.kt` | `Files.hs`, `Store/Files.hs` | +| PC11 | Link Previews | [README.md](README.md) (Messaging) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `common/.../views/helpers/LinkPreviews.kt` | `Protocol.hs` (`MCLink`) | +| PC12 | Contact Connection | [README.md](README.md) (Contacts) | [spec/api.md](../spec/api.md) | `common/.../views/newchat/NewChatView.kt`, `QRCode.kt`, `QRCodeScanner.kt`, `ConnectPlan.kt` | `Controller.hs` (`APIConnect`, `APIAddContact`) | +| PC13 | Contact Verification | [README.md](README.md) (Contacts) | [spec/api.md](../spec/api.md) | `common/.../views/chat/VerifyCodeView.kt` | `Controller.hs` (`APIVerifyContact`) | +| PC14 | Group Management | [README.md](README.md) (Groups) | [spec/api.md](../spec/api.md) | `common/.../views/newchat/AddGroupView.kt`, `group/GroupChatInfoView.kt`, `group/GroupProfileView.kt` | `Controller.hs` (`APINewGroup`), `Store/Groups.hs` | +| PC15 | Group Links | [README.md](README.md) (Groups) | [spec/api.md](../spec/api.md) | `common/.../views/chat/group/GroupLinkView.kt` | `Controller.hs` (`APICreateGroupLink`) | +| PC16 | Member Roles | [README.md](README.md) (Groups) | [spec/api.md](../spec/api.md) | `common/.../model/ChatModel.kt`, `group/GroupMemberInfoView.kt` | `Types/Shared.hs` (`GroupMemberRole`) | +| PC17 | Audio/Video Calls | [README.md](README.md) (Calling) | [spec/services/calls.md](../spec/services/calls.md) | `common/.../views/call/CallView.kt`, `CallManager.kt`, `WebRTC.kt`, `android/.../CallService.kt`, `android/.../views/call/CallActivity.kt` | `Call.hs` (`RcvCallInvitation`, `CallType`) | +| PC18 | Notifications | [README.md](README.md) (Background Messaging) | [spec/services/notifications.md](../spec/services/notifications.md) | `common/.../platform/NtfManager.kt`, `Notifications.kt`, `android/.../SimplexService.kt`, `android/.../MessagesFetcherWorker.kt`, `common/.../views/usersettings/NotificationsSettingsView.kt` | `Controller.hs` | +| PC19 | User Profiles | [README.md](README.md) (User Management) | [spec/state.md](../spec/state.md) | `common/.../views/usersettings/UserProfilesView.kt`, `UserProfileView.kt`, `views/chatlist/UserPicker.kt` | `Types.hs` (`User`), `Store/Profiles.hs` | +| PC20 | Incognito Mode | [README.md](README.md) (Contacts) | [spec/api.md](../spec/api.md) | `common/.../views/usersettings/IncognitoView.kt` | `ProfileGenerator.hs`, `Types.hs` | +| PC21 | Hidden Profiles | [README.md](README.md) (Privacy & Security) | [spec/api.md](../spec/api.md) | `common/.../views/usersettings/HiddenProfileView.kt` | `Controller.hs` (`APIHideUser`, `APIUnhideUser`) | +| PC22 | Local Authentication | [README.md](README.md) (Privacy & Security) | [spec/architecture.md](../spec/architecture.md) | `common/.../views/localauth/LocalAuthView.kt`, `PasscodeView.kt`, `SetAppPasscodeView.kt`, `PasswordEntry.kt`, `AppLock.kt` | N/A (client-only) | +| PC23 | Database Encryption | [README.md](README.md) (Data Management) | [spec/database.md](../spec/database.md) | `common/.../views/database/DatabaseEncryptionView.kt`, `DatabaseView.kt`, `views/helpers/DatabaseUtils.kt` | `Controller.hs` (`APIExportArchive`) | +| PC24 | Theme System | [README.md](README.md) (Customization) | [spec/services/theme.md](../spec/services/theme.md) | `common/.../ui/theme/ThemeManager.kt`, `Theme.kt`, `Color.kt`, `Type.kt`, `Shape.kt` | `Types/UITheme.hs` | +| PC25 | Network Configuration | [README.md](README.md) (Network) | [spec/architecture.md](../spec/architecture.md) | `common/.../views/usersettings/networkAndServers/NetworkAndServers.kt`, `ProtocolServersView.kt`, `AdvancedNetworkSettings.kt`, `OperatorView.kt` | `Controller.hs` (`APISetNetworkConfig`) | +| PC26 | Device Migration | [README.md](README.md) (Data Management) | [spec/database.md](../spec/database.md) | `common/.../views/migration/MigrateFromDevice.kt`, `MigrateToDevice.kt` | `Archive.hs` | +| PC27 | Remote Desktop | [README.md](README.md) (Desktop Features) | [spec/architecture.md](../spec/architecture.md) | `common/.../views/remote/ConnectDesktopView.kt`, `ConnectMobileView.kt` | `Remote.hs`, `Remote/Types.hs` | +| PC28 | Chat Tags | [README.md](README.md) (Navigation Map) | [spec/state.md](../spec/state.md) | `common/.../views/chatlist/TagListView.kt`, `ChatListView.kt` | `Types.hs` (`ChatTag`), `Controller.hs` | +| PC29 | User Address | [README.md](README.md) (Contacts, User Management) | [spec/api.md](../spec/api.md) | `common/.../views/usersettings/UserAddressView.kt`, `UserAddressLearnMore.kt` | `Controller.hs` (`APICreateMyAddress`) | +| PC30 | Member Support Chat | [README.md](README.md) (Groups) | [spec/api.md](../spec/api.md) | `common/.../views/chat/group/MemberSupportView.kt`, `MemberSupportChatView.kt`, `MemberAdmission.kt` | `Messages.hs` (`GroupChatScope`), `Controller.hs` | + +**Legend for abbreviated paths:** +- `common/.../` expands to `common/src/commonMain/kotlin/chat/simplex/common/` +- `android/.../` expands to `android/src/main/java/chat/simplex/app/` +- Haskell files are in `../../src/Simplex/Chat/` (relative to `apps/multiplatform/`) + +--- + +## Section 2: Entity Index + +Core data entities, their storage, and the operations that manage their lifecycle. + +| Entity | DB Table (Haskell) | Created By | Read By | Mutated By | Deleted By | +|--------|-------------------|------------|---------|------------|------------| +| **User** | `users` | `CreateActiveUser` in `Controller.hs` | `ListUsers`, `APISetActiveUser` in `Controller.hs` | `APISetActiveUser`, `APIHideUser`, `APIUnhideUser`, `APIMuteUser`, `APIUpdateProfile` in `Controller.hs` | `APIDeleteUser` in `Controller.hs`; `Store/Profiles.hs` | +| **Contact** | `contacts`, `contact_profiles` | `APIAddContact`, `APIConnect` in `Controller.hs` | `APIGetChat` in `Controller.hs`; `Store/Direct.hs` (`getContact`) | `APISetContactAlias`, `APISetConnectionAlias` in `Controller.hs`; `Store/Direct.hs` | `APIDeleteChat` in `Controller.hs`; `Store/Direct.hs` (`deleteContact`) | +| **GroupInfo** | `groups`, `group_profiles` | `APINewGroup` in `Controller.hs`; `Store/Groups.hs` (`createNewGroup`) | `APIGetChat`, `APIGroupInfo` in `Controller.hs`; `Store/Groups.hs` | `APIUpdateGroupProfile` in `Controller.hs`; `Store/Groups.hs` (`updateGroupProfile`) | `APIDeleteChat` in `Controller.hs`; `Store/Groups.hs` (`deleteGroup`) | +| **GroupMember** | `group_members`, `contact_profiles` | `APIAddMember`, `APIJoinGroup` in `Controller.hs`; `Store/Groups.hs` (`createNewGroupMember`) | `APIListMembers` in `Controller.hs`; `Store/Groups.hs` (`getGroupMembers`) | `APIMembersRole` in `Controller.hs`; `Store/Groups.hs` (`updateGroupMemberRole`) | `APIRemoveMembers` in `Controller.hs`; `Store/Groups.hs` (`deleteGroupMember`) | +| **ChatItem** | `chat_items`, `chat_item_versions` | `APISendMessages` in `Controller.hs`; `Store/Messages.hs` (`createNewChatItem`) | `APIGetChat`, `APIGetChatItems` in `Controller.hs`; `Store/Messages.hs` (`getChatItems`) | `APIUpdateChatItem`, `APIChatItemReaction` in `Controller.hs`; `Store/Messages.hs` (`updateChatItem`) | `APIDeleteChatItem` in `Controller.hs`; `Store/Messages.hs` (`deleteChatItem`) | +| **Connection** | `connections` | `createConnection` via SMP agent; `Store/Connections.hs` | `Store/Connections.hs` (`getConnectionEntity`) | `Store/Connections.hs` (`updateConnectionStatus`) | `Store/Connections.hs` (`deleteConnection`) | +| **FileTransfer** | `files`, `snd_files`, `rcv_files`, `xftp_file_descriptions` | `APISendMessages` (with file), `ReceiveFile` in `Controller.hs`; `Store/Files.hs` | `Store/Files.hs` (`getFileTransfer`) | `Store/Files.hs` (`updateFileStatus`, `updateFileProgress`) | `Store/Files.hs` (`deleteFileTransfer`) | +| **GroupLink** | `user_contact_links` | `APICreateGroupLink` in `Controller.hs`; `Store/Groups.hs` | `APIGetGroupLink` in `Controller.hs`; `Store/Groups.hs` | N/A (recreated on change) | `APIDeleteGroupLink` in `Controller.hs`; `Store/Groups.hs` | +| **ChatTag** | `chat_tags`, `chat_tags_chats` | `APICreateChatTag` in `Controller.hs` | `APIGetChats` in `Controller.hs` | `APIUpdateChatTag`, `APISetChatTags` in `Controller.hs` | `APIDeleteChatTag` in `Controller.hs` | +| **RcvCallInvitation** | In-memory (not persisted) | Received via `XCallInv` message in `Library/Subscriber.hs`; stored in `ChatModel.activeCallInvitation` | `CallManager.kt`, `IncomingCallAlertView.kt` | Updated on call accept/reject in `CallManager.kt` | Removed on call end/reject; `Controller.hs` | + +--- + +## Platform-Specific Source Index + +Key files that exist only on one platform, grouped by concern. + +### Android-Only + +| File | Purpose | +|------|---------| +| `android/src/main/java/chat/simplex/app/SimplexApp.kt` | Application subclass, PlatformInterface setup, Haskell init | +| `android/src/main/java/chat/simplex/app/MainActivity.kt` | Main Activity, deep link handling, lifecycle | +| `android/src/main/java/chat/simplex/app/SimplexService.kt` | Foreground service for persistent messaging | +| `android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt` | WorkManager periodic message fetch | +| `android/src/main/java/chat/simplex/app/CallService.kt` | Foreground service for active calls | +| `android/src/main/java/chat/simplex/app/views/call/CallActivity.kt` | Dedicated Activity for call UI | +| `android/src/main/java/chat/simplex/app/model/NtfManager.android.kt` | Android notification channels and manager | +| `common/src/androidMain/kotlin/chat/simplex/common/platform/*.android.kt` | All `actual` implementations for Android | + +### Desktop-Only + +| File | Purpose | +|------|---------| +| `desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt` | JVM entry point, Haskell/VLC init, PlatformInterface setup | +| `common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt` | Compose Desktop window creation and lifecycle | +| `common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt` | Window position/size persistence | +| `common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt` | In-app update checker | +| `common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt` | VLC-based video detection | +| `common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt` | VLC video player implementation | +| `common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt` | Desktop platform detection (Linux/macOS/Windows) | +| `common/src/desktopMain/kotlin/chat/simplex/common/platform/*.desktop.kt` | All `actual` implementations for Desktop | + +--- + +## Cross-References + +- Product overview: [README.md](README.md) +- Haskell core controller: `../../src/Simplex/Chat/Controller.hs` +- Haskell core types: `../../src/Simplex/Chat/Types.hs` +- Haskell store layer: `../../src/Simplex/Chat/Store/` (`Direct.hs`, `Groups.hs`, `Messages.hs`, `Files.hs`, `Profiles.hs`, `Connections.hs`) +- Kotlin model: `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` +- Kotlin API bridge: `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` +- Kotlin FFI layer: `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` +- Platform abstraction: `common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt` (`PlatformInterface`) diff --git a/apps/multiplatform/product/flows/calling.md b/apps/multiplatform/product/flows/calling.md new file mode 100644 index 0000000000..fae7f42031 --- /dev/null +++ b/apps/multiplatform/product/flows/calling.md @@ -0,0 +1,220 @@ +# Calling Flow + +> **Related spec:** [spec/services/calls.md](../../spec/services/calls.md) + +## Overview + +SimpleX Chat supports audio and video calls using WebRTC, with signaling delivered over the existing SMP messaging channels. Calls are end-to-end encrypted with an additional shared key layer on top of WebRTC's SRTP encryption. + +The architecture differs by platform: +- **Android**: Calls run in a dedicated `CallActivity` (separate from `MainActivity`) with a `WebView` hosting the WebRTC JavaScript. A foreground `CallService` keeps the process alive and shows a persistent notification. +- **Desktop**: Calls open the system browser pointed at a local NanoHTTPD/NanoWSD embedded server on `localhost:50395`, which serves the WebRTC HTML/JS and communicates with the app via WebSocket. + +Both platforms share a common signaling flow through the Haskell core API. + +## Prerequisites + +- Both parties must have an established direct contact connection. +- Microphone permission is required; camera permission is required for video calls. +- On Android, the `CallOnLockScreen` preference controls lock-screen call behavior: `DISABLE`, `SHOW`, or `ACCEPT`. + +--- + +## 1. Outgoing Call (Caller Side) + +### 1.1 Initiate Call + +1. User taps the audio or video call button in `ChatView`. +2. `startChatCall(remoteHostId, chatInfo, media)` is called (in `ChatView.kt`). +3. A `Call` object is created with `callState = CallState.WaitCapabilities`: + ```kotlin + Call( + remoteHostId = remoteHostId, + contact = contact, + callUUID = null, + callState = CallState.WaitCapabilities, + initialCallType = media, // Audio or Video + userProfile = profile, + androidCallState = platform.androidCreateActiveCallState() + ) + ``` +4. `ChatModel.activeCall` is set and `ChatModel.showCallView` is set to `true`. +5. A `WCallCommand.Capabilities(media)` command is added to `ChatModel.callCommand`. + +### 1.2 WebRTC Capabilities Response + +1. The WebRTC engine (WebView on Android, browser on Desktop) receives the `Capabilities` command. +2. It responds with `WCallResponse.Capabilities(capabilities)` containing encryption support info. +3. The app calls `ChatController.apiSendCallInvitation(rh, contact, callType)` to send the invitation via SMP. +4. Call state transitions to `CallState.InvitationSent`. +5. A connecting sound starts playing via `CallSoundsPlayer.startConnectingCallSound`. + +### 1.3 Offer Exchange + +1. When the callee accepts, the WebRTC engine generates an offer. +2. `WCallResponse.Offer(offer, iceCandidates, capabilities)` is received. +3. `ChatController.apiSendCallOffer(rh, contact, rtcSession, rtcIceCandidates, media, capabilities)` sends it. +4. Call state transitions to `CallState.OfferSent`. + +### 1.4 Answer and Connection + +1. The callee's answer arrives via SMP as a chat event. +2. The app dispatches `WCallCommand.Answer(answer, iceCandidates)` to the WebRTC engine. +3. Call state transitions to `CallState.Negotiated`, then to `CallState.Connected` once the ICE connection succeeds. +4. `Call.connectedAt` is set to the current timestamp. + +--- + +## 2. Incoming Call (Callee Side) + +### 2.1 Receive Invitation + +1. An incoming call event arrives from the core as `CR.CallInvitation`. +2. `CallManager.reportNewIncomingCall(invitation)` is called. +3. A `RcvCallInvitation` is stored in `ChatModel.callInvitations` keyed by contact ID. +4. If the invitation is recent (within 3 minutes), a system notification is shown and `ChatModel.activeCallInvitation` is set. +5. On Android, `CallActivity` may be launched on the lock screen if `callOnLockScreen` is `SHOW` or `ACCEPT`. + +### 2.2 Accept Call + +1. User taps "Accept" on the `IncomingCallAlertView` or lock-screen alert. +2. `CallManager.acceptIncomingCall(invitation)` is called. +3. If another call is active, it is ended first (with `switchingCall` flag set). +4. A new `Call` is created with `callState = CallState.InvitationAccepted`. +5. ICE servers are loaded from preferences (`getIceServers()`). +6. `WCallCommand.Start(media, aesKey, iceServers, relay)` is dispatched to the WebRTC engine. +7. The call invitation is removed from `callInvitations` and the notification is cancelled. + +### 2.3 Reject Call + +1. User taps "Reject" or the invitation times out. +2. `CallManager.endCall(invitation)` is called. +3. `ChatController.apiRejectCall(rh, contact)` notifies the caller. +4. The invitation is removed from `callInvitations`. + +--- + +## 3. Call State Machine + +``` +Outgoing: WaitCapabilities -> InvitationSent -> OfferSent -> AnswerReceived -> Negotiated -> Connected -> Ended +Incoming: InvitationAccepted -> OfferReceived -> Negotiated -> Connected -> Ended +``` + +| State | Description | +|-------|-------------| +| `WaitCapabilities` | Querying local WebRTC capabilities | +| `InvitationSent` | Caller sent invitation via SMP | +| `InvitationAccepted` | Callee accepted, starting WebRTC | +| `OfferSent` | Caller sent SDP offer | +| `OfferReceived` | Callee received SDP offer | +| `AnswerReceived` | Caller received SDP answer | +| `Negotiated` | ICE negotiation complete | +| `Connected` | Media flowing | +| `Ended` | Call terminated | + +--- + +## 4. Ending a Call + +1. User taps the end-call button, or the remote side ends the call. +2. `CallManager.endCall(call)` is called. +3. `ChatController.apiEndCall(rh, contact)` notifies the remote side via SMP. +4. `ChatModel.showCallView` is set to `false`. +5. `ChatModel.activeCall` is set to `null`. +6. On Android, `CallService` is stopped and the `WebView` is destroyed. +7. On Desktop, `WCallCommand.End` is sent to the browser via WebSocket, and the NanoWSD server is stopped. + +--- + +## 5. Android-Specific: CallActivity and CallService + +### 5.1 CallActivity + +- `CallActivity` is a separate `ComponentActivity` (not `MainActivity`). +- It is launched via `platform.androidStartCallActivity(acceptCall, remoteHostId, chatId)`. +- It hosts `ActiveCallView` with a `WebView` for WebRTC. +- Supports lock-screen display: `setShowWhenLocked(true)` and `setTurnScreenOn(true)`. +- Supports Picture-in-Picture (PiP) mode for video calls. + - On Android 12+, PiP auto-enters when the user navigates away. + - On older versions, PiP is entered via `enterPictureInPictureMode()` on `onUserLeaveHint`. + - PiP layout switches to `LayoutType.RemoteVideo` to show only the remote video feed. +- The activity finishes itself when both `invitation == null` and (`!showCallView || call == null`) and `!switchingCall`. + +### 5.2 CallService + +- `CallService` is a foreground `Service` that keeps the process alive during calls. +- Started via `CallService.startService()` which calls `ContextCompat.startForegroundService`. +- Acquires a partial `WakeLock` to prevent CPU sleep. +- Shows a persistent notification with: + - Contact name and call type (audio/video). + - An "End Call" action button. + - A chronometer showing call duration (from `connectedAt`). +- The notification taps open `CallActivity`. +- Foreground service type includes `MICROPHONE`, `CAMERA` (if video), and `MEDIA_PLAYBACK`. + +--- + +## 6. Desktop-Specific: Browser-Based WebRTC + +### 6.1 NanoWSD Embedded Server + +1. When a call starts, `startServer(onResponse)` creates a `NanoWSD` server on `localhost:50395`. +2. The server serves static WebRTC HTML/JS from bundled resources at `/assets/www/desktop/call.html`. +3. The system browser is opened to `http://localhost:50395/simplex/call/`. + +### 6.2 WebSocket Communication + +1. The browser page connects back via WebSocket to the same `localhost:50395` server. +2. Commands from the app to the browser are serialized as `WVAPICall(corrId, command)` JSON. +3. Responses from the browser arrive as `WVAPIMessage(corrId, resp, command)` JSON. +4. The `WebRTCController` composable manages the command queue: + - Collects commands from `ChatModel.callCommand` (a `SnapshotStateList`). + - Sends them to the browser via the WebSocket connection. + - Processes responses through the same `WCallResponse` handling as Android. +5. On dispose, `WCallCommand.End` is sent, the server is stopped, and connections are cleared. + +--- + +## 7. Common Signaling API + +| API Function | Purpose | +|-------------|---------| +| `apiSendCallInvitation(rh, contact, callType)` | Send call invitation via SMP | +| `apiRejectCall(rh, contact)` | Reject incoming call | +| `apiSendCallOffer(rh, contact, rtcSession, rtcIceCandidates, media, capabilities)` | Send SDP offer | +| `apiSendCallAnswer(rh, contact, rtcSession, rtcIceCandidates)` | Send SDP answer | +| `apiSendCallExtraInfo(rh, contact, rtcIceCandidates)` | Send additional ICE candidates | +| `apiEndCall(rh, contact)` | End active call | +| `apiCallStatus(rh, contact, status)` | Report WebRTC connection status | + +--- + +## 8. In-Call Media Controls + +During an active call, the user can toggle media sources via `WCallCommand.Media(source, enable)`: + +| Source | Control | +|--------|---------| +| `CallMediaSource.Mic` | Mute/unmute microphone | +| `CallMediaSource.Camera` | Enable/disable camera | +| `CallMediaSource.ScreenAudio` | Screen share audio | +| `CallMediaSource.ScreenVideo` | Screen share video | + +Camera switching (front/back) is done via `WCallCommand.Camera(VideoCamera.User / VideoCamera.Environment)`. + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `Call` | `views/call/WebRTC.kt` | Active call state: contact, callState, media sources, encryption | +| `CallState` | `views/call/WebRTC.kt` | Enum: WaitCapabilities through Ended | +| `RcvCallInvitation` | `views/call/WebRTC.kt` | Incoming call invitation with contact, callType, sharedKey | +| `CallManager` | `views/call/CallManager.kt` | Manages call lifecycle: accept, end, report | +| `WCallCommand` | `views/call/WebRTC.kt` | Commands to WebRTC engine: Capabilities, Start, Offer, Answer, Ice, Media, Camera, End | +| `WCallResponse` | `views/call/WebRTC.kt` | Responses from WebRTC: Capabilities, Offer, Answer, Ice, Connection, Connected, End | +| `CallActivity` | `android/.../views/call/CallActivity.kt` | Android Activity hosting the call UI and WebView | +| `CallService` | `android/.../CallService.kt` | Android foreground Service for call persistence | +| `NanoWSD` | `desktopMain/.../views/call/CallView.desktop.kt` | Desktop embedded HTTP+WebSocket server | diff --git a/apps/multiplatform/product/flows/connection.md b/apps/multiplatform/product/flows/connection.md new file mode 100644 index 0000000000..1b1123b535 --- /dev/null +++ b/apps/multiplatform/product/flows/connection.md @@ -0,0 +1,233 @@ +# Connection Flow + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) | [spec/api.md](../../spec/api.md) + +## Overview + +Establishing a contact connection in SimpleX Chat follows an invitation-link model. One party creates a connection link (one-time invitation or long-term address), shares it out-of-band, and the other party connects via that link. The process uses SMP queues for the handshake, with no central server involved in identity management. + +Connections support incognito mode, where a random profile is used per-connection instead of the user's real profile. + +## Prerequisites + +- Chat is initialized and running. +- An active user profile exists. +- For connecting: a valid SimpleX connection link (invitation or address). + +--- + +## 1. Creating a Connection Link (Inviter Side) + +### 1.1 One-Time Invitation Link + +1. User navigates to "New Chat" and selects "Add Contact" (or uses the "+" action). +2. `ChatController.apiAddContact(rh, incognito)` is called: + +```kotlin +suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair?, (() -> Unit)?> +``` + +3. Internally, `CC.APIAddContact(userId, incognito)` is sent to the core. +4. The core creates a new SMP queue pair and returns: + - `CR.Invitation` with `connLinkInvitation: CreatedConnLink` and `connection: PendingContactConnection`. +5. The `CreatedConnLink` contains the invitation URI (long form and short link). +6. The link is displayed as a QR code in `NewChatView` and can be copied or shared. +7. A `PendingContactConnection` appears in the chat list while waiting. + +### 1.2 Long-Term Contact Address + +1. User goes to Settings and creates a SimpleX address. +2. This creates a persistent address link that multiple people can use. +3. Incoming connection requests from the address require explicit acceptance (see section 4). + +--- + +## 2. Connecting via Link (Connector Side) + +### 2.1 Preview the Connection Plan + +Before connecting, the link is analyzed: + +```kotlin +suspend fun apiConnectPlan(rh: Long?, connLink: String, inProgress: MutableState): Pair? +``` + +1. User pastes or scans a link. +2. `apiConnectPlan` sends `CC.APIConnectPlan(userId, connLink)` to the core. +3. The core resolves short links, validates the link, and returns a `ConnectionPlan`: + +```kotlin +sealed class ConnectionPlan { + class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() + class ContactAddress(val contactAddressPlan: ContactAddressPlan): ConnectionPlan() + class GroupLink(val groupLinkPlan: GroupLinkPlan): ConnectionPlan() + class Error(val chatError: ChatError): ConnectionPlan() +} +``` + +4. For `InvitationLinkPlan`: + - `Ok`: Fresh invitation, safe to connect. + - `OwnLink`: User's own link, alert shown. + - `Connecting(contact_)`: Already connecting to this contact. + - `Known(contact)`: Already connected, existing contact shown. + +5. For `ContactAddressPlan`: + - `Ok`: Fresh address, safe to connect. + - `OwnLink`: User's own address. + - `ConnectingConfirmReconnect`: Was connecting, offer to retry. + - `ConnectingProhibit(contact)`: Connection in progress, cannot duplicate. + - `Known(contact)`: Already a contact. + - `ContactViaAddress(contact)`: Contact already exists via this address. + +6. For `GroupLinkPlan`: + - `Ok`: Fresh group link, safe to join. + - `OwnLink(groupInfo)`: User's own group. + - `ConnectingConfirmReconnect`: Was connecting, offer to retry. + - `ConnectingProhibit(groupInfo_)`: Connection in progress. + - `Known(groupInfo)`: Already a member. + +### 2.2 High-Level Connect Flow: planAndConnect + +The `planAndConnect` function in `ConnectPlan.kt` orchestrates the full connect experience: + +```kotlin +suspend fun planAndConnect( + rhId: Long?, + shortOrFullLink: String, + close: (() -> Unit)?, + cleanup: (() -> Unit)? = null, + filterKnownContact: ((Contact) -> Unit)? = null, + filterKnownGroup: ((GroupInfo) -> Unit)? = null, +): CompletableDeferred +``` + +1. A progress indicator is shown. +2. `apiConnectPlan` is called to analyze the link. +3. Based on the plan type, the appropriate UI is shown: + - For `Ok` plans: proceed to `apiConnect`. + - For `Known`: navigate to the existing contact/group. + - For `OwnLink`: show alert. + - For `Connecting`: show reconnect confirmation or prohibit. +4. Returns a `CompletableDeferred` indicating success. + +### 2.3 Execute Connection + +```kotlin +suspend fun apiConnect(rh: Long?, incognito: Boolean, connLink: CreatedConnLink): PendingContactConnection? +``` + +1. `CC.APIConnect(userId, incognito, connLink)` is sent to the core. +2. The core initiates the SMP handshake: + - For invitation links: `CR.SentConfirmation` is returned. + - For contact addresses: `CR.SentInvitation` is returned. +3. A `PendingContactConnection` is returned and appears in the chat list. +4. The connect progress indicator is shown via `ConnectProgressManager`. + +--- + +## 3. Connection Handshake Completion + +### 3.1 For Invitation Links + +1. After the connector sends confirmation, the inviter's core receives it. +2. Both sides complete the SMP handshake automatically. +3. A `CR.ContactConnected` event is received on both sides. +4. The `PendingContactConnection` in the chat list is replaced by a full `Contact`. +5. Both parties can now exchange messages. + +### 3.2 For Contact Addresses + +1. The connector's confirmation arrives as a `ContactRequest` on the address owner's side. +2. The address owner must explicitly accept or reject (see section 4). +3. Once accepted, the handshake completes and `CR.ContactConnected` fires. + +--- + +## 4. Contact Request Acceptance + +### 4.1 Accept a Contact Request + +```kotlin +suspend fun apiAcceptContactRequest(rh: Long?, incognito: Boolean, contactReqId: Long): Contact? +``` + +1. The address owner sees a contact request notification in the chat list. +2. User taps to open and selects "Accept". +3. `CC.ApiAcceptContact(incognito, contactReqId)` is sent to the core. +4. The core responds with `CR.AcceptingContactRequest` and a `Contact` object. +5. The SMP handshake continues; once complete, `CR.ContactConnected` fires. +6. The `incognito` flag determines whether the real profile or a random profile is shared. + +### 4.2 Reject a Contact Request + +```kotlin +suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Contact? +``` + +1. User selects "Reject" on the contact request. +2. `CC.ApiRejectContact(contactReqId)` is sent to the core. +3. The core responds with `CR.ContactRequestRejected`. +4. The contact request is removed from the chat list. +5. The connector's side eventually times out or receives an error. + +--- + +## 5. Incognito Mode + +### 5.1 Per-Connection Incognito + +1. The `incognito` parameter is available on both `apiAddContact` and `apiConnect`. +2. When `incognito = true`: + - A random display name is generated for this connection. + - The real user profile is not shared with the contact. + - The incognito profile is stored per-connection in the database. +3. The global incognito toggle is in `AppPreferences.incognito`. +4. Incognito status is visible in the chat info view. + +### 5.2 Accept with Incognito + +1. When accepting a contact request with `incognito = true`, a random profile is used. +2. The accepted contact only sees the random profile. +3. The user can have some contacts with real profile and others with incognito profiles. + +--- + +## 6. Connection Progress and UI + +### 6.1 ConnectProgressManager + +```kotlin +object ConnectProgressManager { + fun startConnectProgress(text: String, onCancel: (() -> Unit)? = null) + fun stopConnectProgress() + fun cancelConnectProgress() +} +``` + +1. When a connection is initiated, `startConnectProgress` is called. +2. After a 1-second delay, a progress indicator appears if the operation is still in progress. +3. On completion (success or failure), `stopConnectProgress` is called. +4. The user can cancel via `cancelConnectProgress`. + +### 6.2 Pending Connection States + +While connecting, the chat list shows a `PendingContactConnection` with status: +- Waiting for the other party to scan/use the link. +- Connecting (handshake in progress). +- Connected (transitions to a full Contact chat). + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `CreatedConnLink` | `model/SimpleXAPI.kt` | Connection link with full URI and short link | +| `PendingContactConnection` | `model/ChatModel.kt` | In-progress connection shown in chat list | +| `ConnectionPlan` | `model/SimpleXAPI.kt` | Sealed class: InvitationLink, ContactAddress, GroupLink, Error | +| `InvitationLinkPlan` | `model/SimpleXAPI.kt` | Ok, OwnLink, Connecting, Known | +| `ContactAddressPlan` | `model/SimpleXAPI.kt` | Ok, OwnLink, ConnectingConfirmReconnect, ConnectingProhibit, Known | +| `GroupLinkPlan` | `model/SimpleXAPI.kt` | Ok, OwnLink, ConnectingConfirmReconnect, ConnectingProhibit, Known | +| `ConnectProgressManager` | `model/ChatModel.kt` | Manages connect progress indicator with timeout | +| `Contact` | `model/ChatModel.kt` | Established contact with profile, connection status | +| `ContactRequest` | `model/ChatModel.kt` | Pending inbound contact request | diff --git a/apps/multiplatform/product/flows/file-transfer.md b/apps/multiplatform/product/flows/file-transfer.md new file mode 100644 index 0000000000..edbb565c07 --- /dev/null +++ b/apps/multiplatform/product/flows/file-transfer.md @@ -0,0 +1,252 @@ +# File Transfer Flow + +> **Related spec:** [spec/services/files.md](../../spec/services/files.md) + +## Overview + +SimpleX Chat transfers files using two protocols based on file size: inline delivery through SMP messages for small files, and XFTP (SimpleX File Transfer Protocol) for larger files. All locally stored files can be AES-encrypted via CryptoFile. The system supports automatic receiving of small media, manual download for larger files, and cancellation at any stage. + +## Prerequisites + +- An active chat connection (direct contact or group). +- Sufficient storage space on the device. +- For XFTP: network connectivity to XFTP relay servers. + +--- + +## 1. File Size Thresholds and Constants + +| Constant | Value | Purpose | +|----------|-------|---------| +| `MAX_IMAGE_SIZE` | 261,120 bytes (255 KB) | Maximum inline image thumbnail size (base64 in message body) | +| `MAX_IMAGE_SIZE_AUTO_RCV` | 522,240 bytes (510 KB) | Auto-receive threshold for images | +| `MAX_VOICE_SIZE_AUTO_RCV` | 522,240 bytes (510 KB) | Auto-receive threshold for voice messages | +| `MAX_VIDEO_SIZE_AUTO_RCV` | 1,047,552 bytes (1023 KB) | Auto-receive threshold for video thumbnails | +| `MAX_FILE_SIZE_SMP` | 8,000,000 bytes (~7.6 MB) | Maximum file size for SMP inline transfer | +| `MAX_FILE_SIZE_XFTP` | 1,073,741,824 bytes (1 GB) | Maximum file size for XFTP transfer | +| `MAX_FILE_SIZE_LOCAL` | `Long.MAX_VALUE` | No limit for local files | + +These constants are defined in `views/helpers/Utils.kt`. + +The core decides the transfer protocol: +- Files within the SMP inline threshold are embedded directly in SMP messages. +- Files exceeding the inline threshold (up to 1 GB) use XFTP with chunked, encrypted upload/download through relay servers. + +--- + +## 2. CryptoFile Encryption + +### 2.1 Overview + +When `privacyEncryptLocalFiles` is enabled (default: `true`), files stored on device are AES-GCM encrypted. The encryption/decryption is performed via JNI calls to the Haskell core. + +### 2.2 Key Types + +```kotlin +// model/ChatModel.kt +@Serializable +data class CryptoFileArgs( + val fileKey: String, // AES-256 key (base64) + val fileNonce: String // GCM nonce (base64) +) + +@Serializable +data class CryptoFile { + val filePath: String + val cryptoArgs: CryptoFileArgs? // null for unencrypted files +} +``` + +### 2.3 Write (Encrypt) + +```kotlin +fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs +``` + +1. `ChatController.getChatCtrl()` obtains the active controller handle. +2. Data is placed in a `DirectByteBuffer`. +3. `chatWriteFile(ctrl, path, buffer)` is called via JNI. +4. The core generates a random AES key and nonce, encrypts the data, writes to `path`. +5. Returns `CryptoFileArgs(fileKey, fileNonce)` needed for decryption. +6. On error, throws an exception with the error message. + +### 2.4 Read (Decrypt) + +```kotlin +fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray +``` + +1. `chatReadFile(path, cryptoArgs.fileKey, cryptoArgs.fileNonce)` is called via JNI. +2. Returns a two-element array: `[status: Int, data: ByteArray]`. +3. If `status == 0`, the decrypted data is returned. +4. Otherwise, an exception is thrown with the error message. + +### 2.5 File-to-File Encryption + +```kotlin +fun encryptCryptoFile(fromPath: String, toPath: String): CryptoFileArgs +``` + +Encrypts a plaintext file at `fromPath` to an encrypted file at `toPath`. Used when saving user-selected files to the app's encrypted storage. + +### 2.6 File-to-File Decryption + +```kotlin +fun decryptCryptoFile(fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String) +``` + +Decrypts an encrypted file at `fromPath` to plaintext at `toPath`. Used when exporting/sharing files. + +--- + +## 3. Sending Files + +### 3.1 Attach and Send via ComposeView + +1. User attaches a file via the file picker. +2. File size is validated: `fileSize <= MAX_FILE_SIZE_XFTP` (1 GB). +3. If valid, `ComposeState.preview` is set to `ComposePreview.FilePreview(fileName, uri)`. +4. If too large, an alert is shown with the maximum supported size. +5. On send, the file is copied to the app files directory. +6. If `privacyEncryptLocalFiles` is enabled, the file is encrypted via `encryptCryptoFile`, producing a `CryptoFile` with `cryptoArgs`. +7. A `ComposedMessage` is created with: + - `fileSource`: the `CryptoFile` (path + optional cryptoArgs). + - `msgContent`: `MsgContent.MCFile(text)` for generic files, `MsgContent.MCImage(text, thumbnail)` for images, `MsgContent.MCVideo(text, thumbnail, duration)` for videos, or `MsgContent.MCVoice(text, duration)` for voice. +8. `ChatController.apiSendMessages(...)` dispatches the message. +9. The core determines the transfer protocol and begins the upload. + +### 3.2 Standalone File Upload (XFTP) + +For uploading files outside of a chat message context: + +```kotlin +suspend fun uploadStandaloneFile(user: UserLike, file: CryptoFile, ctrl: ChatCtrl? = null): Pair +``` + +1. `CC.ApiUploadStandaloneFile(userId, file)` is sent to the core. +2. On success, `CR.SndStandaloneFileCreated` returns a `FileTransferMeta`. +3. The meta contains a file description URI that can be shared for download. + +### 3.3 Upload Progress + +1. The core emits `SndFileProgressXFTP` events periodically during upload. +2. `CIFileStatus` on the chat item transitions through: + - `SndStored` (queued) + - `SndTransfer(sndProgress, sndTotal)` (uploading) + - `SndComplete` (upload finished, link sent) +3. The UI updates the progress indicator on the file attachment. + +--- + +## 4. Receiving Files + +### 4.1 Auto-Receive + +When `privacyAcceptImages` is enabled (default: `true`), small media files are auto-received: + +1. On receiving a message with a file attachment, the auto-receive logic checks: + - `MCImage` files with `fileSize <= MAX_IMAGE_SIZE_AUTO_RCV` (510 KB) + - `MCVideo` files with `fileSize <= MAX_VIDEO_SIZE_AUTO_RCV` (1023 KB) + - `MCVoice` files with `fileSize <= MAX_VOICE_SIZE_AUTO_RCV` (510 KB) and not already accepted +2. If criteria are met, `receiveFile` is called automatically. + +### 4.2 Manual Receive + +For files that are not auto-received: + +1. The chat item shows a download button with file size info. +2. File size is validated: `fileSizeValid(file)` checks `file.fileSize <= getMaxFileSize(file.fileProtocol)`. +3. User taps the download button. +4. `ChatController.receiveFile(rhId, user, fileId, userApprovedRelays, auto)` is called: + +```kotlin +suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) +``` + +5. This delegates to `receiveFiles` which handles relay approval: + +```kotlin +suspend fun receiveFiles(rhId: Long?, user: UserLike, fileIds: List, userApprovedRelays: Boolean = false, auto: Boolean = false) +``` + +6. For each file, `CC.ReceiveFile(fileId, userApprovedRelays, encrypted, inline)` is sent to the core. +7. If the file requires unapproved XFTP relays, the user is prompted to approve them. +8. Relay approval errors (`FileError.Auth` with `SMP AUTH` and `PROXY BROKER`) trigger relay approval alerts. +9. Other errors are collected and shown after all files are processed. + +### 4.3 Batch Receive + +Multiple files can be received at once: + +```kotlin +suspend fun receiveFiles(rhId: Long?, user: UserLike, fileIds: List, ...) +``` + +1. Iterates through all `fileIds`. +2. Files needing relay approval are batched and prompted once. +3. After approval, those files are retried with `userApprovedRelays = true`. +4. Errors for individual files are aggregated. + +### 4.4 Download Progress + +1. The core emits `RcvFileProgressXFTP` events during download. +2. `CIFileStatus` transitions through: + - `RcvAccepted` (download initiated) + - `RcvTransfer(rcvProgress, rcvTotal)` (downloading) + - `RcvComplete` (download finished) +3. On completion, if the file is encrypted, it remains encrypted on disk with `cryptoArgs` stored in the database. +4. When the user opens/views the file, `readCryptoFile` or `decryptCryptoFile` is called on demand. + +--- + +## 5. Cancelling a File Transfer + +### 5.1 Cancel via API + +```kotlin +suspend fun cancelFile(rh: Long?, user: User, fileId: Long) +``` + +1. `apiCancelFile(rh, fileId)` sends `CC.CancelFile(fileId)` to the core. +2. The core cancels any in-progress upload or download. +3. On success, the chat item is updated via `chatItemSimpleUpdate`. +4. `cleanupFile(chatItem)` removes any partial local files. + +### 5.2 Cancel via UI + +1. User long-presses a file message and selects "Cancel". +2. `cancelFileAlertDialog(fileId, cancelFile, cancelAction)` shows a confirmation dialog. +3. `CancelAction` provides the appropriate alert text based on direction (sending/receiving). +4. On confirmation, `cancelFile` is called. + +### 5.3 Compose Cancel + +Before sending, user can cancel the file attachment: + +1. User taps the "X" on the file preview in the compose area. +2. `ComposeState.preview` is reset to `ComposePreview.NoPreview`. +3. No API call is needed since the file was not yet sent. + +--- + +## 6. File Cleanup + +1. Files pending deletion are tracked in `ChatModel.filesToDelete`. +2. When a chat item with a file is deleted, the file path is added to `filesToDelete`. +3. The actual file deletion happens asynchronously. +4. Encrypted files require no special cleanup beyond deleting the encrypted file; the key exists only in the database record. + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `CryptoFile` | `model/ChatModel.kt` | File reference with path and optional encryption args | +| `CryptoFileArgs` | `model/ChatModel.kt` | AES key + nonce for encrypted files | +| `WriteFileResult` | `model/CryptoFile.kt` | Result of `writeCryptoFile`: success with args or error | +| `CIFile` | `model/ChatModel.kt` | Chat item file metadata: fileId, fileName, fileSize, fileStatus, fileProtocol | +| `CIFileStatus` | `model/ChatModel.kt` | File transfer status: SndStored, SndTransfer, SndComplete, RcvInvitation, RcvAccepted, RcvTransfer, RcvComplete, etc. | +| `FileProtocol` | `model/ChatModel.kt` | Transfer protocol: XFTP, SMP, LOCAL | +| `FileTransferMeta` | `model/ChatModel.kt` | Metadata for standalone XFTP uploads | +| `ComposePreview.FilePreview` | `views/chat/ComposeView.kt` | Compose state for file attachment | diff --git a/apps/multiplatform/product/flows/group-lifecycle.md b/apps/multiplatform/product/flows/group-lifecycle.md new file mode 100644 index 0000000000..60311f7b47 --- /dev/null +++ b/apps/multiplatform/product/flows/group-lifecycle.md @@ -0,0 +1,283 @@ +# Group Lifecycle Flow + +> **Related spec:** [spec/api.md](../../spec/api.md) | [spec/client/chat-view.md](../../spec/client/chat-view.md) + +## Overview + +Groups in SimpleX Chat are decentralized: there is no central group server. The group owner's device coordinates membership, and messages are delivered via pairwise SMP connections between members. Groups support roles, invitation links, member admission review, blocking, and profile updates. + +## Prerequisites + +- Chat is initialized and running. +- An active user profile exists. +- For creating a group: no special requirements. +- For joining: a group invitation link or a direct invitation from an existing member. + +--- + +## 1. Creating a Group + +### 1.1 Create Group + +1. User navigates to "New Chat" and selects "Create Group". +2. The `AddGroupView` collects a group profile: display name, full name, optional image, and optional description. +3. `ChatController.apiNewGroup(rh, incognito, groupProfile)` is called: + +```kotlin +suspend fun apiNewGroup(rh: Long?, incognito: Boolean, groupProfile: GroupProfile): GroupInfo? +``` + +4. `CC.ApiNewGroup(userId, incognito, groupProfile)` is sent to the core. +5. The core creates the group and returns `CR.GroupCreated` with a `GroupInfo` object. +6. The creating user is automatically assigned the `Owner` role. +7. The new group appears in the chat list. +8. If `incognito = true`, a random profile is used for the user within this group. + +### 1.2 Update Group Profile + +```kotlin +suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile): GroupInfo? +``` + +1. Owner or Admin navigates to group info and edits the profile. +2. `CC.ApiUpdateGroupProfile(groupId, groupProfile)` is sent to the core. +3. On success, `CR.GroupUpdated` returns the updated `GroupInfo` with `toGroup`. +4. The chat model is updated via `chatModel.chatsContext.updateGroup(rh, groupInfo)`. +5. Profile changes are propagated to all connected members. + +--- + +## 2. Adding Members + +### 2.1 Invite a Contact + +1. Owner or Admin opens group info and taps "Add Members". +2. `AddGroupMembersView` displays the user's contacts eligible for invitation. +3. A role is selected for the invitee (default: `Member`). +4. `ChatController.apiAddMember(rh, groupId, contactId, memberRole)` is called: + +```kotlin +suspend fun apiAddMember(rh: Long?, groupId: Long, contactId: Long, memberRole: GroupMemberRole): GroupMember? +``` + +5. `CC.ApiAddMember(groupId, contactId, memberRole)` is sent to the core. +6. The core sends a group invitation to the contact via their direct SMP connection. +7. `CR.SentGroupInvitation` returns a `GroupMember` in `Invited` status. +8. The member list updates to show the pending invitation. + +### 2.2 Invitee Joins + +1. The invited contact receives a group invitation event. +2. A group invitation chat item appears in their chat list. +3. The invitee taps "Join" to accept. +4. `ChatController.apiJoinGroup(rh, groupId)` is called. +5. `CC.ApiJoinGroup(groupId)` is sent to the core. +6. `CR.UserAcceptedGroupSent` confirms the join request was sent. +7. The owner's/admin's device processes the join and establishes pairwise connections with existing members. +8. `CR.MemberConnected` events fire as connections to each member are established. + +--- + +## 3. Member Roles + +### 3.1 Role Hierarchy + +```kotlin +enum class GroupMemberRole(val memberRole: String) { + Observer("observer"), // Can only read messages + Author("author"), // Can send messages but limited + Member("member"), // Standard member + Moderator("moderator"), // Can moderate content + Admin("admin"), // Can manage members + Owner("owner") // Full control, can delete group +} +``` + +Selectable roles for assignment: `Observer`, `Member`, `Moderator`, `Admin`, `Owner`. + +### 3.2 Change Member Role + +```kotlin +suspend fun apiMembersRole(rh: Long?, groupId: Long, memberIds: List, memberRole: GroupMemberRole): List +``` + +1. Owner or Admin navigates to member info in `GroupMemberInfoView`. +2. Selects a new role from the role picker. +3. `CC.ApiMembersRole(groupId, memberIds, memberRole)` is sent to the core. +4. The core responds with `CR.MembersRoleUser` returning updated `GroupMember` objects. +5. The change is propagated to all group members. +6. Supports batch role changes (multiple `memberIds`). + +--- + +## 4. Removing and Blocking Members + +### 4.1 Remove Members + +```kotlin +suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean): Pair>? +``` + +1. Owner or Admin selects a member and taps "Remove". +2. `CC.ApiRemoveMembers(groupId, memberIds, withMessages)` is sent. +3. If `withMessages = true`, the removed member's messages are also deleted from all members. +4. `CR.UserDeletedMembers` returns the updated `GroupInfo` and removed `GroupMember` list. +5. The removed member receives a notification and loses access to the group. + +### 4.2 Block Members for All + +```kotlin +suspend fun apiBlockMembersForAll(rh: Long?, groupId: Long, memberIds: List, blocked: Boolean): List +``` + +1. Owner, Admin, or Moderator selects a member and taps "Block for all". +2. `CC.ApiBlockMembersForAll(groupId, memberIds, blocked)` is sent. +3. `blocked = true` blocks; `blocked = false` unblocks. +4. `CR.MembersBlockedForAllUser` returns the updated member list. +5. Blocked members' messages are hidden from all group members. +6. The blocked member can still see the group but their messages are not delivered. + +--- + +## 5. Group Links + +### 5.1 Create Group Link + +```kotlin +suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? +``` + +1. Owner or Admin navigates to group info and taps "Create Group Link". +2. `CC.APICreateGroupLink(groupId, memberRole)` is sent. +3. A default role for joiners is specified (default: `Member`). +4. `CR.GroupLinkCreated` returns a `GroupLink` containing the link URI. +5. The link is displayed in `GroupLinkView` as a QR code and copyable text. + +### 5.2 Update Group Link Role + +```kotlin +suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? +``` + +1. Owner or Admin changes the default role for new members joining via link. +2. `CC.APIGroupLinkMemberRole(groupId, memberRole)` is sent. +3. `CR.CRGroupLink` returns the updated link with the new default role. + +### 5.3 Get Group Link + +```kotlin +suspend fun apiGetGroupLink(rh: Long?, groupId: Long): GroupLink? +``` + +1. Retrieves the existing group link for display. +2. `CC.APIGetGroupLink(groupId)` is sent. +3. Returns `null` if no link exists. + +### 5.4 Delete Group Link + +```kotlin +suspend fun apiDeleteGroupLink(rh: Long?, groupId: Long): Boolean +``` + +1. Owner or Admin navigates to group link settings and taps "Delete Link". +2. `CC.APIDeleteGroupLink(groupId)` is sent. +3. `CR.GroupLinkDeleted` confirms deletion. +4. The link becomes invalid; anyone with the old link can no longer join. + +--- + +## 6. Member Admission Workflow + +### 6.1 Admission Configuration + +Group owners can require review of new members before they are fully admitted: + +```kotlin +data class GroupMemberAdmission( + val review: MemberCriteria? = null +) + +enum class MemberCriteria { + All // All joining members require review +} +``` + +1. Owner opens group info and navigates to "Member Admission" (`MemberAdmissionView`). +2. The `review` field is set to `MemberCriteria.All` to require review of all new members. +3. The admission configuration is saved by updating the group profile: + - `groupProfile.copy(memberAdmission = admission)` is passed to `apiUpdateGroup`. +4. Changes are tracked with unsaved-changes detection (save/discard prompt on navigation). + +### 6.2 Accept a Pending Member + +```kotlin +suspend fun apiAcceptMember(rh: Long?, groupId: Long, groupMemberId: Long, memberRole: GroupMemberRole): Pair? +``` + +1. When admission review is enabled, new members joining via link arrive in a pending state. +2. Owner or Admin sees pending members in the member support chat / member list. +3. User selects "Accept" and optionally adjusts the role. +4. `CC.ApiAcceptMember(groupId, groupMemberId, memberRole)` is sent. +5. `CR.MemberAccepted` returns the updated `GroupInfo` and accepted `GroupMember`. +6. The member is now fully connected and can participate in the group. + +### 6.3 Reject a Pending Member + +1. Owner or Admin selects "Reject" on a pending member. +2. The member is removed via `apiRemoveMembers`. +3. The rejected member receives a removal notification. + +--- + +## 7. Leaving a Group + +```kotlin +suspend fun apiLeaveGroup(rh: Long?, groupId: Long): GroupInfo? +``` + +1. User navigates to group info and taps "Leave Group". +2. A confirmation dialog is shown. +3. `CC.ApiLeaveGroup(groupId)` is sent to the core. +4. `CR.LeftMemberUser` returns the updated `GroupInfo`. +5. The user's membership status changes and they can no longer send or receive messages. +6. The group remains in the chat list in a "left" state, and can be deleted locally. + +--- + +## 8. Listing Members + +```kotlin +suspend fun apiListMembers(rh: Long?, groupId: Long): List +``` + +1. When opening group info or the member list, `apiListMembers` is called. +2. `CC.ApiListMembers(groupId)` is sent to the core. +3. `CR.GroupMembers` returns the member list. +4. `ChatModel.groupMembers` and `ChatModel.groupMembersIndexes` are updated. +5. `ChatModel.membersLoaded` is set to `true`. + +--- + +## 9. Group Chat Scope (Support Channels) + +Groups support scoped conversations for member support: + +- `GroupChatScope` parameter on message APIs allows sending messages within a specific scope (e.g., member support chat). +- `MemberSupportChatView` and `MemberSupportView` provide UI for admin-to-member private conversations within the group context. +- `GroupReportsView` shows moderation reports scoped to the group. + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `GroupInfo` | `model/ChatModel.kt` | Group metadata: groupId, groupProfile, membership, fullGroupPreferences | +| `GroupProfile` | `model/ChatModel.kt` | Group display info: displayName, fullName, description, image, memberAdmission | +| `GroupMember` | `model/ChatModel.kt` | Member info: groupMemberId, memberRole, memberStatus, memberProfile | +| `GroupMemberRole` | `model/ChatModel.kt` | Enum: Observer, Author, Member, Moderator, Admin, Owner | +| `GroupMemberAdmission` | `model/ChatModel.kt` | Admission settings: review criteria | +| `MemberCriteria` | `model/ChatModel.kt` | Enum: All (require review for all) | +| `GroupLink` | `model/SimpleXAPI.kt` | Group link: connLinkContact, acceptMemberRole, userContactLinkId, shortLinkDataSet, shortLinkLargeDataSet, groupLinkId | +| `GroupChatScope` | `model/ChatModel.kt` | Scoped conversation within a group | +| `ConnectionPlan.GroupLink` | `model/SimpleXAPI.kt` | Plan result when connecting via a group link | diff --git a/apps/multiplatform/product/flows/messaging.md b/apps/multiplatform/product/flows/messaging.md new file mode 100644 index 0000000000..771eae1c4e --- /dev/null +++ b/apps/multiplatform/product/flows/messaging.md @@ -0,0 +1,195 @@ +# Messaging Flow + +> **Related spec:** [spec/client/compose.md](../../spec/client/compose.md) | [spec/api.md](../../spec/api.md) + +## Overview + +Messaging is the core interaction in SimpleX Chat. Users compose and send text, images, video, voice notes, files, and link previews. Messages can be replied to, edited, deleted, forwarded, and reacted to with emoji. Special modes include timed (disappearing) messages, live messages (real-time typing), and message reports for moderation. + +All message operations flow through the Haskell core via `ChatController.apiSendMessages`, with responses updating `ChatModel` and triggering Compose UI recomposition. + +## Prerequisites + +- Chat is initialized and running (`ChatModel.chatRunning == true`). +- An active user exists (`ChatModel.currentUser != null`). +- A chat is open (`ChatModel.chatId != null`) with an established connection. + +--- + +## 1. Sending a Text Message + +### 1.1 Compose and Send + +1. User types in the compose field. `ComposeState.message` is updated as a `ComposeMessage(text, selection)`. +2. The compose area tracks context via `ComposeContextItem`: `NoContextItem` for a fresh message, `QuotedItem` for a reply, `EditingItem` for an edit, `ForwardingItems` for forwarding, or `ReportedItem` for a report. +3. User taps the send button. The `ComposeView` builds a `ComposedMessage`: + ```kotlin + class ComposedMessage( + val fileSource: CryptoFile?, + val quotedItemId: Long?, + val msgContent: MsgContent, + val mentions: Map + ) + ``` +4. For plain text, `msgContent` is `MsgContent.MCText(text)`. +5. `ChatController.apiSendMessages(rh, type, id, scope, live, ttl, composedMessages)` is called. +6. The core command `CC.ApiSendMessages` is dispatched via `sendCmd`. +7. On success, the response `CR.NewChatItems` returns a list of `AChatItem`. +8. `ChatModel` is updated and the chat item list recomposes to show the new message. +9. `ComposeState` is reset to its default. + +### 1.2 Link Preview + +1. As the user types, the text is parsed for URLs. +2. If `privacyLinkPreviews` preference is enabled and a URL is detected, a `LinkPreview` is fetched asynchronously. +3. The compose preview is set to `ComposePreview.CLinkPreview(linkPreview)`. +4. When sent, the `msgContent` is `MsgContent.MCLink(text, preview)`. + +--- + +## 2. Sending Media (Image, Video, Voice) + +### 2.1 Image + +1. User picks or captures an image. +2. The image is resized (max inline data size `MAX_IMAGE_SIZE` = 255 KB for the preview thumbnail). +3. The full-size file is saved to the app files directory. +4. If local file encryption is enabled (`privacyEncryptLocalFiles`), the file is encrypted via `encryptCryptoFile`, producing a `CryptoFile` with `CryptoFileArgs(fileKey, fileNonce)`. +5. Compose preview becomes `ComposePreview.MediaPreview(images, content)`. +6. On send, `msgContent` is `MsgContent.MCImage(text, imageBase64)` and `fileSource` is the `CryptoFile`. +7. The core handles inline delivery (for small files) or XFTP upload (for larger files). + +### 2.2 Video + +1. User picks or records a video. +2. A thumbnail image is extracted and resized. +3. The video file is saved and optionally encrypted. +4. On send, `msgContent` is `MsgContent.MCVideo(text, image, duration)`. + +### 2.3 Voice Message + +1. User records a voice note. Recording state is tracked via `RecordingState` (NotStarted, Started, Finished). +2. The compose preview becomes `ComposePreview.VoicePreview(voice, durationMs, finished)`. +3. On send, `msgContent` is `MsgContent.MCVoice(text, durationSeconds)`. +4. A file attachment carries the actual audio data. + +--- + +## 3. Sending Files + +1. User picks a file via the file chooser. +2. File size is validated against `MAX_FILE_SIZE_XFTP` (1 GB). +3. Compose preview becomes `ComposePreview.FilePreview(fileName, uri)`. +4. On send, `msgContent` is `MsgContent.MCFile(text)` and the `fileSource` is populated. +5. Delivery via inline (small files under SMP threshold) or XFTP (large files) is determined by the core. + +--- + +## 4. Receiving Messages + +1. The `ChatController` receiver loop calls `chatRecvMsgWait` on the Haskell core. +2. Incoming messages arrive as `CR.NewChatItems` events. +3. `ChatModel` chat items list is updated, triggering recomposition. +4. For media messages, images below `MAX_IMAGE_SIZE_AUTO_RCV` (510 KB), videos below `MAX_VIDEO_SIZE_AUTO_RCV` (1023 KB), and voice notes below `MAX_VOICE_SIZE_AUTO_RCV` (510 KB) are auto-received if `privacyAcceptImages` is enabled. +5. Larger files require manual download initiation (see File Transfer Flow). + +--- + +## 5. Editing a Message + +1. User long-presses a sent message and selects "Edit". +2. `ComposeContextItem` becomes `EditingItem(chatItem)`. +3. The original text populates the compose field. +4. On send, `ChatController.apiUpdateChatItem(rh, type, id, scope, itemId, updatedMessage, live)` is called. +5. `updatedMessage` is an `UpdatedMessage(msgContent, mentions)`. +6. The core responds with `CR.ChatItemUpdated` or `CR.ChatItemNotChanged`. +7. The chat item in `ChatModel` is updated in place. + +--- + +## 6. Deleting a Message + +1. User long-presses a message and selects "Delete". +2. A delete mode is chosen: `CIDeleteMode.cidmBroadcast` (delete for everyone), `CIDeleteMode.cidmInternal` (delete for self), or `CIDeleteMode.cidmInternalMark` (mark as deleted internally). +3. `ChatController.apiDeleteChatItems(rh, type, id, scope, itemIds, mode)` is called. +4. The core responds with `CR.ChatItemsDeleted`, returning a list of `ChatItemDeletion`. +5. For group chats by moderators, `apiDeleteMemberChatItems(rh, groupId, itemIds)` is used. +6. Deleted items are either removed from the UI or replaced with a "deleted" marker. + +--- + +## 7. Reacting to a Message + +1. User long-presses a message and selects an emoji reaction. +2. `ChatController.apiChatItemReaction(rh, type, id, scope, itemId, add, reaction)` is called. +3. `reaction` is a `MsgReaction` (typically emoji). +4. `add = true` to add, `add = false` to remove a reaction. +5. The core responds with `CR.ChatItemReaction`, and the chat item's reaction list is updated. +6. In groups, `apiGetReactionMembers` can be called to see who reacted. + +--- + +## 8. Replying to a Message + +1. User swipes or long-presses a message and selects "Reply". +2. `ComposeContextItem` becomes `QuotedItem(chatItem)`. +3. The quoted item preview is shown above the compose field. +4. On send, the `ComposedMessage.quotedItemId` is set to the quoted item's ID. +5. The sent message renders with the quoted content inline. + +--- + +## 9. Forwarding Messages + +1. User selects one or more messages and taps "Forward". +2. `ChatController.apiPlanForwardChatItems(rh, fromChatType, fromChatId, fromScope, chatItemIds)` is called first to get a `CR.ForwardPlan` with forwardable/non-forwardable item categorization. +3. `ComposeContextItem` becomes `ForwardingItems(chatItems, fromChatInfo)`. +4. User picks a destination chat. +5. `ChatController.apiForwardChatItems(rh, toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl)` is called. +6. New chat items are created in the destination chat. + +--- + +## 10. Timed (Disappearing) Messages + +1. Timed messages are enabled per-chat via chat feature preferences. +2. When composing, a TTL (time-to-live) in seconds is passed as the `ttl` parameter to `apiSendMessages`. +3. The core attaches the TTL to the message metadata. +4. After the TTL expires, the message is automatically deleted on both sides. +5. The UI shows a countdown indicator on timed messages via `CIMetaView`. + +--- + +## 11. Live Messages + +1. User enables live message mode (long-press on send button if `liveMessageAlertShown` preference allows). +2. `ComposeState.liveMessage` is set to a `LiveMessage(chatItem, typedMsg, sentMsg, sent)`. +3. As the user types, `apiSendMessages` is called with `live = true` for the initial send, then `apiUpdateChatItem` with `live = true` for subsequent updates. +4. The recipient sees the message content updating in real-time. +5. When the user finalizes (taps send), a final `apiUpdateChatItem` with `live = false` is sent. + +--- + +## 12. Message Reports + +1. User long-presses a message and selects "Report". +2. `ComposeContextItem` becomes `ReportedItem(chatItem, reason)` where `reason` is a `ReportReason`. +3. On send, `msgContent` is `MsgContent.MCReport(text, reason)`. +4. The report is sent to group owners/admins for moderation review. +5. Group admins see reports in the `GroupReportsView`. + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `ComposeState` | `views/chat/ComposeView.kt` | Tracks compose field state | +| `ComposePreview` | `views/chat/ComposeView.kt` | Preview type: NoPreview, CLinkPreview, MediaPreview, VoicePreview, FilePreview | +| `ComposeContextItem` | `views/chat/ComposeView.kt` | Context: NoContextItem, QuotedItem, EditingItem, ForwardingItems, ReportedItem | +| `ComposedMessage` | `model/SimpleXAPI.kt` | Wire format for sending: fileSource, quotedItemId, msgContent, mentions | +| `UpdatedMessage` | `model/SimpleXAPI.kt` | Wire format for editing: msgContent, mentions | +| `MsgContent` | `model/ChatModel.kt` | Sealed class: MCText, MCLink, MCImage, MCVideo, MCVoice, MCFile, MCReport, MCChat, MCUnknown | +| `LiveMessage` | `views/chat/ComposeView.kt` | Tracks live message state | +| `MsgReaction` | `model/ChatModel.kt` | Emoji reaction type | +| `ChatItemDeletion` | `model/ChatModel.kt` | Deletion result with old/new item | diff --git a/apps/multiplatform/product/flows/onboarding.md b/apps/multiplatform/product/flows/onboarding.md new file mode 100644 index 0000000000..b6b3e835a5 --- /dev/null +++ b/apps/multiplatform/product/flows/onboarding.md @@ -0,0 +1,205 @@ +# Onboarding Flow + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) | [spec/architecture.md](../../spec/architecture.md) + +## Overview + +Onboarding is the first-run experience that initializes the Haskell chat core, creates the local database, sets up the user profile, configures server operators, and (on Android) selects the notification mode. The flow is tracked by the `OnboardingStage` enum persisted in `AppPreferences.onboardingStage`. + +The initialization path differs slightly between Android and Desktop, but both converge on the common `chatMigrateInit` JNI call and shared `ChatController` logic. + +## Prerequisites + +- Fresh install or database reset. +- On Android: `SimplexApp.onCreate()` has been called. +- On Desktop: `main()` has been called. + +--- + +## 1. Platform Initialization + +### 1.1 Android: SimplexApp.onCreate() + +1. `SimplexApp.onCreate()` is called by the Android framework. +2. `AppContextProvider.initialize(this)` sets the application context. +3. Phoenix process detection: if this is a restart process, return early. +4. A global error handler is registered. +5. `initHaskell(packageName)` loads the native `libapp-lib.so` and calls `initHS()` to initialize the Haskell runtime. +6. `initMultiplatform()` sets up: + - `androidAppContext` reference. + - `ntfManager` (notification manager bridge to Android `NtfManager`). + - `platform` interface implementation with Android-specific callbacks for services, notifications, call management, and UI configuration. +7. `reconfigureBroadcastReceivers()` ensures notification-related receivers match saved preferences. +8. `runMigrations()` performs any pending app-level data migrations. +9. Temp directory is cleaned and recreated. +10. If a migration state exists (`chatModel.migrationState.value != null`), onboarding is forced to `Step1_SimpleXInfo`. +11. Otherwise, if authentication keys are available, `initChatControllerOnStart()` is called. + +### 1.2 Desktop: Main.kt main() + +1. `initHaskell()` loads native libraries: + - On Linux/macOS: `libapp-lib.so` / `libapp-lib.dylib`. + - On Windows: `libcrypto-3-x64.dll`, `libsimplex.dll`, `libapp-lib.dll` plus VLC libraries. +2. `initHS()` initializes the Haskell runtime. +3. `platform` interface is set with Desktop-specific callbacks (app update notice). +4. `runMigrations()` performs pending app-level data migrations. +5. `setupUpdateChecker()` configures the desktop update channel. +6. `initApp()` initializes common app state. +7. Temp directory is cleaned and recreated. +8. `showApp()` launches the Compose Desktop window, which renders the `AppView`. + +--- + +## 2. Database Initialization (chatMigrateInit) + +### 2.1 initChatController + +1. `initChatController(useKey, confirmMigrations, startChat)` is called (from `Core.kt`). +2. If `ctrlInitInProgress` is already true, return (prevents double initialization). +3. The database key is resolved: + - From `useKey` parameter if provided. + - Otherwise from `DatabaseUtils.useDatabaseKey()` which reads from the keystore. +4. Migration confirmation mode is determined: + - `MigrationConfirmation.YesUp` (auto-confirm forward migrations) by default. + - `MigrationConfirmation.Error` if developer tools + confirm upgrades are enabled. +5. `chatMigrateInit(dbPath, dbKey, confirm)` is called via JNI. This: + - Opens (or creates) the SQLite database at `dbAbsolutePrefixPath`. + - Runs all pending schema migrations. + - Returns a `ChatCtrl` handle (Long) and a `DBMigrationResult`. +6. On `DBMigrationResult.OK`: + - The `ChatCtrl` is stored globally. + - `ChatModel.chatDbStatus` is set. + - App file paths are configured via `apiSetAppFilePaths`. + - `apiGetActiveUser` checks for an existing user. +7. If an active user exists, `startChat(user)` is called. +8. If no user exists, `startChatWithoutUser()` is called and onboarding begins at `Step1_SimpleXInfo`. + +### 2.2 Error Handling + +- `DBMigrationResult.ErrorNotADatabase`: Wrong passphrase or corrupted DB. User is prompted. +- `DBMigrationResult.ErrorMigration`: Migration failed. Details shown to user. +- `DBMigrationResult.ErrorKeyNotSet`: Encryption key missing. +- `DBMigrationResult.InvalidConfirmation`: Migrations need manual confirmation (developer mode). +- On any error, `ChatModel.chatDbStatus` is set and the UI shows the appropriate database error screen. + +--- + +## 3. Onboarding Stages + +The onboarding flow is controlled by `OnboardingStage`, persisted in `AppPreferences.onboardingStage`: + +```kotlin +enum class OnboardingStage { + Step1_SimpleXInfo, + Step2_CreateProfile, + LinkAMobile, + Step2_5_SetupDatabasePassphrase, + Step3_ChooseServerOperators, + Step3_CreateSimpleXAddress, + Step4_SetNotificationsMode, + OnboardingComplete +} +``` + +### 3.1 Step1_SimpleXInfo + +1. The `SimpleXInfo` screen is shown. +2. Explains what SimpleX Chat is: privacy, no user identifiers, decentralized. +3. User taps "Create your profile" to proceed. +4. On Desktop, a "Link a Mobile" option is also available. + +### 3.2 Step2_CreateProfile + +1. The `CreateProfile` screen is shown. +2. User enters a display name (validated via `chatValidName` JNI) and optional full name. +3. On submit, `ChatController.apiCreateActiveUser(rh, profile)` is called: + ```kotlin + suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? + ``` +4. The core command `CC.CreateActiveUser(p, pastTimestamp)` creates the user in the database. +5. On success, `CR.ActiveUser` returns the new `User` object. +6. `ChatModel.currentUser` is set. +7. If the chat is not yet running, `startChat(user)` is called: + - `apiSetNetworkConfig` configures network settings. + - `apiStartChat` starts the message receiver. + - `startReceiver()` begins polling for incoming messages. +8. Onboarding advances to `Step3_ChooseServerOperators`. + +### 3.3 LinkAMobile (Desktop Only) + +1. Available as an alternative to creating a profile on Desktop. +2. Shows a QR code for linking with a mobile device. +3. The desktop acts as a remote host controlled by the mobile app. + +### 3.4 Step2_5_SetupDatabasePassphrase (Desktop Only) + +1. On Desktop, after profile creation, the user is optionally prompted to set a database passphrase. +2. If skipped, a random passphrase is used (`desktopOnboardingRandomPassword` flag). +3. `ChatModel.desktopOnboardingRandomPassword` tracks this state. + +### 3.5 Step3_ChooseServerOperators + +1. The `ChooseServerOperators` screen is shown. +2. User selects which preset server operators to use for messaging and file transfer. +3. Server operator conditions may need to be accepted. +4. The selection is saved via the server configuration APIs. + +### 3.6 Step3_CreateSimpleXAddress + +1. User is prompted to create a SimpleX address for receiving contact requests. +2. This calls the address creation API. +3. Can be skipped. + +### 3.7 Step4_SetNotificationsMode (Android Only) + +1. The `SetNotificationsMode` screen is shown. +2. Three modes are available: + - `NotificationsMode.SERVICE`: Persistent background service (instant notifications). + - `NotificationsMode.PERIODIC`: Periodic background work (delayed notifications). + - `NotificationsMode.OFF`: No background processing (manual check only). +3. On selection, `appPrefs.notificationsMode` is set. +4. On Desktop, this step is skipped entirely. + +### 3.8 OnboardingComplete + +1. `appPrefs.onboardingStage` is set to `OnboardingComplete`. +2. The chat list view (`ChatListView`) is shown. +3. On Android, `SimplexService.showBackgroundServiceNoticeIfNeeded()` may show additional setup prompts. +4. On Android with `NotificationsMode.SERVICE`, `SimplexService.start()` is called. + +--- + +## 4. startChat Flow + +After the user is created and onboarding progresses, `ChatController.startChat(user)` orchestrates the final setup: + +1. `apiSetNetworkConfig(getNetCfg())` applies network configuration. +2. `apiCheckChatRunning()` checks if the core is already running. +3. `listUsers(null)` loads all user profiles into `ChatModel.users`. +4. If chat is not running: + - `ChatModel.currentUser` is set. + - `apiStartChat()` starts the core's message processing. + - `startReceiver()` begins the message receive loop. + - `setLocalDeviceName` sets the device name for remote access. +5. `apiGetChats` loads the chat list. +6. `chatModel.chatsContext.updateChats(chats)` populates the UI. +7. User address and chat item TTL are loaded. +8. `appPrefs.chatLastStart` is updated. +9. `ChatModel.chatRunning` is set to `true`. +10. `platform.androidChatInitializedAndStarted()` is called for Android-specific post-start tasks. + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `OnboardingStage` | `views/onboarding/OnboardingView.kt` | Enum tracking onboarding progress | +| `SimplexApp` | `android/.../SimplexApp.kt` | Android Application class, entry point | +| `Main.kt` | `desktop/.../Main.kt` | Desktop entry point | +| `ChatController` | `model/SimpleXAPI.kt` | Core API controller, manages chat lifecycle | +| `ChatModel` | `model/ChatModel.kt` | Global observable state | +| `DBMigrationResult` | `views/helpers/DatabaseUtils.kt` | Database migration outcome | +| `chatMigrateInit` | `platform/Core.kt` | JNI function: initialize DB and run migrations | +| `initChatController` | `platform/Core.kt` | High-level initialization orchestrator | +| `AppPreferences` | `model/SimpleXAPI.kt` | Persistent user preferences | diff --git a/apps/multiplatform/product/gaps.md b/apps/multiplatform/product/gaps.md new file mode 100644 index 0000000000..25535d8003 --- /dev/null +++ b/apps/multiplatform/product/gaps.md @@ -0,0 +1,290 @@ +# Known Gaps & Recommendations -- SimpleX Chat (Android & Desktop, Kotlin Multiplatform) + +This document catalogs known gaps in the multiplatform codebase (Android and Desktop) with severity, impact, and recommendations. + +--- + +## Table of Contents + +1. [UI: Error Feedback](#gap-01-ui-error-feedback) +2. [UI: Loading States](#gap-02-ui-loading-states) +3. [Security: Database Passphrase Not Enforced](#gap-03-security-database-passphrase-not-enforced) +4. [Security: No Forward Secrecy Indicator](#gap-04-security-no-forward-secrecy-indicator) +5. [Documentation: Haskell Store Layer Not Fully Specified](#gap-05-documentation-haskell-store-layer-not-fully-specified) +6. [Desktop: Recording Not Implemented](#gap-06-desktop-recording-not-implemented) +7. [Desktop: Cryptor Not Implemented](#gap-07-desktop-cryptor-not-implemented) + +--- + +## GAP-01: UI Error Feedback + +**Severity:** Medium +**Category:** UI / UX +**Platforms:** Android, Desktop + +### Description + +Many API calls through `ChatController.sendCmd()` return `API.Error` responses that are logged but not surfaced to the user. The general pattern is: + +```kotlin +val r = sendCmd(rh, cmd) +if (r is API.Result && r.res is CR.ExpectedResponse) return r.res.value +Log.e(TAG, "someFunction bad response: ${r.responseType} ${r.details}") +return null +``` + +When the call fails, the caller receives `null` and either silently does nothing or shows a generic error. The specific `ChatError` details (which may contain actionable information like quota exceeded, server unreachable, or store errors) are lost to the user. + +### Affected Locations + +- `SimpleXAPI.kt` -- `getAgentSubsTotal()`, `getAgentServersSummary()`, and dozens of similar `api*` functions +- Throughout the codebase wherever `sendCmd` results are pattern-matched + +### Impact + +Users experience silent failures with no indication of what went wrong. This is particularly problematic for: +- Connection attempts that fail due to network issues +- File transfer failures +- Group operations that fail due to role permissions +- Server configuration errors + +### Recommendation + +1. Introduce a structured error-handling utility that maps `ChatError` subtypes to user-visible messages, similar to how `retryableNetworkErrorAlert` already handles a subset of `AgentErrorType.BROKER` errors. +2. At minimum, surface a dismissible snackbar/toast with a summary when an API call fails unexpectedly. +3. For critical operations (send message, join group, create connection), show a dialog with retry/cancel options (the `sendCmdWithRetry` pattern already exists for some cases -- extend it). + +--- + +## GAP-02: UI Loading States + +**Severity:** Low-Medium +**Category:** UI / UX +**Platforms:** Android, Desktop + +### Description + +Several long-running operations lack loading indicators, leaving the user uncertain whether the action is in progress. The `ComposeState.inProgress` flag and `progressByTimeout` mechanism exist for the compose area, and `ConnectProgressManager` handles connection progress, but many other flows have no visual feedback. + +### Affected Locations + +- Group member list loading (`ChatModel.membersLoaded` exists but is not always checked before displaying stale data) +- Server configuration validation (`ApiValidateServers` can take several seconds with no indicator) +- Database export/import (`ApiExportArchive`, `ApiImportArchive`) +- Profile switching (`changeActiveUser_` acquires `changingActiveUserMutex` but the UI may appear frozen) + +### Impact + +Users may tap actions multiple times, causing duplicate requests, or assume the app is frozen and force-quit during a long operation like database export. + +### Recommendation + +1. Introduce a centralized `ProgressOverlay` composable that can be shown/hidden via a `ChatModel` flag. +2. Wrap all operations that acquire `changingActiveUserMutex` or take > 1 second with a visible loading state. +3. Use `ChatModel.switchingUsersAndHosts` (which already exists) more consistently as a gate for showing a blocking progress indicator. + +--- + +## GAP-03: Security: Database Passphrase Not Enforced + +**Severity:** High +**Category:** Security +**Platforms:** Android, Desktop + +### Description + +When the app is first installed, a random database passphrase is generated and stored in encrypted preferences. The user is never required to set a custom passphrase. The `initialRandomDBPassphrase` flag tracks this state, and a setup prompt exists in onboarding (`SetupDatabasePassphrase`), but the user can skip it. + +On Android, the encrypted passphrase is stored via the Android Keystore, which provides hardware-backed security. On Desktop, the `Cryptor` is a **placeholder** (see GAP-07), meaning the passphrase is stored in plaintext. + +### Affected Locations + +- `SimpleXAPI.kt` -- `AppPreferences.storeDBPassphrase`, `AppPreferences.initialRandomDBPassphrase`, `AppPreferences.encryptedDBPassphrase` +- `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` +- `common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt` + +### Impact + +- Users who skip passphrase setup rely entirely on device security. If the device is compromised, the database can be decrypted using the stored passphrase. +- On Desktop, the passphrase is effectively stored in plaintext (see GAP-07), meaning anyone with filesystem access can read the database. + +### Recommendation + +1. Consider making passphrase setup mandatory during onboarding (or at least prominently warn users who skip it). +2. On Desktop, implement proper key storage (GAP-07) before any passphrase enforcement is meaningful. +3. Add a periodic reminder for users who still have `initialRandomDBPassphrase == true`. + +--- + +## GAP-04: Security: No Forward Secrecy Indicator + +**Severity:** Medium +**Category:** Security / UI +**Platforms:** Android, Desktop + +### Description + +The double-ratchet algorithm provides forward secrecy per message, and PQ key exchange provides resistance to quantum attacks. The `Connection` type tracks `pqSupport`, `pqEncryption`, `pqSndEnabled`, and `pqRcvEnabled`. However, the UI does not prominently display the current forward secrecy state or PQ encryption status for a given conversation. + +### Affected Locations + +- `ChatModel.kt` -- `Connection.pqSupport`, `Connection.pqEncryption`, `Connection.pqSndEnabled`, `Connection.pqRcvEnabled` +- Contact info views, group member info views + +### Impact + +Users cannot easily verify whether their conversations are using PQ-enhanced encryption. Security-conscious users have no visual indicator of the ratchet state or whether PQ key exchange was successful. + +### Recommendation + +1. Add a security badge/icon in the chat header or contact info screen showing: + - Whether PQ key exchange is active (both peers support it) + - Whether the connection has been verified (security code comparison) + - The ratchet state (in-sync vs. needs re-sync) +2. The `connectionCode` field on `Connection` can be used to show verification status. +3. The `Call.encryptionStatus` pattern (used in call views) could be adapted for the chat view. + +--- + +## GAP-05: Documentation: Haskell Store Layer Not Fully Specified + +**Severity:** Medium +**Category:** Documentation / Architecture +**Platforms:** Android, Desktop + +### Description + +The Kotlin client communicates with the Haskell core via a text-based command protocol (`CC.cmdString` -> FFI -> Haskell). The Haskell store layer (SQLite operations, migration logic, and the exact semantics of `StoreError` variants) is not documented from the Kotlin side. The `ChatErrorStore` error type wraps a `StoreError` whose variants are defined in Haskell and deserialized by the Kotlin client, but the conditions under which each error occurs are not specified. + +### Affected Locations + +- `SimpleXAPI.kt:6986` -- `ChatErrorStore(storeError: StoreError)` +- `SimpleXAPI.kt` -- `StoreError` sealed class (deserialized from Haskell responses) +- `SimpleXAPI.kt` -- `ChatErrorDatabase(databaseError: DatabaseError)` for migration errors + +### Impact + +- Developers cannot predict which `StoreError` will occur for a given operation without reading the Haskell source. +- Error handling in the Kotlin layer is necessarily generic since the error semantics are not specified. +- Migration failures (`ChatErrorDatabase`) are particularly opaque. + +### Recommendation + +1. Create a specification document mapping each `CC` command to its possible `StoreError` / `DatabaseError` responses. +2. Document the database migration versioning scheme and the conditions under which `confirmDBUpgrades` is triggered. +3. Add inline documentation to the `StoreError` sealed class variants explaining their trigger conditions. + +--- + +## GAP-06: Desktop: Recording Not Implemented + +**Severity:** High +**Category:** Feature / Platform +**Platform:** Desktop only + +### Description + +The `RecorderNative` class on Desktop is a placeholder. Both `start()` and `stop()` are stubbed with `/*LALAL*/` comments and return dummy values (empty string and 0, respectively). Users cannot record voice messages on Desktop. + +```kotlin +// common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +actual class RecorderNative: RecorderInterface { + override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String { + /*LALAL*/ + return "" + } + + override fun stop(): Int { + /*LALAL*/ + return 0 + } +} +``` + +Audio playback IS implemented on Desktop (via VLC/`vlcj` library), so received voice messages can be played. Only recording is missing. + +### Affected Locations + +- `common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt:15-25` +- `common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt` -- `RecorderInterface` + +### Impact + +Desktop users cannot send voice messages. The record button either does nothing or produces a zero-length file. + +### Recommendation + +1. Implement `RecorderNative` using a JVM audio capture library (e.g., `javax.sound.sampled`, or integrate with the existing `vlcj` dependency for capture). +2. The output format should match the mobile app's voice message format (likely Opus in an OGG container) for cross-platform compatibility. +3. Until implemented, the record button should be hidden or disabled on Desktop with a tooltip explaining the limitation. + +### Additional Desktop LALAL Placeholders + +Several other Desktop features are also marked with `LALAL` placeholders: +- **QR Code Scanner** (`QRCodeScanner.desktop.kt:12`) -- scanning QR codes is not implemented on Desktop +- **Animated Drawables** (`Utils.desktop.kt:179`) -- animated image support (e.g., GIF in-line rendering) is not implemented +- **Animated Chat Images** (`CIImageView.desktop.kt:19`) -- animated image rendering in chat items +- **isImage detection** (`Images.desktop.kt:168`) -- image type detection (implemented but marked as incomplete) + +--- + +## GAP-07: Desktop: Cryptor Not Implemented + +**Severity:** Critical +**Category:** Security / Platform +**Platform:** Desktop only + +### Description + +The `CryptorInterface` implementation on Desktop is a non-functional placeholder. All three methods are stubbed: + +```kotlin +// common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt +actual val cryptor: CryptorInterface = object : CryptorInterface { + override fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? { + return String(data) // LALAL + } + + override fun encryptText(text: String, alias: String): Pair { + return text.toByteArray() to text.toByteArray() // LALAL + } + + override fun deleteKey(alias: String) { + // LALAL + } +} +``` + +- `decryptData` returns the data as-is (no decryption) +- `encryptText` returns the plaintext as both "encrypted data" and "IV" +- `deleteKey` is a no-op + +### Affected Locations + +- `common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt` +- `common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt` -- `CryptorInterface` +- `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` -- uses `cryptor` for passphrase encryption + +### Impact + +**This is a critical security gap.** On Desktop: +- The database passphrase is stored **in plaintext** in the preferences file. Anyone with read access to the user's home directory can extract the passphrase and decrypt the database. +- The self-destruct passphrase is similarly stored in plaintext. +- The app passphrase (for local authentication) provides no real protection. +- Key deletion is a no-op, so "deleting" a key has no effect. + +This directly undermines RULE-02 (Database Encryption at Rest) and RULE-04 (Self-Destruct Profile) on the Desktop platform. + +### Recommendation + +1. **Priority: Critical.** Implement proper key storage on Desktop using one of: + - **OS Keychain integration:** macOS Keychain, Windows Credential Manager, Linux Secret Service (via `libsecret`/GNOME Keyring/KWallet) + - **Java Cryptography Architecture (JCA)** with a PKCS#12 keystore file protected by a master password + - **Bouncy Castle** library for platform-independent key management +2. Until a real implementation exists, display a prominent warning to Desktop users that their database passphrase is not securely stored. +3. Consider requiring the user to enter their passphrase on each app launch (do not store it) as an interim measure. + +### Related + +- GAP-03 (Database Passphrase Not Enforced) is compounded by this gap on Desktop. +- The `testCrypto()` function referenced in `AppCommon.desktop.kt:39` is commented out with a `// LALAL` marker, suggesting crypto testing was planned but never completed. diff --git a/apps/multiplatform/product/glossary.md b/apps/multiplatform/product/glossary.md new file mode 100644 index 0000000000..10203d8a2a --- /dev/null +++ b/apps/multiplatform/product/glossary.md @@ -0,0 +1,561 @@ +# Domain Term Glossary -- SimpleX Chat (Android & Desktop, Kotlin Multiplatform) + +This glossary is self-contained and covers the Android and Desktop (Kotlin/Compose Multiplatform) codebase only. + +--- + +## Table of Contents + +1. [Protocols & Cryptography](#1-protocols--cryptography) +2. [Core Data Types](#2-core-data-types) +3. [Commands & Events](#3-commands--events) +4. [Connection & Identity](#4-connection--identity) +5. [Messaging Features](#5-messaging-features) +6. [Calling & Media](#6-calling--media) +7. [Notifications & Background](#7-notifications--background) +8. [Application Architecture](#8-application-architecture) +9. [Configuration & Preferences](#9-configuration--preferences) + +--- + +## 1. Protocols & Cryptography + +### SMP (SimpleX Messaging Protocol) +The core message-relay protocol. Clients send and receive messages through SMP relay servers without exposing sender/receiver identity correlation. The protocol uses unidirectional queues -- each contact pair maintains separate send and receive queues on potentially different servers. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` -- `SMPErrorType`, `SMPProxyMode`, `SMPProxyFallback`, `SMPWebPortServers` + +### XFTP (SimpleX File Transfer Protocol) +Protocol for transferring files through relay servers. Files are chunked, encrypted, and uploaded to XFTP relays. Recipients download chunks and reassemble locally. Supports inline transfer for small files. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` -- `CC.ApiUploadStandaloneFile`, `CC.ApiDownloadStandaloneFile`, `CC.ApiStandaloneFileInfo` + +### E2E Encryption (End-to-End Encryption) +All messages are encrypted end-to-end. The app never transmits plaintext to relay servers. Encryption keys are negotiated during connection establishment using X3DH-like key agreement and then maintained via the double-ratchet algorithm. + +### Double Ratchet +The core key-management algorithm. After initial key agreement, each message derives a new symmetric key, providing forward secrecy per message. Ratchet state can be re-synchronized via `APISyncContactRatchet` / `APISyncGroupMemberRatchet` commands. + +*See:* `SimpleXAPI.kt` -- `CC.APISyncContactRatchet(contactId, force)`, `CC.APISyncGroupMemberRatchet(groupId, groupMemberId, force)`, `CR.ContactRatchetSync`, `CR.GroupMemberRatchetSync` + +### PQ (Post-Quantum) +Post-quantum key exchange support. Connections track PQ state via `Connection.pqSupport`, `Connection.pqEncryption`, `Connection.pqSndEnabled`, and `Connection.pqRcvEnabled` fields. When both peers support PQ, the key exchange incorporates a post-quantum KEM to resist future quantum attacks. + +*See:* `ChatModel.kt` -- `Connection.pqSupport`, `Connection.pqEncryption`; `SimpleXAPI.kt` -- `SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED` (legacy, no longer used) + +### SMP Proxy / Private Routing +Messages can be sent through an intermediate SMP proxy relay to hide the sender's IP from the destination relay. Controlled by `SMPProxyMode` (Always, Unknown, Unprotected, Never) and `SMPProxyFallback` (Allow, AllowProtected, Prohibit). + +*See:* `SimpleXAPI.kt` -- `AppPreferences.networkSMPProxyMode`, `AppPreferences.networkSMPProxyFallback` + +### Transport Session Mode +Controls how TCP sessions to SMP relays are multiplexed. Options: `User` (one session per user profile), `Session` (single shared session), `Server` (one per server), `Entity` (one per queue/entity -- maximum metadata protection). + +*See:* `SimpleXAPI.kt` -- `AppPreferences.networkSessionMode`, `TransportSessionMode` + +--- + +## 2. Core Data Types + +### ChatItem +A single item in a conversation -- a sent or received message, call event, group event, connection event, feature change, or moderation action. Contains direction (`CIDirection`), metadata (`CIMeta`), content (`CIContent`), optional formatted text, mentions, quoted item, reactions, and file attachment. + +*See:* `ChatModel.kt:2720` -- `data class ChatItem` + +### ChatInfo +The top-level discriminated union representing a conversation. Variants: +- `ChatInfo.Direct` -- wraps a `Contact` +- `ChatInfo.Group` -- wraps a `GroupInfo` +- `ChatInfo.Local` -- wraps a `NoteFolder` (saved messages / notes to self) +- `ChatInfo.ContactRequest` -- wraps a `UserContactRequest` +- `ChatInfo.ContactConnection` -- wraps a `PendingContactConnection` +- `ChatInfo.InvalidJSON` -- fallback for unrecognized data + +*See:* `ChatModel.kt:1391` -- `sealed class ChatInfo` + +### CIContent (Chat Item Content) +The content payload of a `ChatItem`. Over 30 variants including: +- `SndMsgContent` / `RcvMsgContent` -- regular message with `MsgContent` +- `SndCall` / `RcvCall` -- call event with status and duration +- `RcvIntegrityError` -- message integrity violation +- `RcvDecryptionError` -- decryption failure with error type and count +- `RcvGroupInvitation` / `SndGroupInvitation` -- group invite +- `RcvGroupEventContent` / `SndGroupEventContent` -- group lifecycle events +- `RcvChatFeature` / `SndChatFeature` -- per-chat feature toggle notifications +- `SndModerated` / `RcvModerated` / `RcvBlocked` -- moderation events +- `RcvDirectEventContent` -- direct chat lifecycle events + +*See:* `ChatModel.kt:3554` -- `sealed class CIContent` + +### MsgContent +The wire-format message body. Variants: `MCText`, `MCLink`, `MCImage`, `MCVideo`, `MCVoice`, `MCFile`, `MCReport`, `MCUnknown`. Each carries text plus optional media/file metadata. + +*See:* `ChatModel.kt` -- `sealed class MsgContent` + +### User +The local user profile. Fields: `userId`, `userContactId`, `localDisplayName`, `profile` (LocalProfile), `fullPreferences` (FullChatPreferences), `activeUser`, `activeOrder`, `showNtfs`, `sendRcptsContacts`, `sendRcptsSmallGroups`, `viewPwdHash` (for hidden profiles), `uiThemes`, `remoteHostId` (Long?), `autoAcceptMemberContacts` (Boolean). + +*See:* `ChatModel.kt:1208` -- `data class User` + +### Contact +A remote contact. Fields: `contactId`, `localDisplayName`, `profile` (LocalProfile), `activeConn` (Connection?), `viaGroup`, `contactUsed`, `contactStatus`, `chatSettings`, `userPreferences`, `mergedPreferences`, `preparedContact`, `contactRequestId`, `contactGroupMemberId`, `chatTags`, `chatItemTTL`. + +*See:* `ChatModel.kt:1711` -- `data class Contact` + +### GroupInfo +Metadata for a group conversation. Fields: `groupId`, `localDisplayName`, `groupProfile` (GroupProfile), `businessChat` (BusinessChatInfo?), `fullGroupPreferences`, `membership` (GroupMember -- the local user's membership), `chatSettings`, `preparedGroup`, `membersRequireAttention`, `chatTags`, `chatItemTTL`. + +*See:* `ChatModel.kt:2004` -- `data class GroupInfo` + +### GroupMember +A member of a group. Fields: `groupMemberId`, `groupId`, `memberId`, `memberRole` (GroupMemberRole), `memberCategory` (GroupMemberCategory), `memberStatus` (GroupMemberStatus), `memberSettings` (GroupMemberSettings), `blockedByAdmin`, `invitedBy`, `localDisplayName`, `memberProfile`, `memberContactId`, `memberContactProfileId`, `activeConn` (Connection?), `supportChat` (GroupSupportChat?). + +*See:* `ChatModel.kt:2177` -- `data class GroupMember` + +### GroupMemberRole +Enumeration of group roles, ordered for comparison: `Observer` < `Author` < `Member` < `Moderator` < `Admin` < `Owner`. Selectable roles for assignment: Observer, Member, Moderator, Admin, Owner. + +*See:* `ChatModel.kt:2369` -- `enum class GroupMemberRole` + +### Connection +An active or pending cryptographic connection to a peer. Fields: `connId`, `agentConnId`, `peerChatVRange` (VersionRange), `connStatus` (ConnStatus), `connLevel`, `viaGroupLink`, `customUserProfileId`, `connectionCode` (SecurityCode?), `pqSupport`, `pqEncryption`, `pqSndEnabled`, `pqRcvEnabled`, `connectionStats`, `authErrCounter`, `quotaErrCounter`. + +*See:* `ChatModel.kt:1882` -- `data class Connection` + +### Chat +A composite type holding `chatInfo` (ChatInfo), `chatItems` (list of ChatItem), and `chatStats` (ChatStats -- unread count, min unread item ID, etc.). Represents a full conversation for the chat list. + +*See:* `ChatModel.kt` -- `data class Chat` + +### PendingContactConnection +Represents an in-progress connection that has not yet been established. Contains the connection link and state but no contact profile yet. + +*See:* `ChatModel.kt` -- referenced in `ChatInfo.ContactConnection` + +### CryptoFile +A file reference that optionally carries `CryptoFileArgs` (key + nonce) for local encryption. `CryptoFile.plain(path)` creates an unencrypted reference. + +*See:* `ChatModel.kt` -- `data class CryptoFile` + +--- + +## 3. Commands & Events + +The codebase uses short type names for the command/event protocol: `CC` (Chat Command), `CR` (Chat Response -- also carries asynchronous events), `API` (top-level response wrapper), and `ChatError` (error hierarchy). There is no separate "ChatEvent" class; asynchronous events from the core (new messages, connection changes, call signaling) are all `CR` subclasses received via the `recvMsg` loop. + +### CC (Chat Command) +The sealed class representing all commands the app can send to the Haskell core library. Over 140 command variants organized by domain: + +**User management:** `ShowActiveUser`, `CreateActiveUser`, `ListUsers`, `ApiSetActiveUser`, `ApiHideUser`, `ApiUnhideUser`, `ApiMuteUser`, `ApiUnmuteUser`, `ApiDeleteUser` + +**Chat lifecycle:** `StartChat`, `CheckChatRunning`, `ApiStopChat`, `ApiSetAppFilePaths`, `ApiSetEncryptLocalFiles` + +**Database:** `ApiExportArchive`, `ApiImportArchive`, `ApiDeleteStorage`, `ApiStorageEncryption`, `TestStorageEncryption` + +**Messaging:** `ApiSendMessages`, `ApiUpdateChatItem`, `ApiDeleteChatItem`, `ApiDeleteMemberChatItem`, `ApiChatItemReaction`, `ApiForwardChatItems`, `ApiPlanForwardChatItems`, `ApiReportMessage` + +**Groups:** `ApiNewGroup`, `ApiAddMember`, `ApiJoinGroup`, `ApiAcceptMember`, `ApiMembersRole`, `ApiBlockMembersForAll`, `ApiRemoveMembers`, `ApiLeaveGroup`, `ApiListMembers`, `ApiUpdateGroupProfile`, `APICreateGroupLink`, `APIDeleteGroupLink`, `APIGetGroupLink`, `ApiAddGroupShortLink` + +**Connections:** `APIAddContact`, `APIConnect`, `APIConnectPlan`, `APIPrepareContact`, `APIPrepareGroup`, `APIConnectPreparedContact`, `APIConnectPreparedGroup`, `ApiConnectContactViaAddress` + +**Contacts:** `ApiDeleteChat`, `ApiClearChat`, `ApiListContacts`, `ApiUpdateProfile`, `ApiSetContactPrefs`, `ApiSetContactAlias` + +**Address:** `ApiCreateMyAddress`, `ApiDeleteMyAddress`, `ApiShowMyAddress`, `ApiAddMyAddressShortLink`, `ApiSetProfileAddress`, `ApiSetAddressSettings` + +**Calls:** `ApiGetCallInvitations`, `ApiSendCallInvitation`, `ApiRejectCall`, `ApiSendCallOffer`, `ApiSendCallAnswer`, `ApiSendCallExtraInfo`, `ApiEndCall`, `ApiCallStatus` + +**Server config:** `ApiGetServerOperators`, `ApiSetServerOperators`, `ApiGetUserServers`, `ApiSetUserServers`, `ApiValidateServers`, `APITestProtoServer` + +**Network:** `APISetNetworkConfig`, `APIGetNetworkConfig`, `APISetNetworkInfo`, `ReconnectServer`, `ReconnectAllServers` + +**Files:** `ReceiveFile`, `CancelFile`, `ApiUploadStandaloneFile`, `ApiDownloadStandaloneFile`, `ApiStandaloneFileInfo` + +**Remote access:** `SetLocalDeviceName`, `ListRemoteHosts`, `StartRemoteHost`, `SwitchRemoteHost`, `StopRemoteHost`, `DeleteRemoteHost`, `StoreRemoteFile`, `GetRemoteFile`, `ConnectRemoteCtrl`, `FindKnownRemoteCtrl`, `ConfirmRemoteCtrl`, `VerifyRemoteCtrlSession`, `ListRemoteCtrls`, `StopRemoteCtrl`, `DeleteRemoteCtrl` + +**Read status:** `ApiChatRead`, `ApiChatItemsRead`, `ApiChatUnread` + +**Settings:** `APISetChatSettings`, `ApiSetMemberSettings`, `APISetChatItemTTL`, `APIGetChatItemTTL`, `APISetChatTTL`, `ApiSaveSettings`, `ApiGetSettings` + +**Ratchet & verification:** `APISwitchContact`, `APISwitchGroupMember`, `APIAbortSwitchContact`, `APIAbortSwitchGroupMember`, `APISyncContactRatchet`, `APISyncGroupMemberRatchet`, `APIGetContactCode`, `APIGetGroupMemberCode`, `APIVerifyContact`, `APIVerifyGroupMember` + +Each command variant has a `cmdString` property that serializes it to the text protocol consumed by the Haskell FFI. + +*See:* `SimpleXAPI.kt:3529` -- `sealed class CC` + +### CR (Chat Response) +The sealed class representing all responses / events received from the Haskell core. Over 130 response types. Examples: + +- `ActiveUser`, `UsersList` -- user management results +- `ChatStarted`, `ChatRunning`, `ChatStopped` -- lifecycle +- `ApiChats`, `ApiChat` -- chat list data +- `NewChatItems`, `ChatItemUpdated`, `ChatItemsDeleted` -- message events +- `ContactConnected`, `ContactConnecting`, `ContactSndReady` -- connection lifecycle +- `GroupCreated`, `ReceivedGroupInvitation`, `JoinedGroupMemberConnecting`, `MemberAccepted` -- group events +- `RcvFileStart`, `RcvFileComplete`, `SndFileComplete` -- file transfer progress +- `CallInvitation`, `CallOffer`, `CallAnswer`, `CallExtraInfo`, `CallEnded` -- call signaling +- `ChatError` -- error wrapper + +*See:* `SimpleXAPI.kt:6114` -- `sealed class CR` + +### API +The top-level response wrapper. Two variants: +- `API.Result(remoteHostId, res: CR)` -- successful response +- `API.Error(remoteHostId, err: ChatError)` -- error response + +Properties: `ok` (Boolean -- true if `CR.CmdOk`), `result` (CR?), `rhId` (Long? -- remote host ID). + +*See:* `SimpleXAPI.kt:5975` -- `sealed class API` + +### ChatError +The error hierarchy returned from the Haskell core: +- `ChatErrorChat(errorType: ChatErrorType)` -- application-level errors (NoActiveUser, UserUnknown, DifferentActiveUser, etc.) +- `ChatErrorAgent(agentError: AgentErrorType)` -- SMP agent errors (BROKER, SMP, PROXY, etc.) +- `ChatErrorStore(storeError: StoreError)` -- database/store errors +- `ChatErrorDatabase(databaseError: DatabaseError)` -- database migration/encryption errors +- `ChatErrorRemoteHost(remoteHostError)` -- remote host control errors +- `ChatErrorRemoteCtrl(remoteCtrlError)` -- remote controller errors +- `ChatErrorInvalidJSON(json)` -- parse failure + +*See:* `SimpleXAPI.kt:6974` -- `sealed class ChatError` + +### sendCmd / recvMsg +The core FFI bridge. `sendCmd(rhId, cmd)` serializes a `CC` command and sends it to the Haskell backend via `chatSendCmd`. `recvMsg(ctrl)` blocks on `chatRecvMsg` to receive the next `API` response/event. The receiver loop runs in `ChatController.startReceiver()` on `Dispatchers.IO`. + +*See:* `SimpleXAPI.kt` -- `ChatController.sendCmd()`, `ChatController.startReceiver()` + +--- + +## 4. Connection & Identity + +### SimpleX Address (User Address) +A long-lived contact address that others can use to send connection requests. Created via `ApiCreateMyAddress`, retrieved via `ApiShowMyAddress`, deleted via `ApiDeleteMyAddress`. Can optionally include a short link (`ApiAddMyAddressShortLink`). Stored as `ChatModel.userAddress` (`UserContactLinkRec`). + +### Contact Link / Connection Link +A one-time or reusable invitation link. The `CreatedConnLink` type wraps the link string. Contact links can be one-time (single use) or long-lived (user address). Created via `APIAddContact` (one-time) or `ApiCreateMyAddress` (reusable). + +### Group Link +A reusable invitation link for joining a group. Created via `APICreateGroupLink(groupId, memberRole)`. The default role for new members joining via the link is configurable. Can also have a short link variant via `ApiAddGroupShortLink`. + +### Short Link +A compact form of a contact or group link. Created via `ApiAddMyAddressShortLink` (for user addresses) or `ApiAddGroupShortLink` (for groups). Short links resolve to the full connection link data including `ContactShortLinkData` or `GroupShortLinkData`. + +### Incognito Mode +When enabled (`AppPreferences.incognito`), the app generates a random profile name for new connections instead of using the user's real profile. Each connection gets a unique random identity. The `customUserProfileId` on a `Connection` tracks which incognito profile is used for that connection. + +*See:* `SimpleXAPI.kt` -- `AppPreferences.incognito`; `ChatModel.kt` -- `Connection.customUserProfileId` + +### Hidden Profile +A user profile protected by a password (`viewPwdHash`). Hidden profiles do not appear in the profile list unless unlocked with the password. Created via `ApiHideUser(userId, viewPwd)`, revealed via `ApiUnhideUser(userId, viewPwd)`. When switching away from a hidden profile, its notifications are cancelled. + +*See:* `SimpleXAPI.kt` -- `CC.ApiHideUser`, `CC.ApiUnhideUser`; `ChatModel.kt` -- `User.viewPwdHash` + +### Connection Verification (Security Code) +Each connection has an optional `SecurityCode` (`Connection.connectionCode`). Users can verify connections out-of-band by comparing security codes displayed via `APIGetContactCode` / `APIGetGroupMemberCode` and confirming via `APIVerifyContact` / `APIVerifyGroupMember`. + +### Connection Plan +Before connecting via a link, `APIConnectPlan` analyzes the link and returns a `ConnectionPlan` indicating whether the link leads to an existing contact, a new contact, a group join, etc. This prevents duplicate connections. + +*See:* `SimpleXAPI.kt` -- `CC.APIConnectPlan`, `CR.CRConnectionPlan` + +### Prepared Contact / Prepared Group +An intermediate state in the connection flow. `APIPrepareContact` / `APIPrepareGroup` creates the local record and displays the contact/group preview before the user confirms the connection. The user can then change the active profile (`APIChangePreparedContactUser` / `APIChangePreparedGroupUser`) and finally confirm via `APIConnectPreparedContact` / `APIConnectPreparedGroup`. + +--- + +## 5. Messaging Features + +### Delivery Receipt +Confirmation that a message was delivered to the recipient's device. Controlled per-user via `sendRcptsContacts` and `sendRcptsSmallGroups` on `User`. The global setting flow is triggered by `ChatModel.setDeliveryReceipts`. Individual overrides per-contact are managed via `ApiSetUserContactReceipts` / `ApiSetUserGroupReceipts`. + +*See:* `SimpleXAPI.kt` -- `CC.SetAllContactReceipts`, `CC.ApiSetUserContactReceipts`, `CC.ApiSetUserGroupReceipts`; `AppPreferences.privacyDeliveryReceiptsSet` + +### Timed Message (Disappearing Message) +Messages with a time-to-live after which they are automatically deleted. Configured as a `ChatFeature` / `GroupFeature` with a TTL parameter in seconds. The `customDisappearingMessageTime` preference stores the last custom duration used. Per-chat TTL can be set via `APISetChatTTL`. Global TTL via `APISetChatItemTTL`. + +*See:* `SimpleXAPI.kt` -- `CC.APISetChatItemTTL`, `CC.APISetChatTTL`; `AppPreferences.customDisappearingMessageTime` + +### Live Message +A message that updates in real-time as the sender types. Controlled by `CC.ApiSendMessages` with `live=true`. The `ComposeState.liveMessage` tracks the current live message being composed. An alert is shown on first use (`AppPreferences.liveMessageAlertShown`). + +### Message Reactions +Emoji reactions on messages. Added/removed via `ApiChatItemReaction(type, id, scope, itemId, add, reaction)`. Reaction members in groups can be queried via `ApiGetReactionMembers`. Each `ChatItem` carries a `reactions: List`. + +### Message Forwarding +Messages can be forwarded between chats. `ApiPlanForwardChatItems` checks feasibility (e.g., file availability), and `ApiForwardChatItems` performs the forward. A `ForwardConfirmation` may be required if files need downloading first. + +### Message Reports +Users can report messages in groups via `ApiReportMessage(groupId, chatItemId, reportReason, reportText)`. Admins can archive (`ApiArchiveReceivedReports`) or delete (`ApiDeleteReceivedReports`) reports. + +### Mentions +In-message mentions of group members. Stored as `mentions: Map` on `ChatItem` and `mentions: MentionedMembers` on `ComposeState`. + +### Link Previews +Automatic preview generation for URLs in messages. Controlled by `AppPreferences.privacyLinkPreviews`. An alert is shown on first use (`privacyLinkPreviewsShowAlert`). + +### Local File Encryption +Files stored on device can be encrypted. Controlled by `AppPreferences.privacyEncryptLocalFiles` and toggled via `CC.ApiSetEncryptLocalFiles(enable)`. + +### Chat Tags +User-defined tags for organizing conversations. CRUD via `ApiCreateChatTag`, `ApiUpdateChatTag`, `ApiDeleteChatTag`, `ApiReorderChatTags`. Assignment via `ApiSetChatTags`. The model tracks `userTags`, `presetTags` (system-defined categories), `unreadTags`, and the active filter (`activeChatTagFilter`). + +--- + +## 6. Calling & Media + +### WebRTC +The real-time communication framework used for audio and video calls. The app uses WebRTC for peer-to-peer media streams, with SMP used only for call signaling (offer/answer/ICE candidates). + +### Call (data class) +Represents an active call session. Fields: `remoteHostId`, `userProfile`, `contact`, `callUUID`, `callState` (CallState enum), `initialCallType` (Audio/Video), `localMediaSources`, `localCapabilities`, `peerMediaSources`, `sharedKey` (for E2E call encryption), `connectionInfo`, `connectedAt`. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt:14` + +### CallState +Enum tracking call progression: `WaitCapabilities` -> `InvitationSent` / `InvitationAccepted` -> `OfferSent` / `OfferReceived` -> `Negotiated` -> `Connected` -> `Ended`. + +### WCallCommand / WCallResponse +The command/response protocol between the Kotlin app and the WebRTC JavaScript layer: +- **Commands:** `Capabilities`, `Permission`, `Start`, `Offer`, `Answer`, `Ice`, `Media`, `Camera`, `Description`, `Layout`, `End` +- **Responses:** `Capabilities`, `Offer`, `Answer`, `Ice`, `Connection`, `Connected`, `PeerMedia`, `End`, `Ended`, `Ok`, `Error` + +*See:* `WebRTC.kt:88` -- `sealed class WCallCommand`; `WebRTC.kt:103` -- `sealed class WCallResponse` + +### CallManager +Manages incoming call invitations and the active call lifecycle. Handles reporting new incoming calls, accepting calls, switching between calls, and ending calls. Interacts with `ChatModel.callInvitations`, `ChatModel.activeCall`, and the platform notification manager. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt` + +### Android: CallActivity +A dedicated Android `Activity` that displays the call UI. Launched when accepting an incoming call or initiating an outgoing call. Uses an Android `WebView` to host the WebRTC JavaScript. + +*See:* `android/src/main/java/chat/simplex/app/views/call/CallActivity.kt` + +### Android: CallService +An Android foreground `Service` that keeps the call alive when the app is in the background. Holds a `WakeLock`, displays an ongoing call notification, and manages the call lifecycle. Uses notification channel `CALL_SERVICE_NOTIFICATION`. + +*See:* `android/src/main/java/chat/simplex/app/CallService.kt` + +### Desktop: Browser-based WebRTC via NanoWSD +On Desktop, calls are implemented by opening the system browser to a locally-hosted WebSocket server. A `NanoHTTPD`/`NanoWSD` server runs on `localhost:50395`, serving the WebRTC call page and communicating with the Kotlin app via WebSocket messages. Commands are sent as JSON-serialized `WVAPICall` objects; responses are parsed as `WVAPIMessage` objects. + +*See:* `common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt` + +### ICE Servers +STUN/TURN servers used for WebRTC NAT traversal. Configurable via `AppPreferences.webrtcIceServers`. The relay policy (`AppPreferences.webrtcPolicyRelay`) controls whether calls must use TURN relays (for IP privacy) or can attempt direct connections. + +### CallMediaType +Enum: `Video`, `Audio`. Determines the initial media type of the call. + +### CallMediaSource +Enum: `Mic`, `Camera`, `ScreenAudio`, `ScreenVideo`. Used in `WCallCommand.Media` to toggle individual media streams. + +--- + +## 7. Notifications & Background + +### Android: SimplexService +A foreground `Service` that keeps the chat backend running in the background. Uses a `WakeLock` and displays a persistent notification ("SimpleX Chat service" channel). Started with `START_STICKY` for automatic restart. Manages the `chatRecvMsg` loop indirectly by keeping the process alive. + +Notification channel: `chat.simplex.app.SIMPLEX_SERVICE_NOTIFICATION` ("SimpleX Chat service") + +*See:* `android/src/main/java/chat/simplex/app/SimplexService.kt` + +### Android: MessagesFetcherWorker +A `WorkManager` periodic worker that wakes the app to fetch new messages when the foreground service is not running (i.e., when `NotificationsMode` is `PERIODIC`). Provides a battery-friendly alternative to the always-on service. + +*See:* `android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt` + +### Android: NotificationsMode +Enum controlling background message fetching: +- `OFF` -- no background activity; messages received only when app is open +- `PERIODIC` -- uses `MessagesFetcherWorker` for periodic fetches +- `SERVICE` -- uses `SimplexService` foreground service (default) + +*See:* `SimpleXAPI.kt:7739` -- `enum class NotificationsMode` + +### Android: Notification Channels +Android notification channels registered by the app: +- **Messages:** `chat.simplex.app.MESSAGE_NOTIFICATION` -- high importance, for incoming messages +- **Calls:** `chat.simplex.app.CALL_NOTIFICATION_2` -- high importance, for incoming call alerts with custom sound +- **Service:** `chat.simplex.app.SIMPLEX_SERVICE_NOTIFICATION` -- low importance, persistent foreground service indicator +- **Call Service:** `chat.simplex.app.CALL_SERVICE_NOTIFICATION` -- default importance, ongoing call indicator + +*See:* `android/src/main/java/chat/simplex/app/model/NtfManager.android.kt`, `SimplexService.kt`, `CallService.kt` + +### Android: NtfManager +The Android-specific notification manager. Handles creating notification channels, displaying message notifications (with grouping via `MessageGroup`), displaying incoming call notifications (with full-screen intent for lock-screen calls), and managing notification actions (accept/reject call, open chat). + +*See:* `android/src/main/java/chat/simplex/app/model/NtfManager.android.kt` + +### Desktop: System Notifications +On Desktop, notifications use the system notification mechanism (typically via the JVM's `SystemTray` or platform-specific notification APIs). The notification manager interface is shared (`ntfManager`) but the implementation is platform-specific. + +### NotificationPreviewMode +Controls what information appears in notifications: +- `HIDDEN` -- no message content +- `CONTACT` -- shows sender name only +- `MESSAGE` -- shows sender name and message preview (default) + +*See:* `ChatModel.kt:4823` -- `enum class NotificationPreviewMode` + +### Wake Lock Management +In `ChatController.startReceiver()`, each received message acquires a wake lock (via `getWakeLock(timeout=60000)`) that is released after 30 seconds. This ensures the device stays awake long enough to process incoming messages and display notifications, particularly for incoming calls. + +--- + +## 8. Application Architecture + +### ChatController +The singleton controller that bridges the Kotlin UI layer and the Haskell core library. Responsibilities: +- Manages the `chatCtrl` (FFI handle to the Haskell runtime) +- Sends commands via `sendCmd()` and receives events via the `startReceiver()` coroutine loop +- Processes received messages in `processReceivedMsg()` +- Holds a reference to `AppPreferences` and `ChatModel` +- Provides the `messagesChannel` (Kotlin coroutine `Channel`) for consumers to observe events +- Manages retry logic for transient network errors (`sendCmdWithRetry`) + +*See:* `SimpleXAPI.kt:493` -- `object ChatController` + +### ChatModel +The singleton reactive state container for the entire app. Uses Compose `mutableStateOf` and `mutableStateListOf` for reactive UI updates. Key state: +- `currentUser` -- the active user profile +- `users` -- list of all user profiles (`UserInfo`) +- `chatsContext` / `secondaryChatsContext` -- `ChatsContext` holding the chat list +- `chatId` -- currently open chat +- `groupMembers` -- members of the currently viewed group +- `callInvitations` -- pending incoming call invitations +- `activeCall` -- the currently active call +- `userAddress` -- the user's SimpleX address +- `chatItemTTL` -- global message TTL setting +- `userTags` -- chat tags +- `terminalItems` -- debug terminal log items +- Various UI state flags (`showCallView`, `switchingUsersAndHosts`, `clearOverlays`, etc.) + +*See:* `ChatModel.kt:86` -- `object ChatModel` + +### AppPreferences +A class wrapping platform-specific key-value storage (`Settings` from `com.russhwolf.settings`). On Android, backed by `SharedPreferences`. On Desktop, backed by Java `Properties` files. Provides type-safe accessors for all user preferences. + +*See:* `SimpleXAPI.kt:94` -- `class AppPreferences` + +### ComposeState +Data class holding the state of the message composition area. Fields: `message` (ComposeMessage), `parsedMessage` (formatted text), `liveMessage`, `preview` (ComposePreview), `contextItem` (ComposeContextItem -- reply/edit context), `inProgress`, `progressByTimeout`, `useLinkPreviews`, `mentions`. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt:98` + +### ModalManager +Manages the modal/sheet presentation stack. Supports multiple placements (default, center, fullscreen, end). Holds an ordered list of `ModalViewHolder` items and exposes `showModal`, `showCustomModal`, `showModalCloseable`, `closeModal`. Uses Compose state (`modalCount`) to trigger recomposition. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt:92` + +### AlertManager +Singleton for displaying alert dialogs. Provides `showAlertMsg`, `showAlertDialog`, `showAlertDialogButtons`, etc. Works with `AlertManager.shared` for the default instance. + +### ChatsContext +Holds the chat list state for a particular scope (main or secondary). Manages `chats` (State>), provides `updateChats()` to refresh, and supports filtering/keeping specific chats during updates. + +### ConnectProgressManager +Tracks and displays connection progress in the UI. Methods: `startConnectProgress(text, onCancel)`, `stopConnectProgress()`, `cancelConnectProgress()`. Exposes `showConnectProgress` (nullable string indicating active progress text). + +*See:* `ChatModel.kt:48` -- `object ConnectProgressManager` + +### withBGApi / withLongRunningApi +Utility functions for launching coroutines on background threads. Used throughout the codebase to perform API calls without blocking the UI thread. + +--- + +## 9. Configuration & Preferences + +### AppPreferences (Storage) +All preferences are accessed through `ChatController.appPrefs`, which is a lazy-initialized `AppPreferences` instance. The underlying storage is: +- **Android:** `SharedPreferences` with ID `chat.simplex.app.SIMPLEX_APP_PREFS` +- **Desktop:** Java `Properties` files via `com.russhwolf.settings` + +Theme overrides have separate storage (`SHARED_PREFS_THEMES_ID`). + +### SharedPreference +A generic wrapper providing `get()` and `set(value)` for a single preference. All `AppPreferences` fields are `SharedPreference` instances created by factory methods (`mkBoolPreference`, `mkStrPreference`, `mkIntPreference`, `mkLongPreference`, `mkFloatPreference`, `mkEnumPreference`, `mkSafeEnumPreference`, `mkDatePreference`, `mkMapPreference`, `mkTimeoutPreference`). + +### Key Preference Categories + +**Notifications:** +- `notificationsMode` -- OFF / PERIODIC / SERVICE +- `notificationPreviewMode` -- HIDDEN / CONTACT / MESSAGE +- `canAskToEnableNotifications` -- gate for the notification prompt + +**Privacy:** +- `privacyProtectScreen` -- prevents screenshots (Android FLAG_SECURE) +- `privacyAcceptImages` -- auto-accept inline images +- `privacyLinkPreviews` -- generate URL previews +- `privacySanitizeLinks` -- strip tracking parameters from URLs +- `privacyShowChatPreviews` -- show message preview in chat list +- `privacySaveLastDraft` -- persist draft messages +- `privacyEncryptLocalFiles` -- encrypt files at rest +- `privacyAskToApproveRelays` -- prompt before using relays suggested by contacts +- `privacyMediaBlurRadius` -- blur radius for media in notifications/previews + +**Security:** +- `performLA` -- require local authentication (biometric/PIN) +- `laMode` -- local authentication mode +- `laLockDelay` -- seconds before re-locking +- `storeDBPassphrase` -- whether to persist the DB passphrase +- `initialRandomDBPassphrase` -- indicates the DB uses a random (non-user-chosen) passphrase +- `selfDestruct` -- enable self-destruct profile +- `selfDestructDisplayName` -- display name for the self-destruct profile + +**Network:** +- `networkUseSocksProxy` -- route traffic through SOCKS proxy +- `networkProxy` -- SOCKS proxy host/port configuration +- `networkSessionMode` -- transport session multiplexing mode +- `networkSMPProxyMode` -- SMP proxy / private routing mode +- `networkSMPProxyFallback` -- fallback behavior when proxy fails +- `networkHostMode` -- onion/public host preference +- `networkRequiredHostMode` -- enforce host mode strictly +- Various TCP timeout settings (background, interactive, per-KB) +- Keep-alive settings (idle, interval, count) + +**Calls:** +- `webrtcPolicyRelay` -- force TURN relay usage +- `callOnLockScreen` -- DISABLE / SHOW / ACCEPT calls on lock screen +- `webrtcIceServers` -- custom ICE server configuration +- `experimentalCalls` -- enable experimental call features + +**Appearance:** +- `currentTheme` -- active theme name +- `systemDarkTheme` -- theme for system dark mode +- `themeOverrides` -- per-theme customizations +- `profileImageCornerRadius` -- avatar rounding +- `chatItemRoundness` -- message bubble rounding +- `chatItemTail` -- show/hide message bubble tail +- `fontScale` -- text size scaling +- `densityScale` -- UI density scaling +- `inAppBarsAlpha` -- toolbar transparency +- `appearanceBarsBlurRadius` -- toolbar blur effect + +**UI:** +- `oneHandUI` -- one-handed UI mode (bottom-aligned navigation) +- `chatBottomBar` -- show bottom bar in chat view +- `simplexLinkMode` -- how SimpleX links are displayed (DESCRIPTION / FULL / BROWSER) +- `showUnreadAndFavorites` -- filter chat list to unread/favorites +- `developerTools` -- enable developer tools (terminal, etc.) + +**Database:** +- `encryptedDBPassphrase` -- encrypted form of the DB passphrase +- `initializationVectorDBPassphrase` -- IV for DB passphrase encryption +- `encryptionStartedAt` -- timestamp of encryption operation start (for crash recovery) +- `confirmDBUpgrades` -- prompt before database migrations +- `newDatabaseInitialized` -- flag for incomplete initialization recovery + +**Remote Access:** +- `deviceNameForRemoteAccess` -- device display name for remote control +- `confirmRemoteSessions` -- require confirmation for remote sessions +- `connectRemoteViaMulticast` -- use multicast discovery +- `connectRemoteViaMulticastAuto` -- auto-connect via multicast +- `desktopWindowState` -- persisted window position/size (Desktop only) + +**Migration:** +- `migrationToStage` / `migrationFromStage` -- track migration progress +- `onboardingStage` -- current onboarding step +- `lastMigratedVersionCode` -- last app version that ran migrations + +*See:* `SimpleXAPI.kt:94-489` -- `class AppPreferences` with all `SHARED_PREFS_*` constants diff --git a/apps/multiplatform/product/rules.md b/apps/multiplatform/product/rules.md new file mode 100644 index 0000000000..90a2dadada --- /dev/null +++ b/apps/multiplatform/product/rules.md @@ -0,0 +1,253 @@ +# Business Rules -- SimpleX Chat (Android & Desktop, Kotlin Multiplatform) + +This document specifies invariants enforced by the Android and Desktop (Kotlin/Compose Multiplatform) clients. + +--- + +## Table of Contents + +1. [Security (RULE-01 through RULE-05)](#1-security) +2. [Message Integrity (RULE-06 through RULE-09)](#2-message-integrity) +3. [Group Integrity (RULE-10 through RULE-13)](#3-group-integrity) +4. [File Transfer (RULE-14 through RULE-15)](#4-file-transfer) +5. [Notification Delivery (RULE-16 through RULE-17)](#5-notification-delivery) +6. [Call Integrity (RULE-18)](#6-call-integrity) + +--- + +## 1. Security + +### RULE-01: End-to-End Encryption is Mandatory + +**Invariant:** Every message, file chunk, and call signaling payload MUST be encrypted end-to-end before transmission. The app MUST NOT transmit plaintext content to any relay server. + +**Enforcement:** The Haskell core library handles all encryption. The Kotlin layer never constructs raw SMP messages. All communication flows through `ChatController.sendCmd()` which delegates to the FFI, ensuring the encryption layer cannot be bypassed. + +**Location:** `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` -- `ChatController.sendCmd()`, `chatSendCmd()` FFI call + +--- + +### RULE-02: Database Encryption at Rest + +**Invariant:** The local SQLite database MUST be encrypted. A passphrase (either user-chosen or randomly generated) MUST be set before the database is operational. + +**Enforcement:** On first launch, a random passphrase is generated and stored encrypted via the platform keystore (`CryptorInterface.encryptText`). The `initialRandomDBPassphrase` preference tracks whether the user has set a custom passphrase. Database encryption state is tracked in `ChatModel.chatDbEncrypted`. Encryption/re-encryption is performed via `CC.ApiStorageEncryption(config: DBEncryptionConfig)`. + +**Caveat:** The user is not forced to set a custom passphrase -- the random passphrase is stored in app-accessible encrypted preferences. See GAP: "Database passphrase not enforced." + +**Location:** +- `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` +- `common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt` -- `CryptorInterface` +- Android: `common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt` -- Android Keystore +- Desktop: `common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt` -- **placeholder, not implemented** + +--- + +### RULE-03: Local Authentication Gating + +**Invariant:** When local authentication is enabled (`AppPreferences.performLA == true`), the app MUST require biometric/PIN authentication before displaying any chat content. The lock engages after `laLockDelay` seconds of inactivity. + +**Enforcement:** `AppLock.setPerformLA` controls the lock state. The lock delay is configurable via `AppPreferences.laLockDelay` (default 30 seconds). Authentication mode is set via `AppPreferences.laMode` (system biometric or passcode). + +**Location:** +- `common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt` +- `SimpleXAPI.kt` -- `AppPreferences.performLA`, `AppPreferences.laMode`, `AppPreferences.laLockDelay` + +--- + +### RULE-04: Self-Destruct Profile + +**Invariant:** When self-destruct is enabled (`AppPreferences.selfDestruct == true`), entering the self-destruct passphrase instead of the real passphrase MUST wipe the database and present a clean profile with `selfDestructDisplayName`. + +**Enforcement:** The self-destruct passphrase is stored separately (`encryptedSelfDestructPassphrase` / `initializationVectorSelfDestructPassphrase`). On Android, `SimplexService` checks for self-destruct on initialization. The comparison happens during the local authentication flow. + +**Location:** +- `SimpleXAPI.kt` -- `AppPreferences.selfDestruct`, `AppPreferences.selfDestructDisplayName` +- `android/src/main/java/chat/simplex/app/SimplexService.kt` -- initialization check + +--- + +### RULE-05: Screen Protection + +**Invariant:** When `AppPreferences.privacyProtectScreen == true` (default), the app MUST prevent screenshots and screen recording. On Android this uses `FLAG_SECURE`; on Desktop this is advisory only. + +**Enforcement:** The preference defaults to `true`. The Android activity applies `FLAG_SECURE` to its window based on this preference. The Desktop app cannot enforce this at the OS level. + +**Location:** `SimpleXAPI.kt` -- `AppPreferences.privacyProtectScreen` + +--- + +## 2. Message Integrity + +### RULE-06: Message Ordering Verification + +**Invariant:** The app MUST detect and surface message integrity violations (gaps, duplicates, out-of-order delivery) to the user. + +**Enforcement:** The Haskell core tracks message sequence numbers per connection. When a gap or integrity error is detected, a `CIContent.RcvIntegrityError(msgError: MsgErrorType)` chat item is inserted into the conversation. The UI renders these as system messages indicating the integrity issue. + +**Location:** `ChatModel.kt:3565` -- `CIContent.RcvIntegrityError` + +--- + +### RULE-07: Decryption Error Surfacing + +**Invariant:** When a message cannot be decrypted, the app MUST display a `RcvDecryptionError` item showing the error type and count of affected messages. The app MUST NOT silently drop undecryptable messages. + +**Enforcement:** The Haskell core emits `CIContent.RcvDecryptionError(msgDecryptError, msgCount)` which the UI renders with an explanation and count. Ratchet re-synchronization can be triggered via `APISyncContactRatchet` / `APISyncGroupMemberRatchet`. + +**Location:** `ChatModel.kt:3566` -- `CIContent.RcvDecryptionError` + +--- + +### RULE-08: Delivery Receipt Consistency + +**Invariant:** Delivery receipt settings MUST be consistent: when a user enables/disables receipts globally, the change MUST propagate to all contacts/groups (optionally clearing per-chat overrides via `clearOverrides`). + +**Enforcement:** Global receipt toggle triggers `CC.SetAllContactReceipts(enable)`. Per-type settings use `CC.ApiSetUserContactReceipts` / `CC.ApiSetUserGroupReceipts` with `UserMsgReceiptSettings(enable, clearOverrides)`. The `privacyDeliveryReceiptsSet` preference gates the initial setup prompt shown during onboarding. + +**Location:** +- `SimpleXAPI.kt` -- `CC.SetAllContactReceipts`, `CC.ApiSetUserContactReceipts`, `CC.ApiSetUserGroupReceipts` +- `SimpleXAPI.kt` -- `ChatController.startChat()` -- triggers `setDeliveryReceipts` prompt + +--- + +### RULE-09: Chat Item TTL Enforcement + +**Invariant:** When a chat item TTL (time-to-live) is set globally or per-chat, expired messages MUST be deleted by the core. The app MUST NOT display expired items. + +**Enforcement:** Global TTL set via `CC.APISetChatItemTTL(userId, seconds)`. Per-chat TTL set via `CC.APISetChatTTL(userId, chatType, id, seconds)`. The Haskell core performs periodic cleanup. The current global TTL is stored in `ChatModel.chatItemTTL`. + +**Location:** `SimpleXAPI.kt` -- `CC.APISetChatItemTTL`, `CC.APISetChatTTL` + +--- + +## 3. Group Integrity + +### RULE-10: Role-Based Access Control + +**Invariant:** Group operations MUST respect the member's role. Only members with sufficient role level can perform privileged operations: +- **Owner:** can delete group, change any member's role, transfer ownership +- **Admin:** can add/remove members, change roles (up to Admin), create/delete group links +- **Moderator:** can delete other members' messages, block members +- **Member / Author / Observer:** cannot perform administrative actions + +**Enforcement:** The Haskell core validates role permissions server-side. The Kotlin UI layer uses `GroupMemberRole` comparisons (the enum is ordered: Observer < Author < Member < Moderator < Admin < Owner) to show/hide action buttons. + +**Location:** `ChatModel.kt:2369` -- `enum class GroupMemberRole`; various group management views + +--- + +### RULE-11: Group Member Removal Atomicity + +**Invariant:** When removing members from a group, the removal command MUST specify all member IDs atomically. Partial removal MUST NOT leave the group in an inconsistent state. + +**Enforcement:** `CC.ApiRemoveMembers(groupId, memberIds: List, withMessages: Boolean)` sends all member IDs in a single command. The `withMessages` flag controls whether the removed members' messages are also deleted. + +**Location:** `SimpleXAPI.kt` -- `CC.ApiRemoveMembers` + +--- + +### RULE-12: Group Link Role Default + +**Invariant:** When creating a group link, the default member role for joiners MUST be explicitly specified. The role can be updated after creation without regenerating the link. + +**Enforcement:** `CC.APICreateGroupLink(groupId, memberRole)` requires a role. `CC.APIGroupLinkMemberRole(groupId, memberRole)` updates it. The link itself remains stable. + +**Location:** `SimpleXAPI.kt` -- `CC.APICreateGroupLink`, `CC.APIGroupLinkMemberRole` + +--- + +### RULE-13: Member Blocking Scope + +**Invariant:** Blocking a member (`ApiBlockMembersForAll`) MUST apply the block for all group members (not just the requester). The `blocked` flag is visible to all members. Only roles >= Moderator can block. + +**Enforcement:** `CC.ApiBlockMembersForAll(groupId, memberIds, blocked)` sends the block/unblock to the core, which propagates it to all group members. + +**Location:** `SimpleXAPI.kt` -- `CC.ApiBlockMembersForAll`; `ChatModel.kt` -- `GroupMember.blockedByAdmin` + +--- + +## 4. File Transfer + +### RULE-14: File Encryption in Transit and at Rest + +**Invariant:** Files sent via XFTP MUST be encrypted before upload. Files received MUST be decrypted only after download. When `privacyEncryptLocalFiles` is enabled (default `true`), files stored locally MUST be encrypted with per-file keys (`CryptoFile.cryptoArgs`). + +**Enforcement:** The Haskell core handles XFTP encryption. Local file encryption is toggled via `CC.ApiSetEncryptLocalFiles(enable)`. The `CryptoFile` type carries optional `CryptoFileArgs` (key + nonce) for local decryption. Files are decrypted on-demand for display via `decryptCryptoFile()`. + +**Location:** +- `SimpleXAPI.kt` -- `CC.ApiSetEncryptLocalFiles`, `AppPreferences.privacyEncryptLocalFiles` +- `ChatModel.kt` -- `CryptoFile`, `CryptoFileArgs` +- `RecAndPlay.desktop.kt` -- `decryptCryptoFile()` usage in audio playback + +--- + +### RULE-15: Relay Approval for File Transfer + +**Invariant:** When `privacyAskToApproveRelays` is enabled (default `true`), the app MUST prompt the user before using XFTP relay servers suggested by contacts (as opposed to the user's own configured servers). The `userApprovedRelays` flag on `CC.ReceiveFile` records the user's consent. + +**Enforcement:** `CC.ReceiveFile(fileId, userApprovedRelays, encrypt, inline)` passes the approval flag. The UI prompts the user when the file is from an unapproved relay. + +**Location:** `SimpleXAPI.kt` -- `CC.ReceiveFile`, `AppPreferences.privacyAskToApproveRelays` + +--- + +## 5. Notification Delivery + +### RULE-16: Background Message Delivery (Android) + +**Invariant:** On Android, when `NotificationsMode.SERVICE` is selected (default), the app MUST maintain a foreground service (`SimplexService`) to ensure continuous message delivery. The service MUST survive app backgrounding and device sleep. When `NotificationsMode.PERIODIC` is selected, `MessagesFetcherWorker` MUST periodically wake and fetch messages. When `NotificationsMode.OFF`, no background delivery occurs. + +**Enforcement:** +- `SimplexService` runs as a foreground service with `START_STICKY` and a `WakeLock`. It displays a persistent notification on the `SIMPLEX_SERVICE_NOTIFICATION` channel. +- `MessagesFetcherWorker` is a `PeriodicWorkRequest` scheduled via `WorkManager`. +- The mode is stored in `AppPreferences.notificationsMode` and checked at app startup. + +**Location:** +- `android/src/main/java/chat/simplex/app/SimplexService.kt` +- `android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt` +- `SimpleXAPI.kt:7739` -- `enum class NotificationsMode` + +--- + +### RULE-17: Notification Preview Privacy + +**Invariant:** Notification content MUST respect `notificationPreviewMode`: +- `HIDDEN` -- notification shows no sender or message content +- `CONTACT` -- notification shows sender name only +- `MESSAGE` -- notification shows sender name and message preview + +**Enforcement:** `NtfManager` (Android) reads the preview mode from `AppPreferences.notificationPreviewMode` and constructs notifications accordingly. The `CallService` also respects this mode for call notifications (showing or hiding caller identity). + +**Location:** +- `android/src/main/java/chat/simplex/app/model/NtfManager.android.kt` -- `displayNotification()`, `notifyCallInvitation()` +- `android/src/main/java/chat/simplex/app/CallService.kt` -- `updateNotification()` +- `SimpleXAPI.kt` -- `AppPreferences.notificationPreviewMode` + +--- + +## 6. Call Integrity + +### RULE-18: Call Lifecycle Management + +**Invariant:** An active call MUST be properly managed across the full lifecycle: +1. **Incoming calls** MUST be reported via `CallManager.reportNewIncomingCall()` which triggers a notification (and on Android, a full-screen intent for lock-screen display). +2. **Only one call** can be active at a time. Accepting a new call MUST end any existing call first (`CallManager.acceptIncomingCall` checks `activeCall` and calls `endCall` if needed, guarded by `switchingCall` flag). +3. **Call state** MUST progress through defined states: `WaitCapabilities` -> `InvitationSent`/`InvitationAccepted` -> `OfferSent`/`OfferReceived` -> `Negotiated` -> `Connected` -> `Ended`. +4. **Call end** MUST clean up all resources: send `WCallCommand.End`, call `apiEndCall`, clear `activeCall`, cancel call notifications, and release platform resources. + +**Android enforcement:** +- `CallService` (foreground service) keeps the call alive in background with a `WakeLock` and ongoing notification on `CALL_SERVICE_NOTIFICATION` channel. +- `CallActivity` hosts the WebRTC WebView. +- Lock-screen behavior controlled by `AppPreferences.callOnLockScreen` (DISABLE / SHOW / ACCEPT). + +**Desktop enforcement:** +- Calls run in the system browser via the NanoWSD WebSocket server on `localhost:50395`. +- The `WebRTCController` composable manages the WebSocket lifecycle. +- On dispose, `WCallCommand.End` is sent and the server is stopped. + +**Location:** +- `common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt` +- `common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt` +- Android: `android/src/main/java/chat/simplex/app/CallService.kt`, `android/src/main/java/chat/simplex/app/views/call/CallActivity.kt` +- Desktop: `common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt` diff --git a/apps/multiplatform/product/views/call.md b/apps/multiplatform/product/views/call.md new file mode 100644 index 0000000000..51d323874c --- /dev/null +++ b/apps/multiplatform/product/views/call.md @@ -0,0 +1,115 @@ +# Audio / Video Call + +> **Related spec:** [spec/services/calls.md](../../spec/services/calls.md) + +## Purpose + +Make and receive end-to-end encrypted audio and video calls over WebRTC. The implementation differs significantly between Android (WebView-based with `CallActivity` and PiP support) and Desktop (browser-based WebRTC via NanoHTTPD server on localhost). + +## Route / Navigation + +- **Entry point (outgoing)**: Tap audio or video call button in `ChatInfoView` action buttons or `ChatView` toolbar +- **Entry point (incoming)**: `IncomingCallAlertView` banner appears at top of screen +- **Presented by**: `ActiveCallView()` (expect/actual composable) is shown when `chatModel.showCallView == true` +- **Dismiss**: Call ends when user taps end button or remote party disconnects; `callManager.endCall()` handles cleanup +- **Android PiP**: Call view supports picture-in-picture mode via `CallActivity` + +## Platform Differences + +| Aspect | Android | Desktop | +|---|---|---| +| WebRTC host | `WebView` with `WebViewAssetLoader` serving local assets | NanoHTTPD server on `localhost:50395` opened in system browser | +| Call activity | `CallActivity` (separate Android Activity) with lifecycle management | Inline composable with `WebRTCController` | +| PiP support | Native Android PiP via `CallActivity` | Not supported | +| Audio management | `CallAudioDeviceManager` with Android `AudioManager`, proximity wake lock | System browser audio routing | +| WebSocket | N/A | `NanoWSD` WebSocket server for bidirectional WebRTC signaling | + +## Page Sections + +### Incoming Call Banner (`IncomingCallAlertView`) + +Displayed as an overlay banner when `chatModel.activeCallInvitation` is set: + +| Element | Description | +|---|---| +| User profile image | Shown when multiple profiles exist (32dp `ProfileImage`) | +| Call type icon | `ic_videocam_filled` (green) for video, `ic_call_filled` (green) for audio | +| Call type text | `invitation.callTypeText` with caller info | +| Caller profile | `ProfilePreview` showing caller name and avatar (64dp) | +| Reject button | Red `ic_call_end_filled` icon -- ends the invitation via `callManager.endCall(invitation)` | +| Ignore button | Blue `ic_close` icon -- dismisses banner, cancels notification | +| Accept button | Green `ic_check_filled` icon -- accepts via `callManager.acceptIncomingCall(invitation)` | + +Sound: `SoundPlayer.start()` plays ringtone while banner is visible (unless call view is already showing). + +### Active Call View + +#### Android (`CallView.android.kt`) + +| Element | Description | +|---|---| +| WebView | `AndroidView` wrapping a `WebView` that loads `call.html` via `WebViewAssetLoader`; handles WebRTC JS bridge | +| `ActiveCallState` | Manages proximity lock (`PROXIMITY_SCREEN_OFF_WAKE_LOCK`), audio device manager, call sounds | +| Call controls overlay | Mic toggle, speaker toggle, camera switch, video toggle, end call button | +| Audio device selection | `CallAudioDeviceManager` with device enumeration (earpiece, speaker, Bluetooth, wired headset) | +| Permissions | Runtime permission checks for `CAMERA` and `RECORD_AUDIO` via Accompanist permissions library | + +#### Desktop (`CallView.desktop.kt`) + +| Element | Description | +|---|---| +| NanoHTTPD server | HTTP server on `localhost:50395` serving `call.html` and assets | +| NanoWSD WebSocket | WebSocket endpoint for bidirectional signaling between Kotlin and browser JS | +| `WebRTCController` | Processes `WCallCommand`/`WCallResponse` messages via `chatModel.callCommand` channel | +| Browser launch | `LocalUriHandler.openUri("http://localhost:50395/call.html")` opens system browser | +| Connection list | `connections: ArrayList` tracks active WebSocket connections | + +### WebRTC Signaling Flow + +| Step | Command/Response | Description | +|---|---|---| +| 1. Capabilities | `WCallResponse.Capabilities` | Local capabilities reported; `apiSendCallInvitation()` called | +| 2. Offer | `WCallResponse.Offer` | SDP offer + ICE candidates sent via `apiSendCallOffer()` | +| 3. Answer | `WCallResponse.Answer` | SDP answer + ICE candidates sent via `apiSendCallAnswer()` | +| 4. ICE | `WCallResponse.Ice` | Additional ICE candidates exchanged via `apiSendCallExtraInfo()` | +| 5. Connection | `WCallResponse.Connection` | WebRTC connection state changes; `CallState.Connected` set on success | +| 6. Connected | `WCallResponse.Connected` | Connection info (relay/direct) stored in `call.connectionInfo` | +| 7. PeerMedia | `WCallResponse.PeerMedia` | Remote party media source changes (mic, camera, screen) | +| 8. Media control | `WCallCommand.Media` | Toggle local media sources (mic, camera, screen audio/video) | +| 9. Camera switch | `WCallCommand.Camera` | Switch between front/back camera | +| 10. End | `WCallResponse.End` / `WCallResponse.Ended` | Call termination; cleanup and UI dismissal | + +### Call States (`CallState`) + +| State | Description | +|---|---| +| `WaitCapabilities` | Waiting for WebRTC capabilities | +| `InvitationSent` | Call invitation sent to remote party | +| `InvitationAccepted` | Callee accepted, starting WebRTC | +| `OfferSent` | SDP offer sent | +| `OfferReceived` | Callee received SDP offer | +| `AnswerReceived` | Caller received SDP answer | +| `Negotiated` | ICE negotiation complete | +| `Connected` | WebRTC media flowing; `connectedAt` timestamp set | +| `Ended` | Call terminated | + +### Call Sounds + +| Sound | Trigger | +|---|---| +| Connecting sound | `CallSoundsPlayer.startConnectingCallSound()` after invitation sent | +| In-call sound | `CallSoundsPlayer.startInCallSound()` when delivery receipt received | +| Ringtone | `SoundPlayer.start()` for incoming calls | +| End vibration | `CallSoundsPlayer.vibrate()` on call end (if was connected) | + +## Source Files + +| File | Path | +|---|---| +| `CallView.kt` | `views/call/CallView.kt` (common expect declarations) | +| `CallView.android.kt` | `androidMain/.../views/call/CallView.android.kt` | +| `CallView.desktop.kt` | `desktopMain/.../views/call/CallView.desktop.kt` | +| `IncomingCallAlertView.kt` | `views/call/IncomingCallAlertView.kt` | +| `CallManager.kt` | `views/call/CallManager.kt` | +| `WebRTC.kt` | `views/call/WebRTC.kt` | +| `CallAudioDeviceManager.kt` | `androidMain/.../views/call/CallAudioDeviceManager.kt` | diff --git a/apps/multiplatform/product/views/chat-list.md b/apps/multiplatform/product/views/chat-list.md new file mode 100644 index 0000000000..daa7907c5d --- /dev/null +++ b/apps/multiplatform/product/views/chat-list.md @@ -0,0 +1,136 @@ +# Chat List (Home Screen) + +> **Related spec:** [spec/client/chat-list.md](../../spec/client/chat-list.md) + +## Purpose + +Main screen of the SimpleX Chat Android and Desktop apps. Displays all conversations sorted by last activity, serves as the navigation root, and provides access to user profiles, settings, and new chat creation. + +## Route / Navigation + +- **Entry point**: App launch (root view), or back-navigation from any chat +- **Presented by**: `ChatListView` composable as the default view when `chatModel.chatId == null` +- **Navigation**: `ChatListNavLinkView` handles click routing to `ChatView` for each chat type +- **UserPicker**: Triggered by tapping the user avatar in the toolbar; presents `UserPicker` as a custom sheet (Android: bottom sheet overlay; Desktop: sidebar panel) + +## Platform Layout + +| Platform | Layout | +|---|---| +| Android | Single-column list; toolbar at top or bottom (one-hand UI); FAB for new chat | +| Desktop | 3-column layout: chat list (left), chat view (center), info/detail panel (right via `ModalManager.end`) | + +## Page Sections + +### Toolbar (`ChatListToolbar`) + +| Element | Location | Behavior | +|---|---|---| +| User avatar button | Leading | Opens `UserPicker` sheet (profile switcher, address, settings, preferences, connect to desktop/mobile) | +| "Your chats" title | Center | Tappable to scroll list to top | +| Connection status indicator (`SubscriptionStatusIndicator`) | Adjacent to title | Shows SMP server subscription status; taps open `ServersSummaryView` | +| New chat button (pencil icon) | Trailing (one-hand UI) or FAB (standard) | Opens `NewChatSheet` modal via `showNewChatSheet()` | +| Active call indicator | Trailing (Desktop, one-hand UI) | `ActiveCallInteractiveArea` shown when a call is active | +| Updating progress | Trailing | Shows progress circle/indicator during database updates | +| Stopped indicator | Trailing | Red warning icon when chat engine is stopped | + +The toolbar supports two layout modes controlled by `appPrefs.oneHandUI`: +- **Standard (top)**: `DefaultAppBar` at top with `NavigationButtonMenu` leading, title center, buttons trailing. FAB at bottom-right for new chat. +- **One-hand UI (bottom)**: Toolbar at bottom of screen with `Column(Modifier.align(Alignment.BottomCenter))`; list rendered with `reverseLayout = true`; no FAB (new chat button is inline in toolbar). + +### Search Bar (`ChatListSearchBar`) + +| Element | Description | +|---|---| +| Search icon | Magnifying glass icon at leading edge | +| Text field | `SearchTextField` with placeholder "Search or paste SimpleX link" | +| Filter button | `ToggleFilterEnabledButton` (filter icon) toggles unread-only filter; shown when search text is empty | +| Clear button | Appears when text is entered; `BackHandler` clears search on back | + +Behavior: +- Filters chat list in real-time by contact/group name via `filteredChats()` +- Detects pasted SimpleX links (`strHasSingleSimplexLink`) and triggers `planAndConnect()` connection dialogue +- In one-hand UI mode, search bar appears below tag filters with IME spacer; in standard mode, above tag filters + +### Chat Filter Tags (`TagsView`) + +Managed by `chatModel.userTags`, `chatModel.presetTags`, and `chatModel.activeChatTagFilter`: + +| Filter | `PresetTagKind` | Icon | Description | +|---|---|---|---| +| Group Reports | `GROUP_REPORTS` | Flag | Chats with moderation reports (non-collapsible) | +| Favorites | `FAVORITES` | Star | User-favorited chats | +| Contacts | `CONTACTS` | Person | Direct contacts and contact requests | +| Groups | `GROUPS` | Group | Group conversations (non-business) | +| Business | `BUSINESS` | Work | Business chat conversations | +| Notes | `NOTES` | Folder | Notes to self | +| Custom tags | `UserTag(ChatTag)` | Label/emoji | User-created tags with custom emoji and name | +| Unread | `ActiveFilter.Unread` | Filter list icon | Chats with unread messages (toggle via filter button) | + +Display logic: +- When collapsible preset tags exceed 3 total (with user tags), they collapse into a `CollapsedTagsFilterView` dropdown menu +- Non-collapsible tags (`GROUP_REPORTS`) always show expanded +- User tags show with emoji or label icon; long-press opens `TagsDropdownMenu` (edit, delete, change order) +- "+" button at end opens `TagListEditor` for creating new tags + +### Chat Preview Rows (`ChatPreviewView`) + +Each row rendered by `ChatPreviewView` inside `ChatListNavLinkView`: + +| Element | Description | +|---|---| +| Avatar | `ProfileImage` with overlay icons (inactive contact, left/removed group member) | +| Chat name | Display name with verified icon for verified contacts; colored for pending/connecting states | +| Last message preview | Truncated text of most recent message; draft indicator with edit icon; attachment icons | +| Timestamp | Relative time of last activity | +| Unread badge | Numeric count badge; distinct styling for mentions | +| Muted indicator | Bell-off icon when notifications are muted | +| Favorite indicator | Star icon for favorited chats | +| Incognito indicator | Shows when connected via incognito profile | +| Connection status | Shows connecting/pending state for incomplete connections | + +Chat types handled by `ChatListNavLinkView`: +- `ChatInfo.Direct` -- direct contact chat +- `ChatInfo.Group` -- group chat (with in-progress indicator for joining) +- `ChatInfo.Local` -- note-to-self folder +- `ChatInfo.ContactRequest` -- incoming contact request (tap shows accept/reject alert) +- `ChatInfo.ContactConnection` -- pending connection (tap opens `ContactConnectionView`) + +### Context Menu (Long Press / Right Click) + +Each chat type provides specific dropdown menu items: + +| Chat Type | Menu Items | +|---|---| +| Direct contact | Mark read/unread, toggle favorite, toggle notify, tag list, clear chat, delete contact | +| Group | Mark read/unread, toggle favorite, toggle notify, tag list, clear chat, archive all reports (moderator, when reports exist), leave group, delete group | +| Note folder | Mark read/unread, clear notes | +| Contact request | Accept, reject | +| Contact connection | Set name/alias, delete | + +### Floating Elements + +| Element | Condition | Description | +|---|---|---| +| One-hand UI card (`ToggleChatListCard`) | `oneHandUICardShown == false` | Dismissible card introducing bottom toolbar mode with toggle switch | +| Address creation card (`AddressCreationCard`) | `addressCreationCardShown == false` | Prompts user to create a SimpleX address; tappable card opens `UserAddressLearnMore` | +| FAB (new chat button) | Standard mode, search empty, chat running | `FloatingActionButton` at bottom-right, pencil icon, opens `NewChatSheet` | + +### Empty States + +| State | Display | +|---|---| +| Loading | "Loading chats..." centered text | +| No chats | "You have no chats" centered text | +| No filtered chats | "No chats in list [tag name]" or "No unread chats" with clickable filter reset | +| No search results | "No chats found" centered text | + +## Source Files + +| File | Path | +|---|---| +| `ChatListView.kt` | `views/chatlist/ChatListView.kt` | +| `ChatListNavLinkView.kt` | `views/chatlist/ChatListNavLinkView.kt` | +| `ChatPreviewView.kt` | `views/chatlist/ChatPreviewView.kt` | +| `UserPicker.kt` | `views/chatlist/UserPicker.kt` | +| `TagListView.kt` | `views/chatlist/TagListView.kt` | diff --git a/apps/multiplatform/product/views/chat.md b/apps/multiplatform/product/views/chat.md new file mode 100644 index 0000000000..64abda7ee6 --- /dev/null +++ b/apps/multiplatform/product/views/chat.md @@ -0,0 +1,135 @@ +# Chat View (Conversation) + +> **Related spec:** [spec/client/chat-view.md](../../spec/client/chat-view.md) + +## Purpose + +Full conversation view for displaying and interacting with messages in a direct contact chat, group chat, or note-to-self. Supports text messaging with markdown, media attachments, voice messages, E2E encrypted calls, message reactions, replies, forwarding, reporting, and content search/filtering. + +## Route / Navigation + +- **Entry point**: Tap a chat row in `ChatListView` (routed by `ChatListNavLinkView`) +- **Presented by**: `ChatView` composable bound to `chatModel.chatId`; on Desktop, shown in the center column +- **Back navigation**: Sets `chatModel.chatId = null`, stops `AudioPlayer`, clears group members, returns to chat list +- **Sub-navigation**: + - Info button opens `ChatInfoView` (contact) or `GroupChatInfoView` (group) via `ModalManager.end` + - Member avatars in group chats navigate to `GroupMemberInfoView` + - Reports button opens `GroupReportsView` for groups with moderation reports + - Support chats button opens `MemberSupportView` (moderators) or member support chat (regular members) + +## Page Sections + +### Navigation Bar (`ChatLayout`) + +Custom toolbar with themed background: + +| Element | Description | +|---|---| +| Back button | Returns to chat list; stops audio/video playback | +| Contact/Group avatar | Small profile image in toolbar | +| Chat name | Display name; tappable to open info view | +| Verified shield | Shows verified contact checkmark (direct chats with verified contacts only) | +| More menu button | Opens overflow menu containing search and audio/video call buttons (call buttons shown in direct chats only) | +| Info button | Opens `ChatInfoView` (direct) or `GroupChatInfoView` (group) | +| Reports count | Badge for group reports count; taps open reports view | +| Support chats | Badge for member support; taps open support chat view | + +### Message List + +Rendered by `LazyColumnWithScrollBar` with pagination: + +| Feature | Description | +|---|---| +| Scroll direction | Bottom-to-top (newest messages at bottom) | +| Pagination | `apiLoadMessages` called on scroll to load more; supports `.before`, `.after`, `.around`, `.initial` | +| Merged items | Adjacent messages grouped with `ItemSeparation` (timestamp, large gap, date separators) | +| Floating buttons | Scroll-to-bottom button with unread count | +| Date separators | Date headers between messages from different days | +| Wallpaper | Per-chat themed background via `perChatTheme` from contact/group `uiThemes` | +| Content filter | Filter messages by type via `ContentFilter` (images, files, links, etc.) | + +### Message Types + +Each type has a dedicated composable in `views/chat/item/`: + +| Type | Composable | Description | +|---|---|---| +| Text | `FramedItemView` | Rendered with markdown (bold, italic, code, links, `@mentions`) via `CIMarkdownText` | +| Image | `CIImageView` | Thumbnail with tap-to-fullscreen via `ImageFullScreenView` | +| Video | `CIVideoView` | Video thumbnail with play button; inline playback via `VideoPlayerHolder` | +| Voice | `CIVoiceView` | Waveform visualization with playback controls and duration | +| File | `CIFileView` | File icon, name, size; download/open actions with progress indicator | +| Link preview | `ChatItemLinkView` | URL preview card with title, description, image (defined in `LinkPreviews.kt`) | +| Emoji-only | `EmojiItemView` | Large emoji rendering without message bubble | +| Call event | `CICallItemView` | Call status (missed, ended, duration) | +| Group event | `CIEventView` | Member joined/left, role changes, group updates | +| E2EE info | `CIChatFeatureView` | Encryption status and feature change notifications | +| Group invitation | `CIGroupInvitationView` | Inline group join invitation card | +| Deleted | `DeletedItemView` / `MarkedDeletedItemView` | Placeholder for deleted messages | +| Decryption error | `CIRcvDecryptionError` | Error with ratchet sync suggestion | +| Invalid JSON | `CIInvalidJSONView` | Developer fallback for malformed items | +| Integrity error | `IntegrityErrorItemView` | Message integrity/gap warnings | + +### Message Interactions + +Long-press context menu on any message: + +| Action | Description | +|---|---| +| Reply | Sets compose bar to reply mode with quoted message (`ComposeContextItem.QuotedItem`) | +| Forward | Opens destination picker; uses `apiPlanForwardChatItems` with confirmation for partial forwards | +| Copy | Copies message text to clipboard | +| Edit | Enters edit mode (`ComposeContextItem.EditingItem`); own messages within edit window | +| Delete | Delete for self or delete for everyone (with confirmation via `deleteMessagesAlertDialog`) | +| Moderate | Group moderators can delete messages for all members (`moderateMessagesAlertDialog`) | +| React | Emoji reaction picker | +| Report | Report message to group moderators (`ComposeContextItem.ReportedItem` with `ReportReason`) | +| Select multiple | Enters multi-select mode (`selectedChatItems`) with bulk delete/forward/archive toolbar | +| Archive | Archive selected reports (moderators) | + +### Compose Bar (`ComposeView` + `SendMsgView`) + +Bottom input area for composing messages: + +| Element | Description | +|---|---| +| Text field | `PlatformTextField` with markdown support, `@mention` autocomplete, file paste support | +| Attachment button | Opens `ModalBottomSheetLayout` with options: camera, gallery (image/video), file | +| Send button | Sends message; changes to checkmark for reports; animated size/alpha | +| Voice record button | Shown when text is empty and voice allowed; hold to record, release to preview | +| Live message button | Start/update live typing message (if `liveMessageAlertShown`) | +| Context preview | Shows quoted message, editing indicator, or forwarding source above text field | +| Media preview | Thumbnail row of selected images/videos before sending | +| Link preview | Auto-generated link preview card (`ComposePreview.CLinkPreview`) | +| Connecting status | "Connecting..." text shown when contact is not yet ready | +| Commands menu | Developer commands (`showCommandsMenu`) | + +Compose states (`ComposeState`): +- `NoContextItem` -- normal new message +- `QuotedItem` -- replying to a message +- `EditingItem` -- editing own message +- `ForwardingItems` -- forwarding from another chat +- `ReportedItem` -- reporting a message with reason + +### Multi-Select Toolbar (`SelectedItemsButtonsToolbar`) + +Shown when `selectedChatItems != null`: + +| Button | Description | +|---|---| +| Delete / Archive | Delete selected messages (for self, or for everyone if allowed by `fullDeleteAllowed`); shown as Archive for report items (group moderators only) | +| Forward | Forward selected messages to another chat | +| Moderate | Delete selected messages for all members (group moderators only) | + +### Timed/Disappearing Messages + +When `timedMessageAllowed` is true, compose bar includes a timer icon for setting message disappear time via `customDisappearingMessageTimePref`. + +## Source Files + +| File | Path | +|---|---| +| `ChatView.kt` | `views/chat/ChatView.kt` | +| `ComposeView.kt` | `views/chat/ComposeView.kt` | +| `SendMsgView.kt` | `views/chat/SendMsgView.kt` | +| Chat item views | `views/chat/item/*.kt` | diff --git a/apps/multiplatform/product/views/contact-info.md b/apps/multiplatform/product/views/contact-info.md new file mode 100644 index 0000000000..32793a3b70 --- /dev/null +++ b/apps/multiplatform/product/views/contact-info.md @@ -0,0 +1,104 @@ +# Contact Info + +> **Related spec:** [spec/client/chat-view.md](../../spec/client/chat-view.md) + +## Purpose + +View contact details, manage per-contact preferences, verify security codes for E2E encryption, manage connection settings (switch address, sync ratchet), and perform destructive actions like clearing or deleting a contact. + +## Route / Navigation + +- **Entry point**: Tap the info button in `ChatView` navigation bar (when viewing a direct contact chat) +- **Presented by**: `ChatInfoView` composable shown via `ModalManager.end` from `ChatView` +- **Sub-navigation**: + - Contact preferences -> `ContactPreferencesView` (via `ModalManager.end`) + - Security code verification -> `VerifyCodeView` (via `ModalManager.end`) + - Chat wallpaper -> wallpaper editor + - Group profile view (for group-direct contacts) + +## Page Sections + +### Contact Info Header + +| Element | Description | +|---|---| +| Profile image | Large circular avatar (tappable) | +| Display name | Contact's display name | +| Full name | Optional full name below display name | +| Connection status | Shows if contact is ready, connecting, or has issues | + +### Local Alias + +Editable text field for setting a local-only name visible only on this device. Not shared with the contact. Changes saved via `setContactAlias()`. + +### Action Buttons + +Horizontal row of quick-action buttons: + +| Button | Description | +|---|---| +| Search | Triggers `onSearchClicked` to search messages in chat | +| Audio call | Initiate audio call | +| Video call | Initiate video call | +| Mute/Unmute | Toggle notification mode | + +### Incognito Section + +Shown only when `customUserProfile` is set (connected via incognito profile): + +| Element | Description | +|---|---| +| Incognito icon | Indicates incognito connection | +| Profile name | The random profile name used for this connection | + +### Chat Preferences + +| Setting | Description | +|---|---| +| Send receipts | Per-contact delivery receipt setting (`SendReceipts` tristate: default/on/off) | +| Chat item TTL | Per-contact message retention setting (`ChatItemTTL` with alert confirmation) | +| Contact preferences | Opens `ContactPreferencesView` for feature toggles (timed messages, full delete, reactions, voice, calls) | + +### Connection Details + +Shown when `connectionStats` is available: + +| Element | Description | +|---|---| +| Connection stats | Server information, agent connection ID | +| Switch address | Initiates SMP server address switch (`apiSwitchContact`) with confirmation alert | +| Abort switch | Cancels an in-progress address switch (`apiAbortSwitchContact`) | +| Sync connection | Fixes encryption ratchet synchronization (`apiSyncContactRatchet`) | +| Force sync | Force ratchet re-synchronization with confirmation alert | + +### Security Code Verification + +| Element | Description | +|---|---| +| Verify button | Opens `VerifyCodeView` showing the connection security code | +| Verified badge | Shows checkmark when contact is verified | +| Code comparison | Side-by-side code display for out-of-band verification via `apiVerifyContact` | + +### Developer Tools Section + +Shown when `developerTools` preference is enabled: + +| Element | Description | +|---|---| +| Database ID | Contact's internal database identifier | +| Agent connection ID | Underlying SMP agent connection ID | + +### Destructive Actions + +| Action | Description | +|---|---| +| Clear chat | Deletes all messages in chat (with confirmation via `clearChatDialog`) | +| Delete contact | Removes the contact and all associated data (with confirmation via `deleteContactDialog`) | + +## Source Files + +| File | Path | +|---|---| +| `ChatInfoView.kt` | `views/chat/ChatInfoView.kt` | +| `ContactPreferences.kt` | `views/chat/ContactPreferences.kt` | +| `VerifyCodeView.kt` | `views/chat/VerifyCodeView.kt` | diff --git a/apps/multiplatform/product/views/group-info.md b/apps/multiplatform/product/views/group-info.md new file mode 100644 index 0000000000..65b068adc8 --- /dev/null +++ b/apps/multiplatform/product/views/group-info.md @@ -0,0 +1,145 @@ +# Group Chat Info + +> **Related spec:** [spec/client/chat-view.md](../../spec/client/chat-view.md) + +## Purpose + +View and manage group settings, member list, group preferences, group links, member admission, welcome messages, and moderation features. The scope of available actions depends on the user's role within the group (member, moderator, admin, owner). + +## Route / Navigation + +- **Entry point**: Tap the info button in `ChatView` navigation bar (when viewing a group chat) +- **Presented by**: `GroupChatInfoView` composable shown via `ModalManager.end` from `ChatView` +- **Sub-navigation**: + - Edit group profile -> `GroupProfileView` (via `ModalManager.end`) + - Add members -> `AddGroupMembersView` (via `ModalManager.end`) + - Group link -> `GroupLinkView` (via `ModalManager.end`) + - Group preferences -> `GroupPreferencesView` (via `ModalManager.end`) + - Welcome message -> `GroupWelcomeView` (via `ModalManager.end`) + - Member info -> `GroupMemberInfoView` (via `ModalManager.end`) + - Chat wallpaper -> wallpaper editor + - Member support -> `MemberSupportView` (via `ModalManager.end`) + +## Page Sections + +### Group Info Header + +| Element | Description | +|---|---| +| Group image | Large circular profile image | +| Group name | Display name (editable by owners via `GroupProfileView`) | +| Member count | "N members" label from `activeSortedMembers` | +| Full name | Optional secondary name | +| Description | Group description text (if set) | + +### Local Alias + +Editable text field for a local-only alias (not shared with other members). Changes saved via `setGroupAlias()`. + +### Action Buttons + +Horizontal row of action buttons: + +| Button | Description | +|---|---| +| Search | Triggers `onSearchClicked` callback to search messages in chat | +| Mute/Unmute | Toggle notification mode | +| Add members | Opens `AddGroupMembersView` (shown when user has admin+ role and `groupInfo.canAddMembers`) | + +### Group Management Section + +Available actions depend on role (`GroupMemberRole`): + +| Action | Minimum Role | Description | +|---|---|---| +| Edit group profile | Owner | Opens `GroupProfileView` to edit name, image, description | +| Add members | Admin | Opens `AddGroupMembersView` to invite contacts | +| Manage group link | Admin | Opens `GroupLinkView` to create/share/delete group link | +| Member support | Moderator | Opens `MemberSupportView` to manage member support chats | +| Edit welcome message | Owner | Opens `GroupWelcomeView` to set the auto-sent welcome text | +| Group preferences | Any | Opens `GroupPreferencesView` (read-only; only owners can change settings) | + +### Chat Preferences + +| Setting | Description | +|---|---| +| Send receipts | Per-group delivery receipt setting (`SendReceipts`); limited to groups under `SMALL_GROUPS_RCPS_MEM_LIMIT` (20 members) | +| Chat item TTL | Per-group message retention setting with confirmation alert via `setChatTTLAlert` | + +### Member List + +Displays `activeSortedMembers` (excluding left/removed members, sorted by role descending): + +| Element | Description | +|---|---| +| Member avatar | `MEMBER_ROW_AVATAR_SIZE` (42dp) profile image | +| Member name | Display name with role badge | +| Member role | Owner, Admin, Moderator, Member, Observer | +| Member status | Active, connecting, pending, left, removed | +| Tap action | Opens `GroupMemberInfoView` with connection stats and verification code | + +### Group Link (`GroupLinkView`) + +| Element | Description | +|---|---| +| Create link button | `apiCreateGroupLink` generates a shareable group invitation link | +| QR code display | QR code rendering of the group link | +| Short link toggle | Switch between short and full link display | +| Share button | System share for the link | +| Copy button | Copy link to clipboard | +| Member role selector | Set the default role for members joining via link (`acceptMemberRole`) | +| Add short link | `apiAddGroupShortLink` creates a short link that includes group profile | +| Delete link | Remove the group link with confirmation | + +### Add Members (`AddGroupMembersView`) + +| Element | Description | +|---|---| +| Contact list | Filterable list of contacts to invite | +| Role selector | Set the role for invited members | +| Invite button | Sends group invitations to selected contacts | +| Group link option | Alternative to direct invitation | + +### Group Member Info (`GroupMemberInfoView`) + +| Element | Description | +|---|---| +| Member profile | Avatar, name, role | +| Connection stats | Server information, connection status | +| Security code | Verification code for the member connection | +| Role change | Change member role (admin+ only) | +| Remove member | Remove from group (admin+ only) | +| Block member | Block member for self | +| Direct message | Open direct chat with member | + +### Developer Tools Section + +Shown when `developerTools` preference is enabled: + +| Element | Description | +|---|---| +| Database ID | Group's internal database identifier | + +### Destructive Actions + +| Action | Condition | Description | +|---|---|---| +| Clear chat | Any member | Deletes all messages locally (`clearChatDialog`) | +| Leave group | Non-owner | Leave the group (`leaveGroupDialog`) | +| Delete group | Owner or non-current member | Delete group for all (owner) or for self (`deleteGroupDialog`) | + +Business chats use alternative labels: "Delete chat" instead of "Delete group". + +## Source Files + +| File | Path | +|---|---| +| `GroupChatInfoView.kt` | `views/chat/group/GroupChatInfoView.kt` | +| `GroupMemberInfoView.kt` | `views/chat/group/GroupMemberInfoView.kt` | +| `AddGroupMembersView.kt` | `views/chat/group/AddGroupMembersView.kt` | +| `GroupLinkView.kt` | `views/chat/group/GroupLinkView.kt` | +| `GroupProfileView.kt` | `views/chat/group/GroupProfileView.kt` | +| `GroupPreferences.kt` | `views/chat/group/GroupPreferences.kt` | +| `WelcomeMessageView.kt` | `views/chat/group/WelcomeMessageView.kt` | +| `MemberAdmission.kt` | `views/chat/group/MemberAdmission.kt` | +| `MemberSupportView.kt` | `views/chat/group/MemberSupportView.kt` | diff --git a/apps/multiplatform/product/views/new-chat.md b/apps/multiplatform/product/views/new-chat.md new file mode 100644 index 0000000000..b664fda67f --- /dev/null +++ b/apps/multiplatform/product/views/new-chat.md @@ -0,0 +1,96 @@ +# New Chat / Connection + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) + +## Purpose + +Create new contacts, groups, or connect with others via one-time invitation links or by scanning/pasting SimpleX links. This is the primary entry point for establishing new E2E encrypted connections. + +## Route / Navigation + +- **Entry point**: Tap the new chat button (pencil icon) in `ChatListView` toolbar or FAB +- **Presented by**: `NewChatSheet` modal from `ChatListView` via `showNewChatSheet()`; wraps `NewChatView` and group creation in `ModalManager.start` +- **Internal navigation**: `NewChatSheet` provides 3 action buttons: + - "Create 1-time link" -- opens `NewChatView` with `INVITE` tab (generate and share a one-time invitation link) + - "Scan / paste link" -- opens `NewChatView` with `CONNECT` tab (scan QR code or paste a received link) + - "Create group" -- opens `AddGroupView` +- **Tabs within NewChatView**: `HorizontalPager` with `TabRow` toggles between `NewChatOption.INVITE` (1-time link) and `NewChatOption.CONNECT` (connect via link) +- **Swipe gesture**: Left/right swipe switches between tabs (Android only; `userScrollEnabled = appPlatform.isAndroid`) +- **Dismiss behavior**: On dispose, a `DisposableEffect` shows an alert dialog (via `AlertManager.shared.showAlertDialog`) asking whether to keep an unused invitation link or delete it via `controller.deleteChat()` + +## Page Sections + +### Tab Selector + +| Tab | Icon | Label | Description | +|---|---|---|---| +| 1-time link | `ic_repeat_one` | "1-time link" | Generate and share a one-time invitation link | +| Connect via link | `ic_qr_code` | "Connect via link" | Scan QR code or paste a received link | + +### Invite Tab (1-time Link) -- `PrepareAndInviteView` + +Displayed when `selection == INVITE`: + +| Element | Description | +|---|---| +| QR code display | Generated QR code for the invitation link (`SimpleXLinkQRCode`) | +| Short/full link toggle | Switch between short and full link display | +| Share button | System share for the invitation link | +| Copy button | Copy link to clipboard | +| Incognito toggle | Option to connect with a random profile | +| Loading state | `CreatingLinkProgressView` with "Creating link" text while `creatingConnReq` is true | +| Retry button | `RetryButton` shown if link creation fails; calls `createInvitation()` | + +Link creation calls `apiAddContact` which returns a `CreatedConnLink` with both `connFullLink` and optional `connShortLink`. The invitation is tracked via `chatModel.showingInvitation`. + +### Connect Tab -- `ConnectView` + +Displayed when `selection == CONNECT`: + +| Element | Description | +|---|---| +| QR code scanner | Camera-based QR code scanner (`showQRCodeScanner` state) | +| Paste link field | Text field for pasting a SimpleX link (`pastedLink`) | +| Connect button | Initiates connection via `planAndConnect()` | + +When a valid SimpleX link is detected: +1. `planAndConnect()` is called with the link URI +2. If the link matches a known contact, filters to that chat +3. If the link matches a known group, filters to that group +4. Otherwise, creates a new connection + +### Create Group (`AddGroupView`) + +| Element | Description | +|---|---| +| Group name field | Required display name input with `FocusRequester` | +| Profile image picker | `GetImageBottomSheet` for selecting/cropping a group avatar | +| Incognito toggle | Option to create group with random profile (`incognitoPref`) | +| Create button | Calls `apiNewGroup()`, then opens `AddGroupMembersView` (normal) or `GroupLinkView` (incognito) | + +Group creation flow: +1. User enters group name and optionally selects an image +2. `apiNewGroup()` creates the group and returns `GroupInfo` +3. `openGroupChat()` navigates to the new group chat +4. `setGroupMembers()` preloads member data +5. `AddGroupMembersView` opens for inviting contacts (or `GroupLinkView` for incognito groups) + +### QR Code Components (`QRCode.kt`) + +| Component | Description | +|---|---| +| `SimpleXLinkQRCode` | Renders a QR code for a SimpleX connection link | +| QR scanner | Platform camera scanner for reading QR codes | +| Short link display | Compact link text with copy/share actions | + +## Source Files + +| File | Path | +|---|---| +| `NewChatView.kt` | `views/newchat/NewChatView.kt` | +| `AddGroupView.kt` | `views/newchat/AddGroupView.kt` | +| `QRCode.kt` | `views/newchat/QRCode.kt` | +| `NewChatSheet.kt` | `views/newchat/NewChatSheet.kt` | +| `ConnectPlan.kt` | `views/newchat/ConnectPlan.kt` | +| `QRCodeScanner.kt` | `views/newchat/QRCodeScanner.kt` (expect/actual) | +| `ContactConnectionInfoView.kt` | `views/newchat/ContactConnectionInfoView.kt` | diff --git a/apps/multiplatform/product/views/onboarding.md b/apps/multiplatform/product/views/onboarding.md new file mode 100644 index 0000000000..4127ac65f7 --- /dev/null +++ b/apps/multiplatform/product/views/onboarding.md @@ -0,0 +1,139 @@ +# Onboarding + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) + +## Purpose + +First-time setup flow for new users. Guides through app introduction, profile creation, database passphrase setup (Desktop), server operator conditions acceptance, SimpleX address creation, and notification configuration (Android). Also provides an entry point for device migration. + +## Route / Navigation + +- **Entry point**: App launch when `onboardingStage` is not `OnboardingComplete` +- **Presented by**: `OnboardingView` renders the appropriate step based on `OnboardingStage` enum +- **Flow direction**: Linear progression controlled by `appPrefs.onboardingStage` +- **Completion**: Sets `onboardingStage` to `OnboardingComplete` + +## Onboarding Stages + +The `OnboardingStage` enum defines the flow: + +| Stage | Description | +|---|---| +| `Step1_SimpleXInfo` | Welcome screen with app introduction | +| `Step2_CreateProfile` | Create first user profile | +| `LinkAMobile` | Desktop-only: link a mobile device | +| `Step2_5_SetupDatabasePassphrase` | Desktop-only: set database encryption passphrase | +| `Step3_ChooseServerOperators` | Accept server operator conditions | +| `Step3_CreateSimpleXAddress` | Create a SimpleX contact address | +| `Step4_SetNotificationsMode` | Android-only: configure notification mode | +| `OnboardingComplete` | Onboarding finished | + +## Page Sections + +### Step 1: Welcome / SimpleX Info (`SimpleXInfo`) + +**Stage**: `Step1_SimpleXInfo` + +| Element | Description | +|---|---| +| Logo | `SimpleXLogo` -- SimpleX Chat logo (light/dark variant based on `isInDarkTheme()`) | +| Info button | `OnboardingInformationButton` -- "The next generation of private messaging"; taps open `HowItWorks` fullscreen modal | +| Privacy redefined | `InfoRow` with privacy icon: "No user identifiers" | +| Immune to spam | `InfoRow` with shield icon: "You decide who can connect" | +| Decentralized | `InfoRow` with decentralized icon: "Anybody can host servers" | +| **Create your profile** button | `OnboardingActionButton` -- primary action; advances to profile creation | +| **Migrate from another device** button | `TextButtonBelowOnboardingButton` -- opens `MigrateToDeviceView` fullscreen modal | + +Layout: `ColumnWithScrollBar` with `DEFAULT_ONBOARDING_HORIZONTAL_PADDING`, max width constrained (250dp Android, 500dp Desktop). + +### Step 2: Create Profile + +**Stage**: `Step2_CreateProfile` + +| Element | Description | +|---|---| +| Display name field | Required text input; auto-focused | +| Validation | Name validation with `mkValidName` check | +| Create button | Creates profile via API; advances to next step | + +Profile is stored locally and only shared with contacts. + +### Step 2.5: Setup Database Passphrase (Desktop only) + +**Stage**: `Step2_5_SetupDatabasePassphrase` + +| Element | Description | +|---|---| +| Passphrase field | Secure text input for database encryption key | +| Confirm field | Passphrase confirmation | +| Set button | Encrypts database with passphrase | + +### Link a Mobile (Desktop only) + +**Stage**: `LinkAMobile` + +| Element | Description | +|---|---| +| Instructions | How to connect mobile device to desktop | +| QR code | Connection QR code for mobile scanning | +| Skip button | Skip this step | + +### Step 3: Choose Server Operators + +**Stage**: `Step3_ChooseServerOperators` + +| Element | Description | +|---|---| +| Operator list | Available server operators with conditions | +| Conditions text | Terms of service for selected operators | +| Accept button | Accept conditions and continue | + +Managed by `ChooseServerOperators.kt`. + +### Step 3b: Create SimpleX Address + +**Stage**: `Step3_CreateSimpleXAddress` + +| Element | Description | +|---|---| +| Address creation | Auto-creates a SimpleX contact address | +| QR code | Displays the created address as QR code | +| Share button | Share address link | +| Skip button | Skip address creation | + +### Step 4: Set Notifications Mode (Android only) + +**Stage**: `Step4_SetNotificationsMode` + +| Element | Description | +|---|---| +| Notification options | Instant (background service) / Periodic (every 10 min) / Off | +| Description | Explains battery impact and notification behavior for each mode | +| Continue button | Saves selection and completes onboarding | + +Managed by `SetNotificationsMode.kt`. + +### What's New (`WhatsNewView`) + +Shown after onboarding or when triggered from Settings: + +| Element | Description | +|---|---| +| Version highlights | New features and changes in the current version | +| Updated conditions | Notice about updated server operator conditions (if applicable) | +| Close button | Dismisses the view | + +Triggered in `ChatListView` via `shouldShowWhatsNew()` with a 1-second delay. + +## Source Files + +| File | Path | +|---|---| +| `OnboardingView.kt` | `views/onboarding/OnboardingView.kt` | +| `SimpleXInfo.kt` | `views/onboarding/SimpleXInfo.kt` | +| `HowItWorks.kt` | `views/onboarding/HowItWorks.kt` | +| `SetupDatabasePassphrase.kt` | `views/onboarding/SetupDatabasePassphrase.kt` | +| `SetNotificationsMode.kt` | `views/onboarding/SetNotificationsMode.kt` | +| `ChooseServerOperators.kt` | `views/onboarding/ChooseServerOperators.kt` | +| `WhatsNewView.kt` | `views/onboarding/WhatsNewView.kt` | +| `LinkAMobileView.kt` | `views/onboarding/LinkAMobileView.kt` | diff --git a/apps/multiplatform/product/views/settings.md b/apps/multiplatform/product/views/settings.md new file mode 100644 index 0000000000..e668bf2d04 --- /dev/null +++ b/apps/multiplatform/product/views/settings.md @@ -0,0 +1,159 @@ +# Settings + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) | [spec/services/theme.md](../../spec/services/theme.md) | [spec/services/notifications.md](../../spec/services/notifications.md) + +## Purpose + +Configure all aspects of app behavior including notifications, network/servers, privacy, appearance, database management, call settings, and developer tools. Accessed from the UserPicker or directly from the chat list toolbar. + +## Route / Navigation + +- **Entry point**: Tap user avatar in `ChatListView` toolbar -> `UserPicker` -> Settings option; or directly via `NavigationButtonMenu` when no users exist +- **Presented by**: `SettingsView` composable via `ModalManager.start.showModalCloseable` +- **Navigation title**: "Your settings" (`AppBarTitle`) +- **Sub-navigation**: Each settings row opens a dedicated view via `showSettingsModal` or `showCustomModal` + +## Platform Differences + +| Aspect | Android | Desktop | +|---|---|---| +| App section | Device settings, app version | App updates (`AppUpdater`), device settings, app version | +| Notifications | Full notification mode selection (instant/periodic/off) | Notification settings | +| Use from desktop/mobile | "Use from desktop" option in UserPicker | "Link a mobile" / "Linked mobiles" option in UserPicker | +| Database migration | "Migrate to another device" with auth | Same | + +## Page Sections + +### Settings Section + +| Row | Icon | Destination | Description | +|---|---|---|---| +| Notifications | `ic_bolt` / `ic_bolt_off` | `NotificationsSettingsView` | Push notification mode and preview settings | +| Network & servers | `ic_wifi_tethering` | `NetworkAndServersView` | SMP/XFTP servers, proxy, .onion hosts, advanced network | +| Audio & video calls | `ic_videocam` | `CallSettingsView` | WebRTC relay policy, ICE servers | +| Privacy & security | `ic_lock` | `PrivacySettingsView` | SimpleX Lock, delivery receipts, link previews, auto-accept | +| Appearance | `ic_light_mode` | `AppearanceView` | Theme, language, profile images, chat bubbles | + +All rows disabled when `chatModel.chatRunning != true` (except Appearance). + +#### Notifications (`NotificationsSettingsView`) + +| Setting | Options | +|---|---| +| Notification mode | Instant (background service) / Periodic (every 10 min) / Off | +| Notification preview | Configuration for notification content visibility | + +#### Network & Servers (`NetworkAndServersView`) + +| Setting | Description | +|---|---| +| SMP servers | Messaging relay servers; per-operator configuration | +| XFTP servers | File transfer servers; per-operator configuration | +| Server operators | `OperatorView` for each configured operator | +| Advanced network | `AdvancedNetworkSettings` -- timeouts, TCP keep-alive, reconnect intervals | +| Proxy configuration | SOCKS proxy, .onion host settings | + +Sub-files: `NetworkAndServers.kt`, `ProtocolServersView.kt`, `ProtocolServerView.kt`, `NewServerView.kt`, `ScanProtocolServer.kt`, `AdvancedNetworkSettings.kt`, `OperatorView.kt` + +#### Audio & Video Calls (`CallSettingsView`) + +| Setting | Description | +|---|---| +| WebRTC relay policy | Always relay / relay when needed / never relay | +| ICE servers | Custom STUN/TURN server configuration | + +#### Privacy & Security (`PrivacySettingsView`) + +Organized in sections: + +**Device Section** (`PrivacyDeviceSection`): + +| Setting | Description | +|---|---| +| SimpleX Lock | `SimplexLockView` -- app lock with system auth or passcode (`LAMode.SYSTEM` / `LAMode.PASSCODE`) | + +**Chats Section**: + +| Setting | Preference Key | Description | +|---|---|---| +| Send link previews | `privacyLinkPreviews` | Auto-generate link preview cards | +| Sanitize links | `privacySanitizeLinks` | Strip tracking parameters from URLs | +| Show last messages | `privacyShowChatPreviews` | Show message previews in chat list | +| Message draft | `privacySaveLastDraft` | Save unsent message draft for each chat | + +**Files Section**: + +| Setting | Preference Key | Description | +|---|---|---| +| Encrypt local files | `privacyEncryptLocalFiles` | Encrypt files stored on device | +| Auto-accept images | `privacyAcceptImages` | Automatically download received images | +| Blur media radius | `privacyMediaBlurRadius` | Blur radius for media previews | +| Protect IP address | `privacyAskToApproveRelays` | Prompt before connecting to unknown file relays to protect IP address | + +#### Appearance (`AppearanceView`) + +Platform-specific composable (`expect fun AppearanceView`): + +| Setting | Description | +|---|---| +| Profile images | `ProfileImageSection` -- slider for profile image corner radius | +| Theme selection | Color scheme / theme picker | +| Language | App language selection | +| Chat wallpaper | Background image settings | +| Chat bubbles | Message bubble appearance configuration | +| Toolbar opacity | App bar transparency settings (`inAppBarsAlpha`) | +| Color picker | `ClassicColorPicker` for custom theme colors | + +### Chat Database Section + +| Row | Icon | Destination | Description | +|---|---|---|---| +| Database passphrase & export | `ic_database` | `DatabaseView` | Manage encryption, export/import database | +| Migrate to another device | `ic_ios_share` | `MigrateFromDeviceView` | Device migration (requires auth) | + +Database icon shows warning color (`WarningOrange`) when database is not encrypted or passphrase is not saved. + +### Help Section + +| Row | Icon | Destination | Description | +|---|---|---|---| +| How to use SimpleX Chat | `ic_help` | `HelpView` | Usage guide | +| What's new | `ic_add` | `WhatsNewView` | Version changelog | +| About SimpleX Chat | `ic_info` | `SimpleXInfo` (non-onboarding mode) | App information | +| Chat with the founder | `ic_tag` | Opens SimpleX link | Direct chat with SimpleX team | +| Send us an email | `ic_mail` | Opens mailto: | Email support | + +### Support Section + +| Row | Icon | Description | +|---|---|---| +| Contribute | `ic_keyboard` | Opens GitHub contribution page (hidden for Android Bundle) | +| Rate the app | `ic_star` | Opens Google Play / app store listing | +| Star on GitHub | `ic_github` | Opens GitHub repository | + +### App Section (`SettingsSectionApp`) + +Platform-specific section (expect/actual composable): + +| Row | Description | +|---|---| +| App updates (Desktop) | App update checker and installer | +| Developer tools | Toggle developer mode | +| Chat console | Opens `ChatConsoleView` terminal | +| Terminal always visible (Desktop) | Keep terminal window open | +| Install terminal app | Link to CLI app on GitHub | +| Reset all hints | Reset dismissed hint/card preferences | +| App version | Version string with build info; taps open `VersionInfoView` | + +## Source Files + +| File | Path | +|---|---| +| `SettingsView.kt` | `views/usersettings/SettingsView.kt` | +| `Appearance.kt` | `views/usersettings/Appearance.kt` | +| `PrivacySettings.kt` | `views/usersettings/PrivacySettings.kt` | +| `NetworkAndServers.kt` | `views/usersettings/networkAndServers/NetworkAndServers.kt` | +| `AdvancedNetworkSettings.kt` | `views/usersettings/networkAndServers/AdvancedNetworkSettings.kt` | +| `OperatorView.kt` | `views/usersettings/networkAndServers/OperatorView.kt` | +| `ProtocolServersView.kt` | `views/usersettings/networkAndServers/ProtocolServersView.kt` | +| `NewServerView.kt` | `views/usersettings/networkAndServers/NewServerView.kt` | diff --git a/apps/multiplatform/product/views/user-profiles.md b/apps/multiplatform/product/views/user-profiles.md new file mode 100644 index 0000000000..dfc37a5e8d --- /dev/null +++ b/apps/multiplatform/product/views/user-profiles.md @@ -0,0 +1,122 @@ +# User Profiles + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) + +## Purpose + +Manage multiple chat profiles within a single app instance. Users can create, switch between, hide, mute, and delete profiles. Hidden profiles are protected by password. The UserPicker provides quick profile switching from the chat list, while UserProfilesView offers full profile management. + +## Route / Navigation + +- **Entry point**: Tap user avatar in `ChatListView` toolbar -> `UserPicker` -> "Your chat profiles" +- **Presented by**: `UserProfilesView` composable via `ModalManager.start.showCustomModal` with search bar +- **Navigation title**: "Your chat profiles" (`AppBarTitle`) +- **Sub-navigation**: + - Create profile -> `CreateProfile` (via `ModalManager.center`) + - Edit active profile -> `UserProfileView` (via UserPicker tap on active user) + - User address -> `UserAddressView` (via UserPicker) + - Chat preferences -> `PreferencesView` (via UserPicker) + +## Page Sections + +### UserPicker (`UserPicker.kt`) + +Overlay panel triggered from `ChatListView` toolbar: + +| Section | Description | +|---|---| +| Device picker row | `DevicePickerRow` showing local device and connected remote hosts (Desktop only); pill-shaped buttons with connect/disconnect actions | +| Active user profile | `ProfilePreview` of current user (Desktop: single row; Android: full user list) | +| User list | `UserPickerUsersSection` with all visible non-hidden profiles; tap to switch, long-press disabled | +| SimpleX address | Row to open `UserAddressView` (create or view address) | +| Chat preferences | Row to open `PreferencesView` | +| Chat profiles | Row to open `UserProfilesView` (or `CreateProfile` when no users exist on Desktop) | +| Use from desktop/mobile | Android: "Use from desktop" (`ConnectDesktopView`); Desktop: "Link a mobile" / "Linked mobiles" (`ConnectMobileView`) | +| Settings | Row to open `SettingsView` with `ColorModeSwitcher` trailing | + +Platform behavior: +- **Android**: `PlatformUserPicker` renders as bottom sheet with `AnimatedViewState` transitions; shows all users inline +- **Desktop**: Sidebar panel; shows only active user in header, inactive users in separate section below divider + +### UserProfilesView + +Full profile management screen with search/password field: + +#### Search / Password Field + +Combined text field at the top (`searchTextOrPassword`): +- In normal mode: Filters visible profiles by name +- For hidden profiles: Acts as password entry to reveal hidden profiles +- Trimmed search text compared against `user.anyNameContains()` and `correctPassword()` + +#### Profile List + +Each row rendered by `UserView` -> `UserProfilePickerItem`: + +| Element | Description | +|---|---| +| Active indicator | Checkmark icon (`ic_done_filled`) for the current active profile | +| Profile image | 54dp avatar with `fontSizeSqrtMultiplier` scaling | +| Display name | Profile's display name; bold for active, normal for inactive | +| Unread count | Badge showing unread message count (`unreadCountStr`) with primary/secondary color based on mute state | +| Muted indicator | `ic_notifications_off` icon when profile notifications are muted | +| Hidden indicator | `ic_lock` icon for hidden profiles (only shown when revealed via password) | + +#### Profile Row Tap Action + +| Action | Description | +|---|---| +| Switch active | Tapping a profile row calls `changeActiveUser()` to activate the selected profile; all chats switch context | + +#### Profile Actions (Context Menu) + +Available via long-press / right-click on a profile row (`DefaultDropdownMenu`): + +| Action | Condition | Description | +|---|---|---| +| Mute | Visible, notifications on | `apiMuteUser()` mutes notifications; shows `showMuteProfileAlert` on first use | +| Unmute | Visible, notifications off | `apiUnmuteUser()` restores notifications | +| Hide | Visible, multiple visible users | Opens `HiddenProfileView` to set password | +| Unhide | Hidden profile | `apiUnhideUser()` with password entry (`ProfileActionView` with `UserProfileAction.UNHIDE`) | +| Delete | Any non-sole profile | Delete with confirmation dialog; options: "Delete with connections" (removes SMP queues) or "Delete data only" | + +#### Add Profile + +| Element | Description | +|---|---| +| Add button | "+" icon with "Add profile" text at bottom of list (hidden when searching) | +| Auth required | Profile creation requires authentication via `withAuth` | +| Create view | Opens `CreateProfile` in `ModalManager.center` | + +#### Profile Deletion (`removeUser`) + +Deletion flow: +1. If hidden profile requiring password: opens `ProfileActionView` with `UserProfileAction.DELETE` +2. If active profile: switches to another visible user first via `changeActiveUser_`, then deletes +3. If last visible profile with hidden profiles: deletes user, then changes active to null; on Android, stops chat and resets to onboarding +4. Cleans up wallpaper files and cancels notifications for the deleted user + +#### Hidden Profile Notice + +Shown once via `showHiddenProfilesNotice` preference: + +| Element | Description | +|---|---| +| Alert title | "Make profile private" | +| Alert text | "You can hide or mute user profile" | +| "Don't show again" | Disables the notice permanently | + +### Profile Password Validation + +| Function | Description | +|---|---| +| `correctPassword()` | Validates password against `user.viewPwdHash` using `chatPasswordHash(pwd, salt)` | +| `passwordEntryRequired()` | Returns true if user is hidden, active, and password does not match current search text | +| `userViewPassword()` | Extracts view password from search text for hidden user operations | + +## Source Files + +| File | Path | +|---|---| +| `UserProfilesView.kt` | `views/usersettings/UserProfilesView.kt` | +| `UserPicker.kt` | `views/chatlist/UserPicker.kt` | diff --git a/apps/multiplatform/spec/README.md b/apps/multiplatform/spec/README.md new file mode 100644 index 0000000000..c5d9a3b4f7 --- /dev/null +++ b/apps/multiplatform/spec/README.md @@ -0,0 +1,137 @@ +# SimpleX Chat -- Kotlin Multiplatform Specification + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Dependency Graph](#dependency-graph) +3. [Specification Documents](#specification-documents) +4. [Product Documents](#product-documents) +5. [Source Entry Points](#source-entry-points) + +--- + +## 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](architecture.md) | System layers, module structure, JNI bridge, app lifecycle, event streaming, platform abstraction | +| State Management | [spec/state.md](state.md) | ChatModel singleton, ChatsContext, Chat data class, AppPreferences, ActiveChatState | +| API | [spec/api.md](api.md) | ChatController command dispatch, ~150 API functions in 11 categories, CC/CR/API types | +| Database | [spec/database.md](database.md) | SQLite database files, migrations, encryption, backup/restore | +| Impact | [spec/impact.md](impact.md) | Source file → product concept mapping for change impact analysis | +| Chat View | [spec/client/chat-view.md](client/chat-view.md) | ChatView, ChatItemView, message rendering, item interactions | +| Chat List | [spec/client/chat-list.md](client/chat-list.md) | ChatListView, ChatPreviewView, filtering, search, tags | +| Compose | [spec/client/compose.md](client/compose.md) | ComposeView, SendMsgView, ComposeState, attachments, mentions | +| Navigation | [spec/client/navigation.md](client/navigation.md) | App screen routing, onboarding, settings, new chat flows | +| Calls | [spec/services/calls.md](services/calls.md) | WebRTC call lifecycle, signaling, platform-specific call views | +| Files | [spec/services/files.md](services/files.md) | File transfer (SMP inline / XFTP), CryptoFile encryption, platform file paths | +| Notifications | [spec/services/notifications.md](services/notifications.md) | NtfManager, SimplexService, notification channels, background delivery | +| Theme | [spec/services/theme.md](services/theme.md) | ThemeManager, color system, wallpapers, per-user overrides | + +--- + +## Product Documents + +| Category | Path | Topic | +|---|---|---| +| Overview | [product/README.md](../product/README.md) | Product overview, capability map, navigation map | +| Concepts | [product/concepts.md](../product/concepts.md) | 30 product concepts (PC1-PC30) mapped to docs + source | +| Glossary | [product/glossary.md](../product/glossary.md) | Domain term definitions (9 sections) | +| Rules | [product/rules.md](../product/rules.md) | 18 business rules in 6 categories | +| Gaps | [product/gaps.md](../product/gaps.md) | 7 known gaps with recommendations | +| Flows | [product/flows/](../product/flows/) | onboarding, messaging, connection, calling, file-transfer, group-lifecycle | +| Views | [product/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`](../android/src/main/java/chat/simplex/app/SimplexApp.kt#L41) | `class SimplexApp` | 41 | +| Android Activity | [`MainActivity.kt`](../android/src/main/java/chat/simplex/app/MainActivity.kt#L27) | `class MainActivity` | 27 | +| Desktop Entry | [`Main.kt`](../desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt#L21) | `fun main()` | 21 | +| Desktop App Window | [`DesktopApp.kt`](../common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt#L33) | `fun showApp()` | 33 | +| Desktop Init | [`AppCommon.desktop.kt`](../common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt#L21) | `fun initApp()` | 21 | +| Common App Screen | [`App.kt`](../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L47) | `fun AppScreen()` | 47 | +| JNI Bridge | [`Core.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L18) | `external fun initHS()` | 18 | +| Chat Controller | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L493) | `object ChatController` | 493 | +| Chat Model | [`ChatModel.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L86) | `object ChatModel` | 86 | +| App Preferences | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L94) | `class AppPreferences` | 94 | +| Platform Interface | [`Platform.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt#L15) | `interface PlatformInterface` | 15 | +| Notification Manager | [`NtfManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L19) | `abstract class NtfManager` | 19 | +| Theme Manager | [`ThemeManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L18) | `object ThemeManager` | 18 | +| Android Haskell Init | [`AppCommon.android.kt`](../common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt#L33) | `fun initHaskell(packageName: String)` | 33 | +| Common Migrations | [`AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt#L41) | `fun runMigrations()` | 41 | +| Android Service | [`SimplexService.kt`](../android/src/main/java/chat/simplex/app/SimplexService.kt#L41) | `class SimplexService` | 41 | +| Gradle Root | [`settings.gradle.kts`](../settings.gradle.kts#L22) | `include(":android", ":desktop", ":common")` | 22 | +| Common Build | [`build.gradle.kts`](../common/build.gradle.kts#L14) | `kotlin { androidTarget(); jvm("desktop") }` | 14 | diff --git a/apps/multiplatform/spec/api.md b/apps/multiplatform/spec/api.md new file mode 100644 index 0000000000..15d5e141a0 --- /dev/null +++ b/apps/multiplatform/spec/api.md @@ -0,0 +1,435 @@ +# Chat API Reference + +## Table of Contents + +1. [Overview](#1-overview) +2. [Command Categories](#2-command-categories) + - 2.1 [User Management](#21-user-management) + - 2.2 [Chat Lifecycle](#22-chat-lifecycle) + - 2.3 [Message Operations](#23-message-operations) + - 2.4 [Group Operations](#24-group-operations) + - 2.5 [Contact Operations](#25-contact-operations) + - 2.6 [File Operations](#26-file-operations) + - 2.7 [Call Operations](#27-call-operations) + - 2.8 [Settings & Network](#28-settings--network) + - 2.9 [Chat Tags](#29-chat-tags) + - 2.10 [Server Operators](#210-server-operators) + - 2.11 [Archive](#211-archive) +3. [Response Types](#3-response-types) +4. [Event Types](#4-event-types) +5. [Error Types](#5-error-types) +6. [Source Files](#6-source-files) + +--- + +## 1. Overview + +The SimpleX Chat API bridge connects Kotlin/Compose UI code to the Haskell core via JNI. All communication follows a **command/response JSON protocol**: + +``` +Kotlin suspend fun api*() + -> ChatController.sendCmd(rhId, CC.*, ctrl) + -> serialize CC to cmdString (JSON) + -> chatSendCmdRetry(ctrl, cmdString, retryNum) [JNI / external fun] + -> Haskell core processes command + -> returns JSON response string + -> json.decodeFromString(responseString) + -> API.Result(rhId, CR.*) or API.Error(rhId, ChatError) + -> pattern-match on CR subclass -> update ChatModel / return data to UI +``` + +**Key types in the pipeline:** + +| Type | Role | Location | +|------|------|----------| +| `CC` (sealed class) | Command definitions (~165 subclasses) | [SimpleXAPI.kt#L3529](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L3529) | +| `API` (sealed class) | Top-level response wrapper (`Result` / `Error`) | [SimpleXAPI.kt#L5975](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L5975) | +| `CR` (sealed class) | Chat response variants (~180 subclasses) | [SimpleXAPI.kt#L6114](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L6114) | +| `ChatError` (sealed class) | Error hierarchy | [SimpleXAPI.kt#L6974](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L6974) | +| `ChatController` (object) | Singleton hosting all `api*` functions | [SimpleXAPI.kt#L493](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L493) | + +**JNI bridge functions** (declared in [Core.kt#L25](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L25)): + +```kotlin +external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array +external fun chatCloseStore(ctrl: ChatCtrl): String +external fun chatSendCmdRetry(ctrl: ChatCtrl, msg: String, retryNum: Int): String +external fun chatSendRemoteCmdRetry(ctrl: ChatCtrl, rhId: Int, msg: String, retryNum: Int): String +external fun chatRecvMsg(ctrl: ChatCtrl): String +external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String +``` + + + +**`sendCmd` flow** ([SimpleXAPI.kt#L804](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L804)): + +1. Obtains the `ChatCtrl` handle (or uses the provided `otherCtrl`). +2. Serializes the `CC` command to its `cmdString`. +3. Dispatches to `Dispatchers.IO`; calls `chatSendCmdRetry` (local) or `chatSendRemoteCmdRetry` (remote host). +4. Decodes the returned JSON string into `API`. +5. Logs the result to the terminal item list. + + + + + +**Asynchronous event receiver** (`startReceiver`, [SimpleXAPI.kt#L660](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L660)): + +A long-running coroutine on `Dispatchers.IO` repeatedly calls `chatRecvMsgWait` (blocking JNI). Each received `API` message is dispatched to `processReceivedMsg` ([SimpleXAPI.kt#L2568](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2568)), which pattern-matches on `CR` subclasses to update `ChatModel` state and trigger notifications. + +--- + + + +## 2. Command Categories + +All functions below are `suspend fun` members of `ChatController` ([SimpleXAPI.kt#L493](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L493)). The `rh` / `rhId` parameter is `Long?` identifying a remote host (`null` = local device). + +### 2.1 User Management + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiGetActiveUser` | `rh: Long?, ctrl: ChatCtrl?` | Fetch the currently active user profile | [L841](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L841) | +| `apiCreateActiveUser` | `rh: Long?, p: Profile?, pastTimestamp: Boolean, ctrl: ChatCtrl?` | Create a new user profile and set it as active | [L851](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L851) | +| `listUsers` | `rh: Long?` | List all user profiles sorted by display name | [L871](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L871) | +| `apiSetActiveUser` | `rh: Long?, userId: Long, viewPwd: String?` | Switch the active user to a different profile | [L881](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L881) | +| `apiSetAllContactReceipts` | `rh: Long?, enable: Boolean` | Enable/disable delivery receipts for all contacts globally | [L888](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L888) | +| `apiSetUserContactReceipts` | `u: User, userMsgReceiptSettings: UserMsgReceiptSettings` | Set delivery receipt settings for user contacts | [L894](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L894) | +| `apiSetUserGroupReceipts` | `u: User, userMsgReceiptSettings: UserMsgReceiptSettings` | Set delivery receipt settings for user groups | [L900](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L900) | +| `apiSetUserAutoAcceptMemberContacts` | `u: User, enable: Boolean` | Toggle auto-accept for member contact requests | [L906](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L906) | +| `apiHideUser` | `u: User, viewPwd: String` | Hide a user profile behind a password | [L912](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L912) | +| `apiUnhideUser` | `u: User, viewPwd: String` | Unhide a previously hidden user profile | [L915](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L915) | +| `apiMuteUser` | `u: User` | Mute all notifications for a user profile | [L918](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L918) | +| `apiUnmuteUser` | `u: User` | Unmute notifications for a user profile | [L921](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L921) | +| `apiDeleteUser` | `u: User, delSMPQueues: Boolean, viewPwd: String?` | Delete a user profile and optionally its SMP queues | [L930](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L930) | +| `apiUpdateProfile` | `rh: Long?, profile: Profile` | Update the active user's display profile | [L1682](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1682) | +| `apiSetProfileAddress` | `rh: Long?, on: Boolean` | Enable/disable including address in user profile | [L1694](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1694) | +| `apiSetUserUIThemes` | `rh: Long?, userId: Long, themes: ThemeModeOverrides?` | Set UI theme overrides for a user | [L1732](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1732) | + +### 2.2 Chat Lifecycle + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiStartChat` | `ctrl: ChatCtrl?` | Start the chat engine (returns `true` if newly started) | [L937](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L937) | +| `apiStopChat` | _(none)_ | Stop the chat engine | [L955](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L955) | +| `apiSetAppFilePaths` | `filesFolder, tempFolder, assetsFolder, remoteHostsFolder: String, ctrl: ChatCtrl?` | Configure file-system paths for the Haskell core | [L961](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L961) | +| `apiSetEncryptLocalFiles` | `enable: Boolean` | Enable/disable encryption of locally stored files | [L967](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L967) | +| `apiSaveAppSettings` | `settings: AppSettings` | Persist application settings to the core | [L969](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L969) | +| `apiGetAppSettings` | `settings: AppSettings` | Retrieve application settings from the core | [L975](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L975) | +| `apiGetChats` | `rh: Long?` | Fetch the list of all chats for the active user | [L1013](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1013) | +| `apiGetChat` | `rh, type, id, scope, contentTag, pagination, search` | Fetch a single chat with paginated messages | [L1031](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1031) | +| `apiGetChatContentTypes` | `rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?` | Get available content type filters for a chat | [L1044](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1044) | +| `apiClearChat` | `rh: Long?, type: ChatType, id: Long` | Delete all messages in a chat | [L1675](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1675) | +| `apiDeleteChat` | `rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode` | Delete a chat (contact, group, connection, etc.) | [L1620](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1620) | +| `apiChatRead` | `rh: Long?, type: ChatType, id: Long` | Mark a chat as read | [L1888](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1888) | +| `apiChatItemsRead` | `rh, type, id, scope, itemIds` | Mark specific chat items as read | [L1902](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1902) | +| `apiChatUnread` | `rh: Long?, type: ChatType, id: Long, unreadChat: Boolean` | Toggle a chat's unread flag | [L1909](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1909) | +| `getChatItemTTL` | `rh: Long?` | Get the auto-delete TTL for chat items | [L1286](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1286) | +| `setChatItemTTL` | `rh: Long?, chatItemTTL: ChatItemTTL` | Set the auto-delete TTL for chat items | [L1299](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1299) | +| `setChatTTL` | `rh: Long?, chatType, id, chatItemTTL` | Set TTL for a specific chat | [L1306](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1306) | + +### 2.3 Message Operations + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiSendMessages` | `rh, type, id, scope, live, ttl, composedMessages` | Send one or more messages to a chat | [L1074](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1074) | +| `apiCreateChatItems` | `rh: Long?, noteFolderId: Long, composedMessages: List` | Create items in a private notes folder | [L1111](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1111) | +| `apiReportMessage` | `rh, groupId, chatItemId, reportReason, reportText` | Report a message in a group | [L1119](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1119) | +| `apiGetChatItemInfo` | `rh, type, id, scope, itemId` | Get delivery info for a specific chat item | [L1126](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1126) | +| `apiForwardChatItems` | `rh, toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl` | Forward messages between chats | [L1133](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1133) | +| `apiPlanForwardChatItems` | `rh, fromChatType, fromChatId, fromScope, chatItemIds` | Check forward feasibility before forwarding | [L1138](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1138) | +| `apiUpdateChatItem` | `rh, type, id, scope, itemId, updatedMessage, live` | Edit an existing message | [L1145](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1145) | +| `apiChatItemReaction` | `rh, type, id, scope, itemId, add, reaction` | Add or remove a reaction to a message | [L1168](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1168) | +| `apiGetReactionMembers` | `rh: Long?, groupId: Long, itemId: Long, reaction: MsgReaction` | List members who reacted with a specific emoji | [L1175](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1175) | +| `apiDeleteChatItems` | `rh, type, id, scope, itemIds, mode` | Delete messages (for self or for everyone) | [L1183](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1183) | +| `apiDeleteMemberChatItems` | `rh: Long?, groupId: Long, itemIds: List` | Moderate: delete another member's messages | [L1190](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1190) | +| `apiArchiveReceivedReports` | `rh: Long?, groupId: Long` | Archive all received reports in a group | [L1197](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1197) | +| `apiDeleteReceivedReports` | `rh: Long?, groupId: Long, itemIds: List, mode: CIDeleteMode` | Delete specific received reports | [L1204](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1204) | + +### 2.4 Group Operations + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiNewGroup` | `rh: Long?, incognito: Boolean, groupProfile: GroupProfile` | Create a new group | [L2092](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2092) | +| `apiAddMember` | `rh: Long?, groupId: Long, contactId: Long, memberRole: GroupMemberRole` | Invite a contact to a group | [L2100](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2100) | +| `apiJoinGroup` | `rh: Long?, groupId: Long` | Accept a group invitation | [L2109](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2109) | +| `apiAcceptMember` | `rh: Long?, groupId: Long, groupMemberId: Long, memberRole: GroupMemberRole` | Accept a member joining via group link | [L2135](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2135) | +| `apiDeleteMemberSupportChat` | `rh: Long?, groupId: Long, groupMemberId: Long` | Delete a member's support chat | [L2144](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2144) | +| `apiRemoveMembers` | `rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean` | Remove members from a group | [L2151](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2151) | +| `apiMembersRole` | `rh: Long?, groupId: Long, memberIds: List, memberRole: GroupMemberRole` | Change the role of group members | [L2160](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2160) | +| `apiBlockMembersForAll` | `rh: Long?, groupId: Long, memberIds: List, blocked: Boolean` | Block/unblock members for all group participants | [L2169](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2169) | +| `apiLeaveGroup` | `rh: Long?, groupId: Long` | Leave a group | [L2178](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2178) | +| `apiListMembers` | `rh: Long?, groupId: Long` | List all members of a group | [L2185](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2185) | +| `apiUpdateGroup` | `rh: Long?, groupId: Long, groupProfile: GroupProfile` | Update group profile (name, image, etc.) | [L2192](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2192) | +| `apiCreateGroupLink` | `rh: Long?, groupId: Long, memberRole: GroupMemberRole` | Create a group invitation link | [L2211](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2211) | +| `apiGroupLinkMemberRole` | `rh: Long?, groupId: Long, memberRole: GroupMemberRole` | Update the default role for group link joins | [L2226](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2226) | +| `apiDeleteGroupLink` | `rh: Long?, groupId: Long` | Delete the group invitation link | [L2235](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2235) | +| `apiGetGroupLink` | `rh: Long?, groupId: Long` | Retrieve the current group invitation link | [L2245](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2245) | +| `apiAddGroupShortLink` | `rh: Long?, groupId: Long` | Create a short link for the group | [L2252](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2252) | +| `apiCreateMemberContact` | `rh: Long?, groupId: Long, groupMemberId: Long` | Create a direct contact from a group member | [L2262](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2262) | +| `apiSendMemberContactInvitation` | `rh: Long?, contactId: Long, mc: MsgContent` | Send a direct message invitation to a group member | [L2271](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2271) | +| `apiAcceptMemberContact` | `rh: Long?, contactId: Long` | Accept a member's direct contact invitation | [L2280](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2280) | +| `apiSetMemberSettings` | `rh: Long?, groupId: Long, groupMemberId: Long, memberSettings: GroupMemberSettings` | Configure per-member settings (e.g., mentions) | [L1343](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1343) | +| `apiGroupMemberInfo` | `rh: Long?, groupId: Long, groupMemberId: Long` | Get a group member's info and connection stats | [L1353](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1353) | +| `apiSetGroupAlias` | `rh: Long?, groupId: Long, localAlias: String` | Set a local alias for a group | [L1718](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1718) | + +### 2.5 Contact Operations + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiAddContact` | `rh: Long?, incognito: Boolean` | Create a one-time invitation link for a new contact | [L1444](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1444) | +| `apiSetConnectionIncognito` | `rh: Long?, connId: Long, incognito: Boolean` | Toggle incognito on a pending connection | [L1455](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1455) | +| `apiChangeConnectionUser` | `rh: Long?, connId: Long, userId: Long` | Change the user profile on a pending connection | [L1464](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1464) | +| `apiConnectPlan` | `rh: Long?, connLink: String, inProgress: MutableState` | Analyze a connection link before connecting | [L1474](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1474) | +| `apiConnect` | `rh: Long?, incognito: Boolean, connLink: CreatedConnLink` | Connect via an invitation or address link | [L1482](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1482) | +| `apiPrepareContact` | `rh, connLink, contactShortLinkData` | Prepare a contact chat from a short link (before connecting) | [L1546](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1546) | +| `apiPrepareGroup` | `rh, connLink, groupShortLinkData` | Prepare a group chat from a short link (before connecting) | [L1555](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1555) | +| `apiConnectPreparedContact` | `rh, contactId, incognito, msg` | Connect to a previously prepared contact | [L1580](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1580) | +| `apiConnectPreparedGroup` | `rh, groupId, incognito, msg` | Join a previously prepared group | [L1590](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1590) | +| `apiConnectContactViaAddress` | `rh: Long?, incognito: Boolean, contactId: Long` | Connect to a contact using their public address | [L1600](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1600) | +| `apiDeleteContact` | `rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode` | Delete a contact and return the deleted Contact | [L1644](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1644) | +| `apiContactInfo` | `rh: Long?, contactId: Long` | Get a contact's connection stats and custom profile | [L1346](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1346) | +| `apiSetContactAlias` | `rh: Long?, contactId: Long, localAlias: String` | Set a local display alias for a contact | [L1711](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1711) | +| `apiSetConnectionAlias` | `rh: Long?, connId: Long, localAlias: String` | Set a local display alias for a pending connection | [L1725](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1725) | +| `apiSetContactPrefs` | `rh: Long?, contactId: Long, prefs: ChatPreferences` | Update feature preferences for a contact | [L1704](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1704) | +| `apiCreateUserAddress` | `rh: Long?` | Create a long-term public contact address | [L1746](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1746) | +| `apiDeleteUserAddress` | `rh: Long?` | Delete the user's public contact address | [L1762](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1762) | +| `apiAddMyAddressShortLink` | `rh: Long?` | Create a short link for the user's address | [L1784](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1784) | +| `apiSetUserAddressSettings` | `rh: Long?, settings: AddressSettings` | Configure auto-accept for incoming contact requests | [L1795](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1795) | +| `apiAcceptContactRequest` | `rh: Long?, incognito: Boolean, contactReqId: Long` | Accept an incoming contact request | [L1809](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1809) | +| `apiRejectContactRequest` | `rh: Long?, contactReqId: Long` | Reject an incoming contact request | [L1832](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1832) | +| `apiSwitchContact` | `rh: Long?, contactId: Long` | Initiate SMP server switch for a contact | [L1374](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1374) | +| `apiAbortSwitchContact` | `rh: Long?, contactId: Long` | Abort an in-progress server switch | [L1388](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1388) | +| `apiSyncContactRatchet` | `rh: Long?, contactId: Long, force: Boolean` | Force ratchet synchronization with a contact | [L1402](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1402) | +| `apiGetContactCode` | `rh: Long?, contactId: Long` | Get the security verification code for a contact | [L1416](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1416) | +| `apiVerifyContact` | `rh: Long?, contactId: Long, connectionCode: String?` | Verify a contact's security code | [L1430](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1430) | + +### 2.6 File Operations + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `receiveFiles` | `rhId, user, fileIds, userApprovedRelays, auto` | Accept and download one or more files (handles relay approval) | [L1946](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1946) | +| `receiveFile` | `rhId, user, fileId, userApprovedRelays, auto` | Accept and download a single file (convenience wrapper) | [L2062](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2062) | +| `cancelFile` | `rh: Long?, user: User, fileId: Long` | Cancel an in-progress file transfer and clean up | [L2072](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2072) | +| `apiCancelFile` | `rh: Long?, fileId: Long, ctrl: ChatCtrl?` | Cancel a file transfer (low-level, returns updated chat item) | [L2080](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2080) | +| `uploadStandaloneFile` | `user: UserLike, file: CryptoFile, ctrl: ChatCtrl?` | Upload a standalone file (used for migration) | [L1916](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1916) | +| `downloadStandaloneFile` | `user: UserLike, url: String, file: CryptoFile, ctrl: ChatCtrl?` | Download a standalone file by URL | [L1926](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1926) | +| `standaloneFileInfo` | `url: String, ctrl: ChatCtrl?` | Retrieve metadata for a standalone file link | [L1936](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1936) | + +### 2.7 Call Operations + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiGetCallInvitations` | `rh: Long?` | Retrieve pending call invitations | [L1842](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1842) | +| `apiSendCallInvitation` | `rh: Long?, contact: Contact, callType: CallType` | Initiate a call by sending an invitation | [L1849](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1849) | +| `apiRejectCall` | `rh: Long?, contact: Contact` | Reject an incoming call | [L1854](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1854) | +| `apiSendCallOffer` | `rh, contact, rtcSession, rtcIceCandidates, media, capabilities` | Send a WebRTC call offer | [L1859](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1859) | +| `apiSendCallAnswer` | `rh: Long?, contact: Contact, rtcSession: String, rtcIceCandidates: String` | Send a WebRTC call answer | [L1866](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1866) | +| `apiSendCallExtraInfo` | `rh: Long?, contact: Contact, rtcIceCandidates: String` | Send additional ICE candidates during a call | [L1872](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1872) | +| `apiEndCall` | `rh: Long?, contact: Contact` | End an active call | [L1878](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1878) | +| `apiCallStatus` | `rh: Long?, contact: Contact, status: WebRTCCallStatus` | Report call status updates to the core | [L1883](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1883) | + +### 2.8 Settings & Network + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiSetNetworkConfig` | `cfg: NetCfg, showAlertOnError: Boolean, ctrl: ChatCtrl?` | Apply network configuration (SOCKS proxy, timeouts, etc.) | [L1313](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1313) | +| `apiSetNetworkInfo` | `networkInfo: UserNetworkInfo` | Update network reachability information | [L1340](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1340) | +| `apiSetSettings` | `rh: Long?, type: ChatType, id: Long, settings: ChatSettings` | Update per-chat settings (notifications, favorites) | [L1333](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1333) | +| `apiStorageEncryption` | `currentKey: String, newKey: String` | Change the database encryption passphrase | [L999](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L999) | +| `testStorageEncryption` | `key: String, ctrl: ChatCtrl?` | Verify a database encryption key is correct | [L1006](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1006) | +| `testProtoServer` | `rh: Long?, server: String` | Test connectivity to a protocol server | [L1211](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1211) | +| `reconnectServer` | `rh: Long?, server: String` | Reconnect to a specific server | [L1326](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1326) | +| `reconnectAllServers` | `rh: Long?` | Reconnect to all servers | [L1331](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1331) | +| `apiSetChatUIThemes` | `rh: Long?, chatId: ChatId, themes: ThemeModeOverrides?` | Set per-chat UI theme overrides | [L1739](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1739) | +| `apiContactQueueInfo` | `rh: Long?, contactId: Long` | Get server queue diagnostics for a contact | [L1360](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1360) | +| `apiGroupMemberQueueInfo` | `rh: Long?, groupId: Long, groupMemberId: Long` | Get server queue diagnostics for a group member | [L1367](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1367) | + +### 2.9 Chat Tags + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiCreateChatTag` | `rh: Long?, tag: ChatTagData` | Create a new chat tag (folder/label) | [L1052](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1052) | +| `apiSetChatTags` | `rh: Long?, type: ChatType, id: Long, tagIds: List` | Assign tags to a chat | [L1060](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1060) | +| `apiDeleteChatTag` | `rh: Long?, tagId: Long` | Delete a chat tag | [L1068](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1068) | +| `apiUpdateChatTag` | `rh: Long?, tagId: Long, tag: ChatTagData` | Update a chat tag's name or emoji | [L1070](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1070) | +| `apiReorderChatTags` | `rh: Long?, tagIds: List` | Set the display order of chat tags | [L1072](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1072) | + +### 2.10 Server Operators + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `getServerOperators` | `rh: Long?` | Get server operator conditions detail | [L1219](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1219) | +| `setServerOperators` | `rh: Long?, operators: List` | Update the list of server operators | [L1226](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1226) | +| `getUserServers` | `rh: Long?` | Get the user's configured servers per operator | [L1233](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1233) | +| `setUserServers` | `rh: Long?, userServers: List` | Save user's configured servers per operator | [L1241](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1241) | +| `validateServers` | `rh: Long?, userServers: List` | Validate server configuration for errors | [L1253](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1253) | +| `getUsageConditions` | `rh: Long?` | Get current and accepted usage conditions | [L1261](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1261) | +| `setConditionsNotified` | `rh: Long?, conditionsId: Long` | Mark conditions as shown to user | [L1268](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1268) | +| `acceptConditions` | `rh: Long?, conditionsId: Long, operatorIds: List` | Accept usage conditions for operators | [L1275](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1275) | + +### 2.11 Archive + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiExportArchive` | `config: ArchiveConfig` | Export chat database to a ZIP archive | [L981](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L981) | +| `apiImportArchive` | `config: ArchiveConfig` | Import chat database from a ZIP archive | [L987](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L987) | +| `apiDeleteStorage` | _(none)_ | Delete all chat database storage | [L993](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L993) | + + + +`ArchiveConfig` ([SimpleXAPI.kt#L4162](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L4162)): + +```kotlin +class ArchiveConfig( + val archivePath: String, + val disableCompression: Boolean? = null, + val parentTempDirectory: String? = null +) +``` + +--- + + + +## 3. Response Types + +All command responses are deserialized into the `API` sealed class ([SimpleXAPI.kt#L5975](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L5975)): + +```kotlin +sealed class API { + class Result(val remoteHostId: Long?, val res: CR) : API() + class Error(val remoteHostId: Long?, val err: ChatError) : API() +} +``` + + + +The `CR` sealed class ([SimpleXAPI.kt#L6114](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L6114)) contains approximately 180 response variants. Key categories: + +| Category | Examples | Lines | +|----------|---------|-------| +| User | `ActiveUser`, `UsersList`, `UserPrivacy`, `UserProfileUpdated` | L6104-L6157 | +| Chat state | `ChatStarted`, `ChatRunning`, `ChatStopped`, `ApiChats`, `ApiChat` | L6106-L6110 | +| Tags | `ChatTags`, `TagsUpdated` | L6112, L6137 | +| Contacts | `Invitation`, `SentConfirmation`, `SentInvitation`, `ContactConnected`, `ContactDeleted` | L6138-L6165 | +| Messages | `NewChatItems`, `ChatItemUpdated`, `ChatItemsDeleted`, `ChatItemReaction`, `ForwardPlan` | L6176-L6184 | +| Groups | `GroupCreated`, `SentGroupInvitation`, `UserAcceptedGroupSent`, `GroupUpdated`, `GroupMembers` | L6186-L6219 | +| Files (receive) | `RcvFileAccepted`, `RcvFileStart`, `RcvFileComplete`, `RcvFileCancelled`, `RcvFileError` | L6221-L6232 | +| Files (send) | `SndFileStart`, `SndFileComplete`, `SndFileCancelled`, `SndFileCompleteXFTP` | L6234-L6244 | +| Calls | `CallInvitation`, `CallOffer`, `CallAnswer`, `CallExtraInfo`, `CallEnded` | L6246-L6251 | +| Remote host | `RemoteHostList`, `RemoteHostStarted`, `RemoteHostConnected`, `RemoteHostStopped` | L6255-L6262 | +| Remote ctrl | `RemoteCtrlList`, `RemoteCtrlFound`, `RemoteCtrlConnected`, `RemoteCtrlStopped` | L6264-L6269 | +| Encryption | `ContactPQAllowed`, `ContactPQEnabled` | L6271-L6272 | +| Misc | `CmdOk`, `ArchiveExported`, `ArchiveImported`, `AppSettingsR`, `VersionInfo` | L6274-L6283 | +| Fallback | `Response` (unknown type + raw JSON), `Invalid` (unparseable) | L6282-L6283 | + +Each `CR` subclass is annotated with `@Serializable @SerialName("jsonTag")` for polymorphic JSON deserialization. + +--- + +## 4. Event Types + +The chat core pushes asynchronous events through the same `CR` type hierarchy. The `startReceiver` coroutine ([SimpleXAPI.kt#L660](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L660)) continuously calls `chatRecvMsgWait` (blocking JNI), then dispatches each message to `processReceivedMsg` ([SimpleXAPI.kt#L2568](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2568)). + +Events handled in `processReceivedMsg` include: + +| Event | Description | +|-------|-------------| +| `ContactConnected` | A contact has completed the connection handshake | +| `ContactConnecting` | A contact connection is in progress | +| `ContactSndReady` | Contact's sending channel is ready | +| `ContactDeletedByContact` | A contact deleted their side of the conversation | +| `ReceivedContactRequest` | An incoming contact request arrived | +| `NewChatItems` | New messages received | +| `ChatItemUpdated` | A message was edited | +| `ChatItemsDeleted` | Messages were deleted | +| `ChatItemReaction` | A reaction was added/removed | +| `ChatItemsStatusesUpdated` | Delivery statuses updated | +| `GroupCreated` | A new group was created | +| `ReceivedGroupInvitation` | An invitation to join a group | +| `JoinedGroupMember` | A new member joined | +| `DeletedMember` / `DeletedMemberUser` | A member was removed | +| `LeftMember` | A member left voluntarily | +| `GroupUpdated` | Group profile changed | +| `MemberRole` | A member's role changed | +| `MemberBlockedForAll` | A member was blocked for all | +| `RcvFileStart` / `RcvFileComplete` / `RcvFileError` | File receive progress | +| `SndFileStart` / `SndFileComplete` / `SndFileError` | File send progress | +| `CallInvitation` / `CallOffer` / `CallAnswer` / `CallEnded` | Call signaling events | +| `ContactPQEnabled` | Post-quantum encryption status changed | +| `RemoteHostStopped` / `RemoteCtrlStopped` | Remote access session ended | +| `SubscriptionStatusEvt` | Connection subscription status changed | + +Each event triggers updates to `ChatModel` (reactive Compose state) and optionally fires platform notifications via `ntfManager`. + +--- + + + +## 5. Error Types + +### ChatError ([SimpleXAPI.kt#L6974](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L6974)) + +```kotlin +sealed class ChatError { + class ChatErrorChat(val errorType: ChatErrorType) // Application-level errors + class ChatErrorAgent(val agentError: AgentErrorType) // SMP/XFTP agent errors + class ChatErrorStore(val storeError: StoreError) // Database store errors + class ChatErrorDatabase(val databaseError: DatabaseError)// Database engine errors + class ChatErrorRemoteHost(val remoteHostError: ...) // Remote host errors + class ChatErrorRemoteCtrl(val remoteCtrlError: ...) // Remote controller errors + class ChatErrorInvalidJSON(val json: String) // JSON parsing failure +} +``` + +### ChatErrorType ([SimpleXAPI.kt#L7004](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7004)) + +Common application error codes (~70 variants): + +| Error | Meaning | +|-------|---------| +| `NoActiveUser` | No user profile is set as active | +| `UserExists` | Attempted to create a duplicate user | +| `InvalidDisplayName` | Display name contains invalid characters | +| `ChatNotStarted` / `ChatNotStopped` | Chat engine in wrong state | +| `InvalidConnReq` / `UnsupportedConnReq` | Bad or incompatible connection link | +| `ContactNotReady` / `ContactDisabled` | Contact in unusable state | +| `GroupUserRole` | Insufficient group permissions | +| `GroupNotJoined` | User has not joined the group | +| `FileNotFound` / `FileCancelled` / `FileAlreadyReceiving` | File transfer errors | +| `FileNotApproved` | File from unapproved relay server | +| `HasCurrentCall` / `NoCurrentCall` | Call state conflicts | +| `CommandError` / `InternalError` / `CEException` | Generic/internal errors | + +### StoreError ([SimpleXAPI.kt#L7168](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7168)) + +Database-level errors: `DuplicateName`, `UserNotFound`, `GroupNotFound`, `ChatItemNotFound`, `LargeMsg`, `UserContactLinkNotFound`, etc. + +### ArchiveError ([SimpleXAPI.kt#L7658](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7658)) + +```kotlin +sealed class ArchiveError { + class ArchiveErrorImport(val importError: String) + class ArchiveErrorFile(val file: String, val fileError: String) +} +``` + +--- + +## 6. Source Files + +| File | Purpose | Path | +|------|---------|------| +| SimpleXAPI.kt | API bridge: all `api*` functions, `CC`, `CR`, `ChatError` | `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` | +| Core.kt | JNI externals, `initChatController`, `chatMigrateInit` | `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` | +| ChatModel.kt | Reactive UI state (`ChatModel` object) | `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` | +| DatabaseUtils.kt | `DBMigrationResult`, `MigrationError`, DB password helpers | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` | +| Files.kt | Platform-expect file path declarations | `common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt` | +| Files.android.kt | Android actual file paths | `common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt` | +| Files.desktop.kt | Desktop actual file paths | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt` | +| Cryptor.kt | Platform-expect encryption interface | `common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt` | +| Cryptor.android.kt | Android: AndroidKeyStore AES-GCM encryption | `common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt` | +| Cryptor.desktop.kt | Desktop: placeholder (no-op) encryption | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt` | + +All paths are relative to `apps/multiplatform/`. diff --git a/apps/multiplatform/spec/architecture.md b/apps/multiplatform/spec/architecture.md new file mode 100644 index 0000000000..cfef4d06c2 --- /dev/null +++ b/apps/multiplatform/spec/architecture.md @@ -0,0 +1,423 @@ +# System Architecture + +## Table of Contents + +1. [Overview](#1-overview) +2. [Module Structure](#2-module-structure) +3. [JNI Bridge](#3-jni-bridge) +4. [App Lifecycle](#4-app-lifecycle) +5. [Event Streaming](#5-event-streaming) +6. [Platform Abstraction](#6-platform-abstraction) +7. [Source Files](#7-source-files) + +--- + +## 1. Overview + +The application is a three-layer system: + +``` ++------------------------------------------------------------------+ +| Compose UI (Views) | +| ChatListView, ChatView, ComposeView, SettingsView, CallView | ++------------------------------------------------------------------+ + | ^ + | user actions | Compose MutableState recomposition + v | ++------------------------------------------------------------------+ +| Application Logic Layer | +| ChatModel (state) ChatController (command dispatch) | +| AppPreferences NtfManager ThemeManager | ++------------------------------------------------------------------+ + | ^ + | sendCmd() | recvMsg() / processReceivedMsg() + v | ++------------------------------------------------------------------+ +| JNI Bridge (Core.kt) | +| external fun chatSendCmdRetry() external fun chatRecvMsgWait()| ++------------------------------------------------------------------+ + | ^ + | C FFI | C FFI + v | ++------------------------------------------------------------------+ +| Haskell Core (libsimplex / libapp-lib) | +| chat_ctrl handle SMP/XFTP protocols SQLite/PostgreSQL | ++------------------------------------------------------------------+ +``` + +**Data flow summary:** +1. User interacts with Compose UI. +2. View calls a `suspend fun api*()` method on `ChatController`. +3. `ChatController.sendCmd()` serializes the command to a JSON string and calls `chatSendCmdRetry()` (JNI). +4. The Haskell core processes the command and returns a JSON response string. +5. The response is deserialized to an `API` sealed class and returned to the caller. +6. Asynchronous events from the core (incoming messages, connection updates, call invitations) are delivered via a receiver coroutine that calls `chatRecvMsgWait()` in a loop and dispatches each event through `processReceivedMsg()`. + +--- + +## 2. Module Structure + +### Gradle Configuration + +Root: [`settings.gradle.kts`](../settings.gradle.kts#L22) +``` +include(":android", ":desktop", ":common") +``` + +### `:common` Module + +Build file: [`common/build.gradle.kts`](../common/build.gradle.kts#L14) + +``` +kotlin { + androidTarget() + jvm("desktop") +} +``` + +Source sets: + +| Source Set | Path | Purpose | +|---|---|---| +| `commonMain` | `common/src/commonMain/kotlin/` | All shared UI, models, platform abstractions | +| `androidMain` | `common/src/androidMain/kotlin/` | Android `actual` implementations | +| `desktopMain` | `common/src/desktopMain/kotlin/` | Desktop `actual` implementations | + +Key dependencies (from `commonMain`): +- `kotlinx-serialization-json` -- JSON codec for Haskell core communication +- `kotlinx-datetime` -- cross-platform date/time +- `multiplatform-settings` (russhwolf) -- `SharedPreferences` abstraction +- `kaml` -- YAML parsing (theme import/export) +- `boofcv-core` -- QR code scanning +- `jsoup` -- HTML parsing for link previews +- `moko-resources` -- cross-platform string/image resources +- `multiplatform-markdown-renderer` -- Markdown rendering in chat + +### `:android` Module + +Build file: [`android/build.gradle.kts`](../android/build.gradle.kts) + +Contains: +- `SimplexApp` (Application subclass) +- `MainActivity` (FragmentActivity) +- `SimplexService` (foreground Service) +- `NtfManager` (Android NotificationManager wrapper) +- `CallActivity` (dedicated activity for calls) + +### `:desktop` Module + +Build file: [`desktop/build.gradle.kts`](../desktop/build.gradle.kts) + +Contains: +- `main()` entry point +- `initHaskell()` -- loads native library and calls `initHS()` +- Window management (VLC library loading on Windows) + +--- + +## 3. JNI Bridge + +All JNI declarations reside in [`Core.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt). + + + + +### External Native Functions + +| # | Function | Signature | Line | Purpose | +|---|---|---|---|---| +| 1 | [`initHS()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L18) | `external fun initHS()` | 18 | Initialize GHC runtime system | +| 2 | [`pipeStdOutToSocket()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L20) | `external fun pipeStdOutToSocket(socketName: String): Int` | 20 | Redirect Haskell stdout to Android local socket for logging | +| 3 | [`chatMigrateInit()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L25) | `external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array` | 25 | Initialize database with migration; returns `[jsonResult, chatCtrl]` | +| 4 | [`chatCloseStore()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L26) | `external fun chatCloseStore(ctrl: ChatCtrl): String` | 26 | Close database store | +| 5 | [`chatSendCmdRetry()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L27) | `external fun chatSendCmdRetry(ctrl: ChatCtrl, msg: String, retryNum: Int): String` | 27 | Send command to core with retry count | +| 6 | [`chatSendRemoteCmdRetry()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L28) | `external fun chatSendRemoteCmdRetry(ctrl: ChatCtrl, rhId: Int, msg: String, retryNum: Int): String` | 28 | Send command to remote host | +| 7 | [`chatRecvMsg()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L29) | `external fun chatRecvMsg(ctrl: ChatCtrl): String` | 29 | Receive message (non-blocking) | +| 8 | [`chatRecvMsgWait()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L30) | `external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String` | 30 | Receive message with timeout (blocking up to `timeout` microseconds) | +| 9 | [`chatParseMarkdown()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L31) | `external fun chatParseMarkdown(str: String): String` | 31 | Parse markdown formatting | +| 10 | [`chatParseServer()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L32) | `external fun chatParseServer(str: String): String` | 32 | Parse SMP/XFTP server address | +| 11 | [`chatParseUri()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L33) | `external fun chatParseUri(str: String, safe: Int): String` | 33 | Parse SimpleX connection URI | +| 12 | [`chatPasswordHash()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L34) | `external fun chatPasswordHash(pwd: String, salt: String): String` | 34 | Hash password with salt | +| 13 | [`chatValidName()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L35) | `external fun chatValidName(name: String): String` | 35 | Validate/sanitize display name | +| 14 | [`chatJsonLength()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L36) | `external fun chatJsonLength(str: String): Int` | 36 | Get JSON-encoded string length | +| 15 | [`chatWriteFile()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L37) | `external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String` | 37 | Write encrypted file via core | +| 16 | [`chatReadFile()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L38) | `external fun chatReadFile(path: String, key: String, nonce: String): Array` | 38 | Read and decrypt file | +| 17 | [`chatEncryptFile()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L39) | `external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String` | 39 | Encrypt file on disk | +| 18 | [`chatDecryptFile()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L40) | `external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String` | 40 | Decrypt file on disk | + +**Total: 18 external native functions** (the `ChatCtrl` type alias at [line 23](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L23) is `Long`, representing the Haskell-side controller pointer). + + + + + +### Key Kotlin Functions in Core.kt + +| Function | Line | Purpose | +|---|---|---| +| [`initChatControllerOnStart()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L51) | 51 | Entry point called during app startup; launches `initChatController` in a long-running coroutine | +| [`initChatController()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L62) | 62 | Main initialization: DB migration via `chatMigrateInit`, error recovery (incomplete DB removal), sets file paths, loads active user, starts chat | +| [`chatInitTemporaryDatabase()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L190) | 190 | Creates a temporary database for migration scenarios | +| [`chatInitControllerRemovingDatabases()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L202) | 202 | Removes existing DBs and creates fresh controller (used during re-initialization) | +| [`showStartChatAfterRestartAlert()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L222) | 222 | Shows confirmation dialog when chat was stopped and DB passphrase is stored | + + + +### initChatController Flow + +``` +initChatController(useKey, confirmMigrations, startChat) + | + +-- chatMigrateInit(dbPath, dbKey, confirm) // JNI -> Haskell + | returns [jsonResult, chatCtrl] + | + +-- if migration error and rerunnable: + | chatMigrateInit(dbPath, dbKey, confirm) // retry with user confirmation + | + +-- setChatCtrl(ctrl) // store controller handle + +-- apiSetAppFilePaths(...) // tell core about file dirs + +-- apiSetEncryptLocalFiles(...) + +-- apiGetActiveUser() -> currentUser + +-- getServerOperators() -> conditions + +-- if shouldImportAppSettings: apiGetAppSettings + importIntoApp + +-- if user exists and startChat confirmed: + | startChat(user) // starts receiver, API commands + +-- else if no user: + set onboarding stage, optionally startChatWithoutUser() +``` + +--- + +## 4. App Lifecycle + +### Android + +Entry: [`SimplexApp.onCreate()`](../android/src/main/java/chat/simplex/app/SimplexApp.kt#L47) + +``` +SimplexApp.onCreate() + +-- initHaskell(packageName) // Load native lib, pipe stdout, call initHS() + | +-- System.loadLibrary("app-lib") + | +-- pipeStdOutToSocket(packageName) + | +-- initHS() + +-- initMultiplatform() // Set up ntfManager, platform callbacks + +-- reconfigureBroadcastReceivers() + +-- runMigrations() // Theme migration, version code tracking + +-- initChatControllerOnStart() // -> initChatController() -> chatMigrateInit -> startChat +``` + +Activity: [`MainActivity.onCreate()`](../android/src/main/java/chat/simplex/app/MainActivity.kt#L32) + +``` +MainActivity.onCreate() + +-- processNotificationIntent(intent) // Handle OpenChat/AcceptCall from notifications + +-- processIntent(intent) // Handle VIEW intents (deep links) + +-- processExternalIntent(intent) // Handle SEND/SEND_MULTIPLE (share sheet) + +-- setContent { AppScreen() } // Compose UI entry point +``` + +Lifecycle callbacks in `SimplexApp` (implements `LifecycleEventObserver`): +- `ON_START`: refresh chat list from API if chat is running +- `ON_RESUME`: show background service notice, start `SimplexService` if configured + +### Desktop + +Entry: [`main()`](../desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt#L21) + +``` +main() + +-- initHaskell() // Load native lib from resources dir, call initHS() + | +-- System.load(libapp-lib.so/dll/dylib) + | +-- initHS() + +-- runMigrations() + +-- setupUpdateChecker() + +-- initApp() // Set ntfManager, applyAppLocale, initChatControllerOnStart + +-- showApp() // Compose window with AppScreen() +``` + +[`showApp()`](../common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt#L33) creates a Compose `Window` with error recovery -- if a crash occurs, it closes the offending modal/view and re-opens the window. + +[`initApp()`](../common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt#L21) sets the `ntfManager` implementation (desktop notifications via `NtfManager` in `common/model/`) and calls `initChatControllerOnStart()`. + +--- + +## 5. Event Streaming + +### Receiver Coroutine + +[`ChatController.startReceiver()`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L660) launches a coroutine on `Dispatchers.IO` that continuously polls for events from the Haskell core: + +```kotlin +// SimpleXAPI.kt line 660 +private fun startReceiver() { + if (receiverJob != null || chatCtrl == null) return // guard against double-start + receiverJob = CoroutineScope(Dispatchers.IO).launch { + var releaseLock: (() -> Unit) = {} + while (isActive) { + val ctrl = chatCtrl + if (ctrl == null) { stopReceiver(); break } // chatCtrl became null + try { + val release = releaseLock + launch { delay(30000); release() } // release previous wake lock after 30s + val msg = recvMsg(ctrl) // calls chatRecvMsgWait with 300s timeout + releaseLock = getWakeLock(timeout = 60000) // acquire wake lock (60s timeout) + if (msg != null) { + val finished = withTimeoutOrNull(60_000L) { + processReceivedMsg(msg) + messagesChannel.trySend(msg) + } + if (finished == null) { + Log.e(TAG, "Timeout processing: " + msg.responseType) + } + } + } catch (e: Exception) { + Log.e(TAG, "recvMsg/processReceivedMsg exception: " + e.stackTraceToString()) + } catch (e: Throwable) { + Log.e(TAG, "recvMsg/processReceivedMsg throwable: " + e.stackTraceToString()) + } + } + } +} +``` + +### Message Reception + +[`recvMsg()`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L829) calls `chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT)` where `MESSAGE_TIMEOUT = 300_000_000` microseconds (300 seconds). Returns `null` on timeout (empty string from Haskell), otherwise deserializes the JSON response to an `API` instance. + +### Command Sending + +[`sendCmd()`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L804) runs on `Dispatchers.IO`, serializes the command via `CC.cmdString`, calls `chatSendCmdRetry()` (or `chatSendRemoteCmdRetry()` for remote hosts), deserializes the response, and logs terminal items. + +### Event Processing + +[`processReceivedMsg()`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2568) is a large `when` block that dispatches on the `CR` (ChatResponse) type: + +- `CR.ContactConnected` -- update contact in `ChatModel` +- `CR.NewChatItems` -- add items to chat, trigger notifications +- `CR.RcvCallInvitation` -- add to `callInvitations`, trigger call UI +- `CR.ChatStopped` -- set `chatRunning = false` +- `CR.GroupMemberConnected`, `CR.GroupMemberUpdated`, etc. -- update group state +- Many more event types for connection status, file transfers, SMP relay events, etc. + +### Wake Lock + +On Android, the receiver acquires a wake lock via [`getWakeLock(timeout)`](../common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt#L3) (expect function) after each received message with a 60-second timeout. The previous iteration's wake lock is released after a 30-second delay, ensuring overlap so the CPU does not sleep between messages. + +--- + +## 6. Platform Abstraction + +### expect/actual Pattern + +The `commonMain` source set declares `expect` functions and classes. Each platform source set provides `actual` implementations. + +Examples from platform files: + +| expect Declaration | File | Line | +|---|---|---| +| `expect val appPlatform: AppPlatform` | [`AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt#L20) | 20 | +| `expect val deviceName: String` | [`AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt#L22) | 22 | +| `expect fun isAppVisibleAndFocused(): Boolean` | [`AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt#L24) | 24 | +| `expect val dataDir: File` | [`Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L18) | 18 | +| `expect val tmpDir: File` | [`Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L19) | 19 | +| `expect val filesDir: File` | [`Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L20) | 20 | +| `expect val appFilesDir: File` | [`Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L21) | 21 | +| `expect val dbAbsolutePrefixPath: String` | [`Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L24) | 24 | +| `expect fun showToast(text: String, timeout: Long)` | [`UI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt#L6) | 6 | +| `expect fun hideKeyboard(view: Any?, clearFocus: Boolean)` | [`UI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt#L16) | 16 | +| `expect fun getKeyboardState(): State` | [`UI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt#L15) | 15 | +| `expect fun allowedToShowNotification(): Boolean` | [`Notifications.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Notifications.kt#L3) | 3 | +| `expect class VideoPlayer` | [`VideoPlayer.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt#L25) | 25 | +| `expect class RecorderNative` | [`RecAndPlay.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt#L17) | 17 | +| `expect val cryptor: CryptorInterface` | [`Cryptor.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt#L9) | 9 | +| `expect fun base64ToBitmap(base64ImageString: String): ImageBitmap` | [`Images.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt#L17) | 17 | +| `expect fun getWakeLock(timeout: Long): (() -> Unit)` | [`SimplexService.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt#L3) | 3 | +| `expect class GlobalExceptionsHandler` | [`UI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt#L24) | 24 | +| `expect fun UriHandler.sendEmail(subject: String, body: CharSequence)` | [`Share.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt#L7) | 7 | +| `expect fun ClipboardManager.shareText(text: String)` | [`Share.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt#L9) | 9 | +| `expect fun shareFile(text: String, fileSource: CryptoFile)` | [`Share.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt#L10) | 10 | + +### PlatformInterface Callback Object + +[`PlatformInterface`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt#L15) is an interface with default no-op implementations. It is assigned at runtime by each platform entry point: + +- **Android**: assigned in [`SimplexApp.initMultiplatform()`](../android/src/main/java/chat/simplex/app/SimplexApp.kt#L187) (line 187) +- **Desktop**: assigned in [`Main.kt initHaskell()`](../desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt#L50) (line 50) + +The global variable is declared at [`Platform.kt line 50`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt#L50): +```kotlin +var platform: PlatformInterface = object : PlatformInterface {} +``` + +#### PlatformInterface Callbacks + +| Callback | Default | Android Implementation | +|---|---|---| +| `androidServiceStart()` | no-op | Start `SimplexService` foreground service | +| `androidServiceSafeStop()` | no-op | Stop `SimplexService` | +| `androidCallServiceSafeStop()` | no-op | Stop `CallService` | +| `androidNotificationsModeChanged(mode)` | no-op | Toggle receivers, start/stop service | +| `androidChatStartedAfterBeingOff()` | no-op | Start service or schedule periodic worker | +| `androidChatStopped()` | no-op | Cancel workers, stop service | +| `androidChatInitializedAndStarted()` | no-op | Show background service notice, start service | +| `androidIsBackgroundCallAllowed()` | `true` | Check battery restriction | +| `androidSetNightModeIfSupported()` | no-op | Set `UiModeManager` night mode | +| `androidSetStatusAndNavigationBarAppearance(...)` | no-op | Configure system bar colors/appearance | +| `androidStartCallActivity(acceptCall, rhId, chatId)` | no-op | Launch `CallActivity` | +| `androidPictureInPictureAllowed()` | `true` | Check PiP permission via AppOps | +| `androidCallEnded()` | no-op | Destroy call WebView | +| `androidRestartNetworkObserver()` | no-op | Restart `NetworkObserver` | +| `androidCreateActiveCallState()` | empty `Closeable` | Create `ActiveCallState` | +| `androidIsXiaomiDevice()` | `false` | Check device brand | +| `androidApiLevel` | `null` | `Build.VERSION.SDK_INT` | +| `androidLockPortraitOrientation()` | no-op | Lock to `SCREEN_ORIENTATION_PORTRAIT` | +| `androidAskToAllowBackgroundCalls()` | `true` | Show battery restriction dialog | +| `desktopShowAppUpdateNotice()` | no-op | Show update notice (Desktop only) | + +--- + +## 7. Source Files + +### Core Infrastructure + +| File | Path | Key Contents | +|---|---|---| +| Core.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt) | JNI externals, `initChatController`, `chatInitTemporaryDatabase` | +| SimpleXAPI.kt | [`common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt) | `ChatController`, `AppPreferences`, `startReceiver`, `sendCmd`, `recvMsg`, `processReceivedMsg`, all `api*` functions | +| ChatModel.kt | [`common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt) | `ChatModel` singleton, `ChatsContext`, `Chat`, `ChatInfo`, `ChatItem` and all domain types | +| App.kt | [`common/src/commonMain/kotlin/chat/simplex/common/App.kt`](../common/src/commonMain/kotlin/chat/simplex/common/App.kt) | `AppScreen()`, `MainScreen()` | + +### Platform Layer + +| File | Path | Key Contents | +|---|---|---| +| Platform.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt) | `PlatformInterface`, global `platform` var | +| AppCommon.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt) | `AppPlatform`, `runMigrations()` | +| AppCommon.android.kt | [`common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt`](../common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt) | `initHaskell()`, `androidAppContext` | +| AppCommon.desktop.kt | [`common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt`](../common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt) | `initApp()`, desktop NtfManager setup | +| Files.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) | `expect val dataDir/tmpDir/filesDir/dbAbsolutePrefixPath` | +| NtfManager.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt) | `abstract class NtfManager` | +| Notifications.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Notifications.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Notifications.kt) | `expect fun allowedToShowNotification()` | +| UI.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt) | `showToast`, `hideKeyboard`, `GlobalExceptionsHandler` | +| Share.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt) | `shareText`, `shareFile`, `openFile` | +| VideoPlayer.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt) | `VideoPlayerInterface`, `expect class VideoPlayer` | +| RecAndPlay.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt) | `RecorderInterface`, `AudioPlayerInterface` | +| Cryptor.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt) | `CryptorInterface` | +| Images.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt) | `base64ToBitmap`, `resizeImageToStrSize` | +| SimplexService.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt) | `expect fun getWakeLock()` | + +### Entry Points + +| File | Path | Key Contents | +|---|---|---| +| SimplexApp.kt | [`android/src/main/java/chat/simplex/app/SimplexApp.kt`](../android/src/main/java/chat/simplex/app/SimplexApp.kt) | Android Application class, lifecycle observer | +| MainActivity.kt | [`android/src/main/java/chat/simplex/app/MainActivity.kt`](../android/src/main/java/chat/simplex/app/MainActivity.kt) | Android main activity | +| SimplexService.kt | [`android/src/main/java/chat/simplex/app/SimplexService.kt`](../android/src/main/java/chat/simplex/app/SimplexService.kt) | Android foreground service | +| Main.kt | [`desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt`](../desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt) | Desktop `main()` | +| DesktopApp.kt | [`common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt`](../common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt) | `showApp()`, `SimplexWindowState` | + +### Theme + +| File | Path | Key Contents | +|---|---|---| +| ThemeManager.kt | [`common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt) | Theme resolution, system/light/dark/custom, per-user overrides | diff --git a/apps/multiplatform/spec/client/chat-list.md b/apps/multiplatform/spec/client/chat-list.md new file mode 100644 index 0000000000..b0f3750659 --- /dev/null +++ b/apps/multiplatform/spec/client/chat-list.md @@ -0,0 +1,314 @@ +# Chat List Specification + +Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt` + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ChatListView Composable](#2-chatlistview-composable) +3. [Data Sources](#3-data-sources) +4. [Filter System](#4-filter-system) +5. [Chat Preview](#5-chat-preview) +6. [ChatListNavLinkView](#6-chatlistnavlinkview) +7. [Tag System](#7-tag-system) +8. [UserPicker](#8-userpicker) +9. [Source Files](#9-source-files) + +--- + +## Executive Summary + +The Chat List is the landing screen of SimpleX Chat, rendering all conversations for the active user. Built around `ChatListView` (line 126 in `ChatListView.kt`), it provides a searchable, filterable `LazyColumn` of chat previews with a toolbar, tag-based filtering, and a user-switching side panel. The view adapts between one-hand UI mode (toolbar at bottom, reversed list) and standard mode (toolbar at top). Search also accepts SimpleX links for direct connection. + +--- + +## 1. Overview + +``` +ChatListView +|-- ChatListToolbar (top or bottom app bar) +| |-- UserProfileButton (opens UserPicker) +| |-- Title ("Your chats") +| |-- SubscriptionStatusIndicator +| +-- NewChatButton / StoppedIndicator +|-- ChatListWithLoadingScreen +| |-- ChatList (LazyColumnWithScrollBar) +| | |-- Spacer (top/bottom padding) +| | |-- stickyHeader +| | | |-- ChatListSearchBar (search input + filter toggle) +| | | +-- TagsView (preset + custom tag chips) +| | |-- ChatListNavLinkView[] (per-chat row items) +| | +-- ChatListFeatureCards (one-hand UI card, address card) +| +-- EmptyState text +|-- NewChatSheetFloatingButton (FAB, standard mode only) +|-- UserPicker (slide-in panel, Android) ++-- ActiveCallInteractiveArea (desktop, in-call banner) +``` + +--- + + + +## 2. ChatListView Composable + +**Location:** [`ChatListView.kt#L127`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L127) + +```kotlin +fun ChatListView( + chatModel: ChatModel, + userPickerState: MutableStateFlow, + setPerformLA: (Boolean) -> Unit, + stopped: Boolean +) +``` + +### Initialization + +- Shows "What's New" modal on first launch after update (line ~130), with a 1-second delay. +- On desktop, closing a chat resets audio/video players (line ~138). + +### Layout Modes + +The `oneHandUI` preference (`appPrefs.oneHandUI.state`) controls the layout: + +| Mode | Toolbar Position | List Direction | FAB | Search/Tags Position | +|---|---|---|---|---| +| **Standard** (`oneHandUI = false`) | Top | Top-to-bottom | Bottom-right FAB | Below toolbar | +| **One-hand** (`oneHandUI = true`) | Bottom | Bottom-to-top (reversed) | Integrated in toolbar | Above toolbar | + +### State + +| State | Type | Purpose | +|---|---|---| +| `searchText` | `MutableState` | Search query (saved across recomposition) | +| `listState` | `LazyListState` | Scroll position (persisted in `lazyListState` var) | +| `oneHandUI` | `State` | One-hand UI mode toggle | + +### Android-specific + +- `SetNotificationsModeAdditions`: Notification permission setup (line ~184). +- `UserPicker`: Overlay side panel for user switching (line ~192). + +--- + +## 3. Data Sources + +| Source | Location | Description | +|---|---|---| +| `chatModel.chats` | `ChatModel.chatsContext.chats` | Full list of `Chat` objects for the active user | +| `chatModel.activeChatTagFilter` | `ChatModel.activeChatTagFilter` | Currently active filter (`PresetTag`, `UserTag`, or `Unread`) | +| `chatModel.userTags` | `ChatModel.userTags` | User-created custom tags | +| `chatModel.presetTags` | `ChatModel.presetTags` | Map of `PresetTagKind` to count | +| `chatModel.unreadTags` | `ChatModel.unreadTags` | Map of tag ID to unread count | +| `chatModel.chatId` | `ChatModel.chatId` | Currently selected chat ID (highlights row) | +| `chatModel.currentUser` | `ChatModel.currentUser` | Active user profile | +| `chatModel.users` | `ChatModel.users` | All user profiles (for UserPicker) | +| `chatModel.showChatPreviews` | `ChatModel.showChatPreviews` | Privacy toggle for message previews | + +--- + +## 4. Filter System + +### Active Filter Types + +Defined as sealed class `ActiveFilter` (line ~51): + +```kotlin +sealed class ActiveFilter { + data class PresetTag(val tag: PresetTagKind) : ActiveFilter() + data class UserTag(val tag: ChatTag) : ActiveFilter() + data object Unread : ActiveFilter() +} +``` + +### PresetTagKind Enum + +| Value | Description | +|---|---| +| `GROUP_REPORTS` | Groups with active reports (moderator-visible) | +| `FAVORITES` | Chats marked as favorite | +| `CONTACTS` | Direct (1:1) chats | +| `GROUPS` | Group chats | +| `BUSINESS` | Business-type chats | +| `NOTES` | Local note folders | + +### Search Filtering + +The `filteredChats` function (line ~1188) applies filters in this order: + +1. **SimpleX link match:** If a pasted link resolved to a known contact/group, show only that chat. +2. **Text search:** Case-insensitive match against `chat.chatInfo.chatViewName`, `chat.chatInfo.fullName`, and `chat.chatInfo.localAlias`. +3. **Active filter:** + - `PresetTag`: Matches chat type and characteristics (e.g., `CONTACTS` filters `ChatInfo.Direct`, `GROUPS` filters `ChatInfo.Group`). + - `UserTag`: Matches chats whose `chatTags` contain the tag ID. + - `Unread`: Matches chats with `unreadCount > 0` or `unreadChat == true`. + +### Search Bar + +`ChatListSearchBar` (line ~611) provides: +- Text input with search icon. +- SimpleX link detection: When a pasted string contains a single SimpleX link, it triggers `planAndConnect` for connection, suppressing normal search. +- Unread filter toggle button (right side, when search is empty). + +--- + + + +## 5. Chat Preview + +**Location:** [`ChatPreviewView.kt#L40`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt#L40) + +```kotlin +fun ChatPreviewView( + chat: Chat, + showChatPreviews: Boolean, + chatModelDraft: ComposeState?, + chatModelDraftChatId: ChatId?, + currentUserProfileDisplayName: String?, + disabled: Boolean, + linkMode: SimplexLinkMode, + inProgress: Boolean, + progressByTimeout: Boolean, + defaultClickAction: () -> Unit +) +``` + +### Layout + +Each chat preview row contains: + +| Element | Position | Content | +|---|---|---| +| Profile image | Left | `ChatInfoImage` with overlay icons for inactive contacts/groups | +| Title row | Top-right of image | Chat name (bold), verified shield (direct), timestamp | +| Preview row | Below title | Last message preview or draft indicator, unread badge | +| Unread badge | Right | Circular badge with count, or dot for muted chats | + +### Draft Display + +When `chatModelDraftChatId` matches the chat ID, the preview shows a draft indicator (pencil icon) with the draft message text instead of the last chat item. + +### Inactive Indicators + +- Inactive contacts: cancel icon overlay on profile image. +- Left/removed/deleted groups: cancel icon overlay. + +--- + + + +## 6. ChatListNavLinkView + +**Location:** [`ChatListNavLinkView.kt#L37`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt#L37) + +Routes each chat to the appropriate click action and context menu based on `chat.chatInfo`: + +| ChatInfo Type | Click Action | Context Menu | +|---|---|---| +| `ChatInfo.Direct` | `directChatAction` (opens chat) | `ContactMenuItems`: mark read/unread, mute, favorite, tag, clear, delete | +| `ChatInfo.Group` | `groupChatAction` (opens chat or joins) | `GroupMenuItems`: mark read/unread, mute, favorite, tag, clear, leave, delete | +| `ChatInfo.Local` | `noteFolderChatAction` (opens notes) | `NoteFolderMenuItems`: mark read, clear, delete | +| `ChatInfo.ContactRequest` | `contactRequestAlertDialog` (accept/reject) | `ContactRequestMenuItems`: reject | +| `ChatInfo.ContactConnection` | Sets `chatModel.chatId` (opens connection info) | `ContactConnectionMenuItems`: delete | +| `ChatInfo.InvalidJSON` | Sets `chatModel.chatId` | No menu | + +### Selection Highlight + +On desktop, the currently selected chat (`chatModel.chatId.value == chat.id`) receives a highlight background. `nextChatSelected` state is used to suppress the bottom divider when the next chat in the list is selected. + +--- + +## 7. Tag System + +### TagsView + +**Location:** [`ChatListView.kt#L929`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L929) + +Renders a horizontally scrollable row of tag chips (via `TagsRow`, which is a platform-specific `expect` composable). + +Layout logic: +- If there are more than 1 collapsible preset tags and the total tag count exceeds 3, preset tags collapse into a `CollapsedTagsFilterView` dropdown. +- Otherwise, each preset tag renders as an `ExpandedTagFilterView` chip. +- User tags render as individual chips with emoji or label icon, bold when active. +- A "+" button at the end opens `TagListEditor` for creating new tags. + +### Tag Interactions + +- **Single tap:** Toggles the tag filter on `chatModel.activeChatTagFilter`. +- **Long press / right-click (user tags):** Opens dropdown menu with edit/delete/reorder options. +- **Unread dot:** Shown on tags that have chats with unread messages. + + + +### TagListView + +**Location:** [`TagListView.kt#L48`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt#L48) + +Full-screen tag management view opened from the "+" button or long-press menu. + +```kotlin +fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: Boolean) +``` + +- Displays all user tags in a `LazyColumnWithScrollBar`. +- Supports drag-and-drop reordering via `rememberDragDropState` (calls `apiReorderChatTags`). +- Each tag row shows emoji/icon, name, chat count, and a checkbox if opened for a specific chat (to assign/unassign tags). +- "Create list" button opens `TagListEditor` modal. + +--- + + + +## 8. UserPicker + +**Location:** [`UserPicker.kt#L46`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt#L46) + +```kotlin +fun UserPicker( + chatModel: ChatModel, + userPickerState: MutableStateFlow, + setPerformLA: (Boolean) -> Unit +) +``` + +### Behavior + +- **Android:** Renders as a slide-up overlay panel on the chat list, triggered by tapping the user profile button in the toolbar. +- **Desktop:** Rendered inline in the left column of `DesktopScreen`, always accessible. +- Closes automatically when any `ModalManager.start` modal opens. + +### Content + +| Section | Content | +|---|---| +| **Active user** | Profile image, display name, "active" indicator | +| **Other users** | List of non-hidden user profiles sorted by `activeOrder`; tapping switches user | +| **Remote hosts** | Connected remote devices (desktop linking) | +| **Settings** | Opens `SettingsView` modal | +| **Color mode** | `ColorModeSwitcher` for theme toggle | +| **Add profile** | Opens `CreateProfile` flow | +| **Lock** | Locks app (calls `AppLock.setPerformLA`) | + +### State Machine + +Uses `AnimatedViewState` (`GONE`, `VISIBLE`, `HIDING`) with a `MutableStateFlow` to coordinate animation between the parent screen and the picker overlay. + +--- + +## 9. Source Files + +| File | Description | +|---|---| +| `ChatListView.kt` | Main chat list view, toolbar, search, tags, filtering | +| `ChatListNavLinkView.kt` | Per-chat row routing and context menus | +| `ChatPreviewView.kt` | Chat preview row layout (image, title, last message) | +| `ChatHelpView.kt` | Empty-state help content | +| `ContactConnectionView.kt` | Pending connection preview row | +| `ContactRequestView.kt` | Contact request preview row | +| `ServersSummaryView.kt` | Server connection status summary | +| `ShareListNavLinkView.kt` | Share target list row (forwarding) | +| `ShareListView.kt` | Share target list (forwarding flow) | +| `TagListView.kt` | Tag management and assignment view | +| `UserPicker.kt` | User switching side panel | diff --git a/apps/multiplatform/spec/client/chat-view.md b/apps/multiplatform/spec/client/chat-view.md new file mode 100644 index 0000000000..2819b1e751 --- /dev/null +++ b/apps/multiplatform/spec/client/chat-view.md @@ -0,0 +1,324 @@ +# Chat View Specification + +Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt` + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ChatView Composable](#2-chatview-composable) +3. [Message List](#3-message-list) +4. [ChatItemView](#4-chatitemview) +5. [Message Types](#5-message-types) +6. [Context Menu Actions](#6-context-menu-actions) +7. [ChatInfoView](#7-chatinfoview) +8. [GroupChatInfoView](#8-groupchatinfoview) +9. [Source Files](#9-source-files) + +--- + +## Executive Summary + +The Chat View is the primary message display and interaction surface in SimpleX Chat. It is built around the `ChatView` composable (line ~96 in `ChatView.kt`), which orchestrates a `ChatLayout` containing a reverse-scrolling `LazyColumn` of `ChatItemView` items and a `ComposeView` for message input. The view supports direct chats, group chats, local notes, and contact connections, with per-chat theming, search/filter, multi-select, and side-panel info modals. Message rendering is delegated to type-specific composables in the `views/chat/item/` package. + +--- + +## 1. Overview + +``` +ChatView +|-- ChatLayout +| |-- ChatInfoToolbar (top/bottom app bar with back, title, call, search, menu) +| |-- SupportChatsCountToolbar (reports/support banner, group only) +| |-- ChatItemsList (LazyColumnWithScrollBar, reverse layout) +| | |-- ChatViewListItem +| | | |-- DateSeparator +| | | |-- MemberNameAndRole (group received messages) +| | | |-- MemberImage (group received messages) +| | | +-- ChatItemView (message type routing) +| | |-- ChatBannerView (first item: chat profile banner) +| | +-- FloatingButtons (scroll-to-bottom, unread counter) +| |-- ComposeView (message composition area) +| | |-- ContextItemView (reply/edit/forward/report indicator) +| | |-- previewView (link/media/voice/file preview) +| | +-- SendMsgView (text input + send/voice/timed buttons) +| |-- GroupMentions (mention autocomplete popup) +| |-- CommandsMenuView (bot commands popup) +| +-- ChooseAttachmentView (bottom sheet for attachment type) +|-- ChatInfoView (contact info, end modal) ++-- GroupChatInfoView (group management, end modal) +``` + +--- + + + +## 2. ChatView Composable + +**Location:** [`ChatView.kt#L97`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt#L97) + +```kotlin +fun ChatView( + chatsCtx: ChatModel.ChatsContext, + staleChatId: State, + scrollToItemId: MutableState, + onComposed: suspend (chatId: String) -> Unit +) +``` + +### State Management + +| State Variable | Type | Purpose | +|---|---|---| +| `showSearch` | `MutableState` | Controls search bar visibility | +| `searchText` | `MutableState` | Current search query text | +| `composeState` | `MutableState` | Full compose area state (message, preview, context, mentions) | +| `attachmentOption` | `MutableState` | Selected attachment type from bottom sheet | +| `selectedChatItems` | `MutableState?>` | Multi-select mode item IDs; `null` = selection off | +| `showCommandsMenu` | `MutableState` | Bot commands menu visibility | +| `contentFilter` | `MutableState` | Active content type filter (images, videos, etc.) | +| `availableContent` | `MutableState>` | Content types available in this chat | +| `activeChat` | `State` | Derived from `chatModel.chats` matching `staleChatId` | +| `unreadCount` | `State` | Unread message count derived from chat stats | + +### Chat Loading + +On chat ID change (via `snapshotFlow` on `chatModel.chatId.value`, line ~162): + +1. Marks unread chat as read (`markUnreadChatAsRead`) +2. Clears group members state +3. Resets search, content filter, and selection +4. Fetches available content types (`updateAvailableContent`) +5. For direct chats, loads contact info and connection stats +6. For groups with pending membership, opens member support chat + +### Chat Type Routing + +The outer `when (chatInfo)` (line ~229) branches: + +| ChatInfo Type | Behavior | +|---|---| +| `ChatInfo.Direct`, `ChatInfo.Group`, `ChatInfo.Local` | Full `ChatLayout` with compose, search, reactions, per-chat theme | +| `ChatInfo.ContactConnection` | `ModalView` wrapping `ContactConnectionInfoView` | +| `ChatInfo.InvalidJSON` | `ModalView` with raw JSON display and share button | + +--- + +## 3. Message List + +**Location:** [`ChatView.kt#L1592`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt#L1592) (`ChatItemsList` composable) + +The message list is a `LazyColumnWithScrollBar` with `reverseLayout = true`, meaning index 0 is the newest message at the bottom of the screen. + +### Key Behaviors + +- **Merged Items:** Messages are grouped via `MergedItems.create()` (line ~1653), which collapses consecutive similar system events into expandable groups. Revealed state is tracked in `revealedItems`. +- **Pagination:** `PreloadItems` triggers `loadMessages` with `ChatPagination.Before` (older) or `ChatPagination.Last` (newer) when the user scrolls near list boundaries. +- **Scroll To Item:** `scrollToItem` lambda supports animated scrolling to a specific item ID, used by search result taps and quoted message navigation. +- **Unread Marking:** `MarkItemsReadAfterDelay` composable marks newly visible received items as read after a brief delay. +- **Date Separators:** `DateSeparator` composable renders between messages when the date changes (via `ItemSeparation.date`). +- **Swipe to Reply:** `SwipeToDismiss` modifier on each item (EndToStart direction, 30dp threshold) sets `ComposeContextItem.QuotedItem`. +- **Selection Mode:** When `selectedChatItems` is non-null, a checkbox overlay appears on each item; a full-width clickable overlay toggles selection. + +### Item Layout (ChatViewListItem) + +- **Group received messages** with `showAvatar = true`: Column layout with `MemberNameAndRole` header, `MemberImage` (clickable to `showMemberInfo`), and message bubble. +- **Group received without avatar:** Indented to align with avatar-bearing messages. +- **Sent messages (group or direct):** Right-aligned with larger start padding. +- **Direct messages:** Symmetric padding (76dp opposite side). + +--- + + + +## 4. ChatItemView + +**Location:** [`item/ChatItemView.kt#L66`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L66) + +```kotlin +fun ChatItemView( + chatsCtx, rhId, chat, cItem, composeState, imageProvider, + useLinkPreviews, linkMode, revealed, highlighted, hoveredItemId, + range, selectedChatItems, searchIsNotBlank, fillMaxWidth, + selectChatItem, deleteMessage, deleteMessages, archiveReports, + receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, + openDirectChat, forwardItem, scrollToItem, scrollToItemId, + scrollToQuotedItemFromItem, setReaction, showItemDetails, + reveal, showMemberInfo, showChatInfo, developerTools, showViaProxy, + showTimestamp, itemSeparation, ... +) +``` + +The composable routes based on `cItem.content` and `cItem.meta.itemDeleted`: + +- **Deleted items** -> `DeletedItemView` or `MarkedDeletedItemView` +- **Message content** (`SndMsgContent`, `RcvMsgContent`) -> `FramedItemView` or specialized views depending on `msgContent` type +- **Call items** -> `CICallItemView` +- **Integrity/decryption errors** -> `IntegrityErrorItemView`, `CIRcvDecryptionError` +- **Group invitations** -> `CIGroupInvitationView` +- **Events** (group/direct/connection events) -> `CIEventView` +- **Feature changes** -> `CIChatFeatureView`, `CIFeaturePreferenceView` +- **E2EE info** -> `CIEventView` +- **Chat banner** -> handled at list level, not in `ChatItemView` +- **Invalid JSON** -> `CIInvalidJSONView` + +### Reactions + +`ChatItemReactions` row renders below each message bubble, showing emoji reaction counts. Tapping own reactions removes them; tapping others' opens a member list dropdown. + +### Context Menu + +Long-press or right-click opens a dropdown menu with context-sensitive actions (see section 6). + +--- + +## 5. Message Types + +| CIContent Variant | MsgContent Type | View Composable | Source File | +|---|---|---|---| +| `SndMsgContent` / `RcvMsgContent` | `MCText` | `FramedItemView` -> `TextItemView` or `EmojiItemView` | `TextItemView.kt`, `EmojiItemView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCLink` | `FramedItemView` (with link preview) | `FramedItemView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCImage` | `CIImageView` (inside `FramedItemView`) | `CIImageView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCVideo` | `CIVideoView` (inside `FramedItemView`) | `CIVideoView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCVoice` | `CIVoiceView` | `CIVoiceView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCFile` | `CIFileView` | `CIFileView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCReport` | `FramedItemView` (with report styling) | `FramedItemView.kt` | +| `SndCall` / `RcvCall` | -- | `CICallItemView` | `CICallItemView.kt` | +| `RcvIntegrityError` | -- | `IntegrityErrorItemView` | `IntegrityErrorItemView.kt` | +| `RcvDecryptionError` | -- | `CIRcvDecryptionError` | `CIRcvDecryptionError.kt` | +| `RcvGroupInvitation` / `SndGroupInvitation` | -- | `CIGroupInvitationView` | `CIGroupInvitationView.kt` | +| `RcvDirectEventContent` | -- | `CIEventView` | `CIEventView.kt` | +| `RcvGroupEventContent` / `SndGroupEventContent` | -- | `CIEventView` | `CIEventView.kt` | +| `RcvConnEventContent` / `SndConnEventContent` | -- | `CIEventView` | `CIEventView.kt` | +| `RcvChatFeature` / `SndChatFeature` | -- | `CIChatFeatureView` | `CIChatFeatureView.kt` | +| `RcvChatPreference` / `SndChatPreference` | -- | `CIFeaturePreferenceView` | `CIFeaturePreferenceView.kt` | +| `RcvGroupFeature` / `SndGroupFeature` | -- | `CIChatFeatureView` | `CIChatFeatureView.kt` | +| `SndModerated` / `RcvModerated` / `RcvBlocked` | -- | `MarkedDeletedItemView` | `MarkedDeletedItemView.kt` | +| `SndDirectE2EEInfo` / `RcvDirectE2EEInfo` | -- | `CIEventView` | `CIEventView.kt` | +| `SndGroupE2EEInfo` / `RcvGroupE2EEInfo` | -- | `CIEventView` | `CIEventView.kt` | +| `RcvChatFeatureRejected` / `RcvGroupFeatureRejected` | -- | `CIChatFeatureView` | `CIChatFeatureView.kt` | +| `ChatBanner` | -- | `ChatBannerView` (inline in `ChatItemsList`) | `ChatView.kt` | +| `InvalidJSON` | -- | `CIInvalidJSONView` | `CIInvalidJSONView.kt` | +| `CIMemberCreatedContact` | -- | `CIMemberCreatedContactView` | `CIMemberCreatedContactView.kt` | + +--- + +## 6. Context Menu Actions + +Context menu actions are built dynamically in `ChatItemView` based on message type, direction, chat type, and feature flags. + +| Action | Condition | Effect | +|---|---|---| +| **Reply** | Message content (not event/deleted), not local notes | Sets `ComposeContextItem.QuotedItem` | +| **Edit** | Sent message, editable (`meta.editable`), text/link content | Sets `ComposeContextItem.EditingItem` | +| **Delete for me** | Any deletable item | `apiDeleteChatItems` with `cidmInternal` mode | +| **Delete for everyone** | Sent + within time window, or moderator privilege | `apiDeleteChatItems` with `cidmBroadcast` mode | +| **Moderate** | Group moderator + received message | `apiDeleteMemberChatItems` | +| **Forward** | Message content, not live message | Opens share sheet via `SharedContent.Forward` | +| **Select** | Any selectable item | Enters multi-select mode (`selectedChatItems`) | +| **React** | Message content, reactions enabled | Opens emoji picker; calls `apiChatItemReaction` | +| **Report** | Received group message, reports enabled | Sets `ComposeContextItem.ReportedItem` with reason | +| **Info** | Any message | Opens `ChatItemInfoView` in end modal | +| **Copy** | Text content present | Copies text to clipboard | +| **Save** | Image/video/file with completed download | Saves media to device | +| **Open** | File with completed download | Opens file with system handler | +| **Reveal / Hide** | Part of a merged group; expanded or collapsed | Toggles `revealedItems` state | + +--- + +## 7. ChatInfoView + +**Location:** [`ChatInfoView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt) + +Opened via the `info` callback when the user taps the toolbar title in a direct chat. Displayed in `ModalManager.end`. + +Preloads `apiContactInfo` (connection stats, server profile) and `apiGetContactCode` (verification code) before showing the modal. + +Key sections: contact profile, local alias, connection stats, shared media, disappearing messages preference, voice/call/file feature toggles, encryption verification, and contact deletion. + +--- + +## 8. GroupChatInfoView + +**Location:** [`group/GroupChatInfoView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt) + +Opened via the `info` callback for group chats. Displayed in `ModalManager.end`. + +Preloads group members (`setGroupMembers`) and group link (`apiGetGroupLink`). + +Key sections: group profile, group link, member list with roles, group preferences (disappearing messages, direct messages, full deletion, voice, files, SimpleX links, history), member admission, welcome message, reports view, and group deletion/leave. + +--- + +## 9. Source Files + +### `views/chat/` + +| File | Description | +|---|---| +| `ChatView.kt` | Main chat view, ChatLayout, ChatItemsList, ChatInfoToolbar | +| `ChatInfoView.kt` | Contact info modal | +| `ChatItemInfoView.kt` | Individual message delivery/read info | +| `ChatItemsLoader.kt` | Pagination and message loading logic | +| `ChatItemsMerger.kt` | MergedItems grouping of consecutive events | +| `CommandsMenuView.kt` | Bot `/command` menu popup | +| `ComposeContextContactRequestActionsView.kt` | Contact request action buttons in compose area | +| `ComposeContextGroupDirectInvitationActionsView.kt` | Group direct invitation compose actions | +| `ComposeContextPendingMemberActionsView.kt` | Pending member compose actions | +| `ComposeContextProfilePickerView.kt` | Profile picker in compose context | +| `ComposeFileView.kt` | File attachment preview in compose | +| `ComposeImageView.kt` | Image/video attachment preview in compose | +| `ComposeView.kt` | Main compose area (ComposeState, send logic) | +| `ComposeVoiceView.kt` | Voice recording preview in compose | +| `ContactPreferences.kt` | Per-contact feature preferences | +| `ContextItemView.kt` | Reply/edit/forward context indicator | +| `ScanCodeView.kt` | QR code scanner | +| `SelectableChatItemToolbars.kt` | Multi-select toolbar (delete, forward, moderate) | +| `SendMsgView.kt` | Text input field, send button, voice record button | +| `VerifyCodeView.kt` | Contact/member encryption verification | + +### `views/chat/item/` + +| File | Description | +|---|---| +| `ChatItemView.kt` | Message type routing, context menu, reactions | +| `CIBrokenComposableView.kt` | Fallback for rendering errors | +| `CICallItemView.kt` | Call event display (incoming/outgoing/missed) | +| `CIChatFeatureView.kt` | Chat feature change event | +| `CIEventView.kt` | Generic event display (group/direct/connection) | +| `CIFeaturePreferenceView.kt` | Feature preference change event | +| `CIFileView.kt` | File message (download/upload progress) | +| `CIGroupInvitationView.kt` | Group invitation card | +| `CIImageView.kt` | Image message (thumbnail + fullscreen) | +| `CIInvalidJSONView.kt` | Invalid JSON fallback display | +| `CIMemberCreatedContactView.kt` | Member-created contact event | +| `CIMetaView.kt` | Message metadata (time, status indicators) | +| `CIRcvDecryptionError.kt` | Decryption error display | +| `CIVideoView.kt` | Video message (thumbnail + player) | +| `CIVoiceView.kt` | Voice message (waveform + player) | +| `DeletedItemView.kt` | Deleted message placeholder | +| `EmojiItemView.kt` | Large emoji-only message | +| `FramedItemView.kt` | Message bubble frame (quoted item, text, media) | +| `ImageFullScreenView.kt` | Fullscreen image gallery | +| `IntegrityErrorItemView.kt` | Message integrity error | +| `MarkedDeletedItemView.kt` | Marked-as-deleted / moderated message | +| `TextItemView.kt` | Plain text message with markdown | + +### `views/chat/group/` + +| File | Description | +|---|---| +| `AddGroupMembersView.kt` | Add members to group | +| `GroupChatInfoView.kt` | Group info and management | +| `GroupLinkView.kt` | Group link display and management | +| `GroupMemberInfoView.kt` | Individual member info | +| `GroupMembersToolbar.kt` | Members toolbar in group info | +| `GroupMentions.kt` | @mention autocomplete | +| `GroupPreferences.kt` | Group feature preferences | +| `GroupProfileView.kt` | Group profile editor | +| `GroupReportsView.kt` | Group reports list view | +| `MemberAdmission.kt` | Member admission settings | +| `MemberSupportChatView.kt` | Member support chat (scoped context) | +| `MemberSupportView.kt` | Support chat list for moderators | +| `WelcomeMessageView.kt` | Group welcome message editor | diff --git a/apps/multiplatform/spec/client/compose.md b/apps/multiplatform/spec/client/compose.md new file mode 100644 index 0000000000..241dcf667b --- /dev/null +++ b/apps/multiplatform/spec/client/compose.md @@ -0,0 +1,399 @@ +# Message Composition Specification + +Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt`, `SendMsgView.kt` + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ComposeState Data Class](#2-composestate-data-class) +3. [ComposePreview Sealed Class](#3-composepreview-sealed-class) +4. [ComposeContextItem Sealed Class](#4-composecontextitem-sealed-class) +5. [SendMsgView](#5-sendmsgview) +6. [Attachment Handling](#6-attachment-handling) +7. [Draft Persistence](#7-draft-persistence) +8. [Source Files](#8-source-files) + +--- + +## Executive Summary + +Message composition in SimpleX Chat is managed by `ComposeView` (line ~345 in `ComposeView.kt`) backed by the serializable `ComposeState` data class. The compose area supports text input, link previews, media/file/voice attachments, reply/edit/forward contexts, live (streaming) messages, member @mentions, message reports, and timed (disappearing) messages. The `SendMsgView` composable (in `SendMsgView.kt`) provides the text field and action buttons. Draft state persists across chat switches when the privacy preference is enabled. + +--- + + + +## 1. Overview + +``` +ComposeView +|-- contextItemView() +| |-- ContextItemView (QuotedItem) [reply indicator] +| |-- ContextItemView (EditingItem) [edit indicator] +| |-- ContextItemView (ForwardingItems) [forward indicator] +| +-- ContextItemView (ReportedItem) [report indicator] +|-- ReportReasonView [report reason header] +|-- MsgNotAllowedView [disabled send reason] +|-- previewView() +| |-- ComposeLinkView [link preview card] +| |-- ComposeImageView [media thumbnails] +| |-- ComposeVoiceView [voice recording waveform] +| +-- ComposeFileView [file name display] +|-- AttachmentAndCommandsButtons +| |-- CommandsButton [bot commands "//"] +| +-- AttachmentButton [paperclip icon] ++-- SendMsgView + |-- PlatformTextField [multiline text input] + |-- DeleteTextButton [clear text, shown on long text] + |-- SendMsgButton [arrow/check icon] + |-- RecordVoiceView [microphone + hold-to-record] + |-- StartLiveMessageButton [bolt icon] + |-- CancelLiveMessageButton [cancel live] + +-- TimedMessageDropdown [disappearing message timer] +``` + +--- + + + +## 2. ComposeState Data Class + +**Location:** [`ComposeView.kt#L98`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L98) + +```kotlin +@Serializable +data class ComposeState( + val message: ComposeMessage = ComposeMessage(), + val parsedMessage: List = emptyList(), + val liveMessage: LiveMessage? = null, + val preview: ComposePreview = ComposePreview.NoPreview, + val contextItem: ComposeContextItem = ComposeContextItem.NoContextItem, + val inProgress: Boolean = false, + val progressByTimeout: Boolean = false, + val useLinkPreviews: Boolean, + val mentions: MentionedMembers = emptyMap() +) +``` + +### Fields + +| Field | Type | Description | +|---|---|---| +| `message` | `ComposeMessage` | Current text and cursor selection (`TextRange`) | +| `parsedMessage` | `List` | Markdown-parsed representation of message text | +| `liveMessage` | `LiveMessage?` | Active live (streaming) message state | +| `preview` | `ComposePreview` | Attachment preview (link, media, voice, file) | +| `contextItem` | `ComposeContextItem` | Reply/edit/forward/report context | +| `inProgress` | `Boolean` | Send operation in flight | +| `progressByTimeout` | `Boolean` | Show spinner after 1-second send delay | +| `useLinkPreviews` | `Boolean` | Link preview feature flag | +| `mentions` | `MentionedMembers` | Map of mention display name to `CIMention` | + +### Computed Properties + +| Property | Type | Description | +|---|---|---| +| `editing` | `Boolean` | True when `contextItem` is `EditingItem` | +| `forwarding` | `Boolean` | True when `contextItem` is `ForwardingItems` | +| `reporting` | `Boolean` | True when `contextItem` is `ReportedItem` | +| `sendEnabled` | `() -> Boolean` | True when there is content to send and not in progress | +| `linkPreviewAllowed` | `Boolean` | True when no media/voice/file preview is active | +| `linkPreview` | `LinkPreview?` | Extracts link preview from `CLinkPreview` | +| `attachmentDisabled` | `Boolean` | True when editing, forwarding, live, in-progress, or reporting | +| `attachmentPreview` | `Boolean` | True when a file or media preview is showing | +| `empty` | `Boolean` | True when no text, no preview, and no context item | +| `whitespaceOnly` | `Boolean` | True when message text contains only whitespace | +| `placeholder` | `String` | Input placeholder text (report reason text or default) | +| `memberMentions` | `Map` | Extracted member ID map for API calls | + +### ComposeMessage + +```kotlin +@Serializable +data class ComposeMessage( + val text: String = "", + val selection: TextRange = TextRange.Zero +) +``` + +### LiveMessage + +```kotlin +@Serializable +data class LiveMessage( + val chatItem: ChatItem, + val typedMsg: String, + val sentMsg: String, + val sent: Boolean +) +``` + +Tracks a live (streaming) message: the associated `ChatItem`, the currently typed text, the last sent text, and whether the initial send has occurred. + +### Serialization + +`ComposeState` is fully `@Serializable` with a custom `Saver` (line ~214) that uses `json.encodeToString`/`decodeFromString` for `rememberSaveable` persistence across configuration changes. + +--- + + + +## 3. ComposePreview Sealed Class + +**Location:** [`ComposeView.kt#L52`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L52) + +```kotlin +sealed class ComposePreview { + object NoPreview : ComposePreview() + class CLinkPreview(val linkPreview: LinkPreview?) : ComposePreview() + class MediaPreview(val images: List, val content: List) : ComposePreview() + data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean) : ComposePreview() + class FilePreview(val fileName: String, val uri: URI) : ComposePreview() +} +``` + +| Variant | Fields | View | +|---|---|---| +| `NoPreview` | -- | Nothing shown | +| `CLinkPreview` | `linkPreview: LinkPreview?` (null = loading) | `ComposeLinkView`: title, description, image thumbnail, cancel button | +| `MediaPreview` | `images: List` (base64 thumbnails), `content: List` | `ComposeImageView`: horizontal thumbnail strip, cancel button | +| `VoicePreview` | `voice: String` (file path), `durationMs: Int`, `finished: Boolean` | `ComposeVoiceView`: waveform visualization, duration, play/pause | +| `FilePreview` | `fileName: String`, `uri: URI` | `ComposeFileView`: file icon, file name, cancel button | + +### UploadContent + +Used within `MediaPreview` to track the source type: + +- `SimpleImage(uri: URI)` -- still image +- `AnimatedImage(uri: URI)` -- GIF or animated WebP +- `Video(uri: URI, duration: Int)` -- video with duration in seconds + +--- + +## 4. ComposeContextItem Sealed Class + +**Location:** [`ComposeView.kt#L61`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L61) + +```kotlin +sealed class ComposeContextItem { + object NoContextItem : ComposeContextItem() + class QuotedItem(val chatItem: ChatItem) : ComposeContextItem() + class EditingItem(val chatItem: ChatItem) : ComposeContextItem() + class ForwardingItems(val chatItems: List, val fromChatInfo: ChatInfo) : ComposeContextItem() + class ReportedItem(val chatItem: ChatItem, val reason: ReportReason) : ComposeContextItem() +} +``` + +| Variant | Trigger | Compose Behavior | +|---|---|---| +| `NoContextItem` | Default state | Normal message composition | +| `QuotedItem` | Swipe-to-reply or reply menu action | Shows quoted message indicator; sends with `quoted` parameter | +| `EditingItem` | Edit menu action | Populates text field with existing message; send button becomes checkmark; calls `apiUpdateChatItem` | +| `ForwardingItems` | Forward action from another chat | Shows forwarded items indicator; calls `apiForwardChatItems`; can include optional text message | +| `ReportedItem` | Report menu action | Shows report indicator with reason; placeholder changes to reason text; calls `apiReportMessage` | + +### Context Item View + +`contextItemView()` (line ~1098 in `ComposeView.kt`) renders the active context as a dismissible bar above the text input: + +- Icon: reply (ic_reply), edit (ic_edit_filled), forward (ic_forward), report (ic_flag) +- Content: quoted message preview text with sender name +- Close button: resets `contextItem` to `NoContextItem` (or `clearState()` for editing) + +--- + + + +## 5. SendMsgView + +**Location:** [`SendMsgView.kt#L36`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt#L36) + +```kotlin +fun SendMsgView( + composeState: MutableState, + showVoiceRecordIcon: Boolean, + recState: MutableState, + isDirectChat: Boolean, + liveMessageAlertShown: SharedPreference, + sendMsgEnabled: Boolean, + userCantSendReason: Pair?, + sendButtonEnabled: Boolean, + sendToConnect: (() -> Unit)?, + hideSendButton: Boolean, + nextConnect: Boolean, + needToAllowVoiceToContact: Boolean, + allowedVoiceByPrefs: Boolean, + sendButtonColor: Color, + allowVoiceToContact: () -> Unit, + timedMessageAllowed: Boolean, + customDisappearingMessageTimePref: SharedPreference?, + placeholder: String, + sendMessage: (Int?) -> Unit, + sendLiveMessage: (suspend () -> Unit)?, + updateLiveMessage: (suspend () -> Unit)?, + cancelLiveMessage: (() -> Unit)?, + editPrevMessage: () -> Unit, + onFilesPasted: (List) -> Unit, + onMessageChange: (ComposeMessage) -> Unit, + textStyle: MutableState, + focusRequester: FocusRequester? +) +``` + +### Layout + +The view is a `Box` containing: + +1. **PlatformTextField:** Multiline text input (platform-specific `expect`). Handles text changes via `onMessageChange`, up-arrow to `editPrevMessage`, file paste via `onFilesPasted`, and Enter to send. +2. **DeleteTextButton:** Shown when text is long; clears the field. +3. **Action area** (bottom-right, stacked): + - **Progress indicator:** Shown when `progressByTimeout` is true. + - **Report confirm button:** Checkmark icon when context is `ReportedItem`. + - **Voice record button:** Shown when message is empty, not editing/forwarding, no preview active. + - `RecordVoiceView`: Hold-to-record with waveform display. + - `DisallowedVoiceButton`: Shown when voice is disabled by preferences. + - `VoiceButtonWithoutPermissionByPlatform`: Shown when microphone permission is not granted. + - **Live message button:** Bolt icon, starts streaming message (calls `sendLiveMessage`). + - **Send button:** Arrow icon (new message) or checkmark (editing/live). Long-press opens dropdown: + - "Send live message" option + - Timed message options (1min, 5min, 1hr, 8hr, 1day, 1week, 1month, custom) + +### RecordingState + +```kotlin +sealed class RecordingState { + object NotStarted : RecordingState() + class Started(val filePath: String, val progressMs: Int) : RecordingState() + class Finished(val filePath: String, val durationMs: Int) : RecordingState() +} +``` + +Voice recording of 300ms or less is auto-cancelled. + +### Disabled State + +When `sendMsgEnabled` is false (e.g., contact not ready, group permissions), an overlay covers the text field. If `userCantSendReason` is provided, tapping the overlay shows an alert explaining why sending is disabled. + +--- + +## 6. Attachment Handling + + + +### Attachment Selection + +The `AttachmentSelection` composable (line ~263 in `ComposeView.kt`) is an `expect` function with platform-specific implementations: + +**Android:** +- Camera launcher (image capture) +- Gallery launcher (image/video picker, multi-select) +- File picker (any file type) + +**Desktop:** +- File chooser dialog (filters for images or all files) + +### ChooseAttachmentView + +Bottom sheet (`ModalBottomSheetLayout`) presenting attachment type options: + +| Option | Result | +|---|---| +| Camera (Android) | Launches camera intent; result processed as `SimpleImage` | +| Gallery | Launches media picker; results processed via `processPickedMedia` | +| File | Launches file picker; result processed via `processPickedFile` | + +### File Processing + +**`processPickedFile`** (line ~281): +1. Checks file size against `maxFileSize` (XFTP limit). +2. Extracts file name from URI. +3. Sets `ComposePreview.FilePreview` on compose state. + +**`processPickedMedia`** (line ~300): +1. For each URI, determines type (image, animated image, video). +2. Images: Gets bitmap, creates `SimpleImage` or `AnimatedImage` upload content. +3. Videos: Extracts thumbnail and duration, creates `Video` upload content. +4. Generates base64 preview thumbnails (max 14KB). +5. Sets `ComposePreview.MediaPreview` with thumbnails and content list. + +**`onFilesAttached`** (line ~270): +Groups dropped/pasted files into images and non-images; routes to `processPickedMedia` or `processPickedFile`. + +### Send Flow + +On send (line ~603, `sendMessageAsync`): + +1. **Forwarding:** Calls `apiForwardChatItems`, then optionally sends a text message quoting the last forwarded item. +2. **Editing:** Calls `apiUpdateChatItem` with updated `MsgContent`. +3. **Reporting:** Calls `apiReportMessage` with reason and text. +4. **New message:** Iterates over `msgs` (one per media item or single for text/file/voice): + - Saves file to app storage (or remote host). + - For voice: encrypts if `privacyEncryptLocalFiles` is enabled. + - Calls `apiSendMessages` or `apiCreateChatItems` (local notes). +5. On failure of the last message, restores compose state for retry. + +### Link Preview + +When `privacyLinkPreviews` is enabled and the message contains a URL: + +1. `showLinkPreview` extracts first non-SimpleX, non-cancelled link from parsed markdown. +2. Sets `ComposePreview.CLinkPreview(null)` (loading state). +3. After 1.5s debounce, calls `getLinkPreview(url)`. +4. On success, updates to `CLinkPreview(linkPreview)`. +5. Cancel button adds the URL to `cancelledLinks` set. + +--- + +## 7. Draft Persistence + +**Location:** [`ComposeView.kt#L1230`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L1230) (`KeyChangeEffect(chatModel.chatId.value)`) + +Controlled by the `privacySaveLastDraft` preference. + +### Save Behavior + +When the user navigates away from a chat (`chatModel.chatId.value` changes): + +| Compose State | Action | +|---|---| +| Live message active (text present or already sent) | Sends the live message immediately, clears draft | +| In progress | Clears in-progress flag, clears previous draft | +| Non-empty (text, preview, or context) | If `saveLastDraft` is true: saves `composeState.value` to `chatModel.draft.value` and `chatModel.draftChatId.value` | +| Empty but draft exists for current chat | Restores draft from `chatModel.draft` | +| Empty, no draft | Clears previous draft, deletes unused files | + +### Restore Behavior + +When entering a chat (line ~132 in `ChatView.kt`): + +1. Checks if `chatModel.draftChatId.value` matches the chat ID. +2. If match and draft is not null (and not a cross-chat forward), initializes `composeState` from the draft. +3. Otherwise, creates a fresh `ComposeState`. + +### Desktop-specific + +On desktop, a `DisposableEffect` (line ~1256) saves the draft on dispose when forwarding content, since the `KeyChangeEffect` mechanism is Android-specific. + +### Draft Display in Chat List + +When a draft exists for a chat, `ChatPreviewView` shows a pencil icon with the draft text instead of the last message preview. + +--- + +## 8. Source Files + +| File | Description | +|---|---| +| `ComposeView.kt` | ComposeState, ComposePreview, ComposeContextItem, ComposeView composable, send logic, link preview, draft persistence | +| `SendMsgView.kt` | Text input field, send/voice/live/timed buttons, recording state | +| `ComposeFileView.kt` | File attachment preview (name, cancel) | +| `ComposeImageView.kt` | Media attachment preview (thumbnails, cancel) | +| `ComposeVoiceView.kt` | Voice recording preview (waveform, duration, play) | +| `ContextItemView.kt` | Reply/edit/forward/report context bar | +| `ComposeContextContactRequestActionsView.kt` | Contact request action buttons in compose area | +| `ComposeContextGroupDirectInvitationActionsView.kt` | Group direct invitation compose actions | +| `ComposeContextPendingMemberActionsView.kt` | Pending member compose actions | +| `ComposeContextProfilePickerView.kt` | Profile picker in compose context | +| `SelectableChatItemToolbars.kt` | Multi-select mode toolbar (delete, forward, moderate) | diff --git a/apps/multiplatform/spec/client/navigation.md b/apps/multiplatform/spec/client/navigation.md new file mode 100644 index 0000000000..c9939ea3c0 --- /dev/null +++ b/apps/multiplatform/spec/client/navigation.md @@ -0,0 +1,379 @@ +# 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) | diff --git a/apps/multiplatform/spec/database.md b/apps/multiplatform/spec/database.md new file mode 100644 index 0000000000..f6ecedb721 --- /dev/null +++ b/apps/multiplatform/spec/database.md @@ -0,0 +1,393 @@ +# Database & Storage + +## Table of Contents + +1. [Overview](#1-overview) +2. [Database Files & Paths](#2-database-files--paths) +3. [Haskell Store Modules](#3-haskell-store-modules) +4. [Migrations](#4-migrations) +5. [Database Encryption](#5-database-encryption) +6. [File Storage](#6-file-storage) +7. [Export & Import](#7-export--import) +8. [Source Files](#8-source-files) + +--- + +## 1. Overview + +SimpleX Chat uses **two SQLite databases** managed entirely by the Haskell core. Kotlin code **never reads or writes the databases directly** -- all data access goes through the JNI command/response protocol defined in [SimpleXAPI.kt](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt). + +The two databases are: + +| Database | Suffix | Contents | +|----------|--------|----------| +| Chat database | `_chat.db` | Users, contacts, groups, messages, files metadata, settings | +| Agent database | `_agent.db` | SMP/XFTP agent state: connections, queues, encryption keys, delivery tracking | + +Both databases are created and migrated by the `chatMigrateInit` JNI function. The Kotlin layer handles: +- Providing the correct file path prefix (`dbAbsolutePrefixPath`) +- Providing the encryption key +- Interpreting migration results (`DBMigrationResult`) +- Exposing API functions that proxy to Haskell store operations + +--- + +## 2. Database Files & Paths + +### Expect Declarations + +The common module declares platform-dependent paths as `expect` values in [Files.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt): + +```kotlin +expect val dataDir: File // L18 +expect val tmpDir: File // L19 +expect val filesDir: File // L20 +expect val appFilesDir: File // L21 +expect val wallpapersDir: File // L22 +expect val coreTmpDir: File // L23 +expect val dbAbsolutePrefixPath: String // L24 +expect val preferencesDir: File // L25 +expect val preferencesTmpDir: File // L26 + +expect val chatDatabaseFileName: String // L28 +expect val agentDatabaseFileName: String // L29 + +expect val databaseExportDir: File // L35 +expect val remoteHostsDir: File // L37 +``` + +### Android Actual Values + +From [Files.android.kt](../common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt): + +| Variable | Value | Notes | +|----------|-------|-------| +| `dataDir` | `androidAppContext.dataDir` | `/data/data//` | +| `tmpDir` | `getDir("temp", MODE_PRIVATE)` | Private temp directory | +| `filesDir` | `dataDir/files` | Parent for all file storage | +| `appFilesDir` | `filesDir/app_files` | User-visible chat file attachments | +| `wallpapersDir` | `filesDir/assets/wallpapers` | Custom wallpaper images | +| `coreTmpDir` | `filesDir/temp_files` | Haskell core temp directory | +| `dbAbsolutePrefixPath` | `dataDir/files` | Prefix: core appends `_chat.db` / `_agent.db` | +| `chatDatabaseFileName` | `"files_chat.db"` | Full filename: `files_chat.db` | +| `agentDatabaseFileName` | `"files_agent.db"` | Full filename: `files_agent.db` | +| `databaseExportDir` | `androidAppContext.cacheDir` | Temp location for archive export | +| `remoteHostsDir` | `tmpDir/remote_hosts` | Remote host file staging | +| `preferencesDir` | `dataDir/shared_prefs` | Android SharedPreferences directory | + +### Desktop Actual Values + +From [Files.desktop.kt](../common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt): + +| Variable | Value | Notes | +|----------|-------|-------| +| `dataDir` | `desktopPlatform.dataPath` | XDG_DATA_HOME (Linux), AppData (Windows), Application Support (macOS) | +| `tmpDir` | `java.io.tmpdir/simplex` | System temp with `deleteOnExit` | +| `filesDir` | `dataDir/simplex_v1_files` | Flat file storage | +| `appFilesDir` | Same as `filesDir` | No subdirectory on desktop | +| `wallpapersDir` | `dataDir/simplex_v1_assets/wallpapers` | Custom wallpaper images | +| `coreTmpDir` | `dataDir/tmp` | Haskell core temp directory | +| `dbAbsolutePrefixPath` | `dataDir/simplex_v1` | Prefix: core appends `_chat.db` / `_agent.db` | +| `chatDatabaseFileName` | `"simplex_v1_chat.db"` | Full filename: `simplex_v1_chat.db` | +| `agentDatabaseFileName` | `"simplex_v1_agent.db"` | Full filename: `simplex_v1_agent.db` | +| `databaseExportDir` | Same as `tmpDir` | Temp location for archive export | +| `remoteHostsDir` | `dataDir/remote_hosts` | Remote host file staging | +| `preferencesDir` | `desktopPlatform.configPath` | Platform config directory | + +### Resulting Database Paths + +| Platform | Chat DB | Agent DB | +|----------|---------|----------| +| Android | `/data/data//files_chat.db` | `/data/data//files_agent.db` | +| Desktop (Linux) | `~/.local/share/simplex/simplex_v1_chat.db` | `~/.local/share/simplex/simplex_v1_agent.db` | +| Desktop (macOS) | `~/Library/Application Support/simplex/simplex_v1_chat.db` | ... | +| Desktop (Windows) | `%APPDATA%/simplex/simplex_v1_chat.db` | ... | + +--- + +## 3. Haskell Store Modules + +The Haskell core organizes database access into store modules. Kotlin code invokes these indirectly through `CC` commands. The store modules are: + +| Module | Path | Responsibilities | +|--------|------|-----------------| +| `Messages.hs` | `src/Simplex/Chat/Store/Messages.hs` | Message CRUD, chat items, reactions, delivery statuses, TTL cleanup | +| `Groups.hs` | `src/Simplex/Chat/Store/Groups.hs` | Group profiles, membership, roles, invitations, group links | +| `Direct.hs` | `src/Simplex/Chat/Store/Direct.hs` | Contact management, direct connections, contact requests | +| `Files.hs` | `src/Simplex/Chat/Store/Files.hs` | File transfer metadata, XFTP state, standalone files | +| `Profiles.hs` | `src/Simplex/Chat/Store/Profiles.hs` | User profiles, display names, address book | +| `Connections.hs` | `src/Simplex/Chat/Store/Connections.hs` | SMP agent connections, pending connections, server switches | + +All store operations execute within SQLite transactions managed by the Haskell core. The Kotlin layer has no direct knowledge of table schemas or SQL queries. + +--- + +## 4. Migrations + +### JNI Entry Point + +Database migration is triggered by the `chatMigrateInit` external function ([Core.kt#L25](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L25)): + +```kotlin +external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array +``` + +**Parameters:** +- `dbPath` -- the `dbAbsolutePrefixPath` (core appends `_chat.db` and `_agent.db`) +- `dbKey` -- encryption passphrase (empty string = unencrypted) +- `confirm` -- migration confirmation mode: `"error"`, `"yesUp"`, or `"yesUpDown"` + +**Returns:** `Array` where: +- `[0]` -- JSON string encoding a `DBMigrationResult` +- `[1]` -- `ChatCtrl` handle (Long) if migration succeeded + +### Migration Flow in `initChatController` + +The full initialization sequence is in [Core.kt#L62](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L62): + +1. Obtain the DB encryption key from `DatabaseUtils.useDatabaseKey()`. +2. Determine the confirmation mode (default: `YesUp`; developer mode with confirm upgrades: `Error`). +3. Call `chatMigrateInit(dbAbsolutePrefixPath, dbKey, "error")` -- first attempt with `Error` to detect pending migrations. +4. Parse the result as `DBMigrationResult`. +5. If the result is `ErrorMigration` with an `Upgrade` error and confirmation allows it, re-run `chatMigrateInit` with the appropriate confirmation (`"yesUp"`). +6. If `OK`, store the `ChatCtrl` handle, set `chatDbEncrypted`, and proceed to start the chat. +7. If not `OK`, handle special case: if the `newDatabaseInitialized` preference is not set AND the database was only partially initialized (single DB file exists), remove both files and retry once. + + + +### DBMigrationResult + +Defined in [DatabaseUtils.kt#L79](../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt#L79): + +```kotlin +sealed class DBMigrationResult { + object OK // Migration succeeded + object InvalidConfirmation // Invalid confirmation parameter + data class ErrorNotADatabase(val dbFile: String) // File exists but is not a valid database + data class ErrorMigration(val dbFile: String, // Migration error with details + val migrationError: MigrationError) + data class ErrorSQL(val dbFile: String, // SQL error during migration + val migrationSQLError: String) + object ErrorKeychain // Keychain/keystore error + data class Unknown(val json: String) // Unparseable response +} +``` + +### MigrationError + +```kotlin +sealed class MigrationError { + class Upgrade(val upMigrations: List) // Pending forward migrations + class Downgrade(val downMigrations: List) // Database is newer than app + class Error(val mtrError: MTRError) // Conflict or missing migrations +} +``` + +### MigrationConfirmation + +```kotlin +enum class MigrationConfirmation(val value: String) { + YesUp("yesUp"), // Auto-confirm forward migrations + YesUpDown("yesUpDown"), // Auto-confirm both directions (not used in UI) + Error("error") // Report errors without running migrations +} +``` + +--- + +## 5. Database Encryption + +### Encryption API + +Two API functions manage database encryption, both in [SimpleXAPI.kt](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt): + +| Function | Parameters | Description | Line | +|----------|-----------|-------------|------| +| `apiStorageEncryption` | `currentKey: String, newKey: String` | Change or set the database encryption passphrase | [L999](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L999) | +| `testStorageEncryption` | `key: String, ctrl: ChatCtrl?` | Test whether a given key can decrypt the database | [L1006](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1006) | + +Both delegate to the Haskell core via `CC.ApiStorageEncryption(DBEncryptionConfig)` and `CC.TestStorageEncryption(key)` respectively. + + + +`DBEncryptionConfig` ([SimpleXAPI.kt#L4166](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L4166)): + +```kotlin +class DBEncryptionConfig(val currentKey: String, val newKey: String) +``` + +### Passphrase Storage -- CryptorInterface + +The `CryptorInterface` ([Cryptor.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt)) provides platform-specific key encryption for storing the DB passphrase at rest: + +```kotlin +interface CryptorInterface { + fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? + fun encryptText(text: String, alias: String): Pair + fun deleteKey(alias: String) +} + +expect val cryptor: CryptorInterface +``` + +### Android Implementation + +[Cryptor.android.kt](../common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt): + +- Uses **Android KeyStore** (`"AndroidKeyStore"` provider) +- Algorithm: **AES/GCM/NoPadding** (128-bit authentication tag) +- Keys are hardware-backed when available +- On decryption failure with a random initial passphrase, throws to prevent overwriting +- Shows user alerts for keychain errors + +```kotlin +internal class Cryptor: CryptorInterface { + private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + // AES-GCM encryption/decryption using AndroidKeyStore-managed keys +} +``` + +### Desktop Implementation + +[Cryptor.desktop.kt](../common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt): + +- **Placeholder/no-op implementation** -- data is returned as-is +- No actual encryption of the stored passphrase on desktop +- `decryptData` returns `String(data)` without decryption +- `encryptText` returns the raw bytes without encryption + +```kotlin +actual val cryptor: CryptorInterface = object : CryptorInterface { + override fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? = String(data) + override fun encryptText(text: String, alias: String) = text.toByteArray() to text.toByteArray() + override fun deleteKey(alias: String) {} +} +``` + +### Passphrase Management + +`DatabaseUtils` ([DatabaseUtils.kt](../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt)) provides: + +- `ksDatabasePassword` -- encrypted passphrase stored in platform preferences (SharedPreferences on Android, file-based on desktop) +- `useDatabaseKey()` -- retrieves the passphrase, decrypting it via `CryptorInterface` +- `randomDatabasePassword()` -- generates a 32-byte random passphrase (Base64-encoded) for initial database creation + +The flow: +1. On first launch, `randomDatabasePassword()` generates a key. +2. `CryptorInterface.encryptText()` encrypts the key for storage. +3. The encrypted (data, IV) pair is saved to preferences via `ksDatabasePassword`. +4. On subsequent launches, `ksDatabasePassword.get()` retrieves the encrypted pair, and `CryptorInterface.decryptData()` recovers the plaintext key. +5. The key is passed to `chatMigrateInit` to open the encrypted SQLite databases. + +--- + +## 6. File Storage + +### Directory Layout + +Declared in [Files.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) with platform-specific implementations: + +| Directory | Variable | Android Path | Desktop Path | Purpose | +|-----------|----------|-------------|--------------|---------| +| App files | `appFilesDir` | `dataDir/files/app_files` | `dataDir/simplex_v1_files` | Chat file attachments (images, videos, documents) | +| Wallpapers | `wallpapersDir` | `dataDir/files/assets/wallpapers` | `dataDir/simplex_v1_assets/wallpapers` | Custom chat wallpaper images | +| Core temp | `coreTmpDir` | `dataDir/files/temp_files` | `dataDir/tmp` | Haskell core temporary files (in-progress transfers) | +| App temp | `tmpDir` | `getDir("temp", MODE_PRIVATE)` | `java.io.tmpdir/simplex` | Application-level temporary files | +| Remote hosts | `remoteHostsDir` | `tmpDir/remote_hosts` | `dataDir/remote_hosts` | Files staged for remote host sessions | +| DB export | `databaseExportDir` | `androidAppContext.cacheDir` | Same as `tmpDir` | Temporary storage for database archive ZIP | +| Preferences | `preferencesDir` | `dataDir/shared_prefs` | `desktopPlatform.configPath` | User preferences, theme YAML | +| Migration temp | `getMigrationTempFilesDirectory()` | `dataDir/migration_temp_files` | `dataDir/migration_temp_files` | Temporary files during database migration | + +### File Path Resolution + +Files referenced by chat items use `CryptoFile` (optional encryption metadata + relative path). Path resolution is handled by helper functions in [Files.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt): + +- `getAppFilePath(fileName)` ([L81](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L81)) -- resolves to `appFilesDir/fileName` for local, or `remoteHostsDir//simplex_v1_files/fileName` for remote hosts +- `getWallpaperFilePath(fileName)` ([L91](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L91)) -- resolves wallpaper paths similarly +- `getLoadedFilePath(file)` ([L105](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L105)) -- returns the full path if the file is downloaded and ready + +### Local File Encryption + +The `apiSetEncryptLocalFiles(enable)` command ([SimpleXAPI.kt#L967](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L967)) tells the Haskell core to encrypt files stored in `appFilesDir`. When enabled, files are written as `CryptoFile` with a random AES key and nonce. The JNI functions `chatEncryptFile` and `chatDecryptFile` ([Core.kt#L39-L40](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L39)) handle the actual crypto operations. + +--- + +## 7. Export & Import + +### API Functions + +| Function | CC Command | CR Response | Line | +|----------|-----------|-------------|------| +| `apiExportArchive(config)` | `CC.ApiExportArchive(config)` | `CR.ArchiveExported(archiveErrors)` | [SimpleXAPI.kt#L981](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L981) | +| `apiImportArchive(config)` | `CC.ApiImportArchive(config)` | `CR.ArchiveImported(archiveErrors)` | [SimpleXAPI.kt#L987](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L987) | +| `apiDeleteStorage()` | `CC.ApiDeleteStorage()` | `CR.CmdOk` | [SimpleXAPI.kt#L993](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L993) | + +### ArchiveConfig + +Defined at [SimpleXAPI.kt#L4162](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L4162): + +```kotlin +class ArchiveConfig( + val archivePath: String, // Full path to the ZIP archive + val disableCompression: Boolean?, // Skip compression for speed + val parentTempDirectory: String? // Temp directory for extraction +) +``` + +### Export Flow + +1. UI constructs an `ArchiveConfig` with a path under `databaseExportDir`. +2. Calls `apiExportArchive(config)` which sends `CC.ApiExportArchive` to the Haskell core. +3. The core creates a ZIP containing both `_chat.db` and `_agent.db` (and optionally files). +4. Returns `CR.ArchiveExported` with a list of `ArchiveError` (non-fatal issues during export). +5. UI offers the archive file for sharing/saving. + +### Import Flow + +1. User selects an archive file. +2. UI copies it to a temp location and constructs an `ArchiveConfig`. +3. Calls `apiImportArchive(config)` which sends `CC.ApiImportArchive` to the Haskell core. +4. The core extracts and replaces both databases. +5. Returns `CR.ArchiveImported` with a list of `ArchiveError` (non-fatal issues during import). +6. UI triggers re-initialization via `initChatController`. + + + +### ArchiveError + +Defined at [SimpleXAPI.kt#L7658](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7658): + +```kotlin +sealed class ArchiveError { + class ArchiveErrorImport(val importError: String) // General import error + class ArchiveErrorFile(val file: String, val fileError: String) // Per-file error +} +``` + +### Delete Storage + +`apiDeleteStorage()` removes both database files entirely. This is used during account deletion or database reset operations. After calling this, `initChatController` must be called to create fresh databases. + +--- + +## 8. Source Files + +| File | Purpose | Path | +|------|---------|------| +| SimpleXAPI.kt | API functions: encryption, export/import, storage commands | `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` | +| Core.kt | JNI externals (`chatMigrateInit`, `chatEncryptFile`, etc.), `initChatController` | `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` | +| Files.kt | Platform-expect file/directory path declarations | `common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt` | +| Files.android.kt | Android actual paths (dataDir, appFilesDir, etc.) | `common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt` | +| Files.desktop.kt | Desktop actual paths (XDG/AppData, etc.) | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt` | +| Cryptor.kt | Platform-expect encryption interface for passphrase storage | `common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt` | +| Cryptor.android.kt | Android: AES-GCM via AndroidKeyStore | `common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt` | +| Cryptor.desktop.kt | Desktop: placeholder (no-op) implementation | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt` | +| DatabaseUtils.kt | `DBMigrationResult`, `MigrationError`, `MigrationConfirmation`, passphrase helpers | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` | +| Messages.hs | Haskell store: message CRUD, reactions, delivery | `src/Simplex/Chat/Store/Messages.hs` | +| Groups.hs | Haskell store: groups, membership, roles | `src/Simplex/Chat/Store/Groups.hs` | +| Direct.hs | Haskell store: contacts, direct connections | `src/Simplex/Chat/Store/Direct.hs` | +| Files.hs | Haskell store: file transfer metadata | `src/Simplex/Chat/Store/Files.hs` | +| Profiles.hs | Haskell store: user profiles | `src/Simplex/Chat/Store/Profiles.hs` | +| Connections.hs | Haskell store: SMP agent connections | `src/Simplex/Chat/Store/Connections.hs` | + +All Kotlin paths are relative to `apps/multiplatform/`. All Haskell paths are relative to the repository root. diff --git a/apps/multiplatform/spec/impact.md b/apps/multiplatform/spec/impact.md new file mode 100644 index 0000000000..cd0f836585 --- /dev/null +++ b/apps/multiplatform/spec/impact.md @@ -0,0 +1,532 @@ +# SimpleX Chat Android & Desktop -- Impact Graph + +> Source file to product concept mapping. Use this to identify which product documents must be updated when a source file changes. +> +> Covers Kotlin Multiplatform (Compose) sources: commonMain, androidMain, desktopMain, and the Android and Desktop app modules. Also covers the shared Haskell core. + +--- + +## Product Concept Legend + +| ID | Concept | +|----|---------| +| PC1 | Chat List | +| PC2 | Direct Chat | +| PC3 | Group Chat | +| PC4 | Message Composition | +| PC5 | Message Reactions | +| PC6 | Message Editing | +| PC7 | Message Deletion | +| PC8 | Timed Messages | +| PC9 | Voice Messages | +| PC10 | File Transfer | +| PC11 | Link Previews | +| PC12 | Contact Connection | +| PC13 | Contact Verification | +| PC14 | Group Management | +| PC15 | Group Links | +| PC16 | Member Roles | +| PC17 | Audio/Video Calls | +| PC18 | Notifications | +| PC19 | User Profiles | +| PC20 | Incognito Mode | +| PC21 | Hidden Profiles | +| PC22 | Local Authentication | +| PC23 | Database Encryption | +| PC24 | Theme System | +| PC25 | Network Configuration | +| PC26 | Device Migration | +| PC27 | Remote Desktop | +| PC28 | Chat Tags | +| PC29 | User Address | +| PC30 | Member Support Chat | + +--- + +## 1. Common Sources (commonMain) + +Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` + +### 1.1 Core Model & Platform + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `App.kt` | PC1 through PC30 | High | Root composable — navigation scaffold for all features | +| `AppLock.kt` | PC22 | Medium | App lock state and authorization lifecycle | +| `model/ChatModel.kt` | PC1 through PC30 | High | Central state object — every feature reads or writes here | +| `model/SimpleXAPI.kt` | PC1 through PC30 | High | FFI bridge to Haskell core — all commands and responses | +| `model/CryptoFile.kt` | PC10, PC23 | Medium | Encrypted file read/write helpers | +| `platform/Core.kt` | PC1 through PC30 | High | Native FFI declarations (`chatMigrateInit`, `chatSendCmd`, etc.) — all API traffic | +| `platform/AppCommon.kt` | PC1 through PC30 | Medium | Shared app initialization logic | +| `platform/Files.kt` | PC10, PC23, PC26 | Medium | File path resolution, temp dirs, encryption utilities | +| `platform/NtfManager.kt` | PC18 | High | Notification manager expect declarations | +| `platform/Notifications.kt` | PC18 | Medium | Notification channel and permission abstractions | +| `platform/SimplexService.kt` | PC18 | Medium | Background service expect declarations | +| `platform/RecAndPlay.kt` | PC9 | Medium | Audio recording and playback abstractions | +| `platform/VideoPlayer.kt` | PC10, PC17 | Low | Video playback abstractions | +| `platform/Cryptor.kt` | PC23 | Medium | Keystore encryption expect declarations | +| `platform/Share.kt` | PC10, PC12 | Low | Share sheet abstractions | +| `platform/Images.kt` | PC10, PC19 | Low | Image processing utilities | +| `platform/Platform.kt` | PC1 through PC30 | Low | Platform detection and capability flags | +| `platform/PlatformTextField.kt` | PC4 | Low | Native text input expect declarations | +| `platform/Back.kt` | PC1 | Low | Back navigation handling | +| `platform/UI.kt` | PC24 | Low | UI density and locale helpers | +| `platform/ScrollableColumn.kt` | PC1 | Low | Scrollable list abstractions | +| `platform/Log.kt` | — | Low | Logging utility — no direct product impact | +| `platform/Modifier.kt` | PC24 | Low | Compose modifier extensions | +| `platform/Resources.kt` | PC24 | Low | Resource loading helpers | + +### 1.2 Theme + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `ui/theme/ThemeManager.kt` | PC24 | Medium | Theme resolution engine — all color and wallpaper logic | +| `ui/theme/Theme.kt` | PC24 | Medium | Theme composables and `SimpleXTheme` | +| `ui/theme/Color.kt` | PC24 | Low | Color palette definitions | +| `ui/theme/Shape.kt` | PC24 | Low | Shape token definitions | +| `ui/theme/Type.kt` | PC24 | Low | Typography definitions | + +### 1.3 Views — Chat List + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/chatlist/ChatListView.kt` | PC1, PC28 | High | Main screen — chat list rendering and search | +| `views/chatlist/ChatListNavLinkView.kt` | PC1, PC2, PC3 | Medium | Navigation from chat list item to chat | +| `views/chatlist/ChatPreviewView.kt` | PC1, PC2, PC3, PC11 | Medium | Chat row preview rendering | +| `views/chatlist/TagListView.kt` | PC28 | Medium | Chat tag filter UI | +| `views/chatlist/UserPicker.kt` | PC19, PC21 | Medium | Multi-profile switcher overlay | +| `views/chatlist/ShareListView.kt` | PC10 | Low | Share target list | +| `views/chatlist/ShareListNavLinkView.kt` | PC10 | Low | Share target navigation | +| `views/chatlist/ChatHelpView.kt` | PC1 | Low | Empty-state help content | +| `views/chatlist/ContactRequestView.kt` | PC12 | Medium | Incoming contact request row | +| `views/chatlist/ContactConnectionView.kt` | PC12 | Low | Pending connection row | +| `views/chatlist/ServersSummaryView.kt` | PC25 | Low | Server status summary | + +### 1.4 Views — Chat & Messaging + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/chat/ChatView.kt` | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC11 | High | Core conversation UI — most messaging features | +| `views/chat/ComposeView.kt` | PC4, PC6, PC9, PC10, PC11 | High | Message composition — send path for all messages | +| `views/chat/SendMsgView.kt` | PC4, PC9 | Medium | Send button and voice record toggle | +| `views/chat/ComposeVoiceView.kt` | PC9 | Medium | Voice message recording UI | +| `views/chat/ComposeFileView.kt` | PC10 | Low | File attachment preview in compose area | +| `views/chat/ComposeImageView.kt` | PC10 | Low | Image attachment preview in compose area | +| `views/chat/ContextItemView.kt` | PC6 | Low | Reply/edit quote preview | +| `views/chat/SelectableChatItemToolbars.kt` | PC7, PC10 | Medium | Multi-select toolbar (delete, forward) | +| `views/chat/ChatInfoView.kt` | PC2, PC13, PC20 | Medium | Contact details and verification | +| `views/chat/ContactPreferences.kt` | PC2, PC8 | Medium | Per-contact feature preferences | +| `views/chat/ChatItemInfoView.kt` | PC2, PC3 | Low | Message delivery detail | +| `views/chat/ChatItemsLoader.kt` | PC2, PC3 | Medium | Pagination and message loading logic | +| `views/chat/ChatItemsMerger.kt` | PC2, PC3 | Medium | Merges incremental message updates | +| `views/chat/VerifyCodeView.kt` | PC13 | Medium | Contact security code verification | +| `views/chat/ScanCodeView.kt` | PC13 | Low | QR code scanning for verification | +| `views/chat/CommandsMenuView.kt` | PC4 | Low | Slash-command menu | +| `views/chat/ComposeContextProfilePickerView.kt` | PC20 | Low | Incognito profile picker in compose | +| `views/chat/ComposeContextPendingMemberActionsView.kt` | PC14, PC30 | Low | Pending member action buttons in compose | +| `views/chat/ComposeContextGroupDirectInvitationActionsView.kt` | PC14 | Low | Direct invitation action buttons in compose | +| `views/chat/ComposeContextContactRequestActionsView.kt` | PC12 | Low | Contact request action buttons in compose | + +### 1.5 Views — Chat Items + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/chat/item/ChatItemView.kt` | PC2, PC3, PC5, PC6, PC7, PC8 | High | Root chat item renderer with context menus | +| `views/chat/item/TextItemView.kt` | PC2, PC3, PC4 | Medium | Text message bubble rendering | +| `views/chat/item/FramedItemView.kt` | PC4, PC6, PC10, PC11 | Medium | Framed (quoted/forwarded) message container | +| `views/chat/item/CIImageView.kt` | PC10 | Medium | Image message rendering | +| `views/chat/item/CIVideoView.kt` | PC10 | Medium | Video message rendering | +| `views/chat/item/CIFileView.kt` | PC10 | Medium | File message rendering | +| `views/chat/item/CIVoiceView.kt` | PC9 | Medium | Voice message rendering and playback | +| `views/chat/item/EmojiItemView.kt` | PC5 | Low | Emoji reaction display | +| `views/chat/item/CIMetaView.kt` | PC2, PC3, PC8 | Low | Timestamp, delivery status, timed message indicator | +| `views/chat/item/CICallItemView.kt` | PC17 | Low | Call event item rendering | +| `views/chat/item/CIEventView.kt` | PC3, PC14, PC16 | Low | Group event item rendering | +| `views/chat/item/CIGroupInvitationView.kt` | PC3, PC14 | Low | Group invitation item rendering | +| `views/chat/item/CIMemberCreatedContactView.kt` | PC3, PC12 | Low | Member-created contact event | +| `views/chat/item/CIChatFeatureView.kt` | PC8 | Low | Feature change event rendering | +| `views/chat/item/CIFeaturePreferenceView.kt` | PC8 | Low | Feature preference change rendering | +| `views/chat/item/CIRcvDecryptionError.kt` | PC2, PC3 | Low | Decryption error display | +| `views/chat/item/DeletedItemView.kt` | PC7 | Low | Deleted message placeholder | +| `views/chat/item/MarkedDeletedItemView.kt` | PC7 | Low | Moderated/marked-deleted placeholder | +| `views/chat/item/ImageFullScreenView.kt` | PC10 | Low | Full-screen image viewer | +| `views/chat/item/CIBrokenComposableView.kt` | — | Low | Fallback for render failures | +| `views/chat/item/CIInvalidJSONView.kt` | — | Low | Fallback for malformed items | +| `views/chat/item/IntegrityErrorItemView.kt` | PC2, PC3 | Low | Message integrity error display | + +### 1.6 Views — Groups + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/chat/group/GroupChatInfoView.kt` | PC3, PC14, PC15, PC16, PC30 | High | Group management hub | +| `views/chat/group/AddGroupMembersView.kt` | PC14, PC16 | Medium | Member invitation flow | +| `views/chat/group/GroupMemberInfoView.kt` | PC3, PC14, PC16, PC30 | Medium | Member details and role management | +| `views/chat/group/GroupProfileView.kt` | PC3, PC14 | Medium | Group profile editing | +| `views/chat/group/GroupLinkView.kt` | PC15 | Low | Group link creation and sharing | +| `views/chat/group/GroupPreferences.kt` | PC3, PC8, PC14 | Medium | Group feature toggles | +| `views/chat/group/GroupMentions.kt` | PC3, PC4 | Medium | @mention resolution and display | +| `views/chat/group/GroupMembersToolbar.kt` | PC3, PC14 | Low | Member list toolbar | +| `views/chat/group/GroupReportsView.kt` | PC3, PC14 | Low | Group content reports | +| `views/chat/group/MemberAdmission.kt` | PC14, PC16 | Medium | Member admission settings | +| `views/chat/group/MemberSupportView.kt` | PC30 | Medium | Member support chat toggle | +| `views/chat/group/MemberSupportChatView.kt` | PC30 | Medium | Member support chat conversation | +| `views/chat/group/WelcomeMessageView.kt` | PC3, PC14 | Low | Group welcome message editor | + +### 1.7 Views — Calls + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/call/CallView.kt` | PC17 | High | Call UI and WebRTC composable | +| `views/call/CallManager.kt` | PC17 | High | Call lifecycle management | +| `views/call/WebRTC.kt` | PC17 | High | WebRTC types and signaling | +| `views/call/IncomingCallAlertView.kt` | PC17, PC18 | Medium | Incoming call overlay | + +### 1.8 Views — New Chat & Contacts + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/newchat/NewChatView.kt` | PC12, PC29 | High | New connection creation — onramp for all contacts | +| `views/newchat/NewChatSheet.kt` | PC12 | Medium | Bottom sheet with connection options | +| `views/newchat/ConnectPlan.kt` | PC12, PC15 | Medium | Link parsing and connection plan resolution | +| `views/newchat/AddGroupView.kt` | PC3, PC14 | Medium | New group creation flow | +| `views/newchat/ContactConnectionInfoView.kt` | PC12 | Low | Pending connection details | +| `views/newchat/AddContactLearnMore.kt` | PC12 | Low | Educational content | +| `views/newchat/QRCode.kt` | PC12 | Low | QR code display | +| `views/newchat/QRCodeScanner.kt` | PC12 | Low | QR code camera scanner | +| `views/contacts/ContactListNavView.kt` | PC1, PC12 | Medium | Contact list navigation | +| `views/contacts/ContactPreviewView.kt` | PC12 | Low | Contact row preview | + +### 1.9 Views — User Settings + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/usersettings/SettingsView.kt` | PC18, PC22, PC23, PC24, PC25, PC29 | Medium | Settings navigation hub | +| `views/usersettings/Appearance.kt` | PC24 | Low | Theme and appearance customization | +| `views/usersettings/PrivacySettings.kt` | PC20, PC22 | Medium | Privacy and lock settings | +| `views/usersettings/UserProfileView.kt` | PC19 | Medium | Profile display name and image editing | +| `views/usersettings/UserProfilesView.kt` | PC19, PC21 | Medium | Multi-profile management | +| `views/usersettings/HiddenProfileView.kt` | PC21 | Medium | Hidden profile access | +| `views/usersettings/IncognitoView.kt` | PC20 | Low | Incognito mode explanation | +| `views/usersettings/UserAddressView.kt` | PC29 | Medium | User SimpleX address management | +| `views/usersettings/UserAddressLearnMore.kt` | PC29 | Low | Address educational content | +| `views/usersettings/NotificationsSettingsView.kt` | PC18 | Medium | Notification mode configuration | +| `views/usersettings/CallSettings.kt` | PC17 | Low | Call-related settings | +| `views/usersettings/Preferences.kt` | PC2, PC3, PC8 | Medium | Chat feature preferences UI | +| `views/usersettings/SetDeliveryReceiptsView.kt` | PC2 | Low | Delivery receipts toggle | +| `views/usersettings/RTCServers.kt` | PC17, PC25 | Medium | WebRTC ICE server configuration | +| `views/usersettings/DeveloperView.kt` | — | Low | Developer/debug settings | +| `views/usersettings/HelpView.kt` | — | Low | Help and support links | +| `views/usersettings/MarkdownHelpView.kt` | PC4 | Low | Markdown formatting guide | +| `views/usersettings/VersionInfoView.kt` | — | Low | Version display | +| `views/usersettings/networkAndServers/NetworkAndServers.kt` | PC25 | High | Server and network configuration hub | +| `views/usersettings/networkAndServers/AdvancedNetworkSettings.kt` | PC25 | Medium | SOCKS proxy, timeouts, etc. | +| `views/usersettings/networkAndServers/OperatorView.kt` | PC25 | Medium | Server operator management | +| `views/usersettings/networkAndServers/ProtocolServersView.kt` | PC25 | Medium | SMP/XFTP server list | +| `views/usersettings/networkAndServers/ProtocolServerView.kt` | PC25 | Low | Individual server editing | +| `views/usersettings/networkAndServers/NewServerView.kt` | PC25 | Low | Add new server | +| `views/usersettings/networkAndServers/ScanProtocolServer.kt` | PC25 | Low | QR scan for server address | + +### 1.10 Views — Database & Migration + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/database/DatabaseView.kt` | PC23, PC26 | High | Database management — export, import, passphrase | +| `views/database/DatabaseEncryptionView.kt` | PC23 | High | Database encryption passphrase change | +| `views/database/DatabaseErrorView.kt` | PC23 | Medium | Database open error recovery | +| `views/migration/MigrateFromDevice.kt` | PC26 | High | Outbound device migration | +| `views/migration/MigrateToDevice.kt` | PC26 | High | Inbound device migration | + +### 1.11 Views — Local Auth & Onboarding + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/localauth/LocalAuthView.kt` | PC22 | Medium | App lock authentication flow | +| `views/localauth/SetAppPasscodeView.kt` | PC22 | Medium | Passcode creation and change | +| `views/localauth/PasscodeView.kt` | PC22 | Medium | Passcode entry UI | +| `views/localauth/PasswordEntry.kt` | PC22 | Low | Password input field | +| `views/onboarding/OnboardingView.kt` | PC1 | Medium | Onboarding flow navigation | +| `views/onboarding/SimpleXInfo.kt` | PC1 | Low | Welcome screen | +| `views/onboarding/SetNotificationsMode.kt` | PC18 | Medium | Notification permission and mode setup | +| `views/onboarding/SetupDatabasePassphrase.kt` | PC23 | Medium | Initial database passphrase setup | +| `views/onboarding/ChooseServerOperators.kt` | PC25 | Medium | Initial server operator selection | +| `views/onboarding/WhatsNewView.kt` | — | Low | Release notes display | +| `views/onboarding/HowItWorks.kt` | — | Low | Educational content | +| `views/onboarding/LinkAMobileView.kt` | PC27 | Low | Mobile linking onboarding | + +### 1.12 Views — Remote Desktop + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/remote/ConnectDesktopView.kt` | PC27 | Medium | Connect-to-desktop flow (from mobile) | +| `views/remote/ConnectMobileView.kt` | PC27 | Medium | Connect-to-mobile flow (from desktop) | + +### 1.13 Views — Helpers + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/helpers/AlertManager.kt` | PC1 through PC30 | Medium | Modal alert system used across all features | +| `views/helpers/ModalView.kt` | PC1 through PC30 | Medium | Modal navigation stack | +| `views/helpers/Utils.kt` | PC1 through PC30 | Low | Shared formatting, clipboard, and utility functions | +| `views/helpers/DatabaseUtils.kt` | PC23 | Medium | Keystore passphrase and database helpers | +| `views/helpers/LinkPreviews.kt` | PC11 | Medium | Link preview fetching and rendering | +| `views/helpers/LocalAuthentication.kt` | PC22 | Medium | Biometric/passcode authentication expect | +| `views/helpers/ChatWallpaper.kt` | PC24 | Low | Chat wallpaper rendering | +| `views/helpers/ChatInfoImage.kt` | PC19 | Low | Profile image composable | +| `views/helpers/ThemeModeEditor.kt` | PC24 | Low | Theme mode toggle | +| `views/helpers/ChooseAttachmentView.kt` | PC10 | Low | Attachment picker | +| `views/helpers/GetImageView.kt` | PC10, PC19 | Low | Image capture and crop | +| `views/helpers/TextEditor.kt` | PC4 | Low | Rich text editor helpers | +| `views/helpers/SearchTextField.kt` | PC1 | Low | Search bar composable | +| `views/helpers/CustomTimePicker.kt` | PC8 | Low | Time picker for timed messages | +| `views/helpers/DragAndDrop.kt` | PC10 | Low | Drag-and-drop file handling | +| `views/helpers/ProcessedErrors.kt` | — | Low | Error aggregation | +| `views/helpers/AnimationUtils.kt` | PC24 | Low | Animation helpers | +| `views/helpers/DefaultDialog.kt` | — | Low | Dialog composable primitives | +| `views/helpers/DefaultDropdownMenu.kt` | — | Low | Dropdown menu composable | +| `views/helpers/Section.kt` | — | Low | Settings section composable | +| `views/helpers/SimpleButton.kt` | — | Low | Button composable | +| `views/helpers/DefaultTopAppBar.kt` | — | Low | App bar composable | +| `views/helpers/DefaultBasicTextField.kt` | PC4 | Low | Text field composable | +| `views/helpers/AppBarTitle.kt` | — | Low | App bar title composable | +| `views/helpers/BlurModifier.kt` | PC22 | Low | Blur modifier for app lock | +| `views/helpers/CollapsingAppBar.kt` | — | Low | Collapsing toolbar composable | +| `views/helpers/CustomIcons.kt` | — | Low | Custom icon definitions | +| `views/helpers/DataClasses.kt` | — | Low | Shared data class utilities | +| `views/helpers/DefaultProgressBar.kt` | — | Low | Progress bar composable | +| `views/helpers/DefaultSwitch.kt` | — | Low | Switch composable | +| `views/helpers/Enums.kt` | — | Low | Enum utility extensions | +| `views/helpers/ExposedDropDownSettingRow.kt` | — | Low | Dropdown setting row composable | +| `views/helpers/GestureDetector.kt` | — | Low | Touch gesture utilities | +| `views/helpers/Modifiers.kt` | — | Low | Compose modifier extensions | +| `views/helpers/SubscriptionStatusIcon.kt` | PC25 | Low | Server connection status icon | + +### 1.14 Views — Other + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/TerminalView.kt` | — | Low | Developer chat console | +| `views/SplashView.kt` | — | Low | Splash screen | +| `views/WelcomeView.kt` | PC1 | Low | Empty-state welcome | +| `views/Preview.kt` | — | Low | Compose preview utilities | + +--- + +## 2. Android Sources + +### 2.1 Android App Module + +Path prefix: `android/src/main/java/chat/simplex/app/` + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `SimplexApp.kt` | PC1 through PC30 | High | Application class — initializes core, preferences, and notification channels | +| `MainActivity.kt` | PC1 through PC30 | High | Single-activity host — intent handling, lifecycle, deep links | +| `SimplexService.kt` | PC18 | High | Foreground service — keeps message receiver alive | +| `CallService.kt` | PC17 | Medium | Foreground service for active calls | +| `MessagesFetcherWorker.kt` | PC18 | Medium | WorkManager periodic message fetch | +| `model/NtfManager.android.kt` | PC18 | High | Android notification channels, display, and actions | +| `views/call/CallActivity.kt` | PC17 | Medium | Dedicated activity for full-screen call UI | +| `views/helpers/Util.kt` | — | Low | Android-specific utility extensions | + +### 2.2 Android Platform Implementations (androidMain) + +Path prefix: `common/src/androidMain/kotlin/chat/simplex/common/` + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `platform/AppCommon.android.kt` | PC1 through PC30 | Medium | Android app initialization actual declarations | +| `platform/SimplexService.android.kt` | PC18 | Medium | Android foreground service actual implementation | +| `platform/Files.android.kt` | PC10, PC23, PC26 | Medium | Android file paths and content-URI resolution | +| `platform/Cryptor.android.kt` | PC23 | Medium | Android Keystore encryption actual implementation | +| `platform/RecAndPlay.android.kt` | PC9 | Medium | Android MediaRecorder/MediaPlayer actual implementation | +| `platform/VideoPlayer.android.kt` | PC10 | Low | Android ExoPlayer actual implementation | +| `platform/Notifications.android.kt` | PC18 | Medium | Android notification channel creation | +| `platform/Images.android.kt` | PC10, PC19 | Low | Android bitmap processing | +| `platform/PlatformTextField.android.kt` | PC4 | Low | Android native text field actual implementation | +| `platform/Share.android.kt` | PC10 | Low | Android share intent actual implementation | +| `platform/Back.android.kt` | PC1 | Low | Android back press handler | +| `platform/UI.android.kt` | PC24 | Low | Android density and locale | +| `platform/ScrollableColumn.android.kt` | PC1 | Low | Android lazy list actual implementation | +| `platform/Log.android.kt` | — | Low | Android Log wrapper | +| `platform/Modifier.android.kt` | — | Low | Android modifier extensions | +| `platform/Resources.android.kt` | — | Low | Android resource loading | +| `helpers/NetworkObserver.kt` | PC25 | Medium | Android ConnectivityManager observer | +| `helpers/Permissions.kt` | PC9, PC10, PC17, PC18 | Medium | Android runtime permission requests | +| `helpers/SoundPlayer.kt` | PC17, PC18 | Low | Android sound playback for calls and notifications | +| `helpers/Extensions.kt` | — | Low | Kotlin extension utilities | +| `helpers/Locale.kt` | — | Low | Locale helpers | +| `views/call/CallView.android.kt` | PC17 | Medium | Android WebView-based WebRTC call | +| `views/call/CallAudioDeviceManager.kt` | PC17 | Medium | Android audio routing (speaker, earpiece, bluetooth) | +| `views/chat/ComposeView.android.kt` | PC4, PC10 | Low | Android compose view extensions | +| `views/chat/SendMsgView.android.kt` | PC4 | Low | Android send button extensions | +| `views/chat/item/ChatItemView.android.kt` | PC2, PC3 | Low | Android chat item extensions | +| `views/chat/item/CIImageView.android.kt` | PC10 | Low | Android image rendering extensions | +| `views/chat/item/CIVideoView.android.kt` | PC10 | Low | Android video rendering extensions | +| `views/chat/item/CIFileView.android.kt` | PC10 | Low | Android file view extensions | +| `views/chat/item/EmojiItemView.android.kt` | PC5 | Low | Android emoji rendering extensions | +| `views/chat/item/ImageFullScreenView.android.kt` | PC10 | Low | Android full-screen image viewer | +| `views/chatlist/ChatListView.android.kt` | PC1 | Low | Android chat list extensions | +| `views/chatlist/ChatListNavLinkView.android.kt` | PC1 | Low | Android chat list navigation extensions | +| `views/chatlist/TagListView.android.kt` | PC28 | Low | Android tag list extensions | +| `views/chatlist/UserPicker.android.kt` | PC19 | Low | Android profile picker extensions | +| `views/database/DatabaseView.android.kt` | PC23, PC26 | Low | Android database view extensions | +| `views/database/DatabaseEncryptionView.android.kt` | PC23 | Low | Android encryption view extensions | +| `views/helpers/LocalAuthentication.android.kt` | PC22 | Medium | Android BiometricPrompt actual implementation | +| `views/helpers/ChooseAttachmentView.android.kt` | PC10 | Low | Android file/camera chooser | +| `views/helpers/GetImageView.android.kt` | PC10, PC19 | Low | Android image capture | +| `views/helpers/CustomTimePicker.android.kt` | PC8 | Low | Android time picker | +| `views/helpers/Utils.android.kt` | — | Low | Android utility extensions | +| `views/helpers/DefaultDialog.android.kt` | — | Low | Android dialog extensions | +| `views/helpers/WorkaroundFocusSearchLayout.kt` | — | Low | Android focus workaround | +| `views/newchat/QRCode.android.kt` | PC12 | Low | Android QR code rendering | +| `views/newchat/QRCodeScanner.android.kt` | PC12 | Low | Android camera QR scanner | +| `views/onboarding/SimpleXInfo.android.kt` | PC1 | Low | Android onboarding extensions | +| `views/onboarding/SetNotificationsMode.android.kt` | PC18 | Low | Android notification mode extensions | +| `views/usersettings/Appearance.android.kt` | PC24 | Low | Android appearance extensions | +| `views/usersettings/PrivacySettings.android.kt` | PC20, PC22 | Low | Android privacy settings extensions | +| `views/usersettings/SettingsView.android.kt` | — | Low | Android settings extensions | +| `views/usersettings/networkAndServers/OperatorView.android.kt` | PC25 | Low | Android operator view extensions | +| `views/usersettings/networkAndServers/ScanProtocolServer.android.kt` | PC25 | Low | Android server QR scan | +| `ui/theme/Theme.android.kt` | PC24 | Low | Android dynamic color / system theme | +| `ui/theme/Type.android.kt` | PC24 | Low | Android typography | + +--- + +## 3. Desktop Sources + +### 3.1 Desktop App Module + +Path prefix: `desktop/src/jvmMain/kotlin/chat/simplex/desktop/` + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `Main.kt` | PC1 through PC30 | High | JVM entry point — Haskell init, migrations, app launch | + +### 3.2 Desktop Platform Implementations (desktopMain) + +Path prefix: `common/src/desktopMain/kotlin/chat/simplex/common/` + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `DesktopApp.kt` | PC1, PC2, PC3 | High | Desktop Compose window — window lifecycle, crash recovery | +| `StoreWindowState.kt` | — | Low | Window position/size persistence | +| `model/NtfManager.desktop.kt` | PC18 | Medium | Desktop system tray notification display | +| `platform/AppCommon.desktop.kt` | PC1 through PC30 | Medium | Desktop app initialization actual declarations | +| `platform/SimplexService.desktop.kt` | PC18 | Low | Desktop background receiver (no foreground service) | +| `platform/Files.desktop.kt` | PC10, PC23, PC26 | Medium | Desktop file path resolution | +| `platform/Cryptor.desktop.kt` | PC23 | Medium | Desktop keystore encryption actual implementation | +| `platform/RecAndPlay.desktop.kt` | PC9 | Medium | Desktop audio recording/playback actual implementation | +| `platform/VideoPlayer.desktop.kt` | PC10 | Low | Desktop VLC-based video player | +| `platform/Videos.desktop.kt` | PC10 | Low | Desktop video utilities | +| `platform/Notifications.desktop.kt` | PC18 | Low | Desktop notification setup | +| `platform/Images.desktop.kt` | PC10 | Low | Desktop image processing | +| `platform/PlatformTextField.desktop.kt` | PC4 | Low | Desktop text field actual implementation | +| `platform/Share.desktop.kt` | PC10 | Low | Desktop clipboard/share | +| `platform/Back.desktop.kt` | PC1 | Low | Desktop back navigation | +| `platform/UI.desktop.kt` | PC24 | Low | Desktop density and locale | +| `platform/ScrollableColumn.desktop.kt` | PC1 | Low | Desktop lazy list | +| `platform/Platform.desktop.kt` | — | Low | Platform detection | +| `platform/Log.desktop.kt` | — | Low | Desktop log output | +| `platform/Modifier.desktop.kt` | — | Low | Desktop modifier extensions | +| `platform/Resources.desktop.kt` | — | Low | Desktop resource loading | +| `views/call/CallView.desktop.kt` | PC17 | Medium | Desktop WebView-based WebRTC call | +| `views/chat/ComposeView.desktop.kt` | PC4, PC10 | Low | Desktop compose view (drag-and-drop, paste) | +| `views/chat/SendMsgView.desktop.kt` | PC4 | Low | Desktop send shortcut (Enter key handling) | +| `views/chat/item/ChatItemView.desktop.kt` | PC2, PC3 | Low | Desktop chat item extensions | +| `views/chat/item/CIImageView.desktop.kt` | PC10 | Low | Desktop image rendering | +| `views/chat/item/CIVideoView.desktop.kt` | PC10 | Low | Desktop video rendering | +| `views/chat/item/CIFileView.desktop.kt` | PC10 | Low | Desktop file open/save | +| `views/chat/item/EmojiItemView.desktop.kt` | PC5 | Low | Desktop emoji rendering | +| `views/chat/item/ImageFullScreenView.desktop.kt` | PC10 | Low | Desktop full-screen image | +| `views/chatlist/ChatListView.desktop.kt` | PC1 | Low | Desktop chat list extensions | +| `views/chatlist/ChatListNavLinkView.desktop.kt` | PC1 | Low | Desktop chat list navigation | +| `views/chatlist/TagListView.desktop.kt` | PC28 | Low | Desktop tag list extensions | +| `views/chatlist/UserPicker.desktop.kt` | PC19 | Low | Desktop profile picker | +| `views/database/DatabaseView.desktop.kt` | PC23, PC26 | Low | Desktop database view extensions | +| `views/database/DatabaseEncryptionView.desktop.kt` | PC23 | Low | Desktop encryption view extensions | +| `views/helpers/AppUpdater.kt` | — | Low | Desktop auto-update checker and installer | +| `views/helpers/OkHttpProgressListener.kt` | — | Low | Download progress tracking for updates | +| `views/helpers/LocalAuthentication.desktop.kt` | PC22 | Low | Desktop passcode-only auth (no biometrics) | +| `views/helpers/ChooseAttachmentView.desktop.kt` | PC10 | Low | Desktop file chooser dialog | +| `views/helpers/GetImageView.desktop.kt` | PC10, PC19 | Low | Desktop image file picker | +| `views/helpers/CustomTimePicker.desktop.kt` | PC8 | Low | Desktop time picker | +| `views/helpers/Utils.desktop.kt` | — | Low | Desktop utility extensions | +| `views/helpers/DefaultDialog.desktop.kt` | — | Low | Desktop dialog extensions | +| `views/newchat/QRCode.desktop.kt` | PC12 | Low | Desktop QR code rendering | +| `views/newchat/QRCodeScanner.desktop.kt` | PC12 | Low | Desktop QR code scanner (screen/clipboard) | +| `views/onboarding/SimpleXInfo.desktop.kt` | PC1 | Low | Desktop onboarding extensions | +| `views/onboarding/SetNotificationsMode.desktop.kt` | PC18 | Low | Desktop notification mode extensions | +| `views/usersettings/Appearance.desktop.kt` | PC24 | Low | Desktop appearance extensions | +| `views/usersettings/PrivacySettings.desktop.kt` | PC20, PC22 | Low | Desktop privacy settings extensions | +| `views/usersettings/SettingsView.desktop.kt` | — | Low | Desktop settings extensions | +| `views/usersettings/networkAndServers/OperatorView.desktop.kt` | PC25 | Low | Desktop operator view extensions | +| `views/usersettings/networkAndServers/ScanProtocolServer.desktop.kt` | PC25 | Low | Desktop server address scan | +| `ui/theme/Theme.desktop.kt` | PC24 | Low | Desktop system theme detection | +| `ui/theme/Type.desktop.kt` | PC24 | Low | Desktop typography | +| `other/videoplayer/SkiaBitmapVideoSurface.kt` | PC10 | Low | Desktop Skia video surface for VLC | + +--- + +## 4. Haskell Core Impact + +The Haskell core is compiled as a shared native library (`libsimplex.so` / `libsimplex.dylib`) and linked via JNI through `Core.kt`. Changes here affect both Android and Desktop identically. + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `src/Simplex/Chat.hs` | PC1 through PC30 | High | Main chat module — top-level orchestration | +| `src/Simplex/Chat/Controller.hs` | PC1 through PC30 | High | Command processor — all API commands dispatched here | +| `src/Simplex/Chat/Types.hs` | PC1 through PC30 | High | Core data types shared across all features | +| `src/Simplex/Chat/Core.hs` | PC1 through PC30 | High | Chat engine lifecycle (start, stop, subscribe) | +| `src/Simplex/Chat/Library/Commands.hs` | PC1 through PC30 | High | API command handler implementations | +| `src/Simplex/Chat/Library/Internal.hs` | PC1 through PC30 | High | Internal helpers for command processing | +| `src/Simplex/Chat/Library/Subscriber.hs` | PC1 through PC30 | High | Event subscriber — incoming message routing | +| `src/Simplex/Chat/Protocol.hs` | PC2, PC3, PC4, PC5, PC6, PC7 | High | Chat-level message protocol (x-events) | +| `src/Simplex/Chat/Messages.hs` | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9 | High | Message types and content | +| `src/Simplex/Chat/Messages/CIContent.hs` | PC4, PC5, PC6, PC7, PC8, PC9, PC11 | Medium | Chat item content variants | +| `src/Simplex/Chat/Messages/CIContent/Events.hs` | PC3, PC14, PC16 | Medium | Group event content types | +| `src/Simplex/Chat/Messages/Batch.hs` | PC2, PC3, PC4 | Medium | Message batching for efficient delivery | +| `src/Simplex/Chat/Call.hs` | PC17 | Medium | Call signaling types | +| `src/Simplex/Chat/Files.hs` | PC10 | Medium | File transfer orchestration | +| `src/Simplex/Chat/Delivery.hs` | PC2, PC3 | Medium | Message delivery engine | +| `src/Simplex/Chat/Markdown.hs` | PC4 | Low | Markdown parsing for message formatting | +| `src/Simplex/Chat/Store.hs` | PC1 through PC30 | High | Database store interface | +| `src/Simplex/Chat/Store/Shared.hs` | PC1 through PC30 | Medium | Shared store utilities | +| `src/Simplex/Chat/Store/Messages.hs` | PC4, PC5, PC6, PC7, PC8 | High | Message persistence | +| `src/Simplex/Chat/Store/Groups.hs` | PC3, PC14, PC15, PC16, PC30 | High | Group persistence | +| `src/Simplex/Chat/Store/Direct.hs` | PC2, PC12, PC13 | High | Contact persistence | +| `src/Simplex/Chat/Store/Files.hs` | PC10 | Medium | File transfer persistence | +| `src/Simplex/Chat/Store/Profiles.hs` | PC19, PC21 | Medium | User profile persistence | +| `src/Simplex/Chat/Store/Connections.hs` | PC2, PC12 | High | Connection persistence and entity resolution | +| `src/Simplex/Chat/Store/ContactRequest.hs` | PC12 | Medium | Contact request persistence | +| `src/Simplex/Chat/Store/NoteFolders.hs` | PC1 | Low | Note folder (self-chat) persistence | +| `src/Simplex/Chat/Store/Delivery.hs` | PC2, PC3 | Medium | Delivery task persistence | +| `src/Simplex/Chat/Store/AppSettings.hs` | PC25 | Low | App settings persistence | +| `src/Simplex/Chat/Store/Remote.hs` | PC27 | Low | Remote desktop session persistence | +| `src/Simplex/Chat/Archive.hs` | PC26 | Medium | Database export/import for migration | +| `src/Simplex/Chat/Options.hs` | PC23, PC25 | Low | Startup options (DB path, key, etc.) | +| `src/Simplex/Chat/Remote.hs` | PC27 | Medium | Remote desktop protocol handler | +| `src/Simplex/Chat/Remote/Types.hs` | PC27 | Low | Remote desktop data types | +| `src/Simplex/Chat/Remote/Protocol.hs` | PC27 | Medium | Remote desktop wire protocol | +| `src/Simplex/Chat/Remote/Transport.hs` | PC27 | Low | Remote desktop transport layer | +| `src/Simplex/Chat/Remote/RevHTTP.hs` | PC27 | Low | Reverse HTTP for remote desktop | +| `src/Simplex/Chat/Remote/AppVersion.hs` | PC27 | Low | Remote version negotiation | +| `src/Simplex/Chat/ProfileGenerator.hs` | PC20 | Low | Random profile generation for incognito | +| `src/Simplex/Chat/Types/UITheme.hs` | PC24 | Low | Theme data types for UI customization | +| `src/Simplex/Chat/Types/Preferences.hs` | PC2, PC3, PC8 | Medium | Chat feature preferences (timed messages, etc.) | +| `src/Simplex/Chat/Types/Shared.hs` | PC3, PC16 | Medium | Shared types including GroupMemberRole | +| `src/Simplex/Chat/Types/MemberRelations.hs` | PC3, PC16, PC30 | Medium | Member relationship state machine | +| `src/Simplex/Chat/Operators.hs` | PC25 | Medium | Server operator management | +| `src/Simplex/Chat/Operators/Presets.hs` | PC25 | Low | Preset server operators | +| `src/Simplex/Chat/Operators/Conditions.hs` | PC25 | Low | Operator usage conditions | +| `src/Simplex/Chat/AppSettings.hs` | PC25 | Low | App settings sync types | +| `src/Simplex/Chat/Mobile.hs` | PC1 through PC30 | High | C FFI exports — JNI bridge target | +| `src/Simplex/Chat/Mobile/File.hs` | PC10 | Medium | Mobile file read/write FFI | +| `src/Simplex/Chat/Mobile/Shared.hs` | PC1 through PC30 | Medium | Shared FFI helpers | +| `src/Simplex/Chat/Mobile/WebRTC.hs` | PC17 | Low | WebRTC FFI helpers | +| `src/Simplex/Chat/View.hs` | PC1 through PC30 | Low | Terminal view rendering (not used by mobile/desktop UI) | +| `src/Simplex/Chat/Stats.hs` | PC25 | Low | Server statistics tracking | +| `src/Simplex/Chat/Util.hs` | — | Low | General Haskell utilities | +| `src/Simplex/Chat/Styled.hs` | — | Low | Terminal styled text (not used by mobile/desktop UI) | +| `src/Simplex/Chat/Help.hs` | — | Low | Terminal help text | +| `src/Simplex/Chat/Bot.hs` | — | Low | Chat bot framework | +| `src/Simplex/Chat/Bot/KnownContacts.hs` | — | Low | Bot known contacts | diff --git a/apps/multiplatform/spec/services/calls.md b/apps/multiplatform/spec/services/calls.md new file mode 100644 index 0000000000..a8d056ebea --- /dev/null +++ b/apps/multiplatform/spec/services/calls.md @@ -0,0 +1,175 @@ +# WebRTC Calling Service + +## Table of Contents + +1. [Overview](#1-overview) +2. [Call State Machine](#2-call-state-machine) +3. [Android Implementation](#3-android-implementation) +4. [Desktop Implementation](#4-desktop-implementation) +5. [Common Call API](#5-common-call-api) +6. [IncomingCallAlertView](#6-incomingcallalertview) +7. [Source Files](#7-source-files) + +## Executive Summary + +WebRTC calling in SimpleX Chat operates over SMP (SimpleX Messaging Protocol) for signaling, with platform-specific WebRTC media implementations. Android uses a WebView-based approach with a dedicated `CallActivity` and foreground `CallService`, while Desktop opens the system browser and communicates via a NanoWSD WebSocket server on localhost. Both platforms share a common `CallManager` for call lifecycle and a `CallState` enum for state tracking. Call commands and responses are serialized as JSON and exchanged between the native layer and the WebRTC JavaScript layer. + +--- + +## 1. Overview + +Call signaling uses the same SMP protocol on all platforms -- call invitations, offers, answers, ICE candidates, and status updates flow through the chat backend via API commands. The WebRTC media plane, however, is implemented differently per platform: + +- **Android**: WebView loads `call.html` from bundled assets; a `@JavascriptInterface` bridge (`WebRTCInterface`) forwards JSON messages between Kotlin and JavaScript. +- **Desktop**: The system browser opens `http://localhost:50395/simplex/call/`; a NanoWSD HTTP+WebSocket server serves `call.html` from classpath resources and relays JSON commands/responses over WebSocket. + +Both platforms share the [`CallManager`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt) class (119 lines), which orchestrates incoming call acceptance, call ending, and notification management. + +--- + + + +## 2. Call State Machine + +Defined in [`WebRTC.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt#L50): + +``` +enum class CallState { + WaitCapabilities, // Call initiated, waiting for local WebRTC capabilities + InvitationSent, // Invitation sent to peer via SMP + InvitationAccepted, // Peer's invitation accepted locally + OfferSent, // SDP offer sent to peer + OfferReceived, // SDP offer received from peer + AnswerReceived, // SDP answer received from peer + Negotiated, // ICE negotiation in progress + Connected, // Media flowing + Ended; // Call terminated +} +``` + +**Outgoing call flow**: `WaitCapabilities` -> `InvitationSent` -> `OfferSent` -> `AnswerReceived` -> `Negotiated` -> `Connected` -> `Ended` + +**Incoming call flow**: `InvitationAccepted` -> `OfferReceived` -> `Negotiated` -> `Connected` -> `Ended` + +State transitions are driven by `WCallResponse` messages from the WebRTC layer. Each transition typically triggers a corresponding API command (e.g., `apiSendCallInvitation`, `apiSendCallOffer`). + +--- + + + +## 3. Android Implementation + +### 3.1 CallActivity.kt (464 lines) + +[`CallActivity.kt`](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt) + +A dedicated `ComponentActivity` that hosts the call UI. Key responsibilities: + +- **Intent handling** ([line 64](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L64)): On `AcceptCallAction` intent, looks up the matching `RcvCallInvitation` and calls `callManager.acceptIncomingCall()`. +- **Lock screen support** ([line 160](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L160)): `unlockForIncomingCall()` uses `setShowWhenLocked(true)` / `setTurnScreenOn(true)` on API 27+, falls back to window flags on older versions. `lockAfterIncomingCall()` reverses these settings. +- **Picture-in-Picture** ([line 99](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L99)): `setPipParams()` configures PiP aspect ratio and source rect hint. On Android 12+ (`Build.VERSION_CODES.S`), auto-enter PiP is enabled for video calls. `onPictureInPictureModeChanged` toggles `activeCallViewIsCollapsed` and sends a `WCallCommand.Layout` command. +- **Permission checks** ([line 122](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L122)): Checks `RECORD_AUDIO` and conditionally `CAMERA` permissions. +- **Service binding** ([line 181](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L181)): Binds to `CallService` as a workaround for Android 12 background activity launch restrictions. +- **CallActivityView composable** ([line 208](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L208)): Renders `ActiveCallView()` when permissions are granted and a call is active. Shows `CallPermissionsView` when permissions are needed. Shows `IncomingCallLockScreenAlert` for incoming calls on the lock screen. + +### 3.2 CallService.kt (207 lines) + +[`CallService.kt`](../../android/src/main/java/chat/simplex/app/CallService.kt) + +An Android foreground `Service` that keeps the call alive when the app is backgrounded: + +- **Foreground notification** ([line 131](../../android/src/main/java/chat/simplex/app/CallService.kt#L131)): Shows contact name (respecting `NotificationPreviewMode`), call type (audio/video), a chronometer when connected, and an "End call" action button. +- **WakeLock** ([line 66](../../android/src/main/java/chat/simplex/app/CallService.kt#L66)): Acquires `PARTIAL_WAKE_LOCK` to prevent CPU sleep during calls. +- **Notification channel** ([line 121](../../android/src/main/java/chat/simplex/app/CallService.kt#L121)): Creates `CALL_NOTIFICATION_CHANNEL_ID` with `IMPORTANCE_DEFAULT`. +- **Foreground service type** ([line 100](../../android/src/main/java/chat/simplex/app/CallService.kt#L100)): Uses `MEDIA_PLAYBACK | MICROPHONE` (+ `CAMERA` for video) on API 30+, `REMOTE_MESSAGING` on API 34+ when no active call. +- **Binder** ([line 158](../../android/src/main/java/chat/simplex/app/CallService.kt#L158)): `CallServiceBinder` allows `CallActivity` to call `updateNotification()` when call state changes. +- **CallActionReceiver** ([line 170](../../android/src/main/java/chat/simplex/app/CallService.kt#L170)): `BroadcastReceiver` that handles the `EndCallAction` from the notification. + +### 3.3 CallView.android.kt (891 lines) + +[`CallView.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt) + +The `actual` platform implementation of `ActiveCallView()` and supporting composables: + +- **ActiveCallState** ([line 74](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L74)): Manages proximity lock (screen-off wake lock), `CallAudioDeviceManager` for audio routing (earpiece/speaker/bluetooth), `CallSoundsPlayer` for ringtones and vibration. Implements `Closeable` to clean up resources on call end. +- **ActiveCallView** ([line 114](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L114)): Renders `WebRTCView` plus `ActiveCallOverlay`. Handles `WCallResponse` messages and dispatches corresponding API calls. Manages volume control stream (`STREAM_VOICE_CALL`), screen keep-on, and call command lifecycle. +- **WebRTCView** ([line 691](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L691)): Creates/reuses a static `WebView` via `AndroidView`. Configures `WebViewAssetLoader` for local asset loading. Sets up `WebRTCInterface` JavaScript bridge. Loads `file:android_asset/www/android/call.html`. Processes `WCallCommand` queue by evaluating `processCommand()` JavaScript. +- **ActiveCallOverlayLayout** ([line 329](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L329)): Full overlay with mic toggle, speaker/device selector, end call, video toggle, and camera flip buttons. Adapts layout for video vs audio calls. +- **CallPermissionsView** ([line 569](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L569)): Handles runtime permission requests for microphone and camera with a fallback to settings if the system dialog is not shown. + +### 3.4 ActiveCallState + +[`ActiveCallState`](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L74) (line 74 of `CallView.android.kt`): + +| Component | Purpose | +|---|---| +| `proximityLock` | `PROXIMITY_SCREEN_OFF_WAKE_LOCK` -- turns screen off when phone is held to ear | +| `callAudioDeviceManager` | Manages audio routing between earpiece, speaker, Bluetooth, wired headset | +| `CallSoundsPlayer` | Plays connecting/ringing sounds and vibration patterns | +| `wasConnected` | Tracks if call ever connected (for end-of-call vibration) | +| `close()` | Stops sounds, vibrates on disconnect, releases proximity lock, clears audio manager overrides | + +--- + +## 4. Desktop Implementation + +### 4.1 CallView.desktop.kt (263 lines) + +[`CallView.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt) + +Desktop calls run WebRTC in the system browser, not an embedded WebView: + +- **NanoWSD server** ([line 209](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L209)): `startServer()` creates a `NanoWSD` instance bound to `localhost:50395`. The server serves `call.html` from JAR resources at `/assets/www/desktop/call.html` for the path `/simplex/call/`. All other paths serve resources from `/assets/www/`. +- **WebSocket communication** ([line 238](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L238)): `MyWebSocket` handles WebSocket frames from the browser. `onMessage` deserializes JSON into `WVAPIMessage` and forwards to the response handler. `onClose` triggers `WCallResponse.End`. +- **WebRTCController** ([line 153](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L153)): Opens `http://localhost:50395/simplex/call/` via `LocalUriHandler`. Processes `WCallCommand` queue by sending JSON over WebSocket to all active connections. On dispose, sends `WCallCommand.End` and stops the server. +- **SendStateUpdates** ([line 137](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L137)): Sends `WCallCommand.Description` with call state and encryption info text to the browser for display. +- **ActiveCallView** ([line 28](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L28)): Handles `WCallResponse` messages identically to Android (same state machine), plus a `WCallCommand.Permission` message on `Capabilities` error for browser permission denial guidance. + +--- + +## 5. Common Call API + +Defined in [`SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt): + +| Function | Line | Description | +|---|---|---| +| `apiGetCallInvitations` | [L1842](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1842) | Retrieve pending call invitations from the backend | +| `apiSendCallInvitation` | [L1849](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1849) | Send call invitation to a contact with `CallType` | +| `apiRejectCall` | [L1854](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1854) | Reject an incoming call | +| `apiSendCallOffer` | [L1859](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1859) | Send SDP offer with ICE candidates and capabilities | +| `apiSendCallAnswer` | [L1866](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1866) | Send SDP answer with ICE candidates | +| `apiSendCallExtraInfo` | [L1872](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1872) | Send additional ICE candidates discovered after initial exchange | +| `apiEndCall` | [L1878](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1878) | Terminate a call | +| `apiCallStatus` | [L1883](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1883) | Report WebRTC connection status to the backend | + +All functions send commands via `sendCmd()` to the chat core and return `Boolean` success status (except `apiGetCallInvitations` which returns `List`). + +--- + + + +## 6. IncomingCallAlertView + +[`IncomingCallAlertView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt) (128 lines) + +An in-app notification banner shown when a call invitation arrives while the app is in the foreground: + +- **IncomingCallAlertView** ([line 27](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt#L27)): Starts `SoundPlayer` for the ringtone (suppressed if already in a call view). Shows `IncomingCallAlertLayout`. +- **IncomingCallAlertLayout** ([line 49](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt#L49)): Colored banner with `ProfilePreview` of the caller, call type icon (audio/video), and three action buttons: Reject (red), Ignore (primary), Accept (green). +- **IncomingCallInfo** ([line 74](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt#L74)): Shows the user profile image (for multi-user), call media type icon, and call type text (encrypted/unencrypted audio/video). + +--- + +## 7. Source Files + +| File | Path | Lines | Description | +|---|---|---|---| +| `CallView.kt` | [`common/src/commonMain/.../views/call/CallView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt) | 28 | `expect fun ActiveCallView()`, delivery receipt waiting | +| `CallView.android.kt` | [`common/src/androidMain/.../views/call/CallView.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt) | 891 | Android WebView WebRTC, overlay, permissions | +| `CallView.desktop.kt` | [`common/src/desktopMain/.../views/call/CallView.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt) | 263 | Desktop browser WebRTC via NanoWSD | +| `CallActivity.kt` | [`android/src/main/java/.../views/call/CallActivity.kt`](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt) | 464 | Android call Activity, PiP, lock screen | +| `CallService.kt` | [`android/src/main/java/.../CallService.kt`](../../android/src/main/java/chat/simplex/app/CallService.kt) | 207 | Android foreground service for calls | +| `CallManager.kt` | [`common/src/commonMain/.../views/call/CallManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt) | 119 | Call lifecycle management | +| `WebRTC.kt` | [`common/src/commonMain/.../views/call/WebRTC.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt) | -- | `CallState` enum, `WCallCommand`, `WCallResponse` types | +| `IncomingCallAlertView.kt` | [`common/src/commonMain/.../views/call/IncomingCallAlertView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt) | 128 | In-app incoming call notification banner | +| `SimpleXAPI.kt` | [`common/src/commonMain/.../model/SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt) | -- | Call API commands (L1837--L1881) | diff --git a/apps/multiplatform/spec/services/files.md b/apps/multiplatform/spec/services/files.md new file mode 100644 index 0000000000..329e37dbb1 --- /dev/null +++ b/apps/multiplatform/spec/services/files.md @@ -0,0 +1,213 @@ +# File Transfer Service + +## Table of Contents + +1. [Overview](#1-overview) +2. [File Size Constants](#2-file-size-constants) +3. [CryptoFile](#3-cryptofile) +4. [File Storage Paths](#4-file-storage-paths) +5. [API Commands](#5-api-commands) +6. [Auto-Receive Logic](#6-auto-receive-logic) +7. [Source Files](#7-source-files) + +## Executive Summary + +SimpleX Chat uses two file transfer mechanisms: inline SMP transfers for small files (embedded in message bodies) and XFTP (eXtended File Transfer Protocol) for larger files up to 1 GB. Files are optionally encrypted at rest using `CryptoFile` functions backed by the chat core's native crypto library. File storage paths are platform-specific: Android uses `Context.dataDir`-based directories while Desktop uses platform-appropriate data directories (XDG on Linux, AppData on Windows, Application Support on macOS). Auto-receive logic automatically accepts images, voice messages, and videos below configurable size thresholds. + +--- + +## 1. Overview + +File transfer decision logic: + +- **Inline (SMP)**: Files small enough to be base64-encoded and embedded directly in an SMP message body. The practical limit is defined by `MAX_IMAGE_SIZE` (255 KB) for compressed images. The maximum SMP inline size is `MAX_FILE_SIZE_SMP` (~7.6 MB). +- **XFTP**: For files exceeding the inline threshold, up to `MAX_FILE_SIZE_XFTP` (1 GB). XFTP uses dedicated file relay servers with chunked, encrypted transfers. + +The `receiveFile` / `receiveFiles` API commands handle both protocols transparently -- the chat core selects the appropriate transfer mechanism based on file metadata received from the sender. + +--- + + + +## 2. File Size Constants + +Defined in [`Utils.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L118): + +| Constant | Value | Human-Readable | Line | Purpose | +|---|---|---|---|---| +| `MAX_IMAGE_SIZE` | 261,120 | 255 KB | [L118](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L118) | Inline image compression target | +| `MAX_IMAGE_SIZE_AUTO_RCV` | 522,240 | 510 KB | [L119](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L119) | Auto-receive threshold for images (`2 * MAX_IMAGE_SIZE`) | +| `MAX_VOICE_SIZE_AUTO_RCV` | 522,240 | 510 KB | [L120](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L120) | Auto-receive threshold for voice messages (`2 * MAX_IMAGE_SIZE`) | +| `MAX_VIDEO_SIZE_AUTO_RCV` | 1,047,552 | 1023 KB | [L121](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L121) | Auto-receive threshold for video | +| `MAX_VOICE_MILLIS_FOR_SENDING` | 300,000 | 5 min | [L123](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L123) | Maximum voice message duration | +| `MAX_FILE_SIZE_SMP` | 8,000,000 | ~7.6 MB | [L125](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L125) | Maximum SMP inline file size | +| `MAX_FILE_SIZE_XFTP` | 1,073,741,824 | 1 GB | [L127](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L127) | Maximum XFTP transfer size | +| `MAX_FILE_SIZE_LOCAL` | `Long.MAX_VALUE` | Unlimited | [L129](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L129) | Local file protocol (no size limit) | + +The `getMaxFileSize()` function ([`Utils.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L442)) selects the limit based on `FileProtocol`: + +```kotlin +FileProtocol.XFTP -> MAX_FILE_SIZE_XFTP +FileProtocol.SMP -> MAX_FILE_SIZE_SMP +FileProtocol.LOCAL -> MAX_FILE_SIZE_LOCAL +``` + +--- + +## 3. CryptoFile + +[`CryptoFile.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt) (62 lines) + +Provides encrypted file I/O backed by the chat core's native cryptography (via JNI/JNA calls to `chatWriteFile`, `chatReadFile`, `chatEncryptFile`, `chatDecryptFile`). + +### Data types + +```kotlin +@Serializable +sealed class WriteFileResult { + @SerialName("result") data class Result(val cryptoArgs: CryptoFileArgs): WriteFileResult() + @SerialName("error") data class Error(val writeError: String): WriteFileResult() +} +``` + +`CryptoFileArgs` contains `fileKey` and `fileNonce` -- the symmetric encryption key and nonce for AES-GCM encryption. + + + +### Functions + +| Function | Line | Signature | Description | +|---|---|---|---| +| `writeCryptoFile` | [L24](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt#L24) | `(path: String, data: ByteArray): CryptoFileArgs` | Writes data to an encrypted file via a direct `ByteBuffer`. Returns the generated key and nonce. Requires initialized `ChatController`. | +| `readCryptoFile` | [L36](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt#L36) | `(path: String, cryptoArgs: CryptoFileArgs): ByteArray` | Reads and decrypts a file given its key and nonce. Returns the plaintext bytes. Throws on error (status != 0). | +| `encryptCryptoFile` | [L47](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt#L47) | `(fromPath: String, toPath: String): CryptoFileArgs` | Encrypts an existing plaintext file to a new encrypted file. Returns the generated key and nonce. | +| `decryptCryptoFile` | [L57](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt#L57) | `(fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String)` | Decrypts an encrypted file to a plaintext output file. Throws on non-empty error string. | + +All functions delegate to native C library functions through the chat core JNI bridge. + +--- + + + +## 4. File Storage Paths + +### Common expect declarations + +[`Files.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) (191 lines, commonMain) + +| Property | Line | Description | +|---|---|---| +| `dataDir` | [L18](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L18) | Root application data directory | +| `tmpDir` | [L19](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L19) | Temporary files directory | +| `filesDir` | [L20](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L20) | Base files directory | +| `appFilesDir` | [L21](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L21) | Application files (chat attachments) | +| `wallpapersDir` | [L22](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L22) | Theme wallpaper images | +| `coreTmpDir` | [L23](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L23) | Temporary files for the chat core | +| `dbAbsolutePrefixPath` | [L24](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L24) | Database file path prefix | +| `preferencesDir` | [L25](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L25) | Preferences/config directory | +| `databaseExportDir` | [L35](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L35) | Temporary DB archive storage for export | +| `remoteHostsDir` | [L37](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L37) | Remote host connection data | + +### Android implementation + +[`Files.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt) (79 lines) + +| Property | Value | +|---|---| +| `dataDir` | `androidAppContext.dataDir` | +| `tmpDir` | `androidAppContext.getDir("temp", MODE_PRIVATE)` | +| `filesDir` | `dataDir/files` | +| `appFilesDir` | `dataDir/files/app_files` | +| `wallpapersDir` | `dataDir/files/assets/wallpapers` | +| `coreTmpDir` | `dataDir/files/temp_files` | +| `dbAbsolutePrefixPath` | `dataDir/files` | +| `preferencesDir` | `dataDir/shared_prefs` | +| `databaseExportDir` | `androidAppContext.cacheDir` | +| `remoteHostsDir` | `tmpDir/remote_hosts` | + +### Desktop implementation + +[`Files.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt) (116 lines) + +| Property | Value | +|---|---| +| `dataDir` | `desktopPlatform.dataPath` (XDG_DATA_HOME on Linux, AppData on Windows, Application Support on macOS) | +| `tmpDir` | `java.io.tmpdir/simplex` (deleted on exit) | +| `filesDir` | `dataDir/simplex_v1_files` | +| `appFilesDir` | Same as `filesDir` | +| `wallpapersDir` | `dataDir/simplex_v1_assets/wallpapers` | +| `coreTmpDir` | `dataDir/tmp` | +| `dbAbsolutePrefixPath` | `dataDir/simplex_v1` | +| `preferencesDir` | `desktopPlatform.configPath` | +| `databaseExportDir` | Same as `tmpDir` | +| `remoteHostsDir` | `dataDir/remote_hosts` | + +### Helper functions (common) + +| Function | Line | Description | +|---|---|---| +| `getAppFilePath` | [L81](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L81) | Resolves file path considering remote hosts | +| `getWallpaperFilePath` | [L91](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L91) | Resolves wallpaper image path, creates parent directories | +| `getLoadedFilePath` | [L105](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L105) | Returns path if file exists and is fully loaded | +| `getLoadedFileSource` | [L115](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L115) | Returns `CryptoFile` source if file is loaded | +| `readThemeOverrides` | [L125](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L125) | Reads theme overrides from `themes.yaml` | +| `writeThemeOverrides` | [L151](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L151) | Atomically writes theme overrides to `themes.yaml` | +| `copyFileToFile` | [L47](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L47) | Copies a `File` to a `URI` destination with toast feedback | +| `copyBytesToFile` | [L63](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L63) | Copies a `ByteArrayInputStream` to a `URI` destination | + +--- + +## 5. API Commands + +Defined in [`SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt): + +| Function | Line | Signature | Description | +|---|---|---|---| +| `receiveFiles` | [L1946](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1946) | `(rhId, user, fileIds, userApprovedRelays, auto)` | Receive multiple files. Sends `CC.ReceiveFile` for each ID. Handles relay approval workflow: collects unapproved files, shows alert, re-calls with `userApprovedRelays=true`. Respects `privacyEncryptLocalFiles` preference. | +| `receiveFile` | [L2062](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2062) | `(rhId, user, fileId, userApprovedRelays, auto)` | Delegates to `receiveFiles` with a single-element list. | +| `cancelFile` | [L2072](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2072) | `(rh, user, fileId)` | Cancels an in-progress file transfer (send or receive). Cleans up the local file. | +| `apiCancelFile` | [L2080](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2080) | `(rh, fileId, ctrl?)` | Low-level cancel. Returns `AChatItem?` on success (`SndFileCancelled` or `RcvFileCancelled`). | +| `uploadStandaloneFile` | [L1916](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1916) | `(user, file, ctrl?)` | Upload a standalone file (for database migration). Returns `FileTransferMeta?` with XFTP link. | +| `downloadStandaloneFile` | [L1926](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1926) | `(user, url, file, ctrl?)` | Download a standalone file from an XFTP URL. Returns `RcvFileTransfer?`. | + +--- + +## 6. Auto-Receive Logic + +Located in [`SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2696) within the `CR.NewChatItems` handler: + +```kotlin +if (file != null && + appPrefs.privacyAcceptImages.get() && + ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) + || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) + || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV + && file.fileStatus !is CIFileStatus.RcvAccepted)) +) { + receiveFile(rhId, r.user, file.fileId, auto = true) +} +``` + +**Conditions for auto-receive:** + +1. The `privacyAcceptImages` preference is enabled (user opt-in). +2. The content type and size match one of: + - **Images** (`MCImage`): file size <= 510 KB (`MAX_IMAGE_SIZE_AUTO_RCV`) + - **Video** (`MCVideo`): file size <= 1023 KB (`MAX_VIDEO_SIZE_AUTO_RCV`) + - **Voice** (`MCVoice`): file size <= 510 KB (`MAX_VOICE_SIZE_AUTO_RCV`) AND file is not already accepted +3. The file has a non-null `file` attachment. + +When `auto = true`, relay approval alerts are suppressed (the file is silently received). + +--- + +## 7. Source Files + +| File | Path | Lines | Description | +|---|---|---|---| +| `CryptoFile.kt` | [`common/src/commonMain/.../model/CryptoFile.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt) | 62 | Encrypted file read/write via native crypto | +| `Files.kt` | [`common/src/commonMain/.../platform/Files.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) | 191 | Common file path declarations, theme I/O, file helpers | +| `Files.android.kt` | [`common/src/androidMain/.../platform/Files.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt) | 79 | Android file path implementations | +| `Files.desktop.kt` | [`common/src/desktopMain/.../platform/Files.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt) | 116 | Desktop file path implementations | +| `Utils.kt` | [`common/src/commonMain/.../views/helpers/Utils.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt) | -- | File size constants (L117--L128), `getMaxFileSize()` (L442) | +| `SimpleXAPI.kt` | [`common/src/commonMain/.../model/SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt) | -- | File transfer API commands (L1911--L2085), auto-receive (L2690) | diff --git a/apps/multiplatform/spec/services/notifications.md b/apps/multiplatform/spec/services/notifications.md new file mode 100644 index 0000000000..6ce4bc9dc1 --- /dev/null +++ b/apps/multiplatform/spec/services/notifications.md @@ -0,0 +1,261 @@ +# Notification System + +## Table of Contents + +1. [Overview](#1-overview) +2. [NtfManager Abstract Class](#2-ntfmanager-abstract-class) +3. [Android Notification Manager](#3-android-notification-manager) +4. [Desktop Notification Manager](#4-desktop-notification-manager) +5. [Android Background Messaging](#5-android-background-messaging) +6. [Notification Privacy](#6-notification-privacy) +7. [Source Files](#7-source-files) + +## Executive Summary + +SimpleX Chat uses platform-specific notification strategies. The common `NtfManager` abstract class defines the notification contract with shared helper methods for message, contact, and call notifications. Android implements a full notification system with channels, grouped summaries, full-screen call intents, and a foreground service (`SimplexService`) or periodic `WorkManager` tasks for background message fetching. Desktop uses the TwoSlices library (with OS-native fallbacks) for system notifications. Notification privacy is controlled via `NotificationPreviewMode` (MESSAGE, CONTACT, HIDDEN). + +--- + +## 1. Overview + +Notifications serve three purposes in SimpleX Chat: + +1. **Message notifications** -- alert users to new messages when the app is not focused on the relevant chat. +2. **Call notifications** -- high-priority alerts for incoming WebRTC calls, with full-screen intent support on Android for lock-screen scenarios. +3. **Contact events** -- notifications for contact connection and contact request events. + +The architecture uses an abstract `NtfManager` in common code with platform-specific `actual` implementations. On Android, background message delivery requires a foreground service or periodic WorkManager tasks since SimpleX does not use push notifications (no Firebase/APNs dependency for privacy). + +--- + + + + +## 2. NtfManager Abstract Class + +[`NtfManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt) (139 lines, commonMain) + +The global `ntfManager` instance is declared at [line 17](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L17) and initialized by each platform at startup. + +### Concrete methods + +| Method | Line | Description | +|---|---|---| +| `notifyContactConnected` | [L20](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L20) | Displays "contact connected" notification for a `Contact` | +| `notifyContactRequestReceived` | [L27](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L27) | Shows contact request notification with an "Accept" action button | +| `notifyMessageReceived` | [L38](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L38) | Conditionally shows message notification based on `ntfsEnabled`, `showNotification`, and whether user is viewing that chat | +| `acceptContactRequestAction` | [L51](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L51) | Accepts a contact request from a notification action | +| `openChatAction` | [L59](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L59) | Opens a specific chat from a notification tap, switching user if needed | +| `showChatsAction` | [L74](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L74) | Opens the chat list, switching user if needed | +| `acceptCallAction` | [L88](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L88) | Accepts a call invitation from a notification action | + +### Abstract methods + +| Method | Line | Description | +|---|---|---| +| `notifyCallInvitation` | [L98](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L98) | Show call notification; returns `true` if notification was shown | +| `displayNotification` | [L102](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L102) | Display a message notification with optional image and action buttons | +| `cancelCallNotification` | [L103](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L103) | Cancel the active call notification | +| `hasNotificationsForChat` | [L99](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L99) | Check if notifications exist for a given chat | +| `cancelNotificationsForChat` | [L100](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L100) | Cancel all notifications for a specific chat | +| `cancelNotificationsForUser` | [L101](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L101) | Cancel all notifications for a user profile | +| `cancelAllNotifications` | [L104](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L104) | Cancel all notifications | +| `showMessage` | [L105](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L105) | Show a simple title+text notification | +| `androidCreateNtfChannelsMaybeShowAlert` | [L107](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L107) | Android-only: create notification channels (triggers permission prompt on Android 13+) | + +### Private helpers + +- `awaitChatStartedIfNeeded` ([line 109](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L109)): Waits up to 30 seconds for chat initialization (handles database decryption delay). +- `hideSecrets` ([line 122](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L122)): Replaces `Format.Secret` formatted text with `"..."` in notification previews. + +--- + +## 3. Android Notification Manager + +[`NtfManager.android.kt`](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt) (331 lines) + +Implemented as a Kotlin `object` (singleton) in the Android module. + +### Notification channels + +| Channel | Constant | Importance | Purpose | +|---|---|---|---| +| Messages | `MessageChannel` (`chat.simplex.app.MESSAGE_NOTIFICATION`) | HIGH | All chat message notifications | +| Calls | `CallChannel` (`chat.simplex.app.CALL_NOTIFICATION_2`) | HIGH | Incoming call alerts with custom ringtone and vibration | + +Channel creation happens in `createNtfChannelsMaybeShowAlert()` ([line 298](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L298)). Old channel IDs (`CALL_NOTIFICATION`, `CALL_NOTIFICATION_1`, `LOCK_SCREEN_CALL_NOTIFICATION`) are explicitly deleted. + +### displayNotification (messages) + +[Line 102](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L102): + +- Uses `NotificationCompat.Builder` with `MessageChannel`. +- Groups notifications using `MessageGroup` with `GROUP_ALERT_CHILDREN` behavior. +- Applies rate limiting: silent mode if notification for the same `(userId, chatId)` was shown within 30 seconds (`msgNtfTimeoutMs`). +- Creates a group summary notification ([line 142](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L142)) with `setGroupSummary(true)`. +- Content intent uses `TaskStackBuilder` for proper back stack. +- Supports `NotificationAction.ACCEPT_CONTACT_REQUEST` action buttons via `NtfActionReceiver` broadcast receiver. + +### notifyCallInvitation + +[Line 160](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L160): + +- Returns `false` (no notification) if app is in foreground -- in-app alert is used instead. +- **Lock screen / screen off**: Uses `setFullScreenIntent` with a `PendingIntent` to `CallActivity`, plus `VISIBILITY_PUBLIC`. +- **Foreground / unlocked**: Uses regular notification with Accept/Reject action buttons and a custom ringtone (`ring_once` raw resource). +- Notification flags include `FLAG_INSISTENT` for repeating sound and vibration. +- Call notification channel vibration pattern: `[250, 250, 0, 2600]` ms. + +### Cancel operations + +| Method | Line | Description | +|---|---|---| +| `cancelNotificationsForChat` | [L75](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L75) | Cancels by `chatId.hashCode()`, cleans up group summary if no children remain | +| `cancelNotificationsForUser` | [L88](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L88) | Iterates and cancels all notifications for a given `userId` | +| `cancelCallNotification` | [L261](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L261) | Cancels the singleton call notification (`CallNotificationId = -1`) | +| `cancelAllNotifications` | [L265](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L265) | Cancels all via `NotificationManager.cancelAll()` | + +### NtfActionReceiver + +[Line 311](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L311): A `BroadcastReceiver` that handles notification action intents: +- `ACCEPT_CONTACT_REQUEST` -- calls `ntfManager.acceptContactRequestAction()` +- `RejectCallAction` -- calls `callManager.endCall()` on the invitation + +--- + +## 4. Desktop Notification Manager + +[`NtfManager.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt) (193 lines) + +Implemented as a Kotlin `object` using the [TwoSlices](https://github.com/sshtools/two-slices) library (`Toast` builder API) for cross-platform desktop notifications. + +### displayNotification + +[Line 97](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt#L97): + +- Suppresses if `!user.showNotifications`. +- Respects `NotificationPreviewMode` for title and content. +- Calls `displayNotificationViaLib()` ([line 114](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt#L114)) which builds a `Toast` with title, content, icon, action buttons, and default action. +- Icon images are written to a temporary PNG file via `prepareIconPath()` ([line 150](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt#L150)). +- Default action on click opens the relevant chat via `openChatAction()`. + +### notifyCallInvitation + +[Line 22](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt#L22): + +- Returns `false` if the SimpleX window is focused (in-app alert used instead). +- Creates a notification with Accept and Reject action buttons. +- Default click action opens the chat. + +### OS-native fallbacks + +[Line 162](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt#L162): The `displayNotification` private method dispatches based on `desktopPlatform`: + +| Platform | Method | +|---|---| +| Linux | `notify-send` command with optional `-i` icon | +| Windows | `SystemTray` with `TrayIcon.displayMessage()` | +| macOS | `osascript -e 'display notification ...'` | + +### Notification tracking + +Previous notifications are tracked in `prevNtfs: ArrayList, Slice>>` with a `Mutex` for thread safety. Cancel operations remove entries from this list. + +--- + +## 5. Android Background Messaging + +### 5.1 SimplexService.kt (734 lines) + +[`SimplexService.kt`](../../android/src/main/java/chat/simplex/app/SimplexService.kt) + +A foreground `Service` that keeps the app process alive for continuous message receiving. This is SimpleX's privacy-preserving alternative to push notifications. + +**Service lifecycle:** + +- `startService()` ([line 128](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L128)): Waits for database migration, validates DB status, saves service state as STARTED. WakeLock acquisition is commented out -- the app relies on battery optimization whitelisting instead. +- `onDestroy()` ([line 87](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L87)): Releases wakelocks, saves state as STOPPED, sends broadcast to `AutoRestartReceiver` if allowed. +- `onTaskRemoved()` ([line 211](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L211)): Schedules restart via `AlarmManager` when the app is swiped from recents. + +**Notification:** + +- Channel: `SIMPLEX_SERVICE_NOTIFICATION` with `IMPORTANCE_LOW` and badge disabled ([line 165](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L165)). +- Shows a persistent notification with a "Hide notification" action that opens channel settings. +- Service ID: `6789`. + +**Restart mechanisms:** + +| Receiver | Line | Trigger | +|---|---|---| +| `StartReceiver` | [L234](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L234) | Device boot (`BOOT_COMPLETED`) | +| `AutoRestartReceiver` | [L253](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L253) | Service destruction | +| `AppUpdateReceiver` | [L261](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L261) | App update (`MY_PACKAGE_REPLACED`) | +| `ServiceStartWorker` | [L283](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L283) | WorkManager one-time task | + +**Battery optimization:** + +- `isBackgroundAllowed()` ([line 681](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L681)): Checks both `isIgnoringBatteryOptimizations` and `!isBackgroundRestricted`. +- `showBackgroundServiceNoticeIfNeeded()` ([line 430](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L430)): Shows alerts guiding users to disable battery optimization or background restriction. Includes Xiaomi-specific guidance. +- `disableNotifications()` ([line 722](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L722)): Switches mode to OFF, disables receivers, cancels workers. + +### 5.2 MessagesFetcherWorker.kt (100 lines) + +[`MessagesFetcherWorker.kt`](../../android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt) + +A `CoroutineWorker` used in `PERIODIC` notification mode as an alternative to the persistent foreground service: + +- `scheduleWork()` ([line 18](../../android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt#L18)): Schedules a `OneTimeWorkRequest` with a default 600-second (10 minute) initial delay and 60-second duration. Requires `NetworkType.CONNECTED` constraint. +- `doWork()` ([line 53](../../android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt#L53)): Skips if `SimplexService` is already running. Initializes chat controller if needed (self-destruct mode). Waits for DB migration. Runs for up to `durationSec` seconds, polling every 5 seconds until no messages have been received for 10 seconds (`WAIT_AFTER_LAST_MESSAGE`). +- Self-rescheduling: Always calls `reschedule()` at the end (creating a chain of one-time tasks that simulate periodic execution). + + + +### 5.3 Notification modes + +Defined in [`SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7739): + +```kotlin +enum class NotificationsMode { + OFF, // No background message fetching + PERIODIC, // WorkManager periodic tasks (MessagesFetcherWorker) + SERVICE; // Persistent foreground service (SimplexService) +} +``` + +Default is `SERVICE`. The `requiresIgnoringBattery` property is an Android extension property (defined in `Extensions.kt`, not on the enum itself) whose value depends on the SDK version: `SERVICE` requires ignoring battery optimizations since SDK S (API 31), `PERIODIC` since SDK M (API 23). + +--- + +## 6. Notification Privacy + +Defined in [`ChatModel.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L4823): + +```kotlin +enum class NotificationPreviewMode { + MESSAGE, // Show sender name and message text + CONTACT, // Show sender name, generic "new message" text + HIDDEN; // Show "Somebody" as sender, generic "new message" text +} +``` + +Privacy mode affects: +- **Notification title**: `HIDDEN` uses `"Somebody"` instead of contact name. +- **Notification content**: Only `MESSAGE` mode shows actual message text. +- **Large icon**: `HIDDEN` uses the app icon instead of the contact's profile image. +- **Call notifications**: `HIDDEN` hides the caller's name and profile image. + +Both Android and Desktop implementations check `appPreferences.notificationPreviewMode.get()` before constructing notification content. + +--- + +## 7. Source Files + +| File | Path | Lines | Description | +|---|---|---|---| +| `NtfManager.kt` | [`common/src/commonMain/.../platform/NtfManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt) | 139 | Abstract notification manager with shared logic | +| `NtfManager.android.kt` | [`android/src/main/java/.../model/NtfManager.android.kt`](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt) | 331 | Android notification channels, groups, call intents | +| `NtfManager.desktop.kt` | [`common/src/desktopMain/.../model/NtfManager.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt) | 193 | Desktop notifications via TwoSlices/OS-native | +| `SimplexService.kt` | [`android/src/main/java/.../SimplexService.kt`](../../android/src/main/java/chat/simplex/app/SimplexService.kt) | 734 | Android foreground service for background messaging | +| `MessagesFetcherWorker.kt` | [`android/src/main/java/.../MessagesFetcherWorker.kt`](../../android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt) | 100 | WorkManager periodic message fetcher | +| `ChatModel.kt` | [`common/src/commonMain/.../model/ChatModel.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt) | -- | `NotificationPreviewMode` enum (L4823) | +| `SimpleXAPI.kt` | [`common/src/commonMain/.../model/SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt) | -- | `NotificationsMode` enum (L7739) | diff --git a/apps/multiplatform/spec/services/theme.md b/apps/multiplatform/spec/services/theme.md new file mode 100644 index 0000000000..e5839fc193 --- /dev/null +++ b/apps/multiplatform/spec/services/theme.md @@ -0,0 +1,498 @@ +# Theme Engine + +## Table of Contents + +1. [Overview](#1-overview) +2. [ThemeManager](#2-thememanager) +3. [Default Themes](#3-default-themes) +4. [Theme Types](#4-theme-types) +5. [Color System](#5-color-system) +6. [SimpleXTheme Composable](#6-simplextheme-composable) +7. [Platform Theme](#7-platform-theme) +8. [YAML Import/Export](#8-yaml-importexport) +9. [Source Files](#9-source-files) + +## Executive Summary + +The SimpleX Chat theme engine implements a four-level cascade: per-chat theme overrides take precedence over per-user overrides, which take precedence over global (app-settings) overrides, which take precedence over built-in presets. Four preset themes exist (LIGHT, DARK, SIMPLEX, BLACK), each defining a Material `Colors` palette and custom `AppColors` for chat-specific elements. Themes support wallpaper customization (preset patterns or custom images) with background and tint color overrides. Theme configuration is persisted as YAML and can be imported/exported. The `SimpleXTheme` composable wraps `MaterialTheme` with additional `CompositionLocal` providers for app colors and wallpaper. + +--- + +## 1. Overview + +Theme resolution follows a priority chain: + +``` +per-chat override > per-user override > global override > preset default +``` + +At each level, individual color properties can be overridden. Unspecified properties fall through to the next level. The resolution is performed by `ThemeManager.currentColors()`, which merges all levels into a single `ActiveTheme` containing Material `Colors`, `AppColors`, and `AppWallpaper`. + +Wallpapers follow the same cascade, with additional support for preset wallpapers (built-in patterns like `SCHOOL`) and custom images. Wallpaper presets can define their own color overrides that sit between the global override and the base preset. + +--- + +## 2. ThemeManager + +[`ThemeManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt) (241 lines) + +A singleton `object` that manages theme state, persistence, and resolution. + +### Core resolution + + + +**`currentColors()`** ([line 57](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L57)): + +```kotlin +fun currentColors( + themeOverridesForType: WallpaperType?, + perChatTheme: ThemeModeOverride?, + perUserTheme: ThemeModeOverrides?, + appSettingsTheme: List +): ActiveTheme +``` + +This is the core resolution function. It: +1. Determines the non-system theme name (resolving `SYSTEM` to light or dark based on `systemInDarkThemeCurrently`). +2. Selects the base theme palette (LIGHT/DARK/SIMPLEX/BLACK). +3. Finds the matching `ThemeOverrides` from `appSettingsTheme` based on wallpaper type and theme name. +4. Selects the `perUserTheme` for the current light/dark mode. +5. Resolves wallpaper preset colors if applicable. +6. Merges all color layers via `toColors()`, `toAppColors()`, and `toAppWallpaper()`. + +Returns `ActiveTheme(name, base, colors, appColors, wallpaper)`. + +### Theme application + +**`applyTheme()`** ([line 105](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L105)): + +Persists the theme name, recalculates `CurrentColors`, and updates Android system bar appearance: + +```kotlin +fun applyTheme(theme: String) { + if (appPrefs.currentTheme.get() != theme) { + appPrefs.currentTheme.set(theme) + } + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + platform.androidSetNightModeIfSupported() + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) +} +``` + +**`changeDarkTheme()`** ([line 115](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L115)): + +Sets the dark mode variant (DARK, SIMPLEX, or BLACK) and recalculates colors. + +### Color and wallpaper modification + +**`saveAndApplyThemeColor()`** ([line 120](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L120)): + +Persists a single color change to the global theme overrides: +1. Gets or creates `ThemeOverrides` for the current base theme. +2. Calls `withUpdatedColor()` to update the specific `ThemeColor`. +3. Updates `currentThemeIds` mapping. +4. Recalculates `CurrentColors`. + +**`applyThemeColor()`** ([line 132](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L132)): + +In-memory-only color change (for per-chat/per-user theme editing before save). + +**`saveAndApplyWallpaper()`** ([line 136](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L136)): + +Persists wallpaper type change. Finds or creates matching `ThemeOverrides` (matching by wallpaper type + theme name), updates the wallpaper, and persists. + +### Reset + +**`resetAllThemeColors()` (global)** ([line 204](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L204)): + +Resets all custom colors in the current global theme override to defaults. Preserves wallpaper but clears its background and tint overrides. + +**`resetAllThemeColors()` (per-chat/per-user)** ([line 213](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L213)): + +In-memory reset of a `ThemeModeOverride` state. + +### Import/Export + +**`saveAndApplyThemeOverrides()`** ([line 188](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L188)): + +Imports a complete `ThemeOverrides` (from YAML). Handles wallpaper image import (base64 to file), replaces existing override for the same type, and applies. + +**`currentThemeOverridesForExport()`** ([line 92](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L92)): + +Exports the fully resolved current theme as a `ThemeOverrides` with all colors filled and wallpaper image embedded as base64. + +### Utility + +**`colorFromReadableHex()`** ([line 224](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L224)): + +Parses `#AARRGGBB` hex string to `Color`. + +**`toReadableHex()`** ([line 227](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L227)): + +Converts `Color` to `#AARRGGBB` hex string with intelligent alpha handling. + +--- + + + +## 3. Default Themes + +[`Theme.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L26): + +```kotlin +enum class DefaultTheme { + LIGHT, DARK, SIMPLEX, BLACK; + + companion object { + const val SYSTEM_THEME_NAME: String = "SYSTEM" + } +} +``` + +| Theme | `mode` | Description | +|---|---|---| +| `LIGHT` | LIGHT | Standard light theme with white/light gray surfaces | +| `DARK` | DARK | Standard dark theme with dark gray surfaces | +| `SIMPLEX` | DARK | SimpleX branded dark theme with deep blue background and cyan accent | +| `BLACK` | DARK | AMOLED-optimized pure black theme | + +`SYSTEM` is a virtual theme name that resolves to LIGHT or the configured dark variant at runtime. + +`DefaultThemeMode` ([line 46](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L46)): `LIGHT` or `DARK`, serialized as `"light"` / `"dark"`. + +--- + +## 4. Theme Types + + + +### AppColors (line 53) + +[`Theme.kt` L53](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L53): + +```kotlin +@Stable +class AppColors( + title: Color, + primaryVariant2: Color, + sentMessage: Color, + sentQuote: Color, + receivedMessage: Color, + receivedQuote: Color, +) +``` + +Mutable state properties (for efficient recomposition) representing chat-specific colors not covered by Material's `Colors`. + + + +### AppWallpaper (line 106) + +[`Theme.kt` L106](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L106): + +```kotlin +@Stable +class AppWallpaper( + background: Color? = null, + tint: Color? = null, + type: WallpaperType = WallpaperType.Empty, +) +``` + +Represents the active wallpaper state with optional background color, tint overlay, and wallpaper type (Empty, Preset, or Image). + + + +### ThemeColor (line 140) + +Enum of all customizable color slots: + +`PRIMARY`, `PRIMARY_VARIANT`, `SECONDARY`, `SECONDARY_VARIANT`, `BACKGROUND`, `SURFACE`, `TITLE`, `SENT_MESSAGE`, `SENT_QUOTE`, `RECEIVED_MESSAGE`, `RECEIVED_QUOTE`, `PRIMARY_VARIANT2`, `WALLPAPER_BACKGROUND`, `WALLPAPER_TINT` + +Each has a `fromColors()` method to extract the current value and a `text` property for UI display. + + + +### ThemeColors (line 183) + +[`Theme.kt` L183](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L183): + +Serializable data class with optional hex color strings for each slot. Uses `@SerialName` annotations for YAML compatibility (`accent` for `primary`, `accentVariant` for `primaryVariant`, `menus` for `surface`, etc.). + + + +### ThemeWallpaper (line 224) + +[`Theme.kt` L224](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L224): + +```kotlin +@Serializable +data class ThemeWallpaper( + val preset: String? = null, // Preset wallpaper name + val scale: Float? = null, // Wallpaper scale factor + val scaleType: WallpaperScaleType? = null, // Fill/fit mode + val background: String? = null, // Background color hex + val tint: String? = null, // Tint overlay color hex + val image: String? = null, // Base64-encoded image (for import/export) + val imageFile: String? = null, // Local image file name +) +``` + +Key methods: +- `toAppWallpaper()`: Converts to runtime `AppWallpaper`. +- `withFilledWallpaperBase64()`: Embeds the image as base64 for export. +- `importFromString()`: Saves a base64 image to disk and returns a copy with `imageFile` set. +- `from(type, background, tint)`: Factory from `WallpaperType`. + + + +### ThemeOverrides (line 304) + +[`Theme.kt` L304](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L304): + +```kotlin +@Serializable +data class ThemeOverrides( + val themeId: String = UUID.randomUUID().toString(), + val base: DefaultTheme, + val colors: ThemeColors = ThemeColors(), + val wallpaper: ThemeWallpaper? = null, +) +``` + +A complete theme override entry. Multiple can coexist (one per wallpaper type per base theme). The `themeId` is a UUID for identity tracking. Key methods: +- `isSame(type, themeName)`: Matches by wallpaper type and base theme. +- `withUpdatedColor(name, color)`: Returns a copy with one color changed. +- `toColors()`, `toAppColors()`, `toAppWallpaper()`: Merge with base theme and per-user/per-chat overrides. + + + +### ThemeModeOverrides (line 475) + +[`Theme.kt` L475](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L475): + +```kotlin +@Serializable +data class ThemeModeOverrides( + val light: ThemeModeOverride? = null, + val dark: ThemeModeOverride? = null, +) +``` + +Container for per-user or per-chat overrides, with separate light and dark mode variants. Stored on the `User` model as `uiThemes`. + + + +### ThemeModeOverride (line 487) + +[`Theme.kt` L487](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L487): + +```kotlin +@Serializable +data class ThemeModeOverride( + val mode: DefaultThemeMode = CurrentColors.value.base.mode, + val colors: ThemeColors = ThemeColors(), + val wallpaper: ThemeWallpaper? = null, +) +``` + +A single mode's override with colors and wallpaper. Has `withUpdatedColor()` and `removeSameColors()` (strips colors that match base defaults). + +--- + +## 5. Color System + +Four built-in color palettes, each consisting of a Material `Colors` and an `AppColors`: + +### DarkColorPalette ([line 634](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L634)) + +| Property | Value | Notes | +|---|---|---| +| `primary` | `SimplexBlue` | `#0088ff` | +| `surface` | `#222222` | | +| `sentMessage` | `#18262E` | Dark blue-gray | +| `receivedMessage` | `#262627` | Neutral dark | + +### LightColorPalette ([line 656](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L656)) + +| Property | Value | Notes | +|---|---|---| +| `primary` | `SimplexBlue` | `#0088ff` | +| `surface` | `White` | | +| `sentMessage` | `#E9F7FF` | Light blue | +| `receivedMessage` | `#F5F5F6` | Near-white | + +### SimplexColorPalette ([line 678](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L678)) + +| Property | Value | Notes | +|---|---|---| +| `primary` | `#70F0F9` | Cyan | +| `primaryVariant` | `#1298A5` | Dark cyan | +| `background` | `#111528` | Deep navy | +| `surface` | `#121C37` | Dark navy | +| `title` | `#267BE5` | Blue | + +### BlackColorPalette ([line 701](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L701)) + +| Property | Value | Notes | +|---|---|---| +| `primary` | `#0077E0` | Darker blue | +| `background` | `#070707` | Near-black | +| `surface` | `#161617` | Very dark | +| `sentMessage` | `#18262E` | Same as Dark | +| `receivedMessage` | `#1B1B1B` | Very dark | + +--- + + + +## 6. SimpleXTheme Composable + +[`Theme.kt` line 773](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L773): + +```kotlin +@Composable +fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) +``` + +The root theme composable that wraps all app content: + +1. **System dark mode tracking** ([line 781](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L781)): Uses `snapshotFlow` on `isSystemInDarkTheme()` to call `reactOnDarkThemeChanges()` when the system theme changes. This triggers `ThemeManager.applyTheme(SYSTEM)` if the app is in system theme mode. + +2. **User theme tracking** ([line 790](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L790)): Monitors `chatModel.currentUser.value?.uiThemes` and re-applies the theme when the active user changes. + +3. **MaterialTheme wrapping** ([line 797](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L797)): Provides `theme.colors` to `MaterialTheme`, plus custom `CompositionLocal` providers: + - `LocalContentColor` -- set to `MaterialTheme.colors.onBackground` + - `LocalAppColors` -- the `AppColors` instance (remembered and updated) + - `LocalAppWallpaper` -- the `AppWallpaper` instance (remembered and updated) + - `LocalDensity` -- scaled by `desktopDensityScaleMultiplier` and `fontSizeMultiplier` + +4. **`SimpleXThemeOverride`** ([line 825](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L825)): A variant that accepts an explicit `ActiveTheme` for per-chat theme previews and overlays. + +### CompositionLocal access + +```kotlin +val MaterialTheme.appColors: AppColors // via LocalAppColors +val MaterialTheme.wallpaper: AppWallpaper // via LocalAppWallpaper +``` + +### Global state + + + +`CurrentColors` ([line 727](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L727)): A `MutableStateFlow` that holds the current resolved theme. Updated by `ThemeManager.applyTheme()` and collected by `SimpleXTheme`. + +`systemInDarkThemeCurrently` ([line 724](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L724)): Tracks the current system dark mode state. + +--- + +## 7. Platform Theme + +### isSystemInDarkTheme + +**Android** ([`Theme.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Theme.android.kt)): + +```kotlin +@Composable +actual fun isSystemInDarkTheme(): Boolean = androidx.compose.foundation.isSystemInDarkTheme() +``` + +Delegates to the standard Compose function which reads `Configuration.uiMode`. + +**Desktop** ([`Theme.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt)): + +```kotlin +private val detector: OsThemeDetector = OsThemeDetector.getDetector() + .apply { registerListener(::reactOnDarkThemeChanges) } + +@Composable +actual fun isSystemInDarkTheme(): Boolean = try { + detector.isDark +} catch (e: Exception) { + false // Fallback for macOS exceptions +} +``` + +Uses the [jSystemThemeDetector](https://github.com/Dansoftowner/jSystemThemeDetector) library (`OsThemeDetector`). The detector also registers a listener that calls `reactOnDarkThemeChanges()` proactively when the OS theme changes, ensuring the app responds even outside of composition. + +### reactOnDarkThemeChanges + +[`Theme.kt` line 763](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L763): + +```kotlin +fun reactOnDarkThemeChanges(isDark: Boolean) { + systemInDarkThemeCurrently = isDark + if (appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME + && CurrentColors.value.colors.isLight == isDark) { + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) + } +} +``` + +Only triggers a theme switch if the app is in SYSTEM mode and the current light/dark state disagrees with the OS. + +--- + +## 8. YAML Import/Export + +Theme overrides are persisted in `themes.yaml` (located in `preferencesDir`). + +### readThemeOverrides + +[`Files.kt` line 125](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L125): + +```kotlin +fun readThemeOverrides(): List +``` + +1. Reads `themes.yaml` from `preferencesDir`. +2. Parses the YAML node tree. +3. Extracts the `themes` list. +4. Deserializes each entry as `ThemeOverrides`, skipping entries that fail to parse (with error logging). +5. Calls `skipDuplicates()` to remove entries with the same type+base combination. + +### writeThemeOverrides + +[`Files.kt` line 151](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L151): + +```kotlin +fun writeThemeOverrides(overrides: List): Boolean +``` + +1. Serializes `ThemesFile(themes = overrides)` to YAML string. +2. Writes to a temporary file in `preferencesTmpDir`. +3. Atomically moves the temp file to `themes.yaml` using `Files.move` with `REPLACE_EXISTING`. +4. Thread-safe via `synchronized(lock)`. + +### YAML format + +```yaml +themes: + - themeId: "uuid-string" + base: "LIGHT" + colors: + accent: "#ff0088ff" + background: "#ffffffff" + sentMessage: "#ffe9f7ff" + wallpaper: + preset: "school" + scale: 1.0 + background: "#ccffffff" + tint: "#22000000" +``` + +Uses the [kaml](https://github.com/charleskorn/kaml) YAML library for serialization. `ThemeColors` uses `@SerialName` annotations for cross-platform YAML key compatibility (e.g., `accent` for `primary`, `menus` for `surface`). + +--- + +## 9. Source Files + +| File | Path | Lines | Description | +|---|---|---|---| +| `ThemeManager.kt` | [`common/src/commonMain/.../ui/theme/ThemeManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt) | 241 | Theme resolution, persistence, color/wallpaper management | +| `Theme.kt` | [`common/src/commonMain/.../ui/theme/Theme.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt) | 848 | Type definitions, color palettes, `SimpleXTheme` composable | +| `Theme.android.kt` | [`common/src/androidMain/.../ui/theme/Theme.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Theme.android.kt) | 6 | Android `isSystemInDarkTheme` | +| `Theme.desktop.kt` | [`common/src/desktopMain/.../ui/theme/Theme.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt) | 25 | Desktop `isSystemInDarkTheme` via OsThemeDetector | +| `Files.kt` | [`common/src/commonMain/.../platform/Files.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) | 191 | `readThemeOverrides()` (L125), `writeThemeOverrides()` (L151) | diff --git a/apps/multiplatform/spec/state.md b/apps/multiplatform/spec/state.md new file mode 100644 index 0000000000..900d6593ab --- /dev/null +++ b/apps/multiplatform/spec/state.md @@ -0,0 +1,486 @@ +# State Management + +## Table of Contents + +1. [Overview](#1-overview) +2. [ChatModel](#2-chatmodel) +3. [ChatsContext](#3-chatscontext) +4. [Chat](#4-chat) +5. [AppPreferences](#5-apppreferences) +6. [Source Files](#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`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L86) as `@Stable object ChatModel`. + +### Controller Reference + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`controller`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L87) | `ChatController` | 87 | Reference to the `ChatController` singleton | + +### User State + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`currentUser`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L89) | `MutableState` | 89 | Currently active user profile | +| [`users`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L90) | `SnapshotStateList` | 90 | All user profiles (multi-account) | +| [`localUserCreated`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L91) | `MutableState` | 91 | Whether a local user has been created (null = unknown during init) | +| [`setDeliveryReceipts`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L88) | `MutableState` | 88 | Trigger for delivery receipts setup dialog | +| [`switchingUsersAndHosts`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L100) | `MutableState` | 100 | True while switching active user/remote host | +| [`changingActiveUserMutex`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L193) | `Mutex` | 193 | Prevents concurrent user switches | + +### Chat Runtime State + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`chatRunning`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L92) | `MutableState` | 92 | `null` = initializing, `true` = running, `false` = stopped | +| [`chatDbChanged`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L93) | `MutableState` | 93 | Database was changed externally (needs restart) | +| [`chatDbEncrypted`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L94) | `MutableState` | 94 | Whether database is encrypted | +| [`chatDbStatus`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L95) | `MutableState` | 95 | Result of database migration attempt | +| [`ctrlInitInProgress`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L96) | `MutableState` | 96 | Controller initialization in progress | +| [`dbMigrationInProgress`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L97) | `MutableState` | 97 | Database migration in progress | +| [`incompleteInitializedDbRemoved`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L98) | `MutableState` | 98 | Tracks if incomplete DB files were removed (prevents infinite retry) | + +### Current Chat State + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`chatId`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L103) | `MutableState` | 103 | ID of the currently open chat (null = chat list shown) | +| [`chatAgentConnId`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L104) | `MutableState` | 104 | Agent connection ID for current chat | +| [`chatSubStatus`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L105) | `MutableState` | 105 | Subscription status for current chat | +| [`openAroundItemId`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L106) | `MutableState` | 106 | Item ID to scroll to when opening chat | +| [`chatsContext`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L107) | `ChatsContext` | 107 | Primary chat context (see [ChatsContext](#3-chatscontext)) | +| [`secondaryChatsContext`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L108) | `MutableState` | 108 | Optional secondary context for dual-pane views | +| [`chats`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L110) | `State>` | 110 | Derived from `chatsContext.chats` | +| [`deletedChats`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L112) | `MutableState>>` | 112 | Recently deleted chats (rhId, chatId) | + +### Group Members + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`groupMembers`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L113) | `MutableState>` | 113 | Members of currently viewed group | +| [`groupMembersIndexes`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L114) | `MutableState>` | 114 | Index lookup by `groupMemberId` | +| [`membersLoaded`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L115) | `MutableState` | 115 | Whether group members have been loaded | + +### Chat Tags and Filters + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`userTags`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L118) | `MutableState>` | 118 | User-defined chat tags | +| [`activeChatTagFilter`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L119) | `MutableState` | 119 | Currently active filter in chat list | +| [`presetTags`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L120) | `SnapshotStateMap` | 120 | Counts for preset tag categories (favorites, groups, contacts, etc.) | +| [`unreadTags`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L121) | `SnapshotStateMap` | 121 | Unread counts per user-defined tag | + +### Terminal and Developer + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`terminalsVisible`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L125) | `Set` | 125 | Tracks which terminal views are visible (default vs floating) | +| [`terminalItems`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L126) | `MutableState>` | 126 | Command/response log for developer terminal | + +### Calls (WebRTC) + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`callManager`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L161) | `CallManager` | 161 | WebRTC call lifecycle manager | +| [`callInvitations`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L162) | `SnapshotStateMap` | 162 | Pending incoming call invitations keyed by chatId | +| [`activeCallInvitation`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L163) | `MutableState` | 163 | Currently displayed incoming call invitation | +| [`activeCall`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L164) | `MutableState` | 164 | Currently active call | +| [`activeCallViewIsVisible`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L165) | `MutableState` | 165 | Whether call UI is showing | +| [`activeCallViewIsCollapsed`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L166) | `MutableState` | 166 | Whether call UI is in PiP/collapsed mode | +| [`callCommand`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L167) | `SnapshotStateList` | 167 | Pending WebRTC commands | +| [`showCallView`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L168) | `MutableState` | 168 | Call view visibility toggle | +| [`switchingCall`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L169) | `MutableState` | 169 | True during call switching | + +### Compose Draft and Sharing + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`draft`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L176) | `MutableState` | 176 | Saved compose draft for current chat | +| [`draftChatId`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L177) | `MutableState` | 177 | Chat ID the draft belongs to | +| [`sharedContent`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L180) | `MutableState` | 180 | Content received via share intent or internal forwarding | + +### Remote Hosts + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`remoteHosts`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L199) | `SnapshotStateList` | 199 | Connected remote hosts (for desktop-mobile pairing) | +| [`currentRemoteHost`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L200) | `MutableState` | 200 | Currently selected remote host | +| [`remoteHostPairing`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L203) | `MutableState?>` | 203 | Remote host pairing state | +| [`remoteCtrlSession`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L204) | `MutableState` | 204 | Remote controller session | + +### Miscellaneous UI State + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`userAddress`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L127) | `MutableState` | 127 | User's public contact address | +| [`chatItemTTL`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L128) | `MutableState` | 128 | Chat item time-to-live setting | +| [`clearOverlays`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L131) | `MutableState` | 131 | Signal to close all overlays/modals | +| [`appOpenUrl`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L137) | `MutableState?>` | 137 | URL opened via deep link (rhId, uri) | +| [`appOpenUrlConnecting`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L138) | `MutableState` | 138 | Whether a deep link connection is in progress | +| [`newChatSheetVisible`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L141) | `MutableState` | 141 | Whether new chat bottom sheet is visible | +| [`fullscreenGalleryVisible`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L144) | `MutableState` | 144 | Fullscreen gallery mode | +| [`notificationPreviewMode`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L147) | `MutableState` | 147 | Notification content preview level | +| [`showAuthScreen`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L156) | `MutableState` | 156 | Whether to show authentication screen | +| [`showChatPreviews`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L158) | `MutableState` | 158 | Whether to show chat preview text in list | +| [`clipboardHasText`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L185) | `MutableState` | 185 | System clipboard has text content | +| [`networkInfo`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L186) | `MutableState` | 186 | Network type and online status | +| [`conditions`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L188) | `MutableState` | 188 | Server operator terms/conditions | +| [`updatingProgress`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L190) | `MutableState` | 190 | Progress indicator for app updates | +| [`simplexLinkMode`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L183) | `MutableState` | 183 | How SimpleX links are displayed | +| [`migrationState`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L174) | `MutableState` | 174 | Database migration to new device state | +| [`showingInvitation`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L172) | `MutableState` | 172 | Currently displayed invitation | +| [`desktopOnboardingRandomPassword`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L134) | `MutableState` | 134 | Desktop: user skipped password setup | +| [`filesToDelete`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L182) | `MutableSet` | 182 | Temporary files pending cleanup | + +--- + + + +## 3. ChatsContext + +Defined as inner class at [`ChatModel.kt line 339`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L339): + +```kotlin +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`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L339) | `SecondaryContextFilter?` | 339 | Filter type: null = primary, GroupChatScope or MsgContentTag | +| [`chats`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L340) | `MutableState>` | 340 | List of all chats in this context | +| [`chatItems`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L345) | `MutableState>` | 345 | Items for the currently open chat in this context | +| [`chatState`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L347) | `ActiveChatState` | 347 | Tracks unread counts, splits, scroll state | + +### Derived Properties + +| Property | Line | Purpose | +|---|---|---| +| [`contentTag`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L353) | 353 | `MsgContentTag?` -- content filter tag if context is MsgContentTag | +| [`groupScopeInfo`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L360) | 360 | `GroupChatScopeInfo?` -- group scope if context is GroupChatScope | +| [`isUserSupportChat`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L367) | 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`](../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt#L196): + +```kotlin +data class ActiveChatState( + val splits: MutableStateFlow> = MutableStateFlow(emptyList()), + val unreadAfterItemId: MutableStateFlow = MutableStateFlow(-1L), + val totalAfter: MutableStateFlow = MutableStateFlow(0), + val unreadTotal: MutableStateFlow = MutableStateFlow(0), + val unreadAfter: MutableStateFlow = MutableStateFlow(0), + val unreadAfterNewestLoaded: MutableStateFlow = 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`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L1328): + +```kotlin +@Serializable @Stable +data class Chat( + val remoteHostId: Long?, + val chatInfo: ChatInfo, + val chatItems: List, + 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` | Latest chat items (summary; full list is in `ChatsContext.chatItems`) | +| `chatStats` | `ChatStats` | Unread counts and stats | + + + +### ChatStats + +Defined at [`ChatModel.kt line 1370`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L1370): + +```kotlin +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`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L1391): + +| 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`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L94) 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`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L496): +```kotlin +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` | empty | Theme ID per base theme | +| `themeOverrides` | `List` | 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` wrapper (defined in SimpleXAPI.kt) provides: +- `get(): T` -- read current value +- `set(value: T)` -- write value +- `state: MutableState` -- 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`](../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`](../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`](../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`](../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`](../common/src/commonMain/kotlin/chat/simplex/common/App.kt) | `AppScreen` (line 47), `MainScreen` (line 84), top-level UI state reads |