# SimpleX Chat iOS -- Chat List Module > Technical specification for the conversation list, filtering, search, swipe actions, and user picker. > > Related specs: [Chat View](chat-view.md) | [Navigation](navigation.md) | [State Management](../state.md) | [README](../README.md) > Related product: [Chat List View](../../product/views/chat-list.md) **Source:** [`ChatListView.swift`](../../Shared/Views/ChatList/ChatListView.swift) --- ## Table of Contents 1. [Overview](#1-overview) 2. [ChatListView](#2-chatlistview) 3. [ChatPreviewView](#3-chatpreviewview) 4. [ChatListNavLink](#4-chatlistnavlink) 5. [Filtering & Tags](#5-filtering--tags) 6. [Search](#6-search) 7. [Swipe Actions](#7-swipe-actions) 8. [UserPicker](#8-userpicker) 9. [Floating Action Button](#9-floating-action-button) --- ## 1. Overview The chat list is the main screen of the app, displaying all conversations for the current user. It provides: - Conversation previews with unread badges - Filter tabs (All, Unread, Favorites, Groups, Contacts, Business, user-defined tags) - Search across chat names and message content - Swipe actions for quick operations - User profile switcher - Floating action button for new conversations ``` ChatListView ├── Navigation Bar │ ├── User avatar (tap → UserPicker) │ └── Filter tabs (TagListView) ├── Search bar (on pull-down or tap) ├── Chat List (List/LazyVStack) │ └── ChatListNavLink (per conversation) │ └── ChatPreviewView │ ├── Avatar │ ├── Chat name + last message preview │ ├── Timestamp │ └── Unread badge ├── FAB (New Chat button) └── Pending connection cards ``` --- ## 2. [`ChatListView`](../../Shared/Views/ChatList/ChatListView.swift#L142) {#2-chatlistview} **File**: `Shared/Views/ChatList/ChatListView.swift` The root list view. Key responsibilities: ### Data Source - Reads `ChatModel.shared.chats` (all conversations) - Applies active filter from `ChatTagsModel.shared.activeFilter` - Applies search query filtering via [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L480) - Sorts by last activity (most recent first), with pinned chats at top ### Layout - Uses SwiftUI `List` with `ForEach` over filtered chats - Each row is a `ChatListNavLink` wrapping a `ChatPreviewView` - Pull-to-refresh triggers `updateChats()` API call - Empty state: `ChatHelp` view with getting-started guidance ### Connection Cards - Pending contact connections (`ChatInfo.contactConnection`) shown as cards - Contact requests (`ChatInfo.contactRequest`) shown with accept/reject UI via `ContactRequestView` ### Key Functions | Function | Line | Description | |----------|------|-------------| | [`body`](../../Shared/Views/ChatList/ChatListView.swift#L168) | 163 | Main view body | | [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L480) | 472 | Applies active filter and search to chat list | | [`searchString()`](../../Shared/Views/ChatList/ChatListView.swift#L523) | 514 | Normalizes search text for comparison | | [`unreadBadge()`](../../Shared/Views/ChatList/ChatListView.swift#L454) | 448 | Renders unread count circle badge | | [`stopAudioPlayer()`](../../Shared/Views/ChatList/ChatListView.swift#L474) | 467 | Stops any playing voice message | --- ## 3. [`ChatPreviewView`](../../Shared/Views/ChatList/ChatPreviewView.swift#L13) {#3-chatpreviewview} **File**: `Shared/Views/ChatList/ChatPreviewView.swift` Renders a single row in the chat list. Shows: | Element | Source | Description | |---------|--------|-------------| | Avatar | `chatInfo.image` | Profile image or default icon | | Chat name | `chatInfo.displayName` | Contact name, group name, or connection label | | Last message | `chat.chatItems.last` | Preview text of most recent message | | Timestamp | `chat.chatItems.last?.timestampText` | Relative time of last message | | Unread badge | `chat.chatStats.unreadCount` | Circular badge with unread count | | Mute icon | `chatInfo.chatSettings?.enableNtfs` | Bell-slash icon if notifications muted | | Pin icon | -- | Pin indicator for pinned chats | | Incognito icon | Contact.contactConnIncognito | Incognito mode indicator | | Delivery status | Last sent item's `meta.itemStatus` | Check marks for delivery confirmation | ### Preview Text Rendering - Text messages: first line of message content - Images: camera icon + caption (if any) - Files: paperclip icon + filename - Voice: microphone icon + duration - Calls: phone icon + call status - Group events: system event description - Encrypted/deleted: placeholder text --- ## 4. [`ChatListNavLink`](../../Shared/Views/ChatList/ChatListNavLink.swift#L44) {#4-chatlistnavlink} **File**: `Shared/Views/ChatList/ChatListNavLink.swift` Wraps `ChatPreviewView` in a navigation link with tap and swipe behavior: ### Tap Behavior - Direct chat: navigates to `ChatView` via `ItemsModel.loadOpenChat(chatId)` -- [`contactNavLink()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L95) L93 - Group chat: navigates to `ChatView` -- [`groupNavLink()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L217) L214 - Contact request: shows `ContactRequestView` with accept/reject -- [`contactRequestNavLink()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L495) L486 - Contact connection: shows `ContactConnectionInfo` -- [`contactConnectionNavLink()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L530) L520 - Notes folder: navigates to `ChatView` -- [`noteFolderNavLink()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L302) L298 ### Navigation - Uses `NavigationLink` (iOS 15) or programmatic navigation (iOS 16+) - Sets `ChatModel.chatId` to trigger navigation - `ItemsModel.loadOpenChat()` loads messages with a 250ms navigation delay for smooth animation ### Channel Adaptations in ChatListNavLink When `groupInfo.useRelays == true`: | Change | Behavior | |--------|----------| | Swipe "Leave" | Hidden when `useRelays && isOwner` | | Context menu "Leave" | Hidden under same condition | | `deleteGroupAlert` label | "Delete channel?" | | `leaveGroupAlert` title | "Leave channel?" | | `leaveGroupAlert` message | "You will stop receiving messages from this channel. Chat history will be preserved." | ### ServerSettings `ServerSettings` struct (defined in `ChatListView.swift`) includes `serverWarnings: [UserServersWarning]` field, initialized to `[]`. This field stores validation warnings from `validateServers` and is consumed by NetworkAndServers views. --- ## 5. Filtering & Tags ### Filter Tabs ([`TagListView`](../../Shared/Views/ChatList/TagListView.swift#L20)) **File**: `Shared/Views/ChatList/TagListView.swift` Horizontal scrolling tab bar below the navigation bar. Tabs: | Tab | Filter | Shows | |-----|--------|-------| | All | `nil` | All conversations | | Unread | `.unread` | Conversations with unread messages | | Favorites | `.presetTag(.favorites)` | Favorited conversations | | Groups | `.presetTag(.groups)` | Group conversations | | Contacts | `.presetTag(.contacts)` | Direct conversations | | Business | `.presetTag(.business)` | Business conversations | | Group Reports | `.presetTag(.groupReports)` | Groups with pending reports | | User tags | `.userTag(ChatTag)` | User-defined custom tags | Filter matching is handled by [`presetTagMatchesChat()`](../../Shared/Views/ChatList/ChatListView.swift#L910) (L910) and the in-view [`TagsView`](../../Shared/Views/ChatList/ChatListView.swift#L705) struct (L705). ### ChatTagsModel State Filtering state is managed by [`ChatTagsModel`](../../Shared/Model/ChatModel.swift#L189) (`ChatModel.swift` L183): ```swift class ChatTagsModel: ObservableObject { @Published var userTags: [ChatTag] = [] @Published var activeFilter: ActiveFilter? = nil @Published var presetTags: [PresetTag: Int] = [:] // count per preset tag @Published var unreadTags: [Int64: Int] = [:] // unread count per user tag } ``` - `presetTags` counts are updated whenever `chats` changes via [`updateChatTags()`](../../Shared/Model/ChatModel.swift#L197) (L197) - Tags with zero matching chats are auto-hidden - Active filter is auto-cleared when its tag has no matching chats ### Supporting Types | Type | File | Line | Description | |------|------|------|-------------| | [`PresetTag`](../../Shared/Views/ChatList/ChatListView.swift#L36) | ChatListView.swift | 34 | Enum of built-in filter categories | | [`ActiveFilter`](../../Shared/Views/ChatList/ChatListView.swift#L52) | ChatListView.swift | 49 | Enum wrapping preset, user-tag, or unread filter | | [`setActiveFilter()`](../../Shared/Views/ChatList/ChatListView.swift#L889) | ChatListView.swift | 878 | Applies a filter and persists selection | ### Tag Management Commands - `apiCreateChatTag(tag: ChatTagData)` -- create tag - `apiSetChatTags(type:, id:, tagIds:)` -- assign tags to a chat - `apiDeleteChatTag(tagId:)` -- delete tag - `apiUpdateChatTag(tagId:, tagData:)` -- rename tag - `apiReorderChatTags(tagIds:)` -- reorder tags --- ## 6. Search Search is available via pull-down gesture or search button in the navigation bar. **Search bar UI:** [`ChatListSearchBar`](../../Shared/Views/ChatList/ChatListView.swift#L587) (ChatListView.swift L578) ### Filtering Logic - Filters `ChatModel.chats` by matching search text against: - `chatInfo.displayName` (contact/group name) - `chatInfo.localAlias` (local alias) - `chatInfo.fullName` (full name) - For deeper message content search, uses `apiGetChat(chatId:, search:)` parameter - Core logic in [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L480) (L480) and [`searchString()`](../../Shared/Views/ChatList/ChatListView.swift#L523) (L523) ### Search Results - Matching chats are displayed in the same list format - Results update as the user types (debounced) - Clearing search restores the full filtered list --- ## 7. Swipe Actions `ChatListNavLink` provides swipe actions on each row: ### Leading Swipe (left-to-right) | Action | Icon | Handler | Line | API | Condition | |--------|------|---------|------|-----|-----------| | Pin / Unpin | pin | [`toggleFavoriteButton()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L353) | 347 | `apiSetChatSettings` (favorite) | Always | | Read / Unread | envelope | [`markReadButton()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L333) | 328 | `apiChatRead` / `apiChatUnread` | Always | ### Trailing Swipe (right-to-left) | Action | Icon | Handler | Line | API | Condition | |--------|------|---------|------|-----|-----------| | Mute / Unmute | bell.slash | [`toggleNtfsButton()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L372) | 365 | `apiSetChatSettings` (enableNtfs) | Always | | Clear | trash | [`clearChatButton()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L393) | 385 | `apiClearChat` | Has messages | | Delete | trash.fill | -- | -- | `apiDeleteChat` | Not active chat | | Tag | tag | -- | -- | `apiSetChatTags` | Always | --- ## 8. [`UserPicker`](../../Shared/Views/ChatList/UserPicker.swift#L10) {#8-userpicker} **File**: `Shared/Views/ChatList/UserPicker.swift` Triggered by tapping the user avatar in the navigation bar. Presented as a sheet with: | Section | Contents | |---------|----------| | User list | All non-hidden users with unread counts | | Active user | Highlighted with checkmark | | Actions | Settings, Your SimpleX address, User profiles | ### User Switching - Tapping a different user calls `apiSetActiveUser(userId:)` - Triggers `apiGetChats` for the new user - `ChatModel.currentUser` updates, causing full UI refresh - Hidden users are not shown (require password entry via settings) --- ## 9. Floating Action Button The FAB (floating action button) in the bottom-right corner opens the new chat flow: - Tap: opens `NewChatView` sheet for creating a new contact connection or group - Shows options: Create link, Scan QR code, Paste link, Create group --- ## Source Files | File | Path | Key struct | Line | |------|------|------------|------| | Chat list view | [`ChatListView.swift`](../../Shared/Views/ChatList/ChatListView.swift) | `ChatListView` | [138](../../Shared/Views/ChatList/ChatListView.swift#L142) | | Chat preview row | [`ChatPreviewView.swift`](../../Shared/Views/ChatList/ChatPreviewView.swift) | `ChatPreviewView` | [12](../../Shared/Views/ChatList/ChatPreviewView.swift#L13) | | Navigation link wrapper | [`ChatListNavLink.swift`](../../Shared/Views/ChatList/ChatListNavLink.swift) | `ChatListNavLink` | [43](../../Shared/Views/ChatList/ChatListNavLink.swift#L44) | | Tag filter tabs | [`TagListView.swift`](../../Shared/Views/ChatList/TagListView.swift) | `TagListView` | [19](../../Shared/Views/ChatList/TagListView.swift#L20) | | User picker sheet | [`UserPicker.swift`](../../Shared/Views/ChatList/UserPicker.swift) | `UserPicker` | [9](../../Shared/Views/ChatList/UserPicker.swift#L10) | | Getting started help | [`ChatHelp.swift`](../../Shared/Views/ChatList/ChatHelp.swift) | | | | Contact request view | [`ContactRequestView.swift`](../../Shared/Views/ChatList/ContactRequestView.swift) | | | | Contact connection info | [`ContactConnectionInfo.swift`](../../Shared/Views/ChatList/ContactConnectionInfo.swift) | | | | Contact connection view | [`ContactConnectionView.swift`](../../Shared/Views/ChatList/ContactConnectionView.swift) | | | | Server summary | [`ServersSummaryView.swift`](../../Shared/Views/ChatList/ServersSummaryView.swift) | | | | One-hand UI card | [`OneHandUICard.swift`](../../Shared/Views/ChatList/OneHandUICard.swift) | | |