mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
4a9cd02fbc
Introduces the GalleryRoomTimelineItem and its typed GalleryItem model (reusing the existing per-message content types), builds it in the RoomTimelineItemFactory and routes it through RoomTimelineItemView. For now it renders as a placeholder; the grid/list media views and the full-screen preview land in follow-up PRs. Co-authored-by: svetoslav.mitov <svetoslav.mitov@sportinno.com>
159 lines
5.2 KiB
Swift
159 lines
5.2 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 isLoadingRooms = false
|
|
var isLoadingMessages = false
|
|
var bindings: SearchScreenViewStateBindings
|
|
|
|
var isSearching: Bool {
|
|
!bindings.searchQuery.isEmpty
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
struct SearchScreenRoom: Identifiable, Equatable {
|
|
let id: String
|
|
let title: String
|
|
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, .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
|
|
}
|