# Message Composition Specification Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt`, `SendMsgView.kt` --- ## Table of Contents 1. [Overview](#1-overview) 2. [ComposeState Data Class](#2-composestate-data-class) 3. [ComposePreview Sealed Class](#3-composepreview-sealed-class) 4. [ComposeContextItem Sealed Class](#4-composecontextitem-sealed-class) 5. [SendMsgView](#5-sendmsgview) 6. [Attachment Handling](#6-attachment-handling) 7. [Draft Persistence](#7-draft-persistence) 8. [Source Files](#8-source-files) --- ## Executive Summary Message composition in SimpleX Chat is managed by `ComposeView` (line ~345 in `ComposeView.kt`) backed by the serializable `ComposeState` data class. The compose area supports text input, link previews, media/file/voice attachments, reply/edit/forward contexts, live (streaming) messages, member @mentions, message reports, and timed (disappearing) messages. The `SendMsgView` composable (in `SendMsgView.kt`) provides the text field and action buttons. Draft state persists across chat switches when the privacy preference is enabled. --- ## 1. Overview ``` ComposeView |-- contextItemView() | |-- ContextItemView (QuotedItem) [reply indicator] | |-- ContextItemView (EditingItem) [edit indicator] | |-- ContextItemView (ForwardingItems) [forward indicator] | +-- ContextItemView (ReportedItem) [report indicator] |-- ReportReasonView [report reason header] |-- MsgNotAllowedView [disabled send reason] |-- previewView() | |-- ComposeLinkView [link preview card] | |-- ComposeImageView [media thumbnails] | |-- ComposeVoiceView [voice recording waveform] | +-- ComposeFileView [file name display] |-- AttachmentAndCommandsButtons | |-- CommandsButton [bot commands "//"] | +-- AttachmentButton [paperclip icon] +-- SendMsgView |-- PlatformTextField [multiline text input] |-- DeleteTextButton [clear text, shown on long text] |-- SendMsgButton [arrow/check icon] |-- RecordVoiceView [microphone + hold-to-record] |-- StartLiveMessageButton [bolt icon] |-- CancelLiveMessageButton [cancel live] +-- TimedMessageDropdown [disappearing message timer] ``` --- ## 2. ComposeState Data Class **Location:** [`ComposeView.kt#L98`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L98) ```kotlin @Serializable data class ComposeState( val message: ComposeMessage = ComposeMessage(), val parsedMessage: List = emptyList(), val liveMessage: LiveMessage? = null, val preview: ComposePreview = ComposePreview.NoPreview, val contextItem: ComposeContextItem = ComposeContextItem.NoContextItem, val inProgress: Boolean = false, val progressByTimeout: Boolean = false, val useLinkPreviews: Boolean, val mentions: MentionedMembers = emptyMap() ) ``` ### Fields | Field | Type | Description | |---|---|---| | `message` | `ComposeMessage` | Current text and cursor selection (`TextRange`) | | `parsedMessage` | `List` | Markdown-parsed representation of message text | | `liveMessage` | `LiveMessage?` | Active live (streaming) message state | | `preview` | `ComposePreview` | Attachment preview (link, media, voice, file) | | `contextItem` | `ComposeContextItem` | Reply/edit/forward/report context | | `inProgress` | `Boolean` | Send operation in flight | | `progressByTimeout` | `Boolean` | Show spinner after 1-second send delay | | `useLinkPreviews` | `Boolean` | Link preview feature flag | | `mentions` | `MentionedMembers` | Map of mention display name to `CIMention` | ### Computed Properties | Property | Type | Description | |---|---|---| | `editing` | `Boolean` | True when `contextItem` is `EditingItem` | | `forwarding` | `Boolean` | True when `contextItem` is `ForwardingItems` | | `reporting` | `Boolean` | True when `contextItem` is `ReportedItem` | | `sendEnabled` | `() -> Boolean` | True when there is content to send and not in progress | | `linkPreviewAllowed` | `Boolean` | True when no media/voice/file preview is active | | `linkPreview` | `LinkPreview?` | Extracts link preview from `CLinkPreview` | | `attachmentDisabled` | `Boolean` | True when editing, forwarding, live, in-progress, or reporting | | `attachmentPreview` | `Boolean` | True when a file or media preview is showing | | `empty` | `Boolean` | True when no text, no preview, and no context item | | `whitespaceOnly` | `Boolean` | True when message text contains only whitespace | | `placeholder` | `String` | Input placeholder text (report reason text or default) | | `memberMentions` | `Map` | Extracted member ID map for API calls | ### ComposeMessage ```kotlin @Serializable data class ComposeMessage( val text: String = "", val selection: TextRange = TextRange.Zero ) ``` ### LiveMessage ```kotlin @Serializable data class LiveMessage( val chatItem: ChatItem, val typedMsg: String, val sentMsg: String, val sent: Boolean ) ``` Tracks a live (streaming) message: the associated `ChatItem`, the currently typed text, the last sent text, and whether the initial send has occurred. ### Serialization `ComposeState` is fully `@Serializable` with a custom `Saver` (line ~214) that uses `json.encodeToString`/`decodeFromString` for `rememberSaveable` persistence across configuration changes. --- ## 3. ComposePreview Sealed Class **Location:** [`ComposeView.kt#L52`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L52) ```kotlin sealed class ComposePreview { object NoPreview : ComposePreview() class CLinkPreview(val linkPreview: LinkPreview?) : ComposePreview() class MediaPreview(val images: List, val content: List) : ComposePreview() data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean) : ComposePreview() class FilePreview(val fileName: String, val uri: URI) : ComposePreview() } ``` | Variant | Fields | View | |---|---|---| | `NoPreview` | -- | Nothing shown | | `CLinkPreview` | `linkPreview: LinkPreview?` (null = loading) | `ComposeLinkView`: title, description, image thumbnail, cancel button | | `MediaPreview` | `images: List` (base64 thumbnails), `content: List` | `ComposeImageView`: horizontal thumbnail strip, cancel button | | `VoicePreview` | `voice: String` (file path), `durationMs: Int`, `finished: Boolean` | `ComposeVoiceView`: waveform visualization, duration, play/pause | | `FilePreview` | `fileName: String`, `uri: URI` | `ComposeFileView`: file icon, file name, cancel button | ### UploadContent Used within `MediaPreview` to track the source type: - `SimpleImage(uri: URI)` -- still image - `AnimatedImage(uri: URI)` -- GIF or animated WebP - `Video(uri: URI, duration: Int)` -- video with duration in seconds --- ## 4. ComposeContextItem Sealed Class **Location:** [`ComposeView.kt#L61`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L61) ```kotlin sealed class ComposeContextItem { object NoContextItem : ComposeContextItem() class QuotedItem(val chatItem: ChatItem) : ComposeContextItem() class EditingItem(val chatItem: ChatItem) : ComposeContextItem() class ForwardingItems(val chatItems: List, val fromChatInfo: ChatInfo) : ComposeContextItem() class ReportedItem(val chatItem: ChatItem, val reason: ReportReason) : ComposeContextItem() } ``` | Variant | Trigger | Compose Behavior | |---|---|---| | `NoContextItem` | Default state | Normal message composition | | `QuotedItem` | Swipe-to-reply or reply menu action | Shows quoted message indicator; sends with `quoted` parameter | | `EditingItem` | Edit menu action | Populates text field with existing message; send button becomes checkmark; calls `apiUpdateChatItem` | | `ForwardingItems` | Forward action from another chat | Shows forwarded items indicator; calls `apiForwardChatItems`; can include optional text message | | `ReportedItem` | Report menu action | Shows report indicator with reason; placeholder changes to reason text; calls `apiReportMessage` | ### Context Item View `contextItemView()` (line ~1098 in `ComposeView.kt`) renders the active context as a dismissible bar above the text input: - Icon: reply (ic_reply), edit (ic_edit_filled), forward (ic_forward), report (ic_flag) - Content: quoted message preview text with sender name - Close button: resets `contextItem` to `NoContextItem` (or `clearState()` for editing) --- ## 5. SendMsgView **Location:** [`SendMsgView.kt#L36`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt#L36) ```kotlin fun SendMsgView( composeState: MutableState, showVoiceRecordIcon: Boolean, recState: MutableState, isDirectChat: Boolean, liveMessageAlertShown: SharedPreference, sendMsgEnabled: Boolean, userCantSendReason: Pair?, sendButtonEnabled: Boolean, sendToConnect: (() -> Unit)?, hideSendButton: Boolean, nextConnect: Boolean, needToAllowVoiceToContact: Boolean, allowedVoiceByPrefs: Boolean, sendButtonColor: Color, allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean, customDisappearingMessageTimePref: SharedPreference?, placeholder: String, sendMessage: (Int?) -> Unit, sendLiveMessage: (suspend () -> Unit)?, updateLiveMessage: (suspend () -> Unit)?, cancelLiveMessage: (() -> Unit)?, editPrevMessage: () -> Unit, onFilesPasted: (List) -> Unit, onMessageChange: (ComposeMessage) -> Unit, textStyle: MutableState, focusRequester: FocusRequester? ) ``` ### Layout The view is a `Box` containing: 1. **PlatformTextField:** Multiline text input (platform-specific `expect`). Handles text changes via `onMessageChange`, up-arrow to `editPrevMessage`, file paste via `onFilesPasted`, and Enter to send. 2. **DeleteTextButton:** Shown when text is long; clears the field. 3. **Action area** (bottom-right, stacked): - **Progress indicator:** Shown when `progressByTimeout` is true. - **Report confirm button:** Checkmark icon when context is `ReportedItem`. - **Voice record button:** Shown when message is empty, not editing/forwarding, no preview active. - `RecordVoiceView`: Hold-to-record with waveform display. - `DisallowedVoiceButton`: Shown when voice is disabled by preferences. - `VoiceButtonWithoutPermissionByPlatform`: Shown when microphone permission is not granted. - **Live message button:** Bolt icon, starts streaming message (calls `sendLiveMessage`). - **Send button:** Arrow icon (new message) or checkmark (editing/live). Long-press opens dropdown: - "Send live message" option - Timed message options (1min, 5min, 1hr, 8hr, 1day, 1week, 1month, custom) ### RecordingState ```kotlin sealed class RecordingState { object NotStarted : RecordingState() class Started(val filePath: String, val progressMs: Int) : RecordingState() class Finished(val filePath: String, val durationMs: Int) : RecordingState() } ``` Voice recording of 300ms or less is auto-cancelled. ### Disabled State When `sendMsgEnabled` is false (e.g., contact not ready, group permissions), an overlay covers the text field. If `userCantSendReason` is provided, tapping the overlay shows an alert explaining why sending is disabled. --- ## 6. Attachment Handling ### Attachment Selection The `AttachmentSelection` composable (line ~263 in `ComposeView.kt`) is an `expect` function with platform-specific implementations: **Android:** - Camera launcher (image capture) - Gallery launcher (image/video picker, multi-select) - File picker (any file type) **Desktop:** - File chooser dialog (filters for images or all files) ### ChooseAttachmentView Bottom sheet (`ModalBottomSheetLayout`) presenting attachment type options: | Option | Result | |---|---| | Camera (Android) | Launches camera intent; result processed as `SimpleImage` | | Gallery | Launches media picker; results processed via `processPickedMedia` | | File | Launches file picker; result processed via `processPickedFile` | ### File Processing **`processPickedFile`** (line ~281): 1. Checks file size against `maxFileSize` (XFTP limit). 2. Extracts file name from URI. 3. Sets `ComposePreview.FilePreview` on compose state. **`processPickedMedia`** (line ~300): 1. For each URI, determines type (image, animated image, video). 2. Images: Gets bitmap, creates `SimpleImage` or `AnimatedImage` upload content. 3. Videos: Extracts thumbnail and duration, creates `Video` upload content. 4. Generates base64 preview thumbnails (max 14KB). 5. Sets `ComposePreview.MediaPreview` with thumbnails and content list. **`onFilesAttached`** (line ~270): Groups dropped/pasted files into images and non-images; routes to `processPickedMedia` or `processPickedFile`. ### Send Flow On send (line ~603, `sendMessageAsync`): 1. **Forwarding:** Calls `apiForwardChatItems`, then optionally sends a text message quoting the last forwarded item. 2. **Editing:** Calls `apiUpdateChatItem` with updated `MsgContent`. 3. **Reporting:** Calls `apiReportMessage` with reason and text. 4. **New message:** Iterates over `msgs` (one per media item or single for text/file/voice): - Saves file to app storage (or remote host). - For voice: encrypts if `privacyEncryptLocalFiles` is enabled. - Calls `apiSendMessages` or `apiCreateChatItems` (local notes). 5. On failure of the last message, restores compose state for retry. ### Link Preview When `privacyLinkPreviews` is enabled and the message contains a URL: 1. `showLinkPreview` extracts first non-SimpleX, non-cancelled link from parsed markdown. 2. Sets `ComposePreview.CLinkPreview(null)` (loading state). 3. After 1.5s debounce, calls `getLinkPreview(url)`. 4. On success, updates to `CLinkPreview(linkPreview)`. 5. Cancel button adds the URL to `cancelledLinks` set. --- ## 7. Draft Persistence **Location:** [`ComposeView.kt#L1230`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L1230) (`KeyChangeEffect(chatModel.chatId.value)`) Controlled by the `privacySaveLastDraft` preference. ### Save Behavior When the user navigates away from a chat (`chatModel.chatId.value` changes): | Compose State | Action | |---|---| | Live message active (text present or already sent) | Sends the live message immediately, clears draft | | In progress | Clears in-progress flag, clears previous draft | | Non-empty (text, preview, or context) | If `saveLastDraft` is true: saves `composeState.value` to `chatModel.draft.value` and `chatModel.draftChatId.value` | | Empty but draft exists for current chat | Restores draft from `chatModel.draft` | | Empty, no draft | Clears previous draft, deletes unused files | ### Restore Behavior When entering a chat (line ~132 in `ChatView.kt`): 1. Checks if `chatModel.draftChatId.value` matches the chat ID. 2. If match and draft is not null (and not a cross-chat forward), initializes `composeState` from the draft. 3. Otherwise, creates a fresh `ComposeState`. ### Desktop-specific On desktop, a `DisposableEffect` (line ~1256) saves the draft on dispose when forwarding content, since the `KeyChangeEffect` mechanism is Android-specific. ### Draft Display in Chat List When a draft exists for a chat, `ChatPreviewView` shows a pencil icon with the draft text instead of the last message preview. --- ## 8. Source Files | File | Description | |---|---| | `ComposeView.kt` | ComposeState, ComposePreview, ComposeContextItem, ComposeView composable, send logic, link preview, draft persistence | | `SendMsgView.kt` | Text input field, send/voice/live/timed buttons, recording state | | `ComposeFileView.kt` | File attachment preview (name, cancel) | | `ComposeImageView.kt` | Media attachment preview (thumbnails, cancel) | | `ComposeVoiceView.kt` | Voice recording preview (waveform, duration, play) | | `ContextItemView.kt` | Reply/edit/forward/report context bar | | `ComposeContextContactRequestActionsView.kt` | Contact request action buttons in compose area | | `ComposeContextGroupDirectInvitationActionsView.kt` | Group direct invitation compose actions | | `ComposeContextPendingMemberActionsView.kt` | Pending member compose actions | | `ComposeContextProfilePickerView.kt` | Profile picker in compose context | | `SelectableChatItemToolbars.kt` | Multi-select mode toolbar (delete, forward, moderate) |