mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
7f1b6086ae
* Add a developer flag for the search tab Introduces a globalSearchEnabled user preference (off by default) with a toggle in Developer Options, used to gate the new search experience. * Support search role tabs in the tab coordinator Migrates the NavigationTabCoordinator to SwiftUI's value-based Tab API and adds an isSearch option so a tab can adopt the iOS 26 .search role, which detaches it into the trailing search button in the tab bar. * Moves room search to a new screen Adds a new screen that filters the alternate room summary provider as you type, shows an empty state, and lists matching rooms with their matrix identifier. Supports hardware keyboard selection (arrow keys to move through results, return to open) by reaching into the system search field. * Present search in a dedicated tab Adds the search tab to the chats flow on iOS 26 when the flag is enabled, rendered as the detached .search role button. The Cmd+K shortcut now selects this tab, and the home screen's inline search is hidden while the tab is available. * Remove the old global search screen The dedicated search tab replaces the Cmd+K floating window, so the GlobalSearchScreen, its window plumbing in the WindowManager, and its presentation in the chats flow are all removed.
195 lines
5.8 KiB
Swift
195 lines
5.8 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Foundation
|
|
import MatrixRustSDK
|
|
|
|
/// A quick summary of a Room, useful to describe and give quick informations for the room list
|
|
nonisolated struct RoomSummary {
|
|
enum JoinRequestType {
|
|
case invite(inviter: RoomMemberProxyProtocol?)
|
|
case knock
|
|
|
|
var isInvite: Bool {
|
|
switch self {
|
|
case .invite: true
|
|
default: false
|
|
}
|
|
}
|
|
|
|
var isKnock: Bool {
|
|
switch self {
|
|
case .knock: true
|
|
default: false
|
|
}
|
|
}
|
|
}
|
|
|
|
enum LastMessageState { case sending, failed }
|
|
|
|
let room: Room
|
|
|
|
let id: String
|
|
|
|
let joinRequestType: JoinRequestType?
|
|
|
|
let name: String
|
|
let isDirect: Bool
|
|
let isSpace: Bool
|
|
let avatarURL: URL?
|
|
|
|
let heroes: [UserProfileProxy]
|
|
let activeMembersCount: UInt
|
|
|
|
let lastMessage: AttributedString?
|
|
let lastMessageDate: Date?
|
|
let lastMessageState: LastMessageState?
|
|
let unreadMessagesCount: UInt
|
|
let unreadMentionsCount: UInt
|
|
let unreadNotificationsCount: UInt
|
|
let notificationMode: RoomNotificationModeProxy?
|
|
let canonicalAlias: String?
|
|
let alternativeAliases: Set<String>
|
|
|
|
let hasOngoingCall: Bool
|
|
let activeCallIntent: CallIntent?
|
|
|
|
let isMarkedUnread: Bool
|
|
let isFavourite: Bool
|
|
let isTombstoned: Bool
|
|
|
|
var hasUnreadMessages: Bool {
|
|
unreadMessagesCount > 0
|
|
}
|
|
|
|
var hasUnreadMentions: Bool {
|
|
unreadMentionsCount > 0
|
|
}
|
|
|
|
var hasUnreadNotifications: Bool {
|
|
unreadNotificationsCount > 0
|
|
}
|
|
|
|
var isMuted: Bool {
|
|
notificationMode == .mute
|
|
}
|
|
}
|
|
|
|
extension RoomSummary: CustomStringConvertible {
|
|
var description: String {
|
|
"""
|
|
RoomSummary: - id: \(id) \
|
|
- isDirect: \(isDirect) \
|
|
- unreadMessagesCount: \(unreadMessagesCount) \
|
|
- unreadMentionsCount: \(unreadMentionsCount) \
|
|
- unreadNotificationsCount: \(unreadNotificationsCount) \
|
|
- notificationMode: \(notificationMode?.rawValue ?? "nil")
|
|
"""
|
|
}
|
|
|
|
/// Used where summaries are shown in a list e.g. message forwarding,
|
|
/// global search, share destination list etc.
|
|
var roomListDescription: String {
|
|
if isDirect {
|
|
return canonicalAlias ?? ""
|
|
}
|
|
|
|
if let alias = canonicalAlias {
|
|
return alias
|
|
}
|
|
|
|
guard heroes.count > 0 else {
|
|
return ""
|
|
}
|
|
|
|
var heroComponents = heroes.compactMap(\.displayName)
|
|
|
|
let othersCount = Int(activeMembersCount) - heroes.count
|
|
if othersCount > 0 {
|
|
heroComponents.append(L10n.commonManyMembers(othersCount))
|
|
}
|
|
|
|
return heroComponents.formatted(.list(type: .and))
|
|
}
|
|
}
|
|
|
|
extension RoomSummary {
|
|
/// An empty summary used when the room info is momentarily unavailable (e.g. while the client tears
|
|
/// down on logout). Keeps the diff indices aligned with the SDK instead of crashing.
|
|
nonisolated static func placeholder(room: Room) -> RoomSummary {
|
|
RoomSummary(room: room,
|
|
id: room.id(),
|
|
joinRequestType: nil,
|
|
name: room.id(),
|
|
isDirect: false,
|
|
isSpace: false,
|
|
avatarURL: nil,
|
|
heroes: [],
|
|
activeMembersCount: 0,
|
|
lastMessage: nil,
|
|
lastMessageDate: nil,
|
|
lastMessageState: nil,
|
|
unreadMessagesCount: 0,
|
|
unreadMentionsCount: 0,
|
|
unreadNotificationsCount: 0,
|
|
notificationMode: nil,
|
|
canonicalAlias: nil,
|
|
alternativeAliases: [],
|
|
hasOngoingCall: false,
|
|
activeCallIntent: nil,
|
|
isMarkedUnread: false,
|
|
isFavourite: false,
|
|
isTombstoned: false)
|
|
}
|
|
|
|
init(room: Room, id: String, settingsMode: RoomNotificationModeProxy, hasUnreadMessages: Bool, hasUnreadMentions: Bool, hasUnreadNotifications: Bool) {
|
|
self.room = room
|
|
self.id = id
|
|
let string = "\(settingsMode) - messages: \(hasUnreadMessages) - mentions: \(hasUnreadMentions) - notifications: \(hasUnreadNotifications)"
|
|
name = string
|
|
isDirect = true
|
|
isSpace = false
|
|
avatarURL = nil
|
|
|
|
heroes = []
|
|
activeMembersCount = 0
|
|
|
|
lastMessage = AttributedString(string)
|
|
lastMessageDate = .mock
|
|
lastMessageState = nil
|
|
unreadMessagesCount = hasUnreadMessages ? 1 : 0
|
|
unreadMentionsCount = hasUnreadMentions ? 1 : 0
|
|
unreadNotificationsCount = hasUnreadNotifications ? 1 : 0
|
|
notificationMode = settingsMode
|
|
canonicalAlias = nil
|
|
alternativeAliases = []
|
|
hasOngoingCall = false
|
|
activeCallIntent = nil
|
|
|
|
joinRequestType = nil
|
|
isMarkedUnread = false
|
|
isFavourite = false
|
|
isTombstoned = false
|
|
}
|
|
|
|
/// This doesn't have to work properly for DM invites, the heroes are always empty
|
|
var avatar: RoomAvatar {
|
|
guard !isTombstoned else {
|
|
return .tombstoned
|
|
}
|
|
|
|
if isSpace {
|
|
return .space(id: id, name: name, avatarURL: avatarURL)
|
|
} else if isDirect, avatarURL == nil, heroes.count == 1 {
|
|
return .heroes(heroes)
|
|
} else {
|
|
return .room(id: id, name: name, avatarURL: avatarURL)
|
|
}
|
|
}
|
|
}
|