mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Move media file loading logic to the SDK. (#702)
* Adopt getMediaFile for media previewing.
This commit is contained in:
+19
-160
@@ -17,7 +17,6 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
private let userId: String
|
||||
@@ -105,44 +104,34 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
guard let timelineItem = timelineItems.first(where: { $0.id == itemID }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
|
||||
var source: MediaSourceProxy?
|
||||
var title: String
|
||||
switch timelineItem {
|
||||
case let item as ImageRoomTimelineItem:
|
||||
await loadFileForImageTimelineItem(item)
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
|
||||
let item = timelineItems[index] as? ImageRoomTimelineItem,
|
||||
let fileURL = item.cachedFileURL else {
|
||||
return .none
|
||||
}
|
||||
return .displayFile(fileURL: fileURL, title: item.body)
|
||||
source = item.source
|
||||
title = item.body
|
||||
case let item as VideoRoomTimelineItem:
|
||||
await loadVideoForTimelineItem(item)
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
|
||||
let item = timelineItems[index] as? VideoRoomTimelineItem,
|
||||
let videoURL = item.cachedVideoURL else {
|
||||
return .none
|
||||
}
|
||||
return .displayVideo(videoURL: videoURL, title: item.body)
|
||||
source = item.source
|
||||
title = item.body
|
||||
case let item as FileRoomTimelineItem:
|
||||
await loadFileForTimelineItem(item)
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
|
||||
let item = timelineItems[index] as? FileRoomTimelineItem,
|
||||
let fileURL = item.cachedFileURL else {
|
||||
return .none
|
||||
}
|
||||
return .displayFile(fileURL: fileURL, title: item.body)
|
||||
source = item.source
|
||||
title = item.body
|
||||
case let item as AudioRoomTimelineItem:
|
||||
await loadAudioForTimelineItem(item)
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
|
||||
let item = timelineItems[index] as? AudioRoomTimelineItem,
|
||||
let audioURL = item.cachedAudioURL else {
|
||||
return .none
|
||||
}
|
||||
// For now we are just displaying audio messages with the File preview until we create a timeline player for them.
|
||||
return .displayFile(fileURL: audioURL, title: item.body)
|
||||
source = item.source
|
||||
title = item.body
|
||||
default:
|
||||
return .none
|
||||
}
|
||||
|
||||
guard let source else { return .none }
|
||||
switch await mediaProvider.loadFileFromSource(source) {
|
||||
case .success(let file):
|
||||
return .displayMediaFile(file: file, title: title)
|
||||
case .failure:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String, inReplyTo itemID: String?) async {
|
||||
@@ -305,134 +294,4 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func loadVideoForTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
|
||||
if timelineItem.cachedVideoURL != nil {
|
||||
// already cached
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = timelineItem.source else {
|
||||
return
|
||||
}
|
||||
|
||||
let fileExtension = movieFileExtension(for: timelineItem.body)
|
||||
switch await mediaProvider.loadFileFromSource(source, fileExtension: fileExtension) {
|
||||
case .success(let fileURL):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? VideoRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.cachedVideoURL = fileURL
|
||||
timelineItems[index] = item
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// Temporary method that generates a file extension for a video file name
|
||||
/// using `UTType.movie` and falls back to .mp4 if anything goes wrong.
|
||||
///
|
||||
/// Ideally Rust should be able to handle this for us, otherwise we should be
|
||||
/// attempting to detect the file type from the data itself.
|
||||
private func movieFileExtension(for text: String) -> String {
|
||||
let fallbackExtension = "mp4"
|
||||
|
||||
// This is not great. We could better estimate file extension from the mimetype.
|
||||
guard let fileExtensionComponent = text.split(separator: ".").last else { return fallbackExtension }
|
||||
let fileExtension = String(fileExtensionComponent)
|
||||
|
||||
// We can't trust that the extension provided is an extension that AVFoundation will accept.
|
||||
guard let fileType = UTType(filenameExtension: fileExtension),
|
||||
fileType.isSubtype(of: .movie)
|
||||
else { return fallbackExtension }
|
||||
|
||||
return fileExtension
|
||||
}
|
||||
|
||||
private func loadFileForImageTimelineItem(_ timelineItem: ImageRoomTimelineItem) async {
|
||||
if timelineItem.cachedFileURL != nil {
|
||||
// already cached
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = timelineItem.source else {
|
||||
return
|
||||
}
|
||||
|
||||
// This is not great. We could better estimate file extension from the mimetype.
|
||||
guard let fileExtension = timelineItem.body.split(separator: ".").last else {
|
||||
return
|
||||
}
|
||||
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
|
||||
case .success(let fileURL):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? ImageRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.cachedFileURL = fileURL
|
||||
timelineItems[index] = item
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadFileForTimelineItem(_ timelineItem: FileRoomTimelineItem) async {
|
||||
if timelineItem.cachedFileURL != nil {
|
||||
// already cached
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = timelineItem.source else {
|
||||
return
|
||||
}
|
||||
|
||||
// This is not great. We could better estimate file extension from the mimetype.
|
||||
guard let fileExtension = timelineItem.body.split(separator: ".").last else {
|
||||
return
|
||||
}
|
||||
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
|
||||
case .success(let fileURL):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? FileRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.cachedFileURL = fileURL
|
||||
timelineItems[index] = item
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAudioForTimelineItem(_ timelineItem: AudioRoomTimelineItem) async {
|
||||
if timelineItem.cachedAudioURL != nil {
|
||||
// already cached
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = timelineItem.source else {
|
||||
return
|
||||
}
|
||||
|
||||
// This is not great. We could better estimate file extension from the mimetype.
|
||||
guard let fileExtension = timelineItem.body.split(separator: ".").last else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
|
||||
case .success(let audioURL):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? AudioRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.cachedAudioURL = audioURL
|
||||
timelineItems[index] = item
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -25,8 +25,7 @@ enum RoomTimelineControllerCallback {
|
||||
}
|
||||
|
||||
enum RoomTimelineControllerAction {
|
||||
case displayVideo(videoURL: URL, title: String?)
|
||||
case displayFile(fileURL: URL, title: String?)
|
||||
case displayMediaFile(file: MediaFileHandleProxy, title: String?)
|
||||
case none
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ protocol MessageContentProtocol: RoomMessageEventContentProtocol, CustomStringCo
|
||||
|
||||
/// A timeline item that represents an `m.room.message` event.
|
||||
struct MessageTimelineItem<Content: MessageContentProtocol> {
|
||||
let item: MatrixRustSDK.EventTimelineItem
|
||||
let item: MatrixRustSDK.EventTimelineItemProtocol
|
||||
let content: Content
|
||||
|
||||
var id: String {
|
||||
@@ -82,7 +82,7 @@ extension MatrixRustSDK.ImageMessageContent: MessageContentProtocol { }
|
||||
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.image`.
|
||||
extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent {
|
||||
var source: MediaSourceProxy {
|
||||
.init(source: content.source)
|
||||
.init(source: content.source, mimeType: content.info?.mimetype)
|
||||
}
|
||||
|
||||
var width: CGFloat? {
|
||||
@@ -97,8 +97,8 @@ extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent
|
||||
content.info?.blurhash
|
||||
}
|
||||
|
||||
var type: UTType? {
|
||||
content.info?.mimetype.flatMap { UTType(mimeType: $0) }
|
||||
var contentType: UTType? {
|
||||
UTType(mimeType: content.info?.mimetype, fallbackFilename: content.body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,14 +107,14 @@ extension MatrixRustSDK.VideoMessageContent: MessageContentProtocol { }
|
||||
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.video`.
|
||||
extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent {
|
||||
var source: MediaSourceProxy {
|
||||
.init(source: content.source)
|
||||
.init(source: content.source, mimeType: content.info?.mimetype)
|
||||
}
|
||||
|
||||
var thumbnailSource: MediaSourceProxy? {
|
||||
guard let src = content.info?.thumbnailSource else {
|
||||
return nil
|
||||
}
|
||||
return .init(source: src)
|
||||
return .init(source: src, mimeType: content.info?.thumbnailInfo?.mimetype)
|
||||
}
|
||||
|
||||
var duration: UInt64 {
|
||||
@@ -132,6 +132,10 @@ extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent
|
||||
var blurhash: String? {
|
||||
content.info?.blurhash
|
||||
}
|
||||
|
||||
var contentType: UTType? {
|
||||
UTType(mimeType: content.info?.mimetype, fallbackFilename: content.body)
|
||||
}
|
||||
}
|
||||
|
||||
extension MatrixRustSDK.FileMessageContent: MessageContentProtocol { }
|
||||
@@ -139,14 +143,18 @@ extension MatrixRustSDK.FileMessageContent: MessageContentProtocol { }
|
||||
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.file`.
|
||||
extension MessageTimelineItem where Content == MatrixRustSDK.FileMessageContent {
|
||||
var source: MediaSourceProxy {
|
||||
.init(source: content.source)
|
||||
.init(source: content.source, mimeType: content.info?.mimetype)
|
||||
}
|
||||
|
||||
var thumbnailSource: MediaSourceProxy? {
|
||||
guard let src = content.info?.thumbnailSource else {
|
||||
return nil
|
||||
}
|
||||
return .init(source: src)
|
||||
return .init(source: src, mimeType: content.info?.thumbnailInfo?.mimetype)
|
||||
}
|
||||
|
||||
var contentType: UTType? {
|
||||
UTType(mimeType: content.info?.mimetype, fallbackFilename: content.body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,10 +163,14 @@ extension MatrixRustSDK.AudioMessageContent: MessageContentProtocol { }
|
||||
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.audio`.
|
||||
extension MessageTimelineItem where Content == MatrixRustSDK.AudioMessageContent {
|
||||
var source: MediaSourceProxy {
|
||||
.init(source: content.source)
|
||||
.init(source: content.source, mimeType: content.info?.mimetype)
|
||||
}
|
||||
|
||||
var duration: UInt64 {
|
||||
content.info?.duration ?? 0
|
||||
}
|
||||
|
||||
var contentType: UTType? {
|
||||
UTType(mimeType: content.info?.mimetype, fallbackFilename: content.body)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct AudioRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||
let id: String
|
||||
@@ -26,7 +27,7 @@ struct AudioRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
||||
|
||||
let duration: UInt64
|
||||
let source: MediaSourceProxy?
|
||||
var cachedAudioURL: URL?
|
||||
var contentType: UTType?
|
||||
|
||||
var properties = RoomTimelineItemProperties()
|
||||
}
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||
let id: String
|
||||
@@ -27,7 +28,7 @@ struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hasha
|
||||
|
||||
let source: MediaSourceProxy?
|
||||
let thumbnailSource: MediaSourceProxy?
|
||||
var cachedFileURL: URL?
|
||||
var contentType: UTType?
|
||||
|
||||
var properties = RoomTimelineItemProperties()
|
||||
}
|
||||
|
||||
+1
-2
@@ -27,13 +27,12 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
||||
let sender: TimelineItemSender
|
||||
|
||||
let source: MediaSourceProxy?
|
||||
var cachedFileURL: URL?
|
||||
|
||||
var width: CGFloat?
|
||||
var height: CGFloat?
|
||||
var aspectRatio: CGFloat?
|
||||
var blurhash: String?
|
||||
var type: UTType?
|
||||
var contentType: UTType?
|
||||
|
||||
var properties = RoomTimelineItemProperties()
|
||||
}
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||
let id: String
|
||||
@@ -28,12 +29,12 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
||||
let duration: UInt64
|
||||
let source: MediaSourceProxy?
|
||||
let thumbnailSource: MediaSourceProxy?
|
||||
var cachedVideoURL: URL?
|
||||
|
||||
var width: CGFloat?
|
||||
var height: CGFloat?
|
||||
var aspectRatio: CGFloat?
|
||||
var blurhash: String?
|
||||
var contentType: UTType?
|
||||
|
||||
var properties = RoomTimelineItemProperties()
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
height: message.height,
|
||||
aspectRatio: aspectRatio,
|
||||
blurhash: message.blurhash,
|
||||
type: message.type,
|
||||
contentType: message.contentType,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus))
|
||||
@@ -250,6 +250,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
height: message.height,
|
||||
aspectRatio: aspectRatio,
|
||||
blurhash: message.blurhash,
|
||||
contentType: message.contentType,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus))
|
||||
@@ -265,7 +266,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: eventItemProxy.sender,
|
||||
duration: message.duration,
|
||||
source: message.source)
|
||||
source: message.source,
|
||||
contentType: message.contentType,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus))
|
||||
}
|
||||
|
||||
private func buildFileTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
@@ -279,6 +284,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
sender: eventItemProxy.sender,
|
||||
source: message.source,
|
||||
thumbnailSource: message.thumbnailSource,
|
||||
contentType: message.contentType,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus))
|
||||
|
||||
Reference in New Issue
Block a user