mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-31 20:36:19 +00:00
356 lines
17 KiB
Markdown
356 lines
17 KiB
Markdown
# 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](chat-view.md) | [File Transfer](../services/files.md) | [API Reference](../api.md) | [README](../README.md)
|
|
> Related product: [Chat View](../../product/views/chat.md)
|
|
|
|
**Source:** [`ComposeView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift)
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#1-overview)
|
|
2. [ComposeView](#2-composeview)
|
|
3. [ComposeState Machine](#3-composestate-machine)
|
|
4. [Attachment Types](#4-attachment-types)
|
|
5. [Reply Mode](#5-reply-mode)
|
|
6. [Edit Mode](#6-edit-mode)
|
|
7. [Forward Mode](#7-forward-mode)
|
|
8. [Live Messages](#8-live-messages)
|
|
9. [Voice Recording](#9-voice-recording)
|
|
10. [Link Previews](#10-link-previews)
|
|
11. [Mentions](#11-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](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L329) (`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` / `draftChatId` for persisted drafts
|
|
- Manages its own internal compose state
|
|
- Coordinates with `ChatView` for scroll-to-bottom behavior on send
|
|
|
|
### Send Flow
|
|
1. User taps send button
|
|
2. ComposeView constructs `[ComposedMessage]` from current state
|
|
3. Calls `apiSendMessages(type:, id:, scope:, live:, ttl:, composedMessages:)`
|
|
4. On success: clears compose state, scrolls to bottom
|
|
5. On failure: shows error alert, preserves compose state
|
|
|
|
### Key Functions
|
|
|
|
| Function | Line | Description |
|
|
|----------|------|-------------|
|
|
| [`body`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L369) | L360 | Main view body |
|
|
| [`sendMessageView()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L693) | L683 | Builds the send-message UI |
|
|
| [`sendMessage(ttl:)`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1106) | L1091 | Entry point: initiates send |
|
|
| [`sendMessageAsync()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1115) | L1099 | Async send implementation |
|
|
| [`clearState(live:)`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1467) | L1446 | Resets compose state after send |
|
|
| [`addMediaContent()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L893) | L882 | Adds media attachment |
|
|
| [`connectCheckLinkPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L866) | L856 | Checks link preview before connect |
|
|
| [`commandsButton()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L754) | L744 | Builds commands menu button |
|
|
|
|
### Draft Persistence
|
|
|
|
| Function | Line | Description |
|
|
|----------|------|-------------|
|
|
| [`saveCurrentDraft()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1481) | L1459 | Saves compose state to `ChatModel.draft` |
|
|
| [`clearCurrentDraft()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1487) | 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](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L45) 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`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L11) | L10 | Preview variants (image, voice, file, etc.) |
|
|
| [`enum ComposeContextItem`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L20) | L18 | Context item for reply/quote |
|
|
| [`enum VoiceMessageRecordingState`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L29) | L26 | Recording state enum |
|
|
| [`struct ComposeState`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L45) | L40 | Full compose state struct |
|
|
| [`copy()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L98) | L93 | Copy compose state with overrides |
|
|
| [`mentionMemberName()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L118) | L113 | Format mention display name |
|
|
| [`chatItemPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L266) | L260 | Build preview from chat item |
|
|
| [`enum UploadContent`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L287) | 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](../../Shared/Views/Chat/ComposeMessage/ComposeImageView.swift#L12)
|
|
|
|
**File**: [`ComposeImageView.swift`](../../Shared/Views/Chat/ComposeMessage/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](../../Shared/Views/Chat/ComposeMessage/ComposeFileView.swift#L11)
|
|
|
|
**File**: [`ComposeFileView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeFileView.swift) (struct at L11)
|
|
|
|
Preview of selected file or video. Shows filename, size, and remove button. Videos show a thumbnail frame.
|
|
|
|
### [ComposeVoiceView](../../Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift#L26)
|
|
|
|
**File**: [`ComposeVoiceView.swift`](../../Shared/Views/Chat/ComposeMessage/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 `ComposedMessage` with `quotedItemId` parameter
|
|
- `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
|
|
1. User selects "Forward" on message(s)
|
|
2. `apiPlanForwardChatItems(fromChatType:, fromChatId:, fromScope:, itemIds:)` is called to plan
|
|
3. Response: `ChatResponse1.forwardPlan(user:, chatItemIds:, forwardConfirmation:)`
|
|
4. User selects destination chat
|
|
5. `apiForwardChatItems(toChatType:, toChatId:, toScope:, fromChatType:, fromChatId:, fromScope:, itemIds:, ttl:)` executes the forward
|
|
6. 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](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L36) (`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()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L922) | L910 | Initiates a live message |
|
|
| [`updateLiveMessage()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L940) | L927 | Sends incremental live update |
|
|
| [`liveMessageToSend()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L959) | L945 | Determines text diff to send |
|
|
| [`truncateToWords()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L964) | 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
|
|
1. User taps (or holds) the microphone button
|
|
2. `AVAudioRecorder` starts recording in compressed format
|
|
3. Waveform visualization shows real-time audio levels
|
|
4. User taps stop (or releases hold) to finish recording
|
|
5. Preview with playback shown in compose area
|
|
6. User taps send to deliver
|
|
|
|
### Voice Functions
|
|
|
|
| Function | Line | Description |
|
|
|----------|------|-------------|
|
|
| [`startVoiceMessageRecording()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1382) | L1365 | Begins audio recording |
|
|
| [`finishVoiceMessageRecording()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1423) | L1405 | Stops recording, shows preview |
|
|
| [`allowVoiceMessagesToContact()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1434) | L1415 | Enables voice messages for contact |
|
|
| [`updateComposeVMRFinished()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1441) | L1422 | Updates state after recording finishes |
|
|
| [`cancelCurrentVoiceRecording()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1453) | L1434 | Cancels in-progress recording |
|
|
| [`cancelVoiceMessageRecording()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1460) | L1440 | Cancels and cleans up recording file |
|
|
|
|
### Constraints
|
|
- Maximum duration: `MAX_VOICE_MESSAGE_LENGTH = 300` seconds (5 minutes)
|
|
- Auto-receive threshold: `MAX_VOICE_SIZE_AUTO_RCV = 522,240` bytes (510KB)
|
|
- Compressed audio format for small file sizes
|
|
|
|
### Audio Management
|
|
- [`AudioRecorder`](../../Shared/Model/AudioRecPlay.swift#L14) (`Shared/Model/AudioRecPlay.swift` L14) manages recording and playback
|
|
- `ChatModel.stopPreviousRecPlay` coordinates exclusive audio playback (only one audio source plays at a time)
|
|
|
|
---
|
|
|
|
## 10. [Link Previews](../../Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift#L13) (`ComposeLinkView`)
|
|
|
|
**File**: [`ComposeLinkView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift) (struct at L13)
|
|
|
|
### Auto-Detection
|
|
- As user types, URLs in the text are detected
|
|
- When a URL is found, `ComposeLinkView` fetches OpenGraph metadata
|
|
- Preview card shows title, description, and thumbnail image
|
|
|
|
### Link Preview Functions
|
|
|
|
| Function | Line | Description |
|
|
|----------|------|-------------|
|
|
| [`showLinkPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1495) | L1471 | Triggers link preview loading |
|
|
| [`getMessageLinks()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1515) | L1490 | Extracts URLs from formatted text |
|
|
| [`isSimplexLink()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1526) | L1501 | Checks if URL is a SimpleX link |
|
|
| [`cancelLinkPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1530) | L1505 | Cancels pending preview |
|
|
| [`loadLinkPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1542) | L1516 | Fetches OpenGraph metadata |
|
|
| [`resetLinkPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1559) | 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 `ComposedMessage` sent to the core
|
|
- Toggle in privacy settings to disable auto-preview generation
|
|
|
|
---
|
|
|
|
## 11. Mentions
|
|
|
|
In group chats, typing `@` triggers member name autocomplete:
|
|
|
|
### Flow
|
|
1. User types `@` in the text field
|
|
2. Autocomplete dropdown appears with matching group members
|
|
3. User selects a member
|
|
4. `@displayName` is inserted into the text
|
|
5. 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`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift) | `ComposeView` | [L321](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L329) |
|
|
| Send message UI | [`SendMessageView.swift`](../../Shared/Views/Chat/ComposeMessage/SendMessageView.swift) | `SendMessageView` | [L14](../../Shared/Views/Chat/ComposeMessage/SendMessageView.swift#L15) |
|
|
| Image preview | [`ComposeImageView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeImageView.swift) | `ComposeImageView` | [L12](../../Shared/Views/Chat/ComposeMessage/ComposeImageView.swift#L12) |
|
|
| File preview | [`ComposeFileView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeFileView.swift) | `ComposeFileView` | [L11](../../Shared/Views/Chat/ComposeMessage/ComposeFileView.swift#L11) |
|
|
| Voice preview | [`ComposeVoiceView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift) | `ComposeVoiceView` | [L26](../../Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift#L26) |
|
|
| Link preview | [`ComposeLinkView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift) | `ComposeLinkView` | [L13](../../Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift#L13) |
|
|
| Audio recording | [`AudioRecPlay.swift`](../../Shared/Model/AudioRecPlay.swift) | `AudioRecorder` | [L14](../../Shared/Model/AudioRecPlay.swift#L14) |
|