mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Add a Messages tab to global search
Split global search into Rooms and Messages tabs. The Messages tab lists matching events with the room's name and avatar, a sender-prefixed preview, rich media (image/video/audio/file) previews, and a loading indicator, then paginates as you scroll. Queries are debounced and the previous one is superseded so late results can't clobber a newer search.
This commit is contained in:
@@ -8,12 +8,31 @@
|
||||
import Foundation
|
||||
|
||||
enum SearchScreenViewModelAction {
|
||||
case presentRoom(roomID: String)
|
||||
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.screenSearchTabRooms
|
||||
case .messages: UntranslatedL10n.screenSearchTabMessages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchScreenViewState: BindableState {
|
||||
var rooms = [SearchScreenRoom]()
|
||||
var messages = [SearchScreenMessage]()
|
||||
var isLoadingRooms = false
|
||||
var isLoadingMessages = false
|
||||
var bindings: SearchScreenViewStateBindings
|
||||
|
||||
var isSearching: Bool {
|
||||
@@ -23,11 +42,13 @@ struct SearchScreenViewState: BindableState {
|
||||
|
||||
struct SearchScreenViewStateBindings {
|
||||
var searchQuery = ""
|
||||
var searchMode: SearchScreenMode = .rooms
|
||||
}
|
||||
|
||||
enum SearchScreenViewAction {
|
||||
case appeared
|
||||
case selectRoom(roomID: String)
|
||||
case selectMessage(roomID: String, eventID: String)
|
||||
case reachedTop
|
||||
case reachedBottom
|
||||
case cancel
|
||||
@@ -39,3 +60,99 @@ struct SearchScreenRoom: Identifiable, Equatable {
|
||||
let description: String
|
||||
let avatar: RoomAvatar
|
||||
}
|
||||
|
||||
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:
|
||||
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:
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user