docs: update desktop flat layout after review

This commit is contained in:
sh committed 2026-10-14 07:43:26 +00:00
1 parent abf7ce023c
commit 6102e4e0ed
8 files changed
+161 -89

No files matched your search

+12 -7
View File
@@ -63,6 +63,7 @@ Managed by `chatModel.userTags`, `chatModel.presetTags`, and `chatModel.activeCh
| Favorites | `FAVORITES` | Star | User-favorited chats |
| Contacts | `CONTACTS` | Person | Direct contacts and contact requests |
| Groups | `GROUPS` | Group | Group conversations (non-business) |
| Channels | `CHANNELS` | Updates | Channels |
| 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 |
@@ -126,18 +127,22 @@ 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 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 (closes `UserPicker` if open); click the active user to open `UserPicker` (address, preferences, remote hosts, other actions) |
| All | Clears the list filter; highlighted when no filter is active |
| Preset lists | Reports, favorites, contacts, groups, channels, 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` |
| Settings | Below a divider at the bottom; opens `SettingsView` |
Avatar clicks are disabled while chat is stopped, unless connected to a remote host. With no users, the rail shows only Settings.
The active item has a tinted background. Clicking a list, All, Add list or Settings closes any open start-panel screen first.
Profile switching from the rail is blocked while forwarding messages, sharing a chat link or the user address, and while a switch is in progress. Switching to another user is also blocked while chat is stopped, unless connected to a remote host; the active user's avatar still opens `UserPicker`. 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.
- The tag filter row (`TagsView`) is hidden and the rail replaces it. The toolbar user avatar button is hidden unless there is no active user and no remote host. With no users while a remote host is connected, the toolbar Settings menu button is hidden too (the rail has Settings).
- Chat rows are a single line: 28dp avatar (with the inactive icon for inactive contacts and groups), chat name, unread or mention badge (or muted/favorite icon), and status icon. Last message, draft and timestamp are not shown.
- The chat name is SemiBold when the chat has unread messages and secondary-colored when the chat is muted with no unread mentions.
- Contact requests and pending connections use the same 28dp image.
- Rows have smaller vertical padding and no dividers.
### Empty States
+13 -7
View File
@@ -131,13 +131,19 @@ Desktop only, enabled by "Flat layout (experimental)" in Developer options while
| 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 |
| Message rows | Full-width rows; sent and received messages are both aligned left. Text, media and deleted messages have no bubble, tail or background; group invitations and error items keep their rounded coloured card |
| Sender header | Avatar, name, role label and time on the first message of a group. The role label is the member role when above Member, or "channel" for channel posts (including an owner posting as the channel). Shown again after an event, a date change, a sender change (including an owner switching between posting as the channel and as themselves), or a gap of 60 seconds or more |
| Continuation rows | No avatar or name; the time appears small and right-aligned in the left gutter on hover |
| Avatar click | Received group message: member info; received direct or channel message: chat info. Own messages: none, except a post shown as the channel, which opens chat info |
| Row background | Light tint over the hovered row; a stronger primary tint on a highlighted message (for example after jumping to a quoted message) |
| Hover action bar | Inside the top-right of the hovered row, for items with message content only (events, calls, errors, group invitations and moderated items have no bar; marked-deleted messages show only More): 3 quick reactions, add reaction (remaining reactions), reply, forward, and More (opens the regular context menu). Each button follows the same rules as the context menu. Hidden in selection mode and when the pointer leaves the row, unless the reaction picker is open |
| Message time | Not shown inside the message. Sent messages keep their delivery status; edited, timer and other icons stay. A received message with nothing but the time shows no meta and reserves no space for it |
| Quotes | A thin bar on the start edge instead of a filled box; the quoted sender in semibold |
| Forwarded and report headers | No background |
| Deleted messages | Transparent, 14sp italic text, no time |
| Reactions | Rounded pills with the count always shown; your own reactions are tinted |
| Date separators | Date in a pill on a full-width line |
| Voice messages | Not mirrored for sent messages |
| 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.
+7 -7
View File
@@ -117,21 +117,21 @@ Common Module (commonMain)
| 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 |
| Android Application | [`SimplexApp.kt`](../android/src/main/java/chat/simplex/app/SimplexApp.kt#L44) | `class SimplexApp` | 44 |
| Android Activity | [`MainActivity.kt`](../android/src/main/java/chat/simplex/app/MainActivity.kt#L24) | `class MainActivity` | 24 |
| Desktop Entry | [`Main.kt`](../desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt#L22) | `fun main()` | 22 |
| Desktop App Window | [`DesktopApp.kt`](../common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt#L34) | `fun showApp()` | 34 |
| 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#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#L536) | `object ChatController` | 536 |
| Chat Model | [`ChatModel.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L86) | `object ChatModel` | 86 |
| Chat Model | [`ChatModel.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L140) | `object ChatModel` | 140 |
| 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 |
| Platform Interface | [`Platform.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt#L19) | `interface PlatformInterface` | 19 |
| 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 |
| 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 |
| Common Migrations | [`AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt#L46) | `fun runMigrations()` | 46 |
| 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 |
+18 -11
View File
@@ -31,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; hidden in desktop flat layout)
| |-- UserProfileButton (opens UserPicker; in desktop flat layout only when desktopNoUserNoRemote)
| |-- Title ("Your chats")
| |-- SubscriptionStatusIndicator
| +-- NewChatButton / StoppedIndicator
@@ -133,6 +133,7 @@ sealed class ActiveFilter {
| `FAVORITES` | Chats marked as favorite |
| `CONTACTS` | Direct (1:1) chats |
| `GROUPS` | Group chats |
| `CHANNELS` | Channels |
| `BUSINESS` | Business-type chats |
| `NOTES` | Local note folders |
@@ -199,7 +200,11 @@ When `chatModelDraftChatId` matches the chat ID, the preview shows a draft indic
### 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.
When `LocalFlatLayout.current` is true, `ChatPreviewView` renders a single line and returns early: `ChatInfoImage` at `COMPACT_CHAT_IMAGE_SIZE` (28dp, scaled by `fontSizeSqrtMultiplier`) with `chatPreviewImageOverlayIcon()` (the inactive icon at 12sp), `chatPreviewTitle()`, `chatPreviewUnreadIndicator()`, a 4sp spacer and `chatStatusImage()`. There is no last message, draft or timestamp. `chatPreviewUnreadIndicator()` is a local composable (unread count or mention badge, muted icon, or favorite star) shared with the regular layout; in the flat layout its `UnreadBadge` has no vertical offset.
Titles of direct and group chats use `titleStyle` and `titleWeight`: `body1`, SemiBold when the chat has unread messages and Normal otherwise (the regular layout keeps `h3` Bold). A title without its own color is drawn in `secondary` when notifications are off and there are no unread mentions. A `LaunchedEffect` on `chatModel.deletedChats` stops a playing voice message of the chat's last item when the chat is deleted, as the voice preview does in the regular row.
`ContactRequestView` and `ContactConnectionView` use `COMPACT_CHAT_IMAGE_SIZE` for their image in the flat layout.
---
@@ -248,7 +253,7 @@ 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.
In the desktop flat layout, `TagsView` renders nothing and the tags move to the rail (section 11); `ChatList` also omits the divider between `TagsView` and the connect-by-name row, so no double divider appears. [`presetTagLabel()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1610) is `internal` so the rail uses the same icons and labels.
<a id="TagListView"></a>
@@ -288,10 +293,12 @@ 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.
- **Desktop flat layout:** The toolbar shows the profile button only when `chatModel.desktopNoUserNoRemote`, and hides the Settings menu button shown when there are no users (the rail has Settings). 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.
[`profileSwitchAllowed(sharedContent)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt#L225-L226) is false while sharing a forward, a chat link or the user address; `ShareListView` and the rail use it to block profile switching.
### Content
| Section | Content |
@@ -334,7 +341,7 @@ Uses `AnimatedViewState` (`GONE`, `VISIBLE`, `HIDING`) with a `MutableStateFlow`
## 10. Crowdfunding Banner
**Location:** [`GetStakeBanner.kt#L31`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/GetStakeBanner.kt#L31)
**Location:** [`GetStakeBanner.kt#L35`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/GetStakeBanner.kt#L35)
```kotlin
fun GetStakeBanner(showDismiss: Boolean, onTap: () -> Unit, onDismiss: () -> Unit)
@@ -378,20 +385,20 @@ Tapping the card runs `openGetStake()`, which opens `GetStakeView(showFirstImage
## 11. Desktop Rail
**Location:** [`DesktopRail.kt#L38`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt#L38)
**Location:** [`DesktopRail.kt#L44`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt#L44)
```kotlin
fun DesktopRail(userPickerState: MutableStateFlow<AnimatedViewState>, 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.
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#L32) (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 | [`RailUsers`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt#L84-L127) | 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`, drawn outside the avatar clip. Clicking an inactive user hides `UserPicker` if visible and calls `switchToUser`; clicking the active user closes start modals and sets `userPickerState` to `VISIBLE`. All avatars are disabled while `profileSwitchAllowed(chatModel.sharedContent)` is false or users are being switched; inactive avatars are also 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#L130-L171) | "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#L57-L81) | Bottom item below a divider, 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.
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#L179-L224): icon (scaled by `fontSizeSqrtMultiplier`) or emoji above a one-line label, primary color when active, with a 40x32dp (scaled) `primary` tinted background behind the icon 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#L173-L176), which sets `chatModel.activeChatTagFilter` to the tag or back to `null`, as `TagsView` does. "All", tags, "Add list", Settings and the active user avatar call `ModalManager.start.closeModals()` first, because the rail stays clickable while a start-pane modal is open.
+62 -26
View File
@@ -37,8 +37,10 @@ ChatView
| | | |-- DateSeparator
| | | |-- MemberNameAndRole (group received messages)
| | | |-- MemberImage (group received messages)
| | | |-- FlatChatItemRow (desktop flat layout, replaces the two lines above)
| | | +-- ChatItemView (message type routing)
| | | |-- ChatItemView (message type routing)
| | | +-- FlatChatItemRow (desktop flat layout, replaces the three lines above)
| | | |-- sender header, hover bar
| | | +-- ChatItemView
| | |-- ChatBannerView (first item: chat profile banner)
| | +-- FloatingButtons (scroll-to-bottom, unread counter)
| |-- ComposeView (message composition area)
@@ -129,7 +131,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.
- **Flat layout (desktop):** When `LocalFlatLayout.current` is true, all chat types render through `FlatChatItemRow`, which wraps `ChatItemView`, instead of the branches above (see section 10). The selection offset applies to sent items too.
---
@@ -176,7 +178,21 @@ Long-press or right-click opens a dropdown menu with context-sensitive actions (
### 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.
`ChatItemView` takes `showMenu: MutableState<Boolean>` as a parameter, so the flat row's hover bar can open the same menu.
When `LocalFlatLayout.current` is true, `ChatItemView` and the item views it calls keep their content and drop the bubble look for text, media and deleted items:
- Items align to the start regardless of direction, `GoToItemButton` is always on the right, and the bubble click has no indication (the row draws the hover background).
- Voice items, with or without a caption, are not mirrored for sent messages.
- [`chatItemTailEnabled()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L1225) returns false, so `shapeStyle` never draws a tail.
- `FramedItemView` has a transparent background and 2dp vertical text padding (`FLAT_TEXT_VERTICAL_PADDING`). Quotes are drawn with `flatQuoteBar` (a thin bar on the start edge, RTL-aware) instead of a filled box, with the quoted sender in SemiBold. The forwarded and report header rows have no fill and 2dp vertical padding.
- `ChatItemsList` passes `showTimestamp = false`. When `flatMetaVisible()` is false (a received item whose meta would show only the time), `FramedItemView` and `CIMarkdownText` omit the meta and the space reserved for it; otherwise the meta shows its icons, and the delivery status only for sent items.
- `DeletedItemView` and `MarkedDeletedItemView` are transparent, with 2dp vertical padding and 14sp text (`FLAT_DELETED_FONT_SIZE`). `CIMetaView` returns early for deleted content when the timestamp is hidden.
- `CIEventView` uses the flat text padding (12dp start, 2dp vertical).
- `ChatItemReactions` renders pills (`flatReactionPill`, tinted and bordered in primary when the user reacted), always shows the count, uses 14sp emoji, and aligns with the message text.
- `DateSeparator` renders `FlatDateSeparator`: the date in a pill on a full-width divider.
Group invitations (`CIGroupInvitationView`) and error items (`IntegrityErrorItemView`, `CIRcvDecryptionError`) keep their rounded coloured card.
---
@@ -244,7 +260,7 @@ 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:
The React, Reply and Forward conditions are computed by [`itemQuickActions()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L802-L811), which returns [`ItemQuickActions`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L800). The menu evaluates it inside its `DefaultDropdownMenu` content, so only while the menu is open. All three are false for items without message content, temporary items (`id < 0`) and reports. Otherwise:
| Field | Condition |
|---|---|
@@ -256,9 +272,9 @@ 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` |
| [`availableReactions(cItem)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L813-L814) | `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#L816-L822) | 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#L825-L842) | Scrollable row of available reactions; a tap sets the reaction and closes `showMenu` |
---
@@ -334,7 +350,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` |
| `FlatChatItemView.kt` | Desktop flat layout: sender header rule, `FlatChatItemRow`, hover bar, flat styling helpers |
| `FramedItemView.kt` | Message bubble frame (quoted item, text, media) |
| `ImageFullScreenView.kt` | Fullscreen image gallery |
| `MarkdownFileView.kt` | Markdown file viewer, formatted or as text |
@@ -376,17 +392,25 @@ The `RelayStatusIndicator` composable in `AddChannelView.kt` renders `RsRejected
**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.
Experimental Slack/Discord-like rendering of the message list: full-width rows, avatar and sender name once per group of messages, and no bubbles for text, media and deleted items (see section 4 for the item-level changes). 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 |
| [`flatItemHasHeader(item, older)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L66-L73) | Whether a row shows the sender header. `older` is the next older item in the list |
| [`sameFlatSender(item, older)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L75-L86) | Private; same sender for header grouping |
| [`flatHoverBarVisible(...)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L90-L91) | Internal; hover bar visibility rule |
| [`CIContent.hasFlatSender`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L93-L127) | Private. 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#L131-L180) | Full-width hoverable row: a 56dp gutter (scaled by `fontSizeSqrtMultiplier`) with the avatar on header rows or, on hover, the time (10sp, end-aligned) on other rows; then a column with the header and the item content; then the `hoverBar` slot |
| [`FlatHoverBarSlot(...)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L183-L215) | Hover bar slot content: holds the reaction picker state, evaluates `itemQuickActions` and `flatHoverBarVisible`, renders the private `FlatHoverBar` |
| [`FlatHoverBar(...)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L304-L347) | Private; bordered action bar |
| [`flatMetaVisible(...)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L357-L361) | False when the meta would show only the time (status counted only for sent items) |
| [`Modifier.flatStartOffset(offset)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L363-L367) | Shifts the row by the selection offset and narrows it by the same amount |
| [`flatSelectionTopPadding(showHeader)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L410-L412) | Top padding that aligns the selection checkbox with the avatar on header rows and with the first text line otherwise |
| [`Modifier.flatQuoteBar()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L370-L378) | Quote bar on the start edge |
| [`Modifier.flatReactionPill(userReacted)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L381-L385) | Reaction pill background and border |
| [`FlatDateSeparator(text)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FlatChatItemView.kt#L388-L403) | Date pill on a divider |
### Header Rule
@@ -394,26 +418,38 @@ Experimental Slack/Discord-like rendering of the message list: full-width rows,
- 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.
- `sameFlatSender` is false;
- the items' `createdAt` are 60 seconds or more apart;
- the items' `itemTs` fall on different local dates.
The rule does not use `previousItemSeparationLargeGap`, which is false for the oldest loaded item.
`sameFlatSender` is true when both items have the same direction and, for `GroupRcv`, the same `groupMemberId`; for `GroupSnd`, the same `meta.showGroupAsSender` (an owner posting as the channel and as themselves are different senders). Any two `ChannelRcv` items match.
### 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` |
| `ChannelRcv` | Channel name | "channel" (`channel_role_label`) | `ChatInfoImage` | `showChatInfo` |
| `GroupSnd` with `showGroupAsSender` | Channel name | "channel" | `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`.
The header shows the name in SemiBold, the role, and `cItem.timestampText`.
### Row Background
| State | Background |
|---|---|
| Highlighted (`highlightedItems` contains the item, e.g. after jumping to a quoted message) | `primary` at 12% |
| Hovered | `onBackground` at 4% (`FLAT_SUBTLE_FILL_ALPHA`) |
| Otherwise | Transparent |
### 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.
`ChatItemsList` creates one `showMenu` state per item and passes it to both `FlatHoverBarSlot` (through `FlatChatItemRow`'s `hoverBar` slot) and `ChatItemViewShortHand`/`ChatItemView`, so the bar's More button opens the item's context menu. Flat rows do not write `hoveredItemId`.
The bar is drawn inside the row at its top end, and rows with message content have a minimum height (`FLAT_ROW_MIN_HEIGHT`) that fits it: the bar hides as soon as the pointer leaves the row. `flatHoverBarVisible` shows it when selection mode is off, the item has message content and is not the live dummy item, and the row is hovered or the reaction picker is open while reactions are available. A `LaunchedEffect` closes the picker when the bar is hidden or reactions become unavailable.
| Button | Shown when | Action |
|---|---|---|
@@ -421,12 +457,12 @@ Rendered by `ChatItemView` as an overlay at the top end of the item when the fla
| 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.
| More | Always | Opens the item's context menu (`showMenu`) |
### 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.
The selection checkbox and the full-row selection click overlay are shared with the bubble layout. In the flat layout the row is moved by `Modifier.flatStartOffset` for sent and received items alike, and the checkbox is top-aligned using `flatSelectionTopPadding` instead of being centered on the row.
[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.
### Roadmap
[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.
+1 -1
View File
@@ -203,7 +203,7 @@ In the flat layout the rail is added before the left column and every start offs
### 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 ~472). 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 ~467). Clicking it closes start modals and hides the UserPicker.
### CenterPartOfScreen
+7 -7
View File
@@ -99,11 +99,11 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
| `views/chatlist/TagListView.kt` | PC28 | Medium | Chat tag filter UI |
| `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/ShareListView.kt` | PC10, PC32 | Low | Share target list; `profileSwitchAllowed` shared with the rail |
| `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/ContactRequestView.kt` | PC12, PC32 | Medium | Incoming contact request row |
| `views/chatlist/ContactConnectionView.kt` | PC12, PC32 | Low | Pending connection row |
| `views/chatlist/ServersSummaryView.kt` | PC25 | Low | Server status summary |
### 1.4 Views — Chat & Messaging
@@ -144,16 +144,16 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
| `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/CIMetaView.kt` | PC2, PC3, PC8, PC32 | 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/CIEventView.kt` | PC3, PC14, PC16, PC32 | 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/DeletedItemView.kt` | PC7, PC32 | Low | Deleted message placeholder |
| `views/chat/item/MarkedDeletedItemView.kt` | PC7, PC32 | 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 |
+41 -23
View File
@@ -33,19 +33,22 @@ Views read `LocalFlatLayout.current` only where the bubble look or the list chro
### 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.
- `ChatItemView`: alignment is start for all items, the bubble click has no indication, voice items (with or without a caption) are not mirrored, and `GoToItemButton` is always on the right.
- `FramedItemView`: transparent background, 2dp vertical text padding, quotes with a start-edge bar (`flatQuoteBar`, RTL-aware) and a SemiBold sender, forwarded and report header rows without fill.
- `ChatItemsList` passes `showTimestamp = itemSeparation.timestamp && !flat`; the time moves to the row header or the hover gutter. `flatMetaVisible` drops the meta and its reserved space for received items that would show only the time; the delivery status is shown only for sent items.
- `DeletedItemView` and `MarkedDeletedItemView`: transparent, 2dp vertical padding, 14sp text; `CIMetaView` shows no time for deleted content. `CIEventView` uses the flat text padding.
- Reactions are pills (`flatReactionPill`) with the count always shown and 14sp emoji. `DateSeparator` renders `FlatDateSeparator`, a date pill on a divider.
- Group invitations and error items (`CIGroupInvitationView`, `IntegrityErrorItemView`, `CIRcvDecryptionError`) keep their rounded coloured card.
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.
- `CIContent.hasFlatSender` (private): 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 from a different sender (`sameFlatSender`: same direction, same `groupMemberId` for `GroupRcv`, same `showGroupAsSender` for `GroupSnd`), is 60s or more apart by `createdAt`, or is on a different local date. `getItemSeparationLargeGap` stays private in `ChatView.kt`. The rule does not use `previousItemSeparationLargeGap`, which is false for the oldest loaded item.
- `FlatChatItemRow`: full-width hoverable row with a `hoverBar` slot. Background: `primary` at 12% when highlighted, `onBackground` at 4% (`FLAT_SUBTLE_FILL_ALPHA`) when hovered. A 56dp gutter shows the avatar on header rows, or the time (10sp, end-aligned) on hover otherwise; a column holds the sender header and the item content. Rows with message content have a minimum height that fits the hover bar.
- Sender: `GroupRcv` member (role above `Member`), `ChannelRcv` and `GroupSnd` with `showGroupAsSender` the channel ("channel" label, group image, opens chat info), other `GroupSnd` the user's membership (incognito-correct), `DirectRcv` the contact, other sent and local directions the current user or "Incognito". Avatar click: member info for `GroupRcv`, chat info for `ChannelRcv`, `DirectRcv` and a post shown as the channel; none for other own messages.
- `ChatView.kt`: `ChatItemsList` computes `flatHeader = LocalFlatLayout.current && flatItemHasHeader(item, merged.oldest().nextItem)` and passes it through `ChatViewListItem`, whose inner wrapper renders `FlatChatItemRow` around `ChatItemViewShortHand` instead of the Group/Direct branches. The selection checkbox and selection click overlay are shared; `Modifier.flatStartOffset` moves the row by the selection offset for sent and received items.
### Hover action bar
@@ -54,20 +57,22 @@ 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`.
`itemQuickActions` is evaluated inside the menu's `DefaultDropdownMenu` content. `ChatItemView` and `ChatItemViewShortHand` take a `showMenu` parameter.
The private `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 `showMenu`. `ChatItemsList` creates one `showMenu` per item and passes it to `ChatItemViewShortHand` and to `FlatHoverBarSlot`, which `FlatChatItemRow` renders in its `hoverBar` slot at the row's top end. `flatHoverBarVisible` shows the bar when selection is off, the item has message content and is not the live dummy, and the row is hovered or the reaction picker is open while reactions are available; a `LaunchedEffect` closes the picker otherwise. Flat rows do not write `hoveredItemId`. 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`.
- Users: active and non-hidden users by `activeOrder`, active-user mark, `userUnreadBadge` for others, drawn outside the avatar clip. Click on an inactive user hides `UserPicker` and calls `switchToUser`, extracted from `UserPicker.onUserClicked`; click on the active user opens `UserPicker`. `profileSwitchAllowed`, extracted from `ShareListView`, blocks switching while sharing a forward, chat link or address; switching is also blocked while users are being switched. Inactive users are disabled while chat is not running and no remote host is connected; the active user still opens the picker.
- 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`. Active items get a 40x32dp tinted background; icons scale with `fontSizeSqrtMultiplier`. List, "All", "Add list" and Settings clicks close start modals first.
- Settings at the bottom, below a divider, 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).
- `ChatListView`: `TagsView` returns early; the toolbar shows the profile button only when `desktopNoUserNoRemote` and hides the no-user Settings menu button; the divider next to the connect-by-name row is dropped.
### 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()`.
- `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 28dp with a 12sp inactive overlay, `chatPreviewTitle()`, the indicator (badge without vertical offset), a spacer and `chatStatusImage()`. Direct and group titles use `titleStyle`/`titleWeight` (`body1`, SemiBold when unread) and the secondary color when muted without unread mentions. A `LaunchedEffect` stops a deleted chat's playing voice message. `ContactRequestView` and `ContactConnectionView` use the 28dp image.
- `views/chatlist/ChatListNavLinkView.desktop.kt` (desktopMain): 4dp vertical padding instead of 8dp, no dividers.
### Deviations from the approved plan
@@ -75,8 +80,7 @@ Refactor of the context menu in `ChatItemView.kt`, no behavior change:
- 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.
- `flatItemHasHeader` uses `hasFlatSender` and its own `sameFlatSender` instead of `mergeCategory`, message-content checks and `getItemSeparationLargeGap`.
- Rail items have text labels under the icons instead of tooltips.
- Chat list rows have no added hover background.
@@ -88,21 +92,35 @@ Presentation-only change. No new data is read, stored or sent; actions in the ho
Each step is one commit.
1. `desktop: add experimental flat layout toggle`: preference, Developer options toggle, string, `LocalFlatLayout`, `desktopFlatLayoutEnabled`, `DesktopScreen` provider.
1. `desktop: add experimental flat layout toggle`: preference, Developer options toggle, string, `LocalFlatLayout`, `DesktopScreen` provider (reading `developerTools && desktopFlatLayout` inline).
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.
6. `desktop: add profile and tag rail to flat layout`: `desktopFlatLayoutEnabled` (also read by `ModalManager`), `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.
11. `desktop: fix flat row header, hover bar and highlight`: `sameFlatSender` header rule, hover bar moved into `FlatChatItemRow` (`hoverBar` slot, `FlatHoverBarSlot`, `flatHoverBarVisible`, `showMenu` parameter), highlight tint, overlay removed from `ChatItemView`, `getItemSeparationLargeGap` private again.
12. `desktop: slack-style quotes, reactions and dates`: `flatQuoteBar`, `flatReactionPill`, `FlatDateSeparator`, flat padding in `FramedItemView`, `CIEventView` and deleted item views.
13. `desktop: fix rail badges, profile switching and lists`: badge outside the clip, `profileSwitchAllowed`, switching guards, rail active background and icon scaling, toolbar buttons, connect-by-name dividers, compact row avatar size, title style and muted color, compact images in contact request and connection rows.
14. `desktop: polish flat meta, deleted items and list`: `flatMetaVisible`, no time for deleted items, divider above Settings, compact row spacing.
15. `desktop: address flat layout re-review findings`: `flatStartOffset`, RTL quote bar, hover bar picker reset in `LaunchedEffect`, row min height only for message items, clickable channel avatar for own channel posts, status only for sent items, `FlatHoverBar` private, deleted-chat voice stop; more tests.
16. `desktop: align flat selection checkbox with avatar`: `flatSelectionTopPadding`; `ChatItemBox` content gets a `BoxScope` receiver.
17. `docs: update desktop flat layout after review`: spec, product, impact, README links, 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.
- `ItemQuickActionsTest` (11): allActionsForRegularMessage, reactDisabledWhenFeatureOff, reactDisabledAfterThreeUserReactions, replyHiddenForLocalNote, replyHiddenWhenContactCannotReceive, replyAndForwardHiddenWhenLive, noQuickActionsForReport, noQuickActionsForLiveDummy, noQuickActionsForDeletedItem, noQuickActionsForEventItem, availableReactionsExcludeUserReacted.
- `FlatChatItemViewTest` (20): headerShownForOldestItem, headerHiddenForSameSenderWithinMinute, headerShownAfterMinuteGap, headerShownForDifferentGroupMember, headerShownWhenDirectionChanges, headerShownAfterEventItem, headerShownOnDateChangeWithinMinute, headerShownWhenChannelPostFollowsMemberMessage, headerShownWhenOwnerSwitchesBetweenChannelAndOwnName, headerHiddenAfterDeletedMessageFromSameSender, headerGroupsLocalNotes, noHeaderForEventItem, hoverBarShownForHoveredMessage, hoverBarHiddenWhenRowNotHovered, hoverBarHiddenInSelectionMode, hoverBarHiddenForEventAndLiveDummyItems, hoverBarStaysOpenForReactionPickerUntilReactionsUnavailable, metaHiddenForPlainReceivedMessage, metaShownForEditedReceivedMessage, metaShownForSentMessage.
## Verification
- Read-only code reviews (initial review of rows, rail and docs; re-review of the fix commits; final docs and code check) and four headless screenshot renders (`ImageComposeScene` with real composables, not the running app) reviewed against Slack; their findings are addressed in steps 11 to 17.
- Deliberate choices kept after review:
- The hover bar stays inside the row rather than overlapping the row above, because the bar hides as soon as the pointer leaves the row; rows with message content get a minimum height so the bar fits.
- Sent messages keep their delivery status ticks, which Slack does not show.
## Affected product and spec documents
@@ -111,11 +129,11 @@ Tests in `common/src/commonTest/kotlin/chat/simplex/app/`:
- `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/chat-view.md`: flat adjustments in ChatItemView (item views, quotes, meta, reactions, date separators), quick action helpers in Context Menu Actions, section 10 Flat Layout (header rule, sender, row background, hover bar, selection), `[REC]` thread side panel.
- `apps/multiplatform/spec/client/chat-list.md`: compact row, nav link layout, `presetTagLabel`, `switchToUser`, `profileSwitchAllowed`, 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/impact.md`: PC32, new files, changed rows (including item, meta, event, deleted, contact request/connection and share list views).
- `apps/multiplatform/spec/README.md`: document index descriptions, concept count.
- `apps/multiplatform/CODE.md`: Document Map rows for `FlatChatItemView.kt` and `DesktopRail.kt`.