mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-06 12:28:29 +00:00
233 lines
8.6 KiB
Swift
233 lines
8.6 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
|
|
import UniformTypeIdentifiers
|
|
|
|
nonisolated struct GalleryRoomTimelineItemContent: Hashable {
|
|
let body: String
|
|
var caption: String?
|
|
var formattedCaption: AttributedString?
|
|
/// The original textual representation of the formatted caption directly from the event (usually HTML code)
|
|
var formattedCaptionHTMLString: String?
|
|
let items: [GalleryItem]
|
|
|
|
/// The items of the given types, paired with their position in the gallery so that they remain
|
|
/// identifiable once separated from it. Items of an unknown type are never included.
|
|
/// - Parameter allowedTypes: The types to include, or `nil` to include them all.
|
|
func items(matching allowedTypes: [TimelineAllowedGalleryItemType]?) -> [(mediaIndex: Int, item: GalleryItem)] {
|
|
items.enumerated().compactMap { mediaIndex, item in
|
|
guard let allowedItemType = item.allowedItemType,
|
|
allowedTypes?.contains(allowedItemType) ?? true else {
|
|
return nil
|
|
}
|
|
|
|
return (mediaIndex, item)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Identifies a single attachment within a gallery message.
|
|
nonisolated struct GalleryItemID: Hashable {
|
|
/// The identifier of the parent gallery timeline item. The whole identifier is used so that an
|
|
/// attachment remains identifiable across timelines, where the unique ID alone wouldn't be.
|
|
let timelineItemID: TimelineItemIdentifier
|
|
/// The item's position within the gallery.
|
|
let mediaIndex: Int
|
|
}
|
|
|
|
/// A single attachment of a gallery message. The SDK hands back the same content types it uses for
|
|
/// individual messages, so we reuse them here rather than duplicating their fields.
|
|
nonisolated enum GalleryItem: Hashable, Identifiable {
|
|
case image(id: GalleryItemID, ImageRoomTimelineItemContent)
|
|
case video(id: GalleryItemID, VideoRoomTimelineItemContent)
|
|
case audio(id: GalleryItemID, AudioRoomTimelineItemContent)
|
|
case file(id: GalleryItemID, FileRoomTimelineItemContent)
|
|
/// An item of an unknown type. It has no media source, so there's nothing to preview.
|
|
case other(id: GalleryItemID, filename: String)
|
|
|
|
var id: GalleryItemID {
|
|
switch self {
|
|
case .image(let id, _), .video(let id, _), .audio(let id, _), .file(let id, _), .other(let id, _):
|
|
id
|
|
}
|
|
}
|
|
|
|
var isImage: Bool {
|
|
if case .image = self {
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
var isVideo: Bool {
|
|
if case .video = self {
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
var isAudio: Bool {
|
|
if case .audio = self {
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
var isFile: Bool {
|
|
if case .file = self {
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
var filename: String {
|
|
switch self {
|
|
case .image(_, let content): content.filename
|
|
case .video(_, let content): content.filename
|
|
case .audio(_, let content): content.filename
|
|
case .file(_, let content): content.filename
|
|
case .other(_, let filename): filename
|
|
}
|
|
}
|
|
|
|
var mediaSource: MediaSourceProxy? {
|
|
switch self {
|
|
case .image(_, let content): content.imageInfo.source
|
|
case .video(_, let content): content.videoInfo.source
|
|
case .audio(_, let content): content.source
|
|
case .file(_, let content): content.source
|
|
case .other: nil
|
|
}
|
|
}
|
|
|
|
var thumbnailSource: MediaSourceProxy? {
|
|
switch self {
|
|
case .image(_, let content): content.thumbnailInfo?.source
|
|
case .video(_, let content): content.thumbnailInfo?.source
|
|
case .file(_, let content): content.thumbnailSource
|
|
case .audio, .other: nil
|
|
}
|
|
}
|
|
|
|
var size: CGSize? {
|
|
switch self {
|
|
case .image(_, let content): content.imageInfo.size
|
|
case .video(_, let content): content.videoInfo.size
|
|
case .audio, .file, .other: nil
|
|
}
|
|
}
|
|
|
|
var fileSize: UInt? {
|
|
switch self {
|
|
case .image(_, let content): content.imageInfo.fileSize
|
|
case .video(_, let content): content.videoInfo.fileSize
|
|
case .audio(_, let content): content.fileSize
|
|
case .file(_, let content): content.fileSize
|
|
case .other: nil
|
|
}
|
|
}
|
|
|
|
var blurhash: String? {
|
|
switch self {
|
|
case .image(_, let content): content.blurhash
|
|
case .video(_, let content): content.blurhash
|
|
case .audio, .file, .other: nil
|
|
}
|
|
}
|
|
|
|
var duration: TimeInterval? {
|
|
switch self {
|
|
case .video(_, let content): content.videoInfo.duration
|
|
case .audio(_, let content): content.duration
|
|
case .image, .file, .other: nil
|
|
}
|
|
}
|
|
|
|
var contentType: UTType? {
|
|
switch self {
|
|
case .image(_, let content): content.contentType
|
|
case .video(_, let content): content.contentType
|
|
case .audio(_, let content): content.contentType
|
|
case .file(_, let content): content.contentType
|
|
case .other: nil
|
|
}
|
|
}
|
|
|
|
/// Whether the item can render a visible thumbnail. Images always can (their media source
|
|
/// IS the image); for everything else we need an explicit thumbnail source.
|
|
var hasThumbnail: Bool {
|
|
isImage || thumbnailSource != nil
|
|
}
|
|
|
|
/// The type this attachment is allowed by, or `nil` when its type is unknown.
|
|
var allowedItemType: TimelineAllowedGalleryItemType? {
|
|
switch self {
|
|
case .image: .image
|
|
case .video: .video
|
|
case .audio: .audio
|
|
case .file: .file
|
|
case .other: nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Mocks
|
|
|
|
nonisolated extension GalleryItemID {
|
|
static func mock(_ index: Int) -> GalleryItemID {
|
|
.init(timelineItemID: .randomEvent, mediaIndex: index)
|
|
}
|
|
}
|
|
|
|
nonisolated extension GalleryItem {
|
|
static func mockImage(index: Int = 0,
|
|
filename: String = "image.jpg",
|
|
source: MediaSourceProxy = ImageInfoProxy.mockImage.source,
|
|
thumbnailSource: MediaSourceProxy? = ImageInfoProxy.mockThumbnail.source,
|
|
blurhash: String? = "L%KUc%kqS$RP?Ks,WEf8OlrqaekW",
|
|
contentType: UTType? = .jpeg) -> GalleryItem {
|
|
let imageInfo = ImageInfoProxy(source: source, width: 1920, height: 1080, mimeType: nil, fileSize: nil)
|
|
let thumbnailInfo = thumbnailSource.map { ImageInfoProxy(source: $0, width: 1920, height: 1080, mimeType: nil, fileSize: nil) }
|
|
return .image(id: .mock(index),
|
|
.init(filename: filename, imageInfo: imageInfo, thumbnailInfo: thumbnailInfo, blurhash: blurhash, contentType: contentType))
|
|
}
|
|
|
|
static func mockVideo(index: Int = 0,
|
|
filename: String = "clip.mp4",
|
|
thumbnailSource: MediaSourceProxy? = ImageInfoProxy.mockVideoThumbnail.source,
|
|
duration: TimeInterval = 42,
|
|
blurhash: String? = "L%KUc%kqS$RP?Ks,WEf8OlrqaekW",
|
|
contentType: UTType? = .mpeg4Movie) -> GalleryItem {
|
|
let thumbnailInfo = thumbnailSource.map { ImageInfoProxy(source: $0, width: 1920, height: 1080, mimeType: nil, fileSize: nil) }
|
|
return .video(id: .mock(index),
|
|
.init(filename: filename, videoInfo: .mockVideo(duration: duration), thumbnailInfo: thumbnailInfo, blurhash: blurhash, contentType: contentType))
|
|
}
|
|
|
|
static func mockFile(index: Int = 0,
|
|
filename: String = "document.pdf",
|
|
source: MediaSourceProxy? = nil,
|
|
thumbnailSource: MediaSourceProxy? = nil,
|
|
contentType: UTType? = .pdf) -> GalleryItem {
|
|
.file(id: .mock(index),
|
|
.init(filename: filename, source: source, fileSize: nil, thumbnailSource: thumbnailSource, contentType: contentType))
|
|
}
|
|
|
|
static func mockAudio(index: Int = 0,
|
|
filename: String = "audio.m4a",
|
|
source: MediaSourceProxy? = nil,
|
|
duration: TimeInterval = 65,
|
|
contentType: UTType? = .mpeg4Audio) -> GalleryItem {
|
|
.audio(id: .mock(index),
|
|
.init(filename: filename, duration: duration, waveform: nil, source: source, fileSize: nil, contentType: contentType))
|
|
}
|
|
}
|