diff --git a/apps/multiplatform/CODE.md b/apps/multiplatform/CODE.md
index 30462d18e8..a87e594fd9 100644
--- a/apps/multiplatform/CODE.md
+++ b/apps/multiplatform/CODE.md
@@ -244,6 +244,7 @@ desktop/src/jvmMain/kotlin/chat/simplex/desktop/ -- Desktop app (1 file)
| 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/DesktopRail.kt | spec/client/chat-list.md, spec/client/navigation.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 |
@@ -251,6 +252,7 @@ desktop/src/jvmMain/kotlin/chat/simplex/desktop/ -- Desktop app (1 file)
| 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/chat/item/FlatChatItemView.kt | 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 |
diff --git a/apps/multiplatform/product/README.md b/apps/multiplatform/product/README.md
index 3dff974366..496d8f0d00 100644
--- a/apps/multiplatform/product/README.md
+++ b/apps/multiplatform/product/README.md
@@ -200,6 +200,7 @@ 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`) |
+| Flat layout (experimental) | Developer option: flat message rows with hover actions, profile and list rail, compact chat list | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt`, `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt` |
| 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` |
diff --git a/apps/multiplatform/product/concepts.md b/apps/multiplatform/product/concepts.md
index d29ac5c2e5..0129c7e5b8 100644
--- a/apps/multiplatform/product/concepts.md
+++ b/apps/multiplatform/product/concepts.md
@@ -50,6 +50,7 @@ This document provides a structured mapping between product-level concepts, thei
| 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` |
| PC31 | Channels (Relays) | [views/group-info.md](views/group-info.md) | [spec/client/chat-view.md](../spec/client/chat-view.md), [spec/state.md](../spec/state.md) | `common/.../model/ChatModel.kt` (`RelayStatus` incl. `RsRejected`, `GroupRelay`, `GroupMemberRole.Relay`, `GroupMemberStatus.MemRejected`), `common/.../views/chat/group/ChannelRelaysView.kt`, `GroupMemberInfoView.kt` (rejected-status row), `common/.../views/newchat/AddChannelView.kt` (`RelayStatusIndicator` rejected branch), `common/.../views/chat/group/AddGroupRelayView.kt` | `Controller.hs` (`APIAddGroupRelays`, `APIAllowRelayGroup`, `XGrpRelayReject` CONF handler) |
+| PC32 | Desktop Flat Layout | [views/chat.md](views/chat.md), [views/chat-list.md](views/chat-list.md), [views/settings.md](views/settings.md), [README.md](README.md) (Desktop Features) | [spec/client/chat-view.md](../spec/client/chat-view.md#FlatLayout), [spec/client/chat-list.md](../spec/client/chat-list.md#DesktopRail), [spec/client/navigation.md](../spec/client/navigation.md#DesktopScreen), [spec/state.md](../spec/state.md#AppPreferences) | `common/.../App.kt` (`LocalFlatLayout`, `desktopFlatLayoutEnabled`, `DesktopScreen`), `common/.../views/chat/item/FlatChatItemView.kt`, `ChatItemView.kt` (`itemQuickActions`, `chatItemTailEnabled`), `common/.../views/chatlist/DesktopRail.kt`, `ChatPreviewView.kt`, `common/.../views/usersettings/DeveloperView.kt`, `common/.../model/SimpleXAPI.kt` (`desktopFlatLayout`); tests: `common/src/commonTest/kotlin/chat/simplex/app/FlatChatItemViewTest.kt`, `ItemQuickActionsTest.kt` | N/A (client-only) |
**Legend for abbreviated paths:**
- `common/.../` expands to `common/src/commonMain/kotlin/chat/simplex/common/`
diff --git a/apps/multiplatform/product/views/chat-list.md b/apps/multiplatform/product/views/chat-list.md
index a09994d1e3..b542ef5666 100644
--- a/apps/multiplatform/product/views/chat-list.md
+++ b/apps/multiplatform/product/views/chat-list.md
@@ -19,6 +19,7 @@ Main screen of the SimpleX Chat Android and Desktop apps. Displays all conversat
|---|---|
| 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`) |
+| Desktop, flat layout (experimental) | Rail at the far left, then the same 3 columns; chat list rows are compact |
## Page Sections
@@ -117,6 +118,28 @@ Each chat type provides specific dropdown menu items:
| FAB (new chat button) | Standard mode, search empty, chat running | `FloatingActionButton` at bottom-right, pencil icon, opens `NewChatSheet` |
| Crowdfunding banner (`GetStakeBanner`) | `crowdfundingAvailable()` and not dismissed | Gradient card above the chats, and below the onboarding cards when there are no conversations; opens `GetStakeView`. The dismiss X appears once the banner has been tapped and there is at least one chat; dismissing hides it until hints are reset |
+### Desktop Flat Layout (experimental)
+
+Enabled by "Flat layout (experimental)" in Developer options (desktop only, while developer tools are on).
+
+Rail (`DesktopRail`), top to bottom:
+
+| Element | Behavior |
+|---|---|
+| User avatars | Active and non-hidden users, most recently active first; the active user is marked on the left edge, others show an unread badge. Click another user to switch to it; click the active user to open `UserPicker` (address, preferences, remote hosts, other actions) |
+| All | Clears the list filter |
+| Preset lists | Reports, favorites, contacts, groups, business, notes, each shown when it has chats; click toggles the filter |
+| User lists | Emoji or label icon with name and unread dot; click toggles the filter; long-press or right-click opens the edit/delete/reorder menu |
+| Add list | Opens `TagListEditor` |
+| Settings | Opens `SettingsView` |
+
+Avatar clicks are disabled while chat is stopped, unless connected to a remote host. With no users, the rail shows only Settings.
+
+Chat list changes:
+- The toolbar user avatar button and the tag filter row (`TagsView`) are hidden; the rail replaces them.
+- Chat rows are a single line: small avatar, chat name, unread or mention badge (or muted/favorite icon), and status icon. Last message, draft and timestamp are not shown.
+- Rows have smaller vertical padding and no dividers.
+
### Empty States
| State | Display |
@@ -136,3 +159,4 @@ Each chat type provides specific dropdown menu items:
| `UserPicker.kt` | `views/chatlist/UserPicker.kt` |
| `TagListView.kt` | `views/chatlist/TagListView.kt` |
| `GetStakeBanner.kt` | `views/chatlist/GetStakeBanner.kt` |
+| `DesktopRail.kt` | `views/chatlist/DesktopRail.kt` |
diff --git a/apps/multiplatform/product/views/chat.md b/apps/multiplatform/product/views/chat.md
index 7862574f04..1f1f07bf24 100644
--- a/apps/multiplatform/product/views/chat.md
+++ b/apps/multiplatform/product/views/chat.md
@@ -125,6 +125,23 @@ Shown when `selectedChatItems != null`:
When `timedMessageAllowed` is true, compose bar includes a timer icon for setting message disappear time via `customDisappearingMessageTimePref`.
+### Desktop Flat Layout (experimental)
+
+Desktop only, enabled by "Flat layout (experimental)" in Developer options while developer tools are on. Applies to all chats, including reports and member support chats in the end panel.
+
+| Element | Description |
+|---|---|
+| Message rows | Full-width rows without bubbles or tails; sent and received messages are both aligned left with a transparent background |
+| Sender header | Avatar, name, role (above member) and time on the first message of a group; shown again after an event, a date change, a sender or direction change, or a gap of 60 seconds or more |
+| Continuation rows | No avatar or name; the time appears in the left gutter on hover |
+| Avatar click | Received group message: member info; direct or channel message: chat info |
+| Hover background | Light tint over the hovered row |
+| Hover action bar | Top-right of the hovered message: 3 quick reactions, add reaction (full reaction list), reply, forward, and More (opens the regular context menu). Each button follows the same rules as the context menu; deleted, call and error items show only More. Hidden in selection mode |
+| Message time | Not shown inside the message; status, edited and timer icons stay |
+| Search results | The "go to message" button is always on the right |
+
+The context menu and multi-select work as in the bubble layout. The Appearance bubble preview keeps the bubble look.
+
## Source Files
| File | Path |
diff --git a/apps/multiplatform/product/views/settings.md b/apps/multiplatform/product/views/settings.md
index 563fa09d5b..5489eb2fce 100644
--- a/apps/multiplatform/product/views/settings.md
+++ b/apps/multiplatform/product/views/settings.md
@@ -142,6 +142,7 @@ Platform-specific section (expect/actual composable):
| Storage (developer options) | Opens `StorageView`: size of each top-level entry in the app's data, config and temp folders |
| Chat console | Opens `ChatConsoleView` terminal |
| Terminal always visible (Desktop) | Keep terminal window open |
+| Flat layout (experimental, Desktop) | Developer option `desktopFlatLayout`: flat message rows, hover action bar, profile and list rail, compact chat list; see [chat.md](chat.md) and [chat-list.md](chat-list.md) |
| 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` |
diff --git a/apps/multiplatform/spec/README.md b/apps/multiplatform/spec/README.md
index c6dbf1abf2..bc5a71df6d 100644
--- a/apps/multiplatform/spec/README.md
+++ b/apps/multiplatform/spec/README.md
@@ -88,8 +88,8 @@ Common Module (commonMain)
| 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 |
+| Chat View | [spec/client/chat-view.md](client/chat-view.md) | ChatView, ChatItemView, message rendering, item interactions, desktop flat layout (FlatChatItemView) |
+| Chat List | [spec/client/chat-list.md](client/chat-list.md) | ChatListView, ChatPreviewView, filtering, search, tags, desktop rail (DesktopRail) |
| 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 |
@@ -104,7 +104,7 @@ Common Module (commonMain)
| 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 |
+| Concepts | [product/concepts.md](../product/concepts.md) | 32 product concepts (PC1-PC32) 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 |
@@ -122,11 +122,11 @@ Common Module (commonMain)
| 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 |
+| Common App Screen | [`App.kt`](../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L48) | `fun AppScreen()` | 48 |
| 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#L525) | `object ChatController` | 525 |
+| Chat Controller | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L536) | `object ChatController` | 536 |
| 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#L102) | `class AppPreferences` | 102 |
+| App Preferences | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L103) | `class AppPreferences` | 103 |
| 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#L20) | `abstract class NtfManager` | 20 |
| Theme Manager | [`ThemeManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L18) | `object ThemeManager` | 18 |
diff --git a/apps/multiplatform/spec/client/chat-list.md b/apps/multiplatform/spec/client/chat-list.md
index e34da9613d..156a77c231 100644
--- a/apps/multiplatform/spec/client/chat-list.md
+++ b/apps/multiplatform/spec/client/chat-list.md
@@ -16,12 +16,13 @@ Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatLis
8. [UserPicker](#8-userpicker)
9. [Source Files](#9-source-files)
10. [Crowdfunding Banner](#10-crowdfunding-banner)
+11. [Desktop Rail](#11-desktop-rail)
---
## Executive Summary
-The Chat List is the landing screen of SimpleX Chat, rendering all conversations for the active user. Built around `ChatListView` (line 179 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.
+The Chat List is the landing screen of SimpleX Chat, rendering all conversations for the active user. Built around `ChatListView` (line 217 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.
---
@@ -30,7 +31,7 @@ The Chat List is the landing screen of SimpleX Chat, rendering all conversations
```
ChatListView
|-- ChatListToolbar (top or bottom app bar)
-| |-- UserProfileButton (opens UserPicker)
+| |-- UserProfileButton (opens UserPicker; hidden in desktop flat layout)
| |-- Title ("Your chats")
| |-- SubscriptionStatusIndicator
| +-- NewChatButton / StoppedIndicator
@@ -39,7 +40,7 @@ ChatListView
| | |-- Spacer (top/bottom padding)
| | |-- stickyHeader
| | | |-- ChatListSearchBar (search input + filter toggle)
-| | | +-- TagsView (preset + custom tag chips)
+| | | +-- TagsView (preset + custom tag chips; hidden in desktop flat layout)
| | |-- ChatListNavLinkView[] (per-chat row items)
| | +-- ChatListFeatureCards (one-hand UI card, address card)
| +-- EmptyState text
@@ -54,7 +55,7 @@ ChatListView
## 2. ChatListView Composable
-**Location:** [`ChatListView.kt#L179`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L179)
+**Location:** [`ChatListView.kt#L217`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L217)
```kotlin
fun ChatListView(
@@ -67,8 +68,8 @@ fun ChatListView(
### Initialization
-- Shows "What's New" modal on first launch after update (line ~185), with a 1-second delay.
-- On desktop, closing a chat resets audio/video players (line ~193).
+- Shows "What's New" modal on first launch after update (line ~221), with a 1-second delay.
+- On desktop, closing a chat resets audio/video players (line ~236).
### Layout Modes
@@ -89,8 +90,8 @@ The `oneHandUI` preference (`appPrefs.oneHandUI.state`) controls the layout:
### Android-specific
-- `SetNotificationsModeAdditions`: Notification permission setup (line ~243).
-- `UserPicker`: Overlay side panel for user switching (line ~247).
+- `SetNotificationsModeAdditions`: Notification permission setup (line ~281).
+- `UserPicker`: Overlay side panel for user switching (line ~285).
---
@@ -114,7 +115,7 @@ The `oneHandUI` preference (`appPrefs.oneHandUI.state`) controls the layout:
### Active Filter Types
-Defined as sealed class `ActiveFilter` (line ~59):
+Defined as sealed class `ActiveFilter` (line ~62):
```kotlin
sealed class ActiveFilter {
@@ -137,7 +138,7 @@ sealed class ActiveFilter {
### Search Filtering
-The `filteredChats` function (line ~1474) applies filters in this order:
+The `filteredChats` function (line ~1543) 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`.
@@ -148,7 +149,7 @@ The `filteredChats` function (line ~1474) applies filters in this order:
### Search Bar
-`ChatListSearchBar` (line ~765) provides:
+`ChatListSearchBar` (line ~789) 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).
@@ -159,7 +160,7 @@ The `filteredChats` function (line ~1474) applies filters in this order:
## 5. Chat Preview
-**Location:** [`ChatPreviewView.kt#L41`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt#L41)
+**Location:** [`ChatPreviewView.kt#L42`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt#L42)
```kotlin
fun ChatPreviewView(
@@ -196,6 +197,10 @@ When `chatModelDraftChatId` matches the chat ID, the preview shows a draft indic
- Inactive contacts: cancel icon overlay on profile image.
- Left/removed/deleted groups: cancel icon overlay.
+### Compact Row (Desktop Flat Layout)
+
+When `LocalFlatLayout.current` is true, `ChatPreviewView` renders a single line and returns early: `ChatInfoImage` at `COMPACT_CHAT_IMAGE_SIZE` (32dp, scaled by `fontSizeSqrtMultiplier`), `chatPreviewTitle()`, `chatPreviewUnreadIndicator()` and `chatStatusImage()`. There is no last message, draft, timestamp or inactive overlay. `chatPreviewUnreadIndicator()` is a local composable (unread count or mention badge, muted icon, or favorite star) shared with the regular layout.
+
---
@@ -219,13 +224,15 @@ Routes each chat to the appropriate click action and context menu based on `chat
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.
+In the desktop flat layout, [`ChatListNavLinkLayout`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt#L29-L69) uses 4dp vertical padding instead of 8dp and draws no dividers.
+
---
## 7. Tag System
### TagsView
-**Location:** [`ChatListView.kt#L1214`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1214)
+**Location:** [`ChatListView.kt#L1282`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1282)
Renders a horizontally scrollable row of tag chips (via `TagsRow`, which is a platform-specific `expect` composable).
@@ -241,6 +248,8 @@ Layout logic:
- **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.
+In the desktop flat layout, `TagsView` renders nothing and the tags move to the rail (section 11). [`presetTagLabel()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1610) is `internal` so the rail uses the same icons and labels.
+
### TagListView
@@ -279,6 +288,9 @@ fun UserPicker(
- **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.
+- **Desktop flat layout:** The toolbar button is hidden; clicking the active user's avatar in the rail opens the picker.
+
+Selecting another user calls [`switchToUser(user)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt#L650-L657), which closes all modals and calls `changeActiveUser` with progress. The rail uses the same function.
### Content
@@ -313,6 +325,7 @@ Uses `AnimatedViewState` (`GONE`, `VISIBLE`, `HIDING`) with a `MutableStateFlow`
| `ShareListView.kt` | Share target list (forwarding flow) |
| `TagListView.kt` | Tag management and assignment view |
| `UserPicker.kt` | User switching side panel |
+| `DesktopRail.kt` | Desktop flat layout rail: users, tag filters, settings |
| `GetStakeBanner.kt` | Crowdfunding banner and the shared banner card chrome |
---
@@ -358,3 +371,27 @@ Tapping the card runs `openGetStake()`, which opens `GetStakeView(showFirstImage
### Shared card chrome
`Modifier.bannerCard(onTap)` (size state, gradient brush, `heightIn`, `clip`, `background`, `clickable`) and `BannerDismissButton` are declared separately from `GetStakeBanner` so other banners can adopt the same chrome; the paddings stay with the caller. The gradient reuses `gradientPoints`, `lightStops` and `darkStops` from `views/newchat/OnboardingCards.kt`.
+
+---
+
+
+
+## 11. Desktop Rail
+
+**Location:** [`DesktopRail.kt#L38`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt#L38)
+
+```kotlin
+fun DesktopRail(userPickerState: MutableStateFlow, setPerformLA: (Boolean) -> Unit)
+```
+
+Vertical column at the far left of `DesktopScreen` in the desktop flat layout (see [navigation.md](navigation.md#DesktopScreen)). Width [`DESKTOP_RAIL_WIDTH`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt#L31) (64dp) scaled by `fontSizeSqrtMultiplier`, with a light `onBackground` tint and a `VerticalDivider` on the right.
+
+### Content
+
+| Part | Function | Behavior |
+|---|---|---|
+| Users | [`RailUsers`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt#L77-L116) | Avatars of the active and non-hidden users, sorted by `activeOrder` descending. The active user has a mark on the left edge. Inactive users show `userUnreadBadge`. Clicking an inactive user calls `switchToUser`; clicking the active user sets `userPickerState` to `VISIBLE`. Disabled while chat is not running and not connected to a remote host |
+| Tags | [`RailTags`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt#L119-L159) | "All" (clears `activeChatTagFilter`), preset tags with a non-zero count in `PresetTagKind` order, user tags (emoji or label icon, unread dot, long-press or right-click opens `TagsDropdownMenu`), and "Add list" opening `TagListEditor` in `ModalManager.start` |
+| Settings | [`RailContent`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt#L51-L74) | Bottom item opening `SettingsView` in `ModalManager.start` as a card screen |
+
+Users and tags are shown only when `chatModel.users` is not empty. Each item is a [`RailItem`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt#L166-L206): icon or emoji above a one-line label, primary color when active. Clicking a preset or user tag toggles it through the private [`toggleFilter`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt#L161-L163), which sets `chatModel.activeChatTagFilter` to the tag or back to `null`, as `TagsView` does. Settings, "Add list" and the active user avatar call `ModalManager.start.closeModals()` first, because the rail stays clickable while a start-pane modal is open.
diff --git a/apps/multiplatform/spec/client/chat-view.md b/apps/multiplatform/spec/client/chat-view.md
index 3bad2e09c4..863979f28e 100644
--- a/apps/multiplatform/spec/client/chat-view.md
+++ b/apps/multiplatform/spec/client/chat-view.md
@@ -15,12 +15,13 @@ Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
7. [ChatInfoView](#7-chatinfoview)
8. [GroupChatInfoView](#8-groupchatinfoview)
9. [Source Files](#9-source-files)
+10. [Flat Layout (Desktop)](#10-flat-layout-desktop)
---
## 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.
+The Chat View is the primary message display and interaction surface in SimpleX Chat. It is built around the `ChatView` composable (line ~99 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.
---
@@ -36,6 +37,7 @@ ChatView
| | | |-- DateSeparator
| | | |-- MemberNameAndRole (group received messages)
| | | |-- MemberImage (group received messages)
+| | | |-- FlatChatItemRow (desktop flat layout, replaces the two lines above)
| | | +-- ChatItemView (message type routing)
| | |-- ChatBannerView (first item: chat profile banner)
| | +-- FloatingButtons (scroll-to-bottom, unread counter)
@@ -56,7 +58,7 @@ ChatView
## 2. ChatView Composable
-**Location:** [`ChatView.kt#L97`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt#L97)
+**Location:** [`ChatView.kt#L99`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt#L99)
```kotlin
fun ChatView(
@@ -84,7 +86,7 @@ fun ChatView(
### Chat Loading
-On chat ID change (via `snapshotFlow` on `chatModel.chatId.value`, line ~162):
+On chat ID change (via `snapshotFlow` on `chatModel.chatId.value`, line ~180):
1. Marks unread chat as read (`markUnreadChatAsRead`)
2. Clears group members state
@@ -95,7 +97,7 @@ On chat ID change (via `snapshotFlow` on `chatModel.chatId.value`, line ~162):
### Chat Type Routing
-The outer `when (chatInfo)` (line ~229) branches:
+The outer `when (chatInfo)` (line ~272) branches:
| ChatInfo Type | Behavior |
|---|---|
@@ -107,18 +109,18 @@ The outer `when (chatInfo)` (line ~229) branches:
## 3. Message List
-**Location:** [`ChatView.kt#L1592`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt#L1592) (`ChatItemsList` composable)
+**Location:** [`ChatView.kt#L1751`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt#L1751) (`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`.
+- **Merged Items:** Messages are grouped via `MergedItems.create()` (line ~1815), 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`.
+- **Swipe to Reply:** `SwipeToDismiss` modifier on each item (EndToStart direction, 30dp threshold) sets `ComposeContextItem.QuotedItem` via `replyToItem()`.
- **Selection Mode:** When `selectedChatItems` is non-null, a checkbox overlay appears on each item; a full-width clickable overlay toggles selection.
### Item Layout (ChatViewListItem)
@@ -127,6 +129,7 @@ The message list is a `LazyColumnWithScrollBar` with `reverseLayout = true`, mea
- **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).
+- **Flat layout (desktop):** When `LocalFlatLayout.current` is true, all chat types render through `FlatChatItemRow` instead of the branches above (see section 10). The selection checkbox offset applies to sent items too.
---
@@ -134,7 +137,7 @@ The message list is a `LazyColumnWithScrollBar` with `reverseLayout = true`, mea
## 4. ChatItemView
-**Location:** [`item/ChatItemView.kt#L66`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L66)
+**Location:** [`item/ChatItemView.kt#L70`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L70)
```kotlin
fun ChatItemView(
@@ -169,7 +172,11 @@ The composable routes based on `cItem.content` and `cItem.meta.itemDeleted`:
### Context Menu
-Long-press or right-click opens a dropdown menu with context-sensitive actions (see section 6).
+Long-press or right-click opens a dropdown menu with context-sensitive actions (see section 6). In the flat layout, the hover bar's More button opens the same menu.
+
+### Flat Layout Adjustments
+
+When `LocalFlatLayout.current` is true, `ChatItemView` keeps its content but drops the bubble look: items align to the start regardless of direction, `GoToItemButton` is always on the right, the bubble click has no indication (the row draws the hover background), voice-only items render as received, and [`chatItemTailEnabled()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L1237) returns false so `shapeStyle` never draws a tail. `FramedItemView` uses a transparent background. `ChatItemsList` passes `showTimestamp = false`, so the meta shows status icons without the time.
---
@@ -237,6 +244,22 @@ Context menu actions are built dynamically in `ChatItemView` based on message ty
| **Open** | File with completed download | Opens file with system handler |
| **Reveal / Hide** | Part of a merged group; expanded or collapsed | Toggles `revealedItems` state |
+The React, Reply and Forward conditions are computed by [`itemQuickActions()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L814-L823), which returns [`ItemQuickActions`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L812). All three are false for items without message content, temporary items (`id < 0`) and reports. Otherwise:
+
+| Field | Condition |
+|---|---|
+| `react` | Reactions feature enabled in the chat and `cItem.allowAddReaction` |
+| `reply` | Not deleted, not live, not a local note, `cInfo.sendMsgEnabled` |
+| `forward` | Not deleted, file (if any) allows forwarding, not the live dummy item, not live |
+
+Shared helpers used by both the menu and the flat-layout hover bar:
+
+| Function | Purpose |
+|---|---|
+| [`availableReactions(cItem)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L825-L826) | `MsgReaction.supported` minus the reactions the user already set |
+| [`replyToItem(composeState, cItem, useLinkPreviews)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L828-L834) | Sets `ComposeContextItem.QuotedItem`; replaces the compose state when editing. Also used by swipe to reply |
+| [`MsgReactionsMenu(cInfo, cItem, setReaction, showMenu)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L837-L854) | Scrollable row of available reactions; a tap sets the reaction and closes `showMenu` |
+
---
## 7. ChatInfoView
@@ -311,6 +334,7 @@ Key sections: group profile, group link, member list with roles, group preferenc
| `CIVoiceView.kt` | Voice message (waveform + player) |
| `DeletedItemView.kt` | Deleted message placeholder |
| `EmojiItemView.kt` | Large emoji-only message |
+| `FlatChatItemView.kt` | Desktop flat layout: sender header rule, `FlatChatItemRow`, `FlatHoverBar` |
| `FramedItemView.kt` | Message bubble frame (quoted item, text, media) |
| `ImageFullScreenView.kt` | Fullscreen image gallery |
| `MarkdownFileView.kt` | Markdown file viewer, formatted or as text |
@@ -343,3 +367,66 @@ Key sections: group profile, group link, member list with roles, group preferenc
When a relay operator runs `/leave #channel`, the relay sends `x.grp.relay.reject` over the owner-relay direct contact channel. Owner-side handling: the corresponding `GroupRelay.relayStatus` transitions `RSInvited → RSRejected`; the relay's `GroupMember.memberStatus` is set to `MemLeft` so the owner UI renders the rejected relay identically to one that explicitly ran `/leave` (`MemRejected` is reserved for the knocking-admission flow). In `GroupMemberInfoView`, an additional "Status: rejected by relay operator" `InfoRow` appears when `groupRelay?.relayStatus == RelayStatus.RsRejected`. The status is final on the owner side — clearable only by the relay operator running `/group allow #`, which has no owner-facing event.
The `RelayStatusIndicator` composable in `AddChannelView.kt` renders `RsRejected` with a red dot and "rejected" text, matching the `connFailed`/`removed` rendering.
+
+---
+
+
+
+## 10. Flat Layout (Desktop)
+
+**Location:** [`item/FlatChatItemView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt)
+
+Experimental Slack/Discord-like rendering of the message list: full-width rows, avatar and sender name once per group of messages, no bubbles. Enabled by `LocalFlatLayout` (provided by `DesktopScreen`, see [navigation.md](navigation.md#DesktopScreen)), so it applies only on desktop with developer tools and the `desktopFlatLayout` preference on. It covers every chat shown in the center and end columns, including reports and member support chats. The Appearance message-shape preview provides `LocalFlatLayout = false`, so bubble settings stay previewable.
+
+### Functions
+
+| Function | Purpose |
+|---|---|
+| [`flatItemHasHeader(item, older)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L39-L45) | Whether a row shows the sender header. `older` is the next older item in the list |
+| [`CIContent.hasFlatSender`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L47-L81) | True for content that has a sender (messages, deleted/moderated/blocked, calls, errors, group invitations, invalid JSON); false for events, feature and preference changes, E2EE info and the chat banner |
+| [`FlatChatItemRow(...)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L84-L124) | Full-width hoverable row: a 56dp gutter (scaled by `fontSizeSqrtMultiplier`) with the avatar on header rows or the time on hover for other rows, then a column with the header and the item content |
+| [`FlatHoverBar(...)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L200-L243) | Bordered action bar at the row's top end |
+| [`getItemSeparationLargeGap(chatItem, nextItem)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt#L3785-L3796) | Made `internal` for `flatItemHasHeader`; true when the sender or direction changes or items are 60s or more apart |
+
+### Header Rule
+
+`ChatItemsList` computes `flatHeader = LocalFlatLayout.current && flatItemHasHeader(item, merged.oldest().nextItem)` and passes it through `ChatViewListItem`. A row has a header when its content has a sender and any of these holds:
+
+- it is the oldest loaded item;
+- the older item has no sender (an event);
+- the older item is on a different date;
+- `getItemSeparationLargeGap(item, older)` is true.
+
+The rule does not use `previousItemSeparationLargeGap`, which is false for the oldest loaded item.
+
+### Sender Resolution
+
+| Direction | Name and badge | Role label | Avatar | Avatar click |
+|---|---|---|---|---|
+| `GroupRcv` | Member display name | Member role when above `Member` | `MemberImage` | `showMemberInfo` |
+| `ChannelRcv` | Channel name | `channel_role_label` | `ChatInfoImage` | `showChatInfo` |
+| `GroupSnd` | `groupInfo.membership` (incognito profile when incognito) | Membership role when above `Member` | `MemberImage(membership)` | None |
+| `DirectRcv` | Contact display name | None | `ChatInfoImage` | `showChatInfo` |
+| `DirectSnd`, `LocalSnd`, `LocalRcv` | Current user, or "Incognito" in an incognito chat | None | User profile image or `IncognitoImage` | None |
+
+The header shows the name in semibold, the role, and `cItem.timestampText`.
+
+### Hover Bar
+
+Rendered by `ChatItemView` as an overlay at the top end of the item when the flat layout is on, the item is not a preview or the live dummy item, its content has a sender, selection mode is off, and the item is hovered (`hoveredItemId`) or its reaction picker is open. `FlatChatItemRow` sets `hoveredItemId` from its own hover state.
+
+| Button | Shown when | Action |
+|---|---|---|
+| 3 quick reactions | `itemQuickActions().react` | First 3 of `availableReactions(cItem)`; sets the reaction |
+| Add reaction | `itemQuickActions().react` | Opens `MsgReactionsMenu` in a `DefaultDropdownMenu` |
+| Reply | `itemQuickActions().reply` | `replyToItem()` |
+| Forward | `itemQuickActions().forward` | `forwardItem()` |
+| More | Always | Opens the item's existing context menu (`showMenu`) |
+
+Items without message content (deleted, calls, errors) show only More.
+
+### Selection
+
+The selection checkbox and the full-row selection click overlay are shared with the bubble layout. In the flat layout the row is shifted by the selection offset for sent and received items alike.
+
+[REC] Thread side panel (replies to a message in the end column): needs a core query for items quoting a given message. `getChatItemIDs` (`../../src/Simplex/Chat/Store/Messages.hs`) has no such filter, and `chat_items.quoted_shared_msg_id` has no index.
diff --git a/apps/multiplatform/spec/client/navigation.md b/apps/multiplatform/spec/client/navigation.md
index d0340e8d23..f77ff06d74 100644
--- a/apps/multiplatform/spec/client/navigation.md
+++ b/apps/multiplatform/spec/client/navigation.md
@@ -1,6 +1,6 @@
# Navigation Specification
-Source: `common/src/commonMain/kotlin/chat/simplex/common/App.kt` (470 lines)
+Source: `common/src/commonMain/kotlin/chat/simplex/common/App.kt` (505 lines)
---
@@ -27,10 +27,10 @@ SimpleX Chat navigation is a platform-adaptive system implemented in `App.kt`. T
## 1. Overview
```
-AppScreen (line 46)
+AppScreen (line 48)
+-- SimpleXTheme
+-- Surface
- +-- MainScreen (line 82)
+ +-- MainScreen (line 85)
|-- [Migration in progress] -> DefaultProgressView
|-- [Database opening] -> DefaultProgressView
|-- [Database error] -> DatabaseErrorView
@@ -39,11 +39,12 @@ AppScreen (line 46)
|-- [Onboarding complete]
| |-- [Android]
| | +-- AndroidWrapInCallLayout
- | | +-- AndroidScreen (line 293)
+ | | +-- AndroidScreen (line 313)
| | |-- StartPartOfScreen (ChatListView)
| | +-- ChatView (slide-in panel)
| +-- [Desktop]
- | +-- DesktopScreen (line 406)
+ | +-- DesktopScreen (line 434)
+ | |-- DesktopRail (far left, flat layout only)
| |-- StartPartOfScreen + UserPicker (left column)
| |-- ModalManager.start (overlay on left)
| |-- CenterPartOfScreen / ChatView (center column)
@@ -59,7 +60,7 @@ AppScreen (line 46)
## 2. AppScreen Composable
-**Location:** [`App.kt#L47`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L47)
+**Location:** [`App.kt#L48`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L48)
```kotlin
@Composable
@@ -79,7 +80,7 @@ fun AppScreen()
## 3. MainScreen
-**Location:** [`App.kt#L84`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L84)
+**Location:** [`App.kt#L85`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L85)
```kotlin
@Composable
@@ -126,7 +127,7 @@ When onboarding is complete:
## 4. Android Layout
-**Location:** [`App.kt#L296`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L296)
+**Location:** [`App.kt#L313`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L313)
```kotlin
@Composable
@@ -157,7 +158,7 @@ If the device has a display cutout on horizontal sides (detected via `WindowInse
### 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.
+`AndroidWrapInCallLayout` (line ~298) adds a 40dp top padding when an active call is in progress (not in `WaitCapabilities` or `InvitationAccepted` state), with an `ActiveCallInteractiveArea` banner above.
---
@@ -165,22 +166,36 @@ If the device has a display cutout on horizontal sides (detected via `WindowInse
## 5. Desktop Layout
-**Location:** [`App.kt#L410`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L410)
+**Location:** [`App.kt#L434`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L434)
```kotlin
@Composable
fun DesktopScreen(userPickerState: MutableStateFlow)
```
+`DesktopScreen` provides `LocalFlatLayout` and delegates to the private `DesktopScreenLayout`, which draws the columns below.
+
+### Flat Layout Flag
+
+| Symbol | Location | Description |
+|---|---|---|
+| `LocalFlatLayout` | [`App.kt#L427`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L427) | `staticCompositionLocalOf { false }`; read by chat item, chat list and preview views to switch to the flat layout |
+| `desktopFlatLayoutEnabled` | [`App.kt#L429-L430`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L429-L430) | `appPlatform.isDesktop && developerTools && desktopFlatLayout`; also read outside composition by `ModalManager` |
+
+`DesktopScreen` provides `LocalFlatLayout = desktopFlatLayoutEnabled`, so the flag covers every column and `ModalManager.fullscreen`. Turning developer tools off disables the layout without changing the `desktopFlatLayout` preference. See [chat-view.md](chat-view.md#FlatLayout) and [chat-list.md](chat-list.md#DesktopRail).
+
### 3-Column Layout
| Column | Width | Content |
|---|---|---|
+| **Rail** (flat layout only) | `DESKTOP_RAIL_WIDTH * fontSizeSqrtMultiplier` (64dp, fixed) | `DesktopRail`: users, tag filters, settings |
| **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 with `ModalManager.center` over it; without a chat: `ModalManager.center` or "no selected chat" placeholder) |
| **Right** | `max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier` (flexible, 0 when empty) | `ModalManager.end` (ChatInfoView, GroupChatInfoView, ChatItemInfoView, etc.) |
+In the flat layout the rail is added before the left column and every start offset (left column, left overlay, center/right row, dismiss overlay, left divider) is shifted by the rail width; the left column keeps its width because the desktop `UserPicker` layout assumes it. When an end modal opens, `ModalManager` expands the window by the rail width as well.
+
### Column Separators
- `VerticalDivider` between left and center columns (always visible).
@@ -188,11 +203,11 @@ fun DesktopScreen(userPickerState: MutableStateFlow)
### 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.
+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 ~472). Clicking it closes start modals and hides the UserPicker.
### CenterPartOfScreen
-**Location:** [`App.kt#L373`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L373)
+**Location:** [`App.kt#L390`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L390)
- When `chatId` is null and no center modals: shows "No selected chat" placeholder.
- When `chatId` is null and center modals open: shows `ModalManager.center`.
@@ -201,7 +216,7 @@ When the UserPicker is visible or a start modal is open (but no center modal), a
### StartPartOfScreen
-**Location:** [`App.kt#L352`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L352)
+**Location:** [`App.kt#L369`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L369)
Routes between:
- `SetDeliveryReceiptsView` (if `chatModel.setDeliveryReceipts` is true)
@@ -212,7 +227,7 @@ Routes between:
## 6. ModalManager
-**Location:** `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt` (line 92)
+**Location:** `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt` (line 100)
```kotlin
class ModalManager(private val placement: ModalPlacement?)
@@ -359,7 +374,7 @@ The stage value is stored in `appPrefs.onboardingStage` and persisted across app
| File | Description |
|---|---|
-| `App.kt` | AppScreen, MainScreen, AndroidScreen, DesktopScreen, StartPartOfScreen, CenterPartOfScreen, EndPartOfScreen |
+| `App.kt` | AppScreen, MainScreen, AndroidScreen, DesktopScreen, StartPartOfScreen, CenterPartOfScreen, EndPartOfScreen, LocalFlatLayout |
| `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 |
@@ -371,6 +386,7 @@ The stage value is stored in `appPrefs.onboardingStage` and persisted across app
| `views/onboarding/SetNotificationsMode.kt` | Step 4: Notification setup |
| `views/chatlist/ChatListView.kt` | Chat list (StartPartOfScreen content) |
| `views/chatlist/UserPicker.kt` | User switching panel |
+| `views/chatlist/DesktopRail.kt` | Desktop flat layout rail (users, tag filters, settings) |
| `views/chat/ChatView.kt` | Chat view (CenterPartOfScreen content) |
| `views/database/DatabaseErrorView.kt` | Database error recovery |
| `views/SplashView.kt` | Splash / loading screen |
diff --git a/apps/multiplatform/spec/impact.md b/apps/multiplatform/spec/impact.md
index 7e0fb181d8..f7e74480b3 100644
--- a/apps/multiplatform/spec/impact.md
+++ b/apps/multiplatform/spec/impact.md
@@ -41,6 +41,7 @@
| PC29 | User Address |
| PC30 | Member Support Chat |
| PC31 | Channels (Relays) |
+| PC32 | Desktop Flat Layout |
---
@@ -52,13 +53,13 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
| Source File | Product Concepts Affected | Risk Level | Notes |
|-------------|--------------------------|------------|-------|
-| `App.kt` | PC1 through PC31 | High | Root composable — navigation scaffold for all features |
+| `App.kt` | PC1 through PC32 | High | Root composable — navigation scaffold for all features |
| `AppLock.kt` | PC22 | Medium | App lock state and authorization lifecycle |
-| `model/ChatModel.kt` | PC1 through PC31 | High | Central state object — every feature reads or writes here |
-| `model/SimpleXAPI.kt` | PC1 through PC31 | High | FFI bridge to Haskell core — all commands and responses |
+| `model/ChatModel.kt` | PC1 through PC32 | High | Central state object — every feature reads or writes here |
+| `model/SimpleXAPI.kt` | PC1 through PC32 | 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 PC31 | High | Native FFI declarations (`chatMigrateInit`, `chatSendCmd`, etc.) — all API traffic |
-| `platform/AppCommon.kt` | PC1 through PC31 | Medium | Shared app initialization logic |
+| `platform/Core.kt` | PC1 through PC32 | High | Native FFI declarations (`chatMigrateInit`, `chatSendCmd`, etc.) — all API traffic |
+| `platform/AppCommon.kt` | PC1 through PC32 | 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 |
@@ -68,7 +69,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
| `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 PC31 | Low | Platform detection and capability flags |
+| `platform/Platform.kt` | PC1 through PC32 | 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 |
@@ -91,12 +92,13 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
| Source File | Product Concepts Affected | Risk Level | Notes |
|-------------|--------------------------|------------|-------|
-| `views/chatlist/ChatListView.kt` | PC1, PC28 | High | Main screen — chat list rendering and search |
+| `views/chatlist/ChatListView.kt` | PC1, PC28, PC32 | High | Main screen — chat list rendering and search |
| `views/chatlist/GetStakeBanner.kt` | PC1 | Low | Crowdfunding banner and shared banner card chrome |
| `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/ChatPreviewView.kt` | PC1, PC2, PC3, PC11, PC32 | Medium | Chat row preview rendering; compact row in flat layout |
| `views/chatlist/TagListView.kt` | PC28 | Medium | Chat tag filter UI |
-| `views/chatlist/UserPicker.kt` | PC19, PC21 | Medium | Multi-profile switcher overlay |
+| `views/chatlist/UserPicker.kt` | PC19, PC21, PC32 | Medium | Multi-profile switcher overlay; `switchToUser` shared with the rail |
+| `views/chatlist/DesktopRail.kt` | PC19, PC28, PC32 | Medium | Desktop flat layout rail: user switching, tag filters, settings |
| `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 |
@@ -108,7 +110,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
| 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/ChatView.kt` | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC11, PC32 | 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 |
@@ -133,9 +135,10 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
| 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/ChatItemView.kt` | PC2, PC3, PC5, PC6, PC7, PC8, PC32 | High | Root chat item renderer with context menus; quick action checks shared with the flat hover bar |
| `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/FramedItemView.kt` | PC4, PC6, PC10, PC11, PC32 | Medium | Framed (quoted/forwarded) message container |
+| `views/chat/item/FlatChatItemView.kt` | PC2, PC3, PC5, PC32 | Medium | Desktop flat layout rows, sender headers, hover action bar |
| `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 |
@@ -206,7 +209,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
| 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/Appearance.kt` | PC24, PC32 | 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 |
@@ -219,7 +222,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
| `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/DeveloperView.kt` | PC32 | Low | Developer/debug settings |
| `views/usersettings/StorageView.kt` | — | Low | Storage breakdown (developer options) |
| `views/usersettings/HelpView.kt` | — | Low | Help and support links |
| `views/usersettings/MarkdownHelpView.kt` | PC4 | Low | Markdown formatting guide |
@@ -270,9 +273,9 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
| Source File | Product Concepts Affected | Risk Level | Notes |
|-------------|--------------------------|------------|-------|
-| `views/helpers/AlertManager.kt` | PC1 through PC31 | Medium | Modal alert system used across all features |
-| `views/helpers/ModalView.kt` | PC1 through PC31 | Medium | Modal navigation stack |
-| `views/helpers/Utils.kt` | PC1 through PC31 | Low | Shared formatting, clipboard, and utility functions |
+| `views/helpers/AlertManager.kt` | PC1 through PC32 | Medium | Modal alert system used across all features |
+| `views/helpers/ModalView.kt` | PC1 through PC32 | Medium | Modal navigation stack |
+| `views/helpers/Utils.kt` | PC1 through PC32 | 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 |
@@ -325,8 +328,8 @@ Path prefix: `android/src/main/java/chat/simplex/app/`
| Source File | Product Concepts Affected | Risk Level | Notes |
|-------------|--------------------------|------------|-------|
-| `SimplexApp.kt` | PC1 through PC31 | High | Application class — initializes core, preferences, and notification channels |
-| `MainActivity.kt` | PC1 through PC31 | High | Single-activity host — intent handling, lifecycle, deep links |
+| `SimplexApp.kt` | PC1 through PC32 | High | Application class — initializes core, preferences, and notification channels |
+| `MainActivity.kt` | PC1 through PC32 | 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 |
@@ -340,7 +343,7 @@ Path prefix: `common/src/androidMain/kotlin/chat/simplex/common/`
| Source File | Product Concepts Affected | Risk Level | Notes |
|-------------|--------------------------|------------|-------|
-| `platform/AppCommon.android.kt` | PC1 through PC31 | Medium | Android app initialization actual declarations |
+| `platform/AppCommon.android.kt` | PC1 through PC32 | 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 |
@@ -406,7 +409,7 @@ Path prefix: `desktop/src/jvmMain/kotlin/chat/simplex/desktop/`
| Source File | Product Concepts Affected | Risk Level | Notes |
|-------------|--------------------------|------------|-------|
-| `Main.kt` | PC1 through PC31 | High | JVM entry point — Haskell init, migrations, app launch |
+| `Main.kt` | PC1 through PC32 | High | JVM entry point — Haskell init, migrations, app launch |
### 3.2 Desktop Platform Implementations (desktopMain)
@@ -417,7 +420,7 @@ Path prefix: `common/src/desktopMain/kotlin/chat/simplex/common/`
| `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 PC31 | Medium | Desktop app initialization actual declarations |
+| `platform/AppCommon.desktop.kt` | PC1 through PC32 | 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 |
@@ -446,7 +449,7 @@ Path prefix: `common/src/desktopMain/kotlin/chat/simplex/common/`
| `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/ChatListNavLinkView.desktop.kt` | PC1, PC32 | 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 |
@@ -480,13 +483,13 @@ The Haskell core is compiled as a shared native library (`libsimplex.so` / `libs
| Source File | Product Concepts Affected | Risk Level | Notes |
|-------------|--------------------------|------------|-------|
-| `src/Simplex/Chat.hs` | PC1 through PC31 | High | Main chat module — top-level orchestration |
-| `src/Simplex/Chat/Controller.hs` | PC1 through PC31 | High | Command processor — all API commands dispatched here |
-| `src/Simplex/Chat/Types.hs` | PC1 through PC31 | High | Core data types shared across all features |
-| `src/Simplex/Chat/Core.hs` | PC1 through PC31 | High | Chat engine lifecycle (start, stop, subscribe) |
-| `src/Simplex/Chat/Library/Commands.hs` | PC1 through PC31 | High | API command handler implementations |
-| `src/Simplex/Chat/Library/Internal.hs` | PC1 through PC31 | High | Internal helpers for command processing |
-| `src/Simplex/Chat/Library/Subscriber.hs` | PC1 through PC31 | High | Event subscriber — incoming message routing |
+| `src/Simplex/Chat.hs` | PC1 through PC32 | High | Main chat module — top-level orchestration |
+| `src/Simplex/Chat/Controller.hs` | PC1 through PC32 | High | Command processor — all API commands dispatched here |
+| `src/Simplex/Chat/Types.hs` | PC1 through PC32 | High | Core data types shared across all features |
+| `src/Simplex/Chat/Core.hs` | PC1 through PC32 | High | Chat engine lifecycle (start, stop, subscribe) |
+| `src/Simplex/Chat/Library/Commands.hs` | PC1 through PC32 | High | API command handler implementations |
+| `src/Simplex/Chat/Library/Internal.hs` | PC1 through PC32 | High | Internal helpers for command processing |
+| `src/Simplex/Chat/Library/Subscriber.hs` | PC1 through PC32 | 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 |
@@ -496,8 +499,8 @@ The Haskell core is compiled as a shared native library (`libsimplex.so` / `libs
| `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 PC31 | High | Database store interface |
-| `src/Simplex/Chat/Store/Shared.hs` | PC1 through PC31 | Medium | Shared store utilities |
+| `src/Simplex/Chat/Store.hs` | PC1 through PC32 | High | Database store interface |
+| `src/Simplex/Chat/Store/Shared.hs` | PC1 through PC32 | 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 |
@@ -526,11 +529,11 @@ The Haskell core is compiled as a shared native library (`libsimplex.so` / `libs
| `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 PC31 | High | C FFI exports — JNI bridge target |
+| `src/Simplex/Chat/Mobile.hs` | PC1 through PC32 | 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 PC31 | Medium | Shared FFI helpers |
+| `src/Simplex/Chat/Mobile/Shared.hs` | PC1 through PC32 | Medium | Shared FFI helpers |
| `src/Simplex/Chat/Mobile/WebRTC.hs` | PC17 | Low | WebRTC FFI helpers |
-| `src/Simplex/Chat/View.hs` | PC1 through PC31 | Low | Terminal view rendering (not used by mobile/desktop UI) |
+| `src/Simplex/Chat/View.hs` | PC1 through PC32 | 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) |
diff --git a/apps/multiplatform/spec/state.md b/apps/multiplatform/spec/state.md
index 5003bb60e5..f9edb3d61a 100644
--- a/apps/multiplatform/spec/state.md
+++ b/apps/multiplatform/spec/state.md
@@ -322,11 +322,11 @@ The `text` extension on the enum returns the localized status string (resource k
## 5. AppPreferences
-Defined at [`SimpleXAPI.kt line 94`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L94) as `class AppPreferences`.
+Defined at [`SimpleXAPI.kt line 103`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L103) 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):
+The `AppPreferences` instance is created lazily in `ChatController` at [`SimpleXAPI.kt line 539`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L539):
```kotlin
val appPrefs: AppPreferences by lazy { AppPreferences() }
```
@@ -418,7 +418,7 @@ val appPrefs: AppPreferences by lazy { AppPreferences() }
| `inAppBarsAlpha` | `Float` | varies | Bar transparency |
| `appearanceBarsBlurRadius` | `Int` | 50 or 0 | Bar blur radius (device-dependent) |
-#### Developer (lines 135-139)
+#### Developer (lines 149-154)
| Key | Type | Default | Purpose |
|---|---|---|---|
@@ -426,6 +426,7 @@ val appPrefs: AppPreferences by lazy { AppPreferences() }
| `logLevel` | `LogLevel` | `WARNING` | Log level |
| `showInternalErrors` | `Boolean` | `false` | Show internal errors to user |
| `showSlowApiCalls` | `Boolean` | `false` | Alert on slow API calls |
+| `desktopFlatLayout` | `Boolean` | `false` | Desktop flat layout (experimental); applied only while `developerTools` is on, see [navigation.md](client/navigation.md#DesktopScreen) |
| `terminalAlwaysVisible` | `Boolean` | `false` | Floating terminal window (desktop) |
#### Database (lines 188-208)
diff --git a/plans/2026-10-13-desktop-flat-layout.md b/plans/2026-10-13-desktop-flat-layout.md
new file mode 100644
index 0000000000..7cde5bf16e
--- /dev/null
+++ b/plans/2026-10-13-desktop-flat-layout.md
@@ -0,0 +1,124 @@
+# Desktop: experimental flat (Slack/Discord-like) layout
+
+## Problem
+
+Desktop chats use the mobile bubble layout: sent messages on the right, received on the left, each message in its own bubble with a tail. On a wide window, in groups and in chats with bots, this wastes horizontal space and is harder to scan than the flat row layout of Slack or Discord. Profiles and chat lists (tags) are reachable only through the user picker and the tag chip row above the chat list.
+
+## Solution summary
+
+A desktop-only developer option, "Flat layout (experimental)", switches the whole desktop window to a flat layout for evaluation:
+
+- message rows without bubbles, with the sender avatar, name, role and time shown once per group of messages;
+- a hover action bar on each message: 3 quick reactions, add reaction, reply, forward, and More (the existing context menu);
+- a rail at the far left with user avatars, "All", preset and user lists, "Add list" and settings;
+- compact one-line chat list rows.
+
+Existing theme colors only. No change on Android, and no change on desktop while the option is off.
+
+Out of scope: a thread side panel. It needs core work (see Roadmap).
+
+## Technical design
+
+Paths are relative to `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/` unless stated otherwise.
+
+### Mode flag
+
+- `AppPreferences.desktopFlatLayout` (key `DesktopFlatLayout`, default `false`) in `model/SimpleXAPI.kt`.
+- Toggle in `DeveloperView`, inside the desktop-only block of developer options; string `desktop_flat_layout`.
+- `App.kt`: `LocalFlatLayout = staticCompositionLocalOf { false }` and `desktopFlatLayoutEnabled = appPlatform.isDesktop && developerTools && desktopFlatLayout`. `DesktopScreen` provides `LocalFlatLayout` and its body moved to the private `DesktopScreenLayout`. All columns and `ModalManager.fullscreen` are inside the provider, so end-panel chats (reports, member support) are covered.
+- `Appearance.kt` provides `LocalFlatLayout = false` around the message-shape preview so roundness and tail settings stay previewable.
+
+Views read `LocalFlatLayout.current` only where the bubble look or the list chrome is decided; no existing view is forked.
+
+### Bubble chrome off
+
+- `ChatItemView.kt`: `chatItemTailEnabled()` = tail preference `&& !LocalFlatLayout.current`, used by `chatItemOffset`, `clipChatItem` and `FramedItemView`, so `shapeStyle` never draws a tail.
+- `ChatItemView`: alignment is start for all items, the bubble click has no indication, voice-only items render as received, and `GoToItemButton` is always on the right.
+- `FramedItemView`: transparent background.
+- `ChatItemsList` passes `showTimestamp = itemSeparation.timestamp && !flat`; the time moves to the row header or the hover gutter.
+
+The 3 remaining direct reads of `chatItemTail` in `ChatView.kt` are used only by the bubble branches of `ChatViewListItem`, which the flat layout does not reach.
+
+### Flat rows (`views/chat/item/FlatChatItemView.kt`)
+
+- `CIContent.hasFlatSender`: exhaustive `when`, true for content with a sender (messages, deleted/moderated/blocked, calls, errors, group invitations, invalid JSON), false for events, feature/preference changes, E2EE info and the chat banner.
+- `flatItemHasHeader(item, older)`: pure function. True when the item has a sender and the older item is missing, has no sender, is on a different date, or `getItemSeparationLargeGap(item, older)` is true (different sender or direction, or 60s or more apart). `getItemSeparationLargeGap` in `ChatView.kt` became `internal`. The rule does not use `previousItemSeparationLargeGap`, which is false for the oldest loaded item.
+- `FlatChatItemRow`: full-width hoverable row that sets `hoveredItemId`, tints its background on hover, has a 56dp gutter (avatar on header rows, time on hover otherwise) and a column with the sender header and the item content.
+- Sender: `GroupRcv` member (role above `Member`), `ChannelRcv` channel with `channel_role_label`, `GroupSnd` the user's membership (incognito-correct), `DirectRcv` the contact, other sent and local directions the current user or "Incognito". Avatar click opens member info or chat info for received items.
+- `ChatView.kt`: `ChatItemsList` computes `flatHeader = LocalFlatLayout.current && flatItemHasHeader(item, merged.oldest().nextItem)` and passes it through `ChatViewListItem`, whose inner wrapper renders `FlatChatItemRow` instead of the Group/Direct branches. The selection checkbox and selection click overlay are shared; the selection offset also applies to sent items.
+
+### Hover action bar
+
+Refactor of the context menu in `ChatItemView.kt`, no behavior change:
+
+- `data class ItemQuickActions(react, reply, forward)` and `itemQuickActions(cInfo, cItem, live)`: all false without message content, for temporary items and reports; otherwise the existing menu conditions.
+- `availableReactions(cItem)`, top-level `MsgReactionsMenu(cInfo, cItem, setReaction, showMenu)`, and `replyToItem(composeState, cItem, useLinkPreviews)`. The menu and swipe to reply in `ChatView.kt` use them; swipe keeps its `TEMP_LIVE_CHAT_ITEM_ID` guard.
+
+`FlatHoverBar` (in `FlatChatItemView.kt`) is a bordered `Surface` with the first 3 available reactions, an add-reaction button opening `MsgReactionsMenu` in a `DefaultDropdownMenu`, reply, forward, and More, which sets the existing `showMenu`. `ChatItemView` renders it as an unbounded overlay at its top end when the layout is flat, the item is not a preview or the live dummy, has a sender, selection is off, and the item is hovered or its reaction picker is open. Strings `add_reaction`, `more_actions`.
+
+### Rail (`views/chatlist/DesktopRail.kt`)
+
+- `DESKTOP_RAIL_WIDTH = 64.dp`, scaled by `fontSizeSqrtMultiplier`.
+- Users: active and non-hidden users by `activeOrder`, active-user mark, `userUnreadBadge` for others. Click on an inactive user calls `switchToUser`, extracted from `UserPicker.onUserClicked`; click on the active user opens `UserPicker`. Disabled while chat is not running and no remote host is connected.
+- Lists: "All", preset tags with chats in `PresetTagKind` order (icons and labels from `presetTagLabel`, now `internal`), user tags with unread dot and `TagsDropdownMenu` on long-press or right-click, "Add list" opening `TagListEditor`. A private `toggleFilter` sets `chatModel.activeChatTagFilter`.
+- Settings at the bottom opens `SettingsView` in `ModalManager.start`.
+- `DesktopScreenLayout` draws the rail first and shifts every start offset by the rail width. `ModalManager` adds the rail width when it expands the window for an end modal.
+- `ChatListView`: `TagsView` returns early and the toolbar user button is hidden in the flat layout (the settings menu button for the no-user case stays).
+
+### Compact chat list
+
+- `ChatPreviewView`: the unread/mention/muted/favorite block became the local composable `chatPreviewUnreadIndicator()`, used by both layouts. In the flat layout the view renders one row: `ChatInfoImage` at 32dp, `chatPreviewTitle()`, the indicator and `chatStatusImage()`.
+- `views/chatlist/ChatListNavLinkView.desktop.kt` (desktopMain): 4dp vertical padding instead of 8dp, no dividers.
+
+### Deviations from the approved plan
+
+- The rail widens the start pane by 64dp instead of keeping the total start width and shrinking the chat list: the desktop `UserPicker` computes its item spacing from `DEFAULT_START_MODAL_WIDTH` (388dp, `views/chatlist/UserPicker.desktop.kt` in desktopMain).
+- Tag toggling and the preset tag list were not extracted from `ChatListView`; the rail has its own `toggleFilter` and filter. There is no `ChatTagFilterTest`.
+- The user switch helper is named `switchToUser`, not `changeActiveUser`, which is the existing `ChatController` API it calls.
+- `ChatItemView` has no new `showMenu` parameter: the hover bar is rendered inside `ChatItemView` and uses its own `showMenu`.
+- `flatItemHasHeader` uses `hasFlatSender` instead of `mergeCategory` and message-content checks.
+- Rail items have text labels under the icons instead of tooltips.
+- Chat list rows have no added hover background.
+
+### Threat model
+
+Presentation-only change. No new data is read, stored or sent; actions in the hover bar call the same functions with the same conditions as the context menu, so a malicious contact or member cannot reach any action that the menu would not offer.
+
+## Implementation steps
+
+Each step is one commit.
+
+1. `desktop: add experimental flat layout toggle`: preference, Developer options toggle, string, `LocalFlatLayout`, `desktopFlatLayoutEnabled`, `DesktopScreen` provider.
+2. `ui: extract item reply, reaction and forward checks`: `ItemQuickActions`, `itemQuickActions`, `availableReactions`, top-level `MsgReactionsMenu`, `replyToItem`; context menu and swipe to reply use them; `ItemQuickActionsTest`.
+3. `desktop: drop bubble chrome in flat layout`: `chatItemTailEnabled`, alignment, indication, voice and `GoToItemButton` changes, transparent `FramedItemView` background, Appearance preview override.
+4. `desktop: render chat items as flat rows`: `FlatChatItemView.kt` (`hasFlatSender`, `flatItemHasHeader`, `FlatChatItemRow`, sender header and avatar), `ChatView.kt` branch and `flatHeader`, `getItemSeparationLargeGap` internal, no inline timestamp; `FlatChatItemViewTest`.
+5. `desktop: add hover action bar to flat rows`: `FlatHoverBar` and its overlay in `ChatItemView`; strings `add_reaction`, `more_actions`.
+6. `desktop: add profile and tag rail to flat layout`: `DesktopRail.kt`, `DesktopScreenLayout` offsets, `ModalManager` window expansion, `switchToUser`, `presetTagLabel` internal, hidden `TagsView` and toolbar user button.
+7. `desktop: compact chat list rows in flat layout`: `chatPreviewUnreadIndicator`, compact `ChatPreviewView` row, `ChatListNavLinkLayout` padding and dividers.
+8. `desktop: skip flat header check when layout is off`: `flatHeader` is computed only when `LocalFlatLayout.current` is true.
+9. `desktop: close start modals before rail actions`: settings, "Add list" and the active user avatar close open start-pane modals first, so repeated clicks do not stack modals.
+10. `docs: document desktop flat layout`: spec, product, CODE.md Document Map, this plan.
+
+Tests in `common/src/commonTest/kotlin/chat/simplex/app/`:
+
+- `ItemQuickActionsTest`: allActionsForRegularMessage, reactDisabledWhenFeatureOff, reactDisabledAfterThreeUserReactions, replyHiddenForLocalNote, replyAndForwardHiddenWhenLive, noQuickActionsForReport, noQuickActionsForLiveDummy, noQuickActionsForDeletedItem, noQuickActionsForEventItem, availableReactionsExcludeUserReacted.
+- `FlatChatItemViewTest`: headerShownForOldestItem, headerHiddenForSameSenderWithinMinute, headerShownAfterMinuteGap, headerShownForDifferentGroupMember, headerShownWhenDirectionChanges, headerShownAfterEventItem, headerShownOnDateChangeWithinMinute, noHeaderForEventItem.
+
+## Affected product and spec documents
+
+- `apps/multiplatform/product/views/chat.md`: Desktop Flat Layout section.
+- `apps/multiplatform/product/views/chat-list.md`: platform layout row, Desktop Flat Layout section (rail, compact rows), `DesktopRail.kt` source.
+- `apps/multiplatform/product/views/settings.md`: Flat layout developer option.
+- `apps/multiplatform/product/README.md`: Desktop Features row.
+- `apps/multiplatform/product/concepts.md`: PC32 Desktop Flat Layout.
+- `apps/multiplatform/spec/client/chat-view.md`: flat adjustments in ChatItemView, quick action helpers in Context Menu Actions, section 10 Flat Layout, `[REC]` thread side panel.
+- `apps/multiplatform/spec/client/chat-list.md`: compact row, nav link layout, `presetTagLabel`, `switchToUser`, section 11 Desktop Rail.
+- `apps/multiplatform/spec/client/navigation.md`: `DesktopScreen` flat layout flag, rail column and offsets.
+- `apps/multiplatform/spec/state.md`: `desktopFlatLayout` preference.
+- `apps/multiplatform/spec/impact.md`: PC32, new files, changed rows.
+- `apps/multiplatform/spec/README.md`: document index descriptions, concept count.
+- `apps/multiplatform/CODE.md`: Document Map rows for `FlatChatItemView.kt` and `DesktopRail.kt`.
+
+## Roadmap
+
+Thread side panel (replies to a message in the end column) needs core support: `getChatItemIDs` (`src/Simplex/Chat/Store/Messages.hs`) has no filter for items quoting a given message, `chat_items.quoted_shared_msg_id` has no index, and there is no reply count. The client would add a `SecondaryContextFilter` variant and open the panel via `ModalManager.end`, as `showGroupReportsView` does.