mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
50b38555b6
Each recent search now has a trailing close button to remove it from the history. The view action is logged without its payload so removed queries don't end up in the logs.
201 lines
6.5 KiB
Swift
201 lines
6.5 KiB
Swift
//
|
|
// Copyright 2026 Element Creations 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
|
|
|
|
enum SearchScreenViewModelAction {
|
|
case presentRoom(roomID: String, eventID: String?)
|
|
case cancel
|
|
}
|
|
|
|
enum SearchScreenMode: CaseIterable, Identifiable {
|
|
case rooms
|
|
case messages
|
|
|
|
var id: Self {
|
|
self
|
|
}
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .rooms: UntranslatedL10n.screenSearchTabChats
|
|
case .messages: UntranslatedL10n.screenSearchTabMessages
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SearchScreenViewState: BindableState {
|
|
var rooms = [SearchScreenRoom]()
|
|
var messages = [SearchScreenMessage]()
|
|
var breadcrumbs = [SearchScreenBreadcrumb]()
|
|
var isLoadingRooms = false
|
|
var isLoadingMessages = false
|
|
var bindings: SearchScreenViewStateBindings
|
|
|
|
var isSearching: Bool {
|
|
!bindings.searchQuery.isEmpty
|
|
}
|
|
}
|
|
|
|
struct SearchScreenViewStateBindings {
|
|
var searchQuery = ""
|
|
var searchMode: SearchScreenMode = .rooms
|
|
}
|
|
|
|
enum SearchScreenViewAction: CustomStringConvertible {
|
|
case appeared
|
|
case submitQuery
|
|
case selectRoom(roomID: String)
|
|
case selectMessage(roomID: String, eventID: String)
|
|
case removeBreadcrumb(SearchScreenBreadcrumb)
|
|
case reachedTop
|
|
case reachedBottom
|
|
case cancel
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .appeared: "appeared"
|
|
case .submitQuery: "submitQuery"
|
|
case .selectRoom(let roomID): "selectRoom(\(roomID))"
|
|
case .selectMessage(let roomID, let eventID): "selectMessage(\(roomID), \(eventID))"
|
|
case .removeBreadcrumb: "removeBreadcrumb"
|
|
case .reachedTop: "reachedTop"
|
|
case .reachedBottom: "reachedBottom"
|
|
case .cancel: "cancel"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SearchScreenRoom: Identifiable, Equatable {
|
|
let id: String
|
|
let title: String
|
|
let description: String
|
|
let avatar: RoomAvatar
|
|
|
|
init(_ summary: RoomSummary) {
|
|
let identifier = if summary.isDirect {
|
|
summary.heroes.first?.id ?? summary.canonicalAlias
|
|
} else {
|
|
summary.canonicalAlias
|
|
}
|
|
|
|
id = summary.id
|
|
title = summary.name
|
|
description = identifier ?? ""
|
|
avatar = summary.avatar
|
|
}
|
|
}
|
|
|
|
/// An entry in the search history shown before the user has typed a query.
|
|
enum SearchScreenBreadcrumb: Identifiable, Equatable {
|
|
case query(String)
|
|
case room(SearchScreenRoom)
|
|
|
|
var id: String {
|
|
switch self {
|
|
case .query(let query): "query-\(query)"
|
|
case .room(let room): "room-\(room.id)"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SearchScreenMessage: Identifiable, Equatable {
|
|
let id: String
|
|
let roomID: String
|
|
let roomName: String
|
|
let roomAvatar: RoomAvatar
|
|
let senderName: String
|
|
let content: TimelineEventContent
|
|
let timestamp: Date
|
|
|
|
init(_ result: SearchServiceResult, roomSummary: RoomSummary?, isOutgoing: Bool) {
|
|
id = result.eventID
|
|
roomID = result.roomID
|
|
roomName = roomSummary?.name ?? result.roomID
|
|
roomAvatar = roomSummary?.avatar ?? .room(id: result.roomID, name: roomSummary?.name, avatarURL: nil)
|
|
senderName = isOutgoing ? L10n.commonYou : result.sender.disambiguatedDisplayName ?? result.sender.id
|
|
content = result.content
|
|
timestamp = result.timestamp
|
|
}
|
|
|
|
var preview: AttributedString? {
|
|
guard let messageBody else { return nil }
|
|
return AttributedString("\(senderName): ") + messageBody
|
|
}
|
|
|
|
private var messageBody: AttributedString? {
|
|
switch content {
|
|
case .message(let content):
|
|
switch content {
|
|
case .text(let content):
|
|
content.formattedBody ?? AttributedString(content.body)
|
|
case .notice(let content):
|
|
content.formattedBody ?? AttributedString(content.body)
|
|
case .emote(let content):
|
|
content.formattedBody ?? AttributedString(content.body)
|
|
case .audio, .file, .image, .video, .gallery:
|
|
nil
|
|
case .voice:
|
|
AttributedString(L10n.commonVoiceMessage)
|
|
case .location:
|
|
AttributedString(L10n.commonSharedLocation)
|
|
}
|
|
case .poll(let question):
|
|
AttributedString(question)
|
|
case .liveLocation:
|
|
AttributedString(L10n.commonSharedLiveLocation)
|
|
case .redacted:
|
|
AttributedString(L10n.commonMessageRemoved)
|
|
}
|
|
}
|
|
|
|
var mediaPreview: SearchScreenMediaPreview? {
|
|
guard case .message(let content) = content else { return nil }
|
|
switch content {
|
|
case .file(let content):
|
|
return .init(title: content.caption ?? content.filename,
|
|
details: mediaDetails(filename: content.filename, fileSize: content.fileSize),
|
|
kind: .file)
|
|
case .audio(let content):
|
|
return .init(title: content.caption ?? content.filename,
|
|
details: mediaDetails(filename: content.filename, fileSize: content.fileSize),
|
|
kind: .audio)
|
|
case .image(let content):
|
|
return .init(title: content.caption ?? content.filename,
|
|
details: mediaDetails(filename: content.filename, fileSize: content.imageInfo.fileSize),
|
|
kind: .image(thumbnail: content.thumbnailInfo ?? content.imageInfo, blurhash: content.blurhash))
|
|
case .video(let content):
|
|
return .init(title: content.caption ?? content.filename,
|
|
details: mediaDetails(filename: content.filename, fileSize: content.videoInfo.fileSize),
|
|
kind: .video(thumbnail: content.thumbnailInfo, blurhash: content.blurhash))
|
|
case .text, .notice, .emote, .voice, .location, .gallery:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func mediaDetails(filename: String, fileSize: UInt?) -> String {
|
|
var details = filename.validatedFileExtension.uppercased()
|
|
if let fileSize {
|
|
details += " (\(fileSize.formatted(.byteCount(style: .file))))"
|
|
}
|
|
return details
|
|
}
|
|
}
|
|
|
|
struct SearchScreenMediaPreview: Equatable {
|
|
enum Kind: Equatable {
|
|
case file
|
|
case audio
|
|
case image(thumbnail: ImageInfoProxy?, blurhash: String?)
|
|
case video(thumbnail: ImageInfoProxy?, blurhash: String?)
|
|
}
|
|
|
|
let title: String
|
|
let details: String
|
|
let kind: Kind
|
|
}
|