Handle media source validation more gracefully. (#3571)

* Handle media source validation more gracefully.

* Fix unit tests.
This commit is contained in:
Doug
2024-11-29 12:42:27 +00:00
committed by GitHub
parent c8627cfd64
commit 49a94e0bdb
13 changed files with 95 additions and 90 deletions
@@ -21,7 +21,7 @@ enum LoadableImageMediaType: Equatable {
}
struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
private let mediaSource: MediaSourceProxy
private let mediaSource: MediaSourceProxy?
private let mediaType: LoadableImageMediaType
private let blurhash: String?
private let size: CGSize?
@@ -60,27 +60,31 @@ struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
mediaProvider: MediaProviderProtocol?,
transformer: @escaping (AnyView) -> TransformerView = { $0 },
placeholder: @escaping () -> PlaceholderView) {
self.init(mediaSource: MediaSourceProxy(url: url, mimeType: nil),
mediaType: mediaType,
blurhash: blurhash,
size: size,
mediaProvider: mediaProvider,
transformer: transformer,
placeholder: placeholder)
mediaSource = try? MediaSourceProxy(url: url, mimeType: nil)
self.mediaType = mediaType
self.blurhash = blurhash
self.size = size
self.mediaProvider = mediaProvider
self.transformer = transformer
self.placeholder = placeholder
}
var body: some View {
LoadableImageContent(mediaSource: mediaSource,
mediaType: mediaType,
blurhash: blurhash,
size: size,
mediaProvider: mediaProvider,
transformer: transformer,
placeholder: placeholder)
.id(stableMediaIdentifier)
if let mediaSource {
LoadableImageContent(mediaSource: mediaSource,
mediaType: mediaType,
blurhash: blurhash,
size: size,
mediaProvider: mediaProvider,
transformer: transformer,
placeholder: placeholder)
.id(stableMediaIdentifier)
} else {
placeholder()
}
}
private var stableMediaIdentifier: String {
private var stableMediaIdentifier: String? {
switch mediaType {
case .timelineItem(let uniqueID):
// Consider media for the same item to be the same view
@@ -88,7 +92,7 @@ struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
default:
// Binds the lifecycle of the LoadableImage to the associated URL.
// This fixes the problem of the cache returning old values after a change in the URL.
mediaSource.url.absoluteString
mediaSource?.url.absoluteString
}
}
}
@@ -363,7 +363,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
// We don't actually know the mime type here, assume it's an image.
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) {
if let mediaSource = try? MediaSourceProxy(url: url, mimeType: "image/jpeg"),
case let .success(file) = await mediaProvider.loadFileFromSource(mediaSource) {
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomProxy.infoPublisher.value.displayName)
}
}
@@ -165,7 +165,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
defer { userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) }
// We don't actually know the mime type here, assume it's an image.
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) {
if let mediaSource = try? MediaSourceProxy(url: url, mimeType: "image/jpeg"),
case let .success(file) = await mediaProvider.loadFileFromSource(mediaSource) {
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomMemberProxy.displayName)
}
}
@@ -104,7 +104,8 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
defer { hideLoadingIndicator() }
// We don't actually know the mime type here, assume it's an image.
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) {
if let mediaSource = try? MediaSourceProxy(url: url, mimeType: "image/jpeg"),
case let .success(file) = await mediaProvider.loadFileFromSource(mediaSource) {
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: userProfile.displayName)
}
}
@@ -23,11 +23,8 @@ struct MediaSourceProxy: Hashable {
self.mimeType = mimeType
}
init(url: URL, mimeType: String?) {
guard let mediaSource = try? MediaSource.fromUrl(url: url.absoluteString) else {
fatalError("Unable to create MediaSource from URL: \(url.absoluteString)")
}
underlyingSource = mediaSource
init(url: URL, mimeType: String?) throws {
underlyingSource = try MediaSource.fromUrl(url: url.absoluteString)
self.url = URL(string: underlyingSource.url())
self.mimeType = mimeType
}
@@ -55,7 +55,7 @@ struct NotificationItemProxy: NotificationItemProxyProtocol {
var senderAvatarMediaSource: MediaSourceProxy? {
if let senderAvatarURLString = notificationItem.senderInfo.avatarUrl,
let senderAvatarURL = URL(string: senderAvatarURLString) {
return MediaSourceProxy(url: senderAvatarURL, mimeType: nil)
return try? MediaSourceProxy(url: senderAvatarURL, mimeType: nil)
}
return nil
}
@@ -63,7 +63,7 @@ struct NotificationItemProxy: NotificationItemProxyProtocol {
var roomAvatarMediaSource: MediaSourceProxy? {
if let roomAvatarURLString = notificationItem.roomInfo.avatarUrl,
let roomAvatarURL = URL(string: roomAvatarURLString) {
return MediaSourceProxy(url: roomAvatarURL, mimeType: nil)
return try? MediaSourceProxy(url: roomAvatarURL, mimeType: nil)
}
return nil
}
@@ -195,9 +195,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
if let avatarURL {
let mediaSource = MediaSourceProxy(url: avatarURL, mimeType: nil)
if let avatarURL, let mediaSource = try? MediaSourceProxy(url: avatarURL, mimeType: nil) {
if case let .success(avatarData) = await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 100, height: 100)) {
sendMessageIntent.setImage(INImage(imageData: avatarData), forParameterNamed: \.speakableGroupName)
} else {
@@ -243,11 +243,15 @@ struct VideoInfoProxy: Hashable {
}
static var mockVideo: VideoInfoProxy {
.init(source: .init(url: .mockMXCVideo, mimeType: nil),
duration: 100,
size: .init(width: 1920, height: 1080),
aspectRatio: 1.78,
mimeType: nil)
guard let mediaSource = try? MediaSourceProxy(url: .mockMXCVideo, mimeType: nil) else {
fatalError("Invalid mock media source URL")
}
return .init(source: mediaSource,
duration: 100,
size: .init(width: 1920, height: 1080),
aspectRatio: 1.78,
mimeType: nil)
}
}
@@ -269,10 +273,6 @@ struct ImageInfoProxy: Hashable {
self.init(source: .init(source: source, mimeType: mimeType), width: width, height: height, mimeType: mimeType)
}
init(url: URL, width: UInt64?, height: UInt64?, mimeType: String?) {
self.init(source: .init(url: url, mimeType: mimeType), width: width, height: height, mimeType: mimeType)
}
init(source: MediaSourceProxy, width: UInt64?, height: UInt64?, mimeType: String?) {
self.source = source
@@ -292,17 +292,25 @@ struct ImageInfoProxy: Hashable {
}
static var mockImage: ImageInfoProxy {
.init(source: .init(url: .mockMXCImage, mimeType: "image/png"),
size: .init(width: 100, height: 100),
aspectRatio: 1,
mimeType: "image/png")
guard let mediaSource = try? MediaSourceProxy(url: .mockMXCImage, mimeType: "image/png") else {
fatalError("Invalid mock media source URL")
}
return .init(source: mediaSource,
size: .init(width: 100, height: 100),
aspectRatio: 1,
mimeType: "image/png")
}
static var mockThumbnail: ImageInfoProxy {
.init(source: .init(url: .mockMXCImage, mimeType: nil),
size: nil,
aspectRatio: nil,
mimeType: nil)
guard let mediaSource = try? MediaSourceProxy(url: .mockMXCImage, mimeType: "image/png") else {
fatalError("Invalid mock media source URL")
}
return .init(source: mediaSource,
size: nil,
aspectRatio: nil,
mimeType: nil)
}
}
@@ -33,12 +33,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
case .redactedMessage:
return buildRedactedTimelineItem(eventItemProxy, isOutgoing)
case .sticker(let body, let imageInfo, let mediaSource):
guard let url = URL(string: mediaSource.url()) else {
MXLog.error("Invalid sticker url string: \(mediaSource.url())")
return buildUnsupportedTimelineItem(eventItemProxy, "m.sticker", "Invalid Sticker URL", isOutgoing)
}
return buildStickerTimelineItem(eventItemProxy, body, imageInfo, url, isOutgoing)
return buildStickerTimelineItem(eventItemProxy, body, imageInfo, mediaSource, isOutgoing)
case .failedToParseMessageLike(let eventType, let error):
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing)
case .failedToParseState(let eventType, _, let error):
@@ -119,9 +114,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildStickerTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ body: String,
_ info: MatrixRustSDK.ImageInfo,
_ imageURL: URL,
_ mediaSource: MediaSource,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
let imageInfo = ImageInfoProxy(url: imageURL, width: info.width, height: info.height, mimeType: info.mimetype)
let imageInfo = ImageInfoProxy(source: mediaSource, width: info.width, height: info.height, mimeType: info.mimetype)
return StickerRoomTimelineItem(id: eventItemProxy.id,
body: body,