17 KiB
SimpleX Chat iOS -- Message Composition Module
Technical specification for the compose bar, attachment types, reply/edit/forward modes, voice recording, and mentions.
Related specs: Chat View | File Transfer | API Reference | README Related product: Chat View
Source: ComposeView.swift
Table of Contents
- Overview
- ComposeView
- ComposeState Machine
- Attachment Types
- Reply Mode
- Edit Mode
- Forward Mode
- Live Messages
- Voice Recording
- Link Previews
- Mentions
1. Overview
The compose module handles all message creation, editing, and forwarding. It sits at the bottom of ChatView and adapts its UI based on the current compose state.
ComposeView
├── Context banner (reply quote / edit indicator / forward indicator)
├── Attachment preview (image / video / file / voice waveform)
├── Text input (NativeTextEditor with markdown support)
├── Action buttons
│ ├── Attachment menu (camera, photo library, file picker)
│ ├── Voice record button (hold or toggle)
│ └── Send button (or live message indicator)
└── Link preview (auto-generated when URL detected)
2. ComposeView (struct ComposeView: View)
File: Shared/Views/Chat/ComposeMessage/ComposeView.swift
Layout
- Fixed at the bottom of ChatView
- Expands vertically as text input grows (up to a maximum height)
- Context banner appears above the text field when in reply/edit/forward mode
- Attachment preview appears between context banner and text field
Key Properties
- Reads
ChatModel.shared.draft/draftChatIdfor persisted drafts - Manages its own internal compose state
- Coordinates with
ChatViewfor scroll-to-bottom behavior on send
Send Flow
- User taps send button
- ComposeView constructs
[ComposedMessage]from current state - Calls
apiSendMessages(type:, id:, scope:, live:, ttl:, composedMessages:) - On success: clears compose state, scrolls to bottom
- On failure: shows error alert, preserves compose state
Key Functions
| Function | Line | Description |
|---|---|---|
body |
L360 | Main view body |
sendMessageView() |
L683 | Builds the send-message UI |
sendMessage(ttl:) |
L1091 | Entry point: initiates send |
sendMessageAsync() |
L1099 | Async send implementation |
clearState(live:) |
L1446 | Resets compose state after send |
addMediaContent() |
L882 | Adds media attachment |
connectCheckLinkPreview() |
L856 | Checks link preview before connect |
commandsButton() |
L744 | Builds commands menu button |
Draft Persistence
| Function | Line | Description |
|---|---|---|
saveCurrentDraft() |
L1459 | Saves compose state to ChatModel.draft |
clearCurrentDraft() |
L1464 | Clears persisted draft |
- When navigating away from a chat, compose state is saved to
ChatModel.draft/ChatModel.draftChatId - When returning to the same chat, draft is restored
- Drafts are not persisted across app restarts
3. ComposeState Machine (struct ComposeState)
The compose bar operates as a state machine with these primary states:
┌──────────┐
│ .empty │ ← initial / after send
└─────┬────┘
│ user types / attaches / quotes
v
┌─────────────────────────────────────┐
│ │
┌────▼────┐ ┌──────────────┐ ┌──────────▼───┐
│ .text │ │ .mediaPending │ │ .voiceRecording │
└─────────┘ └──────────────┘ └───────────────┘
│ │
│ long-press reply│ tap edit
v v
┌──────────┐ ┌──────────┐ ┌───────────┐
│ .replying │ │ .editing │ │ .forwarding│
└──────────┘ └──────────┘ └───────────┘
Supporting Types
| Type | Line | Description |
|---|---|---|
enum ComposePreview |
L10 | Preview variants (image, voice, file, etc.) |
enum ComposeContextItem |
L18 | Context item for reply/quote |
enum VoiceMessageRecordingState |
L26 | Recording state enum |
struct ComposeState |
L40 | Full compose state struct |
copy() |
L93 | Copy compose state with overrides |
mentionMemberName() |
L113 | Format mention display name |
chatItemPreview() |
L260 | Build preview from chat item |
enum UploadContent |
L280 | Upload content variants |
States
| State | Description | UI |
|---|---|---|
.empty |
No input, no attachments | Placeholder text, attachment button |
.text |
Text entered, no attachments | Send button visible |
.mediaPending |
Media/file selected, optionally with text | Preview visible, send button |
.voiceRecording |
Voice recording in progress | Waveform animation, stop/send |
.replying |
Replying to a specific message | Quote banner above input |
.editing |
Editing a previously sent message | Edit banner, pre-filled text |
.forwarding |
Forwarding selected messages | Forward banner, item previews |
Transitions
| From | Trigger | To |
|---|---|---|
.empty |
User types text | .text |
.empty |
User selects media | .mediaPending |
.empty |
User holds voice button | .voiceRecording |
.empty |
User long-presses message "Reply" | .replying |
.empty |
User long-presses message "Edit" | .editing |
.empty |
User selects "Forward" | .forwarding |
| Any | User taps send | .empty |
| Any | User taps cancel (X) | .empty |
4. Attachment Types
ComposeImageView
File: ComposeImageView.swift (struct at L12)
Preview of selected image(s) before sending. Shows thumbnail with remove button. Images are compressed to MAX_IMAGE_SIZE (255KB) before sending.
ComposeFileView
File: ComposeFileView.swift (struct at L11)
Preview of selected file or video. Shows filename, size, and remove button. Videos show a thumbnail frame.
ComposeVoiceView
File: ComposeVoiceView.swift (struct at L26)
Voice message recording/playback preview. Shows waveform visualization, duration, and play/delete buttons.
Attachment Menu Options
| Option | Picker | Max Size | Transfer Method |
|---|---|---|---|
| Camera photo | UIImagePickerController | Compressed to 255KB | Inline in SMP message |
| Photo library | PHPickerViewController | Compressed to 255KB | Inline or XFTP |
| Video | PHPickerViewController | Up to 1GB | XFTP |
| File | UIDocumentPickerViewController | Up to 1GB | XFTP |
5. Reply Mode
Activated via long-press context menu "Reply" on any message.
UI
- Quote banner above text input showing original message preview
- X button to cancel reply
- Original message reference stored in compose state
API
- Reply is sent as part of
ComposedMessagewithquotedItemIdparameter apiSendMessages(composedMessages: [ComposedMessage(quotedItemId: originalItem.id, ...)])
6. Edit Mode
Activated via long-press context menu "Edit" on own sent messages (within the edit window).
UI
- Edit banner above text input with pencil icon
- Text field pre-filled with original message content
- Send button changes to "Save" / checkmark
API
apiUpdateChatItem(type:, id:, scope:, itemId:, updatedMessage:, live:)- Response:
ChatResponse1.chatItemUpdated(user:, chatItem:)
Constraints
- Only own sent messages can be edited
- Edit is available within a server-defined time window
- Edited messages show a pencil indicator in
CIMetaView - Edit history is visible in
ChatItemInfoView
7. Forward Mode
Activated via long-press context menu "Forward" or via multi-select toolbar.
Flow
- User selects "Forward" on message(s)
apiPlanForwardChatItems(fromChatType:, fromChatId:, fromScope:, itemIds:)is called to plan- Response:
ChatResponse1.forwardPlan(user:, chatItemIds:, forwardConfirmation:) - User selects destination chat
apiForwardChatItems(toChatType:, toChatId:, toScope:, fromChatType:, fromChatId:, fromScope:, itemIds:, ttl:)executes the forward- Forwarded messages appear with a forwarded indicator
ForwardConfirmation
The plan response may include a forwardConfirmation requiring user confirmation (e.g., forwarding to a less secure chat).
8. Live Messages (struct LiveMessage)
Optional feature where the recipient sees typing in real-time.
How It Works
- User enables live message mode (lightning icon)
- As user types,
apiSendMessages(live: true)is called repeatedly - Each call sends the current text as an update to the same message
- Recipient sees the message being composed in real-time
- Final send marks the message as complete
Key Functions
| Function | Line | Description |
|---|---|---|
sendLiveMessage() |
L910 | Initiates a live message |
updateLiveMessage() |
L927 | Sends incremental live update |
liveMessageToSend() |
L945 | Determines text diff to send |
truncateToWords() |
L950 | Truncates text at word boundary |
API
- Initial:
apiSendMessages(live: true, composedMessages: [...])-- creates live message - Updates:
apiUpdateChatItem(live: true)-- updates content as user types - Final:
apiUpdateChatItem(live: false)-- marks as complete
9. Voice Recording
Recording Flow
- User taps (or holds) the microphone button
AVAudioRecorderstarts recording in compressed format- Waveform visualization shows real-time audio levels
- User taps stop (or releases hold) to finish recording
- Preview with playback shown in compose area
- User taps send to deliver
Voice Functions
| Function | Line | Description |
|---|---|---|
startVoiceMessageRecording() |
L1365 | Begins audio recording |
finishVoiceMessageRecording() |
L1405 | Stops recording, shows preview |
allowVoiceMessagesToContact() |
L1415 | Enables voice messages for contact |
updateComposeVMRFinished() |
L1422 | Updates state after recording finishes |
cancelCurrentVoiceRecording() |
L1434 | Cancels in-progress recording |
cancelVoiceMessageRecording() |
L1440 | Cancels and cleans up recording file |
Constraints
- Maximum duration:
MAX_VOICE_MESSAGE_LENGTH = 300seconds (5 minutes) - Auto-receive threshold:
MAX_VOICE_SIZE_AUTO_RCV = 522,240bytes (510KB) - Compressed audio format for small file sizes
Audio Management
AudioRecorder(Shared/Model/AudioRecPlay.swiftL14) manages recording and playbackChatModel.stopPreviousRecPlaycoordinates exclusive audio playback (only one audio source plays at a time)
10. Link Previews (ComposeLinkView)
File: ComposeLinkView.swift (struct at L13)
Auto-Detection
- As user types, URLs in the text are detected
- When a URL is found,
ComposeLinkViewfetches OpenGraph metadata - Preview card shows title, description, and thumbnail image
Link Preview Functions
| Function | Line | Description |
|---|---|---|
showLinkPreview() |
L1471 | Triggers link preview loading |
getMessageLinks() |
L1490 | Extracts URLs from formatted text |
isSimplexLink() |
L1501 | Checks if URL is a SimpleX link |
cancelLinkPreview() |
L1505 | Cancels pending preview |
loadLinkPreview() |
L1516 | Fetches OpenGraph metadata |
resetLinkPreview() |
L1533 | Resets preview state |
Behavior
- Only the first URL in the message generates a preview
- Preview can be dismissed by the user
- Link preview data is included in the
ComposedMessagesent to the core - Toggle in privacy settings to disable auto-preview generation
11. Mentions
In group chats, typing @ triggers member name autocomplete:
Flow
- User types
@in the text field - Autocomplete dropdown appears with matching group members
- User selects a member
@displayNameis inserted into the text- Mention is rendered with special formatting in the sent message
Data
- Group members loaded from
ChatModel.groupMembers - Mention metadata included in
ComposedMessage
Source Files
| File | Path | Struct/Class | Line |
|---|---|---|---|
| Compose view | ComposeView.swift |
ComposeView |
L321 |
| Send message UI | SendMessageView.swift |
SendMessageView |
L14 |
| Image preview | ComposeImageView.swift |
ComposeImageView |
L12 |
| File preview | ComposeFileView.swift |
ComposeFileView |
L11 |
| Voice preview | ComposeVoiceView.swift |
ComposeVoiceView |
L26 |
| Link preview | ComposeLinkView.swift |
ComposeLinkView |
L13 |
| Audio recording | AudioRecPlay.swift |
AudioRecorder |
L14 |