25 KiB
SimpleX Chat iOS -- Chat View Module
Technical specification for the message rendering, chat item types, and context menu actions in the conversation view.
Related specs: Compose Module | State Management | API Reference | README Related product: Chat View
Source: ChatView.swift | ChatInfoView.swift | GroupChatInfoView.swift | ChannelMembersView.swift | ChannelRelaysView.swift
Table of Contents
- Overview
- ChatView
- ChatItemView -- Message Routing
- Message Renderers
- Media Views
- Metadata & Info
- Context Menu Actions
- Selection Mode
1. Overview
The chat view module renders individual conversations. It consists of:
- ChatView -- The main conversation screen with message list, compose bar, and navigation
- ChatItemView -- Router that dispatches each chat item to the appropriate renderer
- Specialized renderers -- FramedItemView (standard messages), EmojiItemView (emoji-only), CICallItemView (calls), event views, etc.
- Media views -- CIImageView, CIVideoView, CIVoiceView, CIFileView for attachments
ChatView
├── Message List (ScrollView / LazyVStack)
│ ├── ChatItemView (per message)
│ │ ├── FramedItemView (text/media bubbles)
│ │ │ ├── MsgContentView (text with markdown)
│ │ │ ├── CIImageView / CIVideoView / CIVoiceView
│ │ │ └── CIMetaView (timestamp, status)
│ │ ├── EmojiItemView (emoji-only messages)
│ │ ├── CICallItemView (call events)
│ │ ├── CIEventView (system events)
│ │ ├── CIGroupInvitationView (group invitations)
│ │ ├── DeletedItemView / MarkedDeletedItemView
│ │ └── CIInvalidJSONView (decode errors)
│ └── ... (more items)
├── ComposeView (message input)
└── Navigation bar (contact/group info)
2. ChatView
File: Shared/Views/Chat/ChatView.swift
The main conversation view. Key responsibilities:
State
- Uses
ItemsModel.shared.reversedChatItemsfor the primary message list ChatModel.shared.chatIdidentifies the active conversation- Manages compose state, scroll position, keyboard visibility
- Tracks selection mode for multi-message actions
Message List
- Renders messages in a
ScrollViewReaderwithLazyVStack - Items are in reverse chronological order (newest at bottom)
- Supports infinite scroll: preloads older messages when scrolling up via
ItemsModel.preloadState - Handles pagination splits (
chatState.splits) for non-contiguous loaded ranges
Navigation Bar
- Title: contact name / group name with connection status indicator
- Trailing button: navigates to
ChatInfoView(direct) orGroupChatInfoView(group) - Search button: toggles in-chat message search
Scroll Behavior
- Auto-scrolls to bottom on new sent/received messages (if already near bottom)
- "Scroll to bottom" floating button when scrolled up
openAroundItemIdsupport: scrolls to a specific message (e.g., from search or notification)
Key Functions
| Function | Line | Description |
|---|---|---|
body |
L75 | Main view body |
initChatView() |
L660 | Initializes chat view state on appear |
chatItemsList() |
L817 | Builds the scrollable message list |
scrollToItem(_:) |
L731 | Scrolls to a specific message by ID |
searchToolbar() |
L765 | In-chat search toolbar UI |
searchTextChanged(_:) |
L1095 | Handles search query changes |
loadChatItems(_:_:) |
L1531 | Loads chat items with pagination |
filtered(_:) |
L803 | Filters items by content type |
callButton(_:_:imageName:) |
L1273 | Audio/video call toolbar button |
searchButton() |
L1293 | Search toggle toolbar button |
addMembersButton() |
L1361 | Group add-members toolbar button |
forwardSelectedMessages() |
L1420 | Forwards batch-selected messages |
deletedSelectedMessages() |
L1411 | Deletes batch-selected messages |
onChatItemsUpdated() |
L1572 | Reacts to chat items model changes |
contentFilterMenu(withLabel:) |
L1301 | Content filter dropdown menu |
Supporting Types
| Type | Line | Description |
|---|---|---|
ChatItemWithMenu |
L1600 | Wraps each chat item with context menu |
FloatingButtonModel |
L2787 | Manages scroll-to-bottom button state |
ReactionContextMenu |
L2974 | Reaction picker context menu |
ToggleNtfsButton |
L3072 | Mute/unmute notifications button |
ContentFilter |
L3124 | Enum for message content filter types |
deleteMessages() |
L2870 | Deletes messages with confirmation |
archiveReports() |
L2917 | Archives report messages |
3. ChatItemView
File: Shared/Views/Chat/ChatItemView.swift
Routes each ChatItem to the appropriate renderer based on its CIContent type:
Content Types (CIContent enum)
| Content Type | Renderer | Line | Description |
|---|---|---|---|
sndMsgContent / rcvMsgContent |
FramedItemView |
L14 | Standard sent/received text+media message |
sndDeleted / rcvDeleted |
DeletedItemView |
L14 | Locally deleted message placeholder |
sndCall / rcvCall |
CICallItemView |
L13 | Call event (missed, ended, duration) |
rcvIntegrityError |
IntegrityErrorItemView |
L14 | Message integrity error |
rcvDecryptionError |
CIRcvDecryptionError |
L16 | Decryption failure |
sndGroupInvitation / rcvGroupInvitation |
CIGroupInvitationView |
L14 | Group invite |
sndGroupEvent / rcvGroupEvent |
CIEventView |
L14 | Group system event |
rcvConnEvent / sndConnEvent |
CIEventView |
L14 | Connection event |
rcvChatFeature / sndChatFeature |
CIChatFeatureView |
L14 | Feature toggle event |
rcvChatPreference / sndChatPreference |
CIFeaturePreferenceView |
L14 | Preference change |
invalidJSON |
CIInvalidJSONView |
L14 | Failed to decode |
Bubble Direction
- Sent messages: aligned right, sender-colored bubble
- Received messages: aligned left, receiver-colored bubble
- Events/system messages: centered, no bubble
Appearance Dependencies
Each ChatItemWithMenu may depend on the previous and next items for visual decisions:
- Whether to show the sender name (group messages, different sender than previous)
- Whether to show the tail on the bubble (last consecutive message from same sender)
- Date separator between messages on different days
ChatItemDummyModel.shared.sendUpdate() forces a re-render of all items when global appearance changes.
Channel Message Rendering (.channelRcv)
Channel messages (CIDirection.channelRcv) are rendered with the group avatar and group name as sender, with "channel" as the role label. This mirrors the .groupRcv path's showGroupAsSender visual but uses a dedicated code branch in chatItemListView().
Key differences from .groupRcv:
- No
prevMember/memCountlogic — channels have no per-member identity - Always shows group avatar (via
ProfileImagewithgroupInfo.image/groupInfo.chatIconName) - Tapping avatar opens
showChatInfoSheet(not member info) shouldShowAvatar()treats consecutive.channelRcvitems as same sendergetItemSeparation()treats consecutive.channelRcvitems assameMemberAndDirectionshowMemberImage()returnstruewhen previous item is.channelRcv(different sender type)memberToModerate()returnsnilfor.channelRcv(no per-member moderation)
4. Message Renderers
FramedItemView
File: Shared/Views/Chat/ChatItem/FramedItemView.swift
The standard message bubble. Renders:
- Quote/reply preview (if replying to another message)
- Forwarded indicator
- Sender name (in groups)
- Message content (
MsgContentViewwith markdown) - Attached media (image, video, voice, file, link preview)
- Reaction summary bar
- Metadata line (
CIMetaView)
EmojiItemView
File: Shared/Views/Chat/ChatItem/EmojiItemView.swift
Renders emoji-only messages (messages containing only emoji characters) in a larger font without a bubble background.
MsgContentView
File: Shared/Views/Chat/ChatItem/MsgContentView.swift
Renders message text with SimpleX markdown formatting (bold, italic, code, links, mentions).
DeletedItemView / MarkedDeletedItemView
Files: Shared/Views/Chat/ChatItem/DeletedItemView.swift | Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift
DeletedItemView: Placeholder for locally deleted messagesMarkedDeletedItemView: Shows "message deleted" with optional moderation info (who deleted, when)
CIEventView
File: Shared/Views/Chat/ChatItem/CIEventView.swift
Centered system event text for group events (member joined, left, role changed) and connection events.
CIGroupInvitationView
File: Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift
Renders group invitation with accept/reject buttons.
5. Media Views
CIImageView
File: Shared/Views/Chat/ChatItem/CIImageView.swift
Renders inline images. Tapping opens FullScreenMediaView for zooming/panning. Images are compressed to MAX_IMAGE_SIZE (255KB) before sending.
CIVideoView
File: Shared/Views/Chat/ChatItem/CIVideoView.swift
Renders video thumbnails with play button. Tapping opens video player. Videos above auto-receive threshold require manual download.
CIVoiceView / FramedCIVoiceView
Files: Shared/Views/Chat/ChatItem/CIVoiceView.swift | Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift
Renders voice messages with waveform visualization, play/pause control, and duration. FramedCIVoiceView is the version inside a message bubble with additional context.
CIFileView
File: Shared/Views/Chat/ChatItem/CIFileView.swift
Renders file attachments with filename, size, and download/open actions. Shows transfer progress during upload/download.
CILinkView
File: Shared/Views/Chat/ChatItem/CILinkView.swift
Renders link preview cards with OpenGraph metadata (title, description, image).
AnimatedImageView
File: Shared/Views/Chat/ChatItem/AnimatedImageView.swift
Renders animated GIF images.
FullScreenMediaView
File: Shared/Views/Chat/ChatItem/FullScreenMediaView.swift
Full-screen media viewer with zoom, pan, and share actions. Supports images and videos.
6. Metadata & Info
CIMetaView
File: Shared/Views/Chat/ChatItem/CIMetaView.swift
Displays message metadata inline at the bottom of the bubble:
- Timestamp (sent time)
- Delivery status icon (sending, sent, delivered, read, error)
- Edit indicator (pencil icon if message was edited)
- Disappearing message timer (if timed message)
ChatItemInfoView
File: Shared/Views/Chat/ChatItemInfoView.swift
Detailed message information sheet (accessed via long-press menu "Info"):
- Full delivery history (per-member delivery status in groups)
- Edit history (all previous versions of edited messages)
- Forward chain info
- Message timestamps (created, updated, deleted)
7. Context Menu Actions
Long-pressing a message shows a context menu with actions based on message type and ownership:
| Action | Available For | API Command |
|---|---|---|
| Reply | All messages | Sets compose state to .replying |
| Forward | Sent/received content messages | apiForwardChatItems |
| Copy | Text messages | Copies to clipboard |
| Edit | Own sent messages (within edit window) | apiUpdateChatItem |
| Delete for me | All messages | apiDeleteChatItem(mode: .cidmInternal) |
| Delete for everyone | Own sent messages | apiDeleteChatItem(mode: .cidmBroadcast) |
| Moderate | Group admin/owner for others' messages | apiDeleteMemberChatItem |
| React | Content messages (if reactions enabled) | apiChatItemReaction |
| Select | All messages | Enters multi-select mode |
| Info | All messages | Opens ChatItemInfoView |
| Save | Media messages | Saves to photo library / files |
| Share | Content messages | iOS share sheet |
8. Selection Mode
Multi-selection mode allows batch operations on messages:
- Enter via long-press "Select" action
- Toggle individual messages with tap
- Toolbar appears with batch actions: Delete, Forward
- Exit via cancel button or completing batch action
GroupChatInfoView — Channel Adaptations
When groupInfo.useRelays == true, GroupChatInfoView adapts its sections:
Section Structure (Channel)
| Section | Owner | Subscriber |
|---|---|---|
| 1. Links & Members | Channel link (manage via GroupLinkView), Owners & subscribers | Channel link (read-only QR from groupProfile.publicGroup?.groupLink), Owners |
| 2. Profile & Welcome | Edit channel profile, Welcome message | Welcome message (if exists) |
| 3. Theme & TTL | Chat theme, Delete messages after | Chat theme, Delete messages after |
| 4. Actions | Chat relays, Clear chat, Delete channel | Chat relays, Clear chat, Leave channel |
Hidden for channels: Member support, group reports, user support chat, send receipts, inline members list, group preferences.
Label Replacements
All "group" labels are replaced with "channel" equivalents via groupInfo.useRelays ? "Channel..." : ternary prepended before existing businessChat ternary. Affected: delete/leave buttons, delete/leave alerts, remove member alert, edit profile button, group link nav title. Channel link button uses a separate channelLinkButton() with hardcoded "Channel link" label.
channelMembersButton() → ChannelMembersView
Navigates to a dedicated members view with two sections:
- Owners: current user (if owner) + members with
memberRole >= .owner - Subscribers (admin+ only): members with
memberRole < .owner
Member rows show profile image, display name (with verified shield), connection status, and role badge. Non-user rows link to GroupMemberInfoView.
Channel Link
Owner sees channelLinkButton() (navigates to GroupLinkView for full link management), guarded by groupInfo.isOwner && groupLink != nil — channel links can only be created during channel creation, not from the info view. A TODO marks the need for protocol changes to allow other owners to manage the same channel link. Non-owner sees read-only QR code displaying groupProfile.publicGroup?.groupLink via SimpleXLinkQRCode. apiGetGroupLink is skipped in onAppear for non-owner channels.
Groups use separate groupLinkButton() which supports both "Create group link" and "Group link" labels.
channelRelaysButton() → ChannelRelaysView
Navigates to relay list view with role-based branches:
- Owner: loads
[GroupRelay]viaapiGetGroupRelays(owner-only API, guarded byassertUserGroupRole GROwneron backend). Joins withchatModel.groupMembersbygroupMemberIdfor display names. Shows status indicators (colored circle +RelayStatus.text). - Member: filters
chatModel.groupMembersby.memberRole == .relay. Shows relay member display names only (no status data).
Leave Button Logic
Sole channel owner cannot leave (only delete). Guard: members.filter({ $0.wrapped.memberRole == .owner && $0.wrapped.groupMemberId != groupInfo.membership.groupMemberId }).count > 0.