Files
simplex-chat/apps/ios/spec/client/chat-list.md
T

17 KiB

SimpleX Chat iOS -- Chat List Module

Technical specification for the conversation list, filtering, search, swipe actions, and user picker.

Related specs: Chat View | Navigation | State Management | README Related product: Chat List View

Source: ChatListView.swift


Table of Contents

  1. Overview
  2. ChatListView
  3. ChatPreviewView
  4. ChatListNavLink
  5. Filtering & Tags
  6. Search
  7. Swipe Actions
  8. UserPicker
  9. Floating Action Button
  10. Crowdfunding Banner

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

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()
  • 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 183 Main view body
filteredChats() 555 Applies active filter and search to chat list
searchString() 598 Normalizes search text for comparison
unreadBadge() 529 Renders unread count circle badge
stopAudioPlayer() 549 Stops any playing voice message

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

File: Shared/Views/ChatList/ChatListNavLink.swift

Wraps ChatPreviewView in a navigation link with tap and swipe behavior:

Tap Behavior

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

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)

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() and the in-view TagsView struct.

ChatTagsModel State

Filtering state is managed by ChatTagsModel (ChatModel.swift L183):

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() (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 ChatListView.swift 36 Enum of built-in filter categories
ActiveFilter ChatListView.swift 53 Enum wrapping preset, user-tag, or unread filter
setActiveFilter() ChatListView.swift 1113 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

Search is available via pull-down gesture or search button in the navigation bar.

Search bar UI: ChatListSearchBar

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() and searchString()

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() 347 apiSetChatSettings (favorite) Always
Read / Unread envelope markReadButton() 328 apiChatRead / apiChatUnread Always

Trailing Swipe (right-to-left)

Action Icon Handler Line API Condition
Mute / Unmute bell.slash toggleNtfsButton() 365 apiSetChatSettings (enableNtfs) Always
Clear trash clearChatButton() 385 apiClearChat Has messages
Delete trash.fill -- -- apiDeleteChat Not active chat
Tag tag -- -- apiSetChatTags Always

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

10. GetStakeBanner

File: Shared/Views/ChatList/GetStakeBanner.swift

Gradient card inviting the user to invest on Wefunder. Shown only when isInUS — the same condition that gates the Wefunder row in settings — and only while DEFAULT_GET_STAKE_BANNER_DISMISSED is false.

Placement

Where Condition Layout
Chat list rendered whenever chatListContent is, in the List after OneHandUICard and before the chats .padding(.vertical, 3), flipped for one-hand UI, .zIndex(1)
Onboarding below ConnectOnboardingView when shouldShowOnboarding .padding(.horizontal, 20) (the onboarding cards' margin), .padding(.bottom, 8)

The list has a single banner slot, filled by an if/else if chain in priority order: the support-ended alert (supportEnded), the renewal-failure alert (badgeIssueFailed), the pitch, then the Wefunder banner. Each banner records itself in ChatModel.chatListBanner (.badgeExpired, .badgeIssueFailed, .badgePitch, .getStake) in its onAppear, and the pitch and Wefunder conditions start with chatModel.bannerSlotFree(for:) — true only while nothing else was shown this app session — so dismissing a banner never puts another in its place until restart. The alerts have no such check: an alert takes the slot whenever present, and once shown it holds it. The pitch also requires noShownBadge, false until BadgeModel holds the current user's state, so it cannot take the slot from a supporter whose badge loads a moment later. The onboarding placement applies the same bannerSlotFree check and records .getStake.

In the onboarding branch the .scaleEffect and ThemedBackground are applied to the enclosing VStack rather than to each child, so the banner stays below the pages in both toolbar modes.

Dismissal

Default Set by Effect
DEFAULT_GET_STAKE_BANNER_TAPPED openGetStake() the dismiss X appears from then on, while there are chats
DEFAULT_GET_STAKE_BANNER_DISMISSED the dismiss X hides the banner in both placements
DEFAULT_SUPPORTER_BANNER_TAPPED tapping the supporter pitch the pitch's dismiss X appears from then on
DEFAULT_SUPPORTER_BANNER_SHOWN the pitch's dismiss X, through its "You can support SimpleX later in Settings." alert, and a successful code redemption hides the pitch

The two badge alert banners always offer the X; only the pitch waits to be tapped once, so a user who has not looked at it cannot dismiss it unseen.

Both are in hintDefaults, so "Reset all hints" in the developer settings restores the banner. The X is never offered in the onboarding branch, so the banner cannot be dismissed before the user has a chat.

Tapping the card opens GetStakeView as an appSheet.

Shared card chrome

BannerCard (a ViewModifier: paddings, minimum height scaled by Dynamic Type, gradient background, rounded corners) and BannerDismissButton are declared separately from GetStakeBanner so other banners can adopt the same chrome. The gradient reuses OnboardingCardView.gradientPoints, lightStops and darkStops.


Source Files

File Path Key struct Line
Chat list view ChatListView.swift ChatListView 154
Chat preview row ChatPreviewView.swift ChatPreviewView 12
Navigation link wrapper ChatListNavLink.swift ChatListNavLink 43
Tag filter tabs TagListView.swift TagListView 19
User picker sheet UserPicker.swift UserPicker 9
Getting started help ChatHelp.swift
Contact request view ContactRequestView.swift
Contact connection info ContactConnectionInfo.swift
Contact connection view ContactConnectionView.swift
Server summary ServersSummaryView.swift
One-hand UI card OneHandUICard.swift
Crowdfunding banner GetStakeBanner.swift GetStakeBanner 13