Group image and video metadata in specialised structs (#3518)

This commit is contained in:
Stefan Ceriu
2024-11-15 15:00:27 +02:00
committed by GitHub
parent 4ea276dc99
commit ef288a9662
27 changed files with 232 additions and 214 deletions
@@ -10,18 +10,18 @@ import SwiftUI
extension View { extension View {
/// Constrains the max height of a media item in the timeline, whilst preserving its aspect ratio. /// Constrains the max height of a media item in the timeline, whilst preserving its aspect ratio.
@ViewBuilder @ViewBuilder
func timelineMediaFrame(height contentHeight: CGFloat?, aspectRatio contentAspectRatio: CGFloat?) -> some View { func timelineMediaFrame(imageInfo: ImageInfoProxy?) -> some View {
let defaultMediaSize = 100.0 let defaultMediaSize = 100.0
let minMediaHeight = 100.0 let minMediaHeight = 100.0
let maxMediaHeight = 300.0 let maxMediaHeight = 300.0
if let contentHeight, contentHeight < minMediaHeight { // Special case very small images if let contentHeight = imageInfo?.size?.height, contentHeight < minMediaHeight { // Special case very small images
aspectRatio(contentAspectRatio, contentMode: .fit) aspectRatio(imageInfo?.aspectRatio, contentMode: .fit)
.frame(minHeight: minMediaHeight, maxHeight: minMediaHeight) .frame(minHeight: minMediaHeight, maxHeight: minMediaHeight)
} else { } else {
if let contentAspectRatio { if let contentAspectRatio = imageInfo?.aspectRatio {
aspectRatio(contentAspectRatio, contentMode: .fit) aspectRatio(contentAspectRatio, contentMode: .fit)
.frame(maxHeight: min(maxMediaHeight, max(minMediaHeight, contentHeight ?? .infinity))) .frame(maxHeight: min(maxMediaHeight, max(minMediaHeight, imageInfo?.size?.height ?? .infinity)))
// Required to prevent the reply details to get higher priority in rendering the width of the view. // Required to prevent the reply details to get higher priority in rendering the width of the view.
.aspectRatio(contentAspectRatio, contentMode: .fit) .aspectRatio(contentAspectRatio, contentMode: .fit)
} else { // Otherwise force the image to be `defaultMediaSize` x `defaultMediaSize` } else { // Otherwise force the image to be `defaultMediaSize` x `defaultMediaSize`
@@ -226,8 +226,8 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
eventID: "123", eventID: "123",
eventContent: .message(.image(.init(filename: "head.png", eventContent: .message(.image(.init(filename: "head.png",
caption: "Image: Pushead", caption: "Image: Pushead",
source: .init(url: .picturesDirectory, mimeType: nil), imageInfo: .mockImage,
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))), thumbnailInfo: .mockThumbnail)))),
.loaded(sender: .init(id: "Jason"), .loaded(sender: .init(id: "Jason"),
eventID: "123", eventID: "123",
eventContent: .message(.notice(.init(body: "Notice: Too far gone?")))), eventContent: .message(.notice(.init(body: "Notice: Too far gone?")))),
@@ -238,9 +238,8 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
eventID: "123", eventID: "123",
eventContent: .message(.video(.init(filename: "never.mov", eventContent: .message(.video(.init(filename: "never.mov",
caption: "Video: Through the never", caption: "Video: Through the never",
duration: 100, videoInfo: .mockVideo,
source: nil, thumbnailInfo: .mockThumbnail)))),
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))),
.loading(eventID: "") .loading(eventID: "")
] ]
@@ -553,11 +553,11 @@ class TimelineInteractionHandler {
switch timelineItem { switch timelineItem {
case let item as ImageRoomTimelineItem: case let item as ImageRoomTimelineItem:
source = item.content.source source = item.content.imageInfo.source
filename = item.content.filename filename = item.content.filename
caption = item.content.caption caption = item.content.caption
case let item as VideoRoomTimelineItem: case let item as VideoRoomTimelineItem:
source = item.content.source source = item.content.videoInfo.source
filename = item.content.filename filename = item.content.filename
caption = item.content.caption caption = item.content.caption
case let item as FileRoomTimelineItem: case let item as FileRoomTimelineItem:
@@ -42,7 +42,7 @@ struct TimelineReplyView: View {
ReplyView(sender: sender, ReplyView(sender: sender,
plainBody: content.caption ?? content.filename, plainBody: content.caption ?? content.filename,
formattedBody: content.formattedCaption, formattedBody: content.formattedCaption,
icon: .init(kind: .mediaSource(content.thumbnailSource ?? content.source), cornerRadii: iconCornerRadii)) icon: .init(kind: .mediaSource(content.thumbnailInfo?.source ?? content.imageInfo.source), cornerRadii: iconCornerRadii))
case .notice(let content): case .notice(let content):
ReplyView(sender: sender, ReplyView(sender: sender,
plainBody: content.body, plainBody: content.body,
@@ -55,7 +55,7 @@ struct TimelineReplyView: View {
ReplyView(sender: sender, ReplyView(sender: sender,
plainBody: content.caption ?? content.filename, plainBody: content.caption ?? content.filename,
formattedBody: content.formattedCaption, formattedBody: content.formattedCaption,
icon: content.thumbnailSource.map { .init(kind: .mediaSource($0), cornerRadii: iconCornerRadii) }) icon: content.thumbnailInfo.map { .init(kind: .mediaSource($0.source), cornerRadii: iconCornerRadii) })
case .voice: case .voice:
ReplyView(sender: sender, ReplyView(sender: sender,
plainBody: L10n.commonVoiceMessage, plainBody: L10n.commonVoiceMessage,
@@ -222,9 +222,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
}() }()
static var previewItems: [TimelineReplyView] { static var previewItems: [TimelineReplyView] {
let imageSource = MediaSourceProxy(url: "https://mock.com", mimeType: "image/png") [
return [
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .notLoaded(eventID: "")), TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .notLoaded(eventID: "")),
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loading(eventID: "")), TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loading(eventID: "")),
@@ -268,17 +266,16 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
eventID: "123", eventID: "123",
eventContent: .message(.image(.init(filename: "image.jpg", eventContent: .message(.image(.init(filename: "image.jpg",
caption: "Some image", caption: "Some image",
source: imageSource, imageInfo: .mockImage,
thumbnailSource: imageSource))))), thumbnailInfo: .mockThumbnail))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123", eventID: "123",
eventContent: .message(.video(.init(filename: "video.mp4", eventContent: .message(.video(.init(filename: "video.mp4",
caption: "Some video", caption: "Some video",
duration: 0, videoInfo: .mockVideo,
source: nil, thumbnailInfo: .mockThumbnail))))),
thumbnailSource: imageSource))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123", eventID: "123",
@@ -321,7 +318,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
} }
.padding() .padding()
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
// Allow member names to load. Reduce precission as the `imageSource` randomly renders slightly differently // Allow member names to load. Reduce precission as the `mockThumbnail` randomly renders slightly differently
.snapshotPreferences(delay: 0.2, precision: 0.98) .snapshotPreferences(delay: 0.2, precision: 0.98)
.previewLayout(.sizeThatFits) .previewLayout(.sizeThatFits)
} }
@@ -498,8 +498,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isThreaded: false, isThreaded: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(filename: "other.png", content: .init(filename: "other.png",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), imageInfo: .mockImage,
thumbnailSource: nil), thumbnailInfo: nil),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray)))) properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))))
@@ -577,8 +577,8 @@ private struct MockTimelineContent: View {
isThreaded: isThreaded, isThreaded: isThreaded,
sender: .init(id: ""), sender: .init(id: ""),
content: .init(filename: "image.jpg", content: .init(filename: "image.jpg",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), imageInfo: .mockImage,
thumbnailSource: nil), thumbnailInfo: nil),
replyDetails: replyDetails)) replyDetails: replyDetails))
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(), LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
@@ -40,25 +40,23 @@ struct ImageRoomTimelineView: View {
@ViewBuilder @ViewBuilder
private var loadableImage: some View { private var loadableImage: some View {
if timelineItem.content.contentType == .gif { if timelineItem.content.contentType == .gif {
LoadableImage(mediaSource: timelineItem.content.source, LoadableImage(mediaSource: timelineItem.content.imageInfo.source,
mediaType: .timelineItem, mediaType: .timelineItem,
blurhash: timelineItem.content.blurhash, blurhash: timelineItem.content.blurhash,
size: timelineItem.content.size, size: timelineItem.content.imageInfo.size,
mediaProvider: context.mediaProvider) { mediaProvider: context.mediaProvider) {
placeholder placeholder
} }
.timelineMediaFrame(height: timelineItem.content.thumbnailSize?.height, .timelineMediaFrame(imageInfo: timelineItem.content.imageInfo)
aspectRatio: timelineItem.content.aspectRatio)
} else { } else {
LoadableImage(mediaSource: timelineItem.content.thumbnailSource ?? timelineItem.content.source, LoadableImage(mediaSource: timelineItem.content.thumbnailInfo?.source ?? timelineItem.content.imageInfo.source,
mediaType: .timelineItem, mediaType: .timelineItem,
blurhash: timelineItem.content.blurhash, blurhash: timelineItem.content.blurhash,
size: timelineItem.content.thumbnailSize ?? timelineItem.content.size, size: timelineItem.content.thumbnailInfo?.size ?? timelineItem.content.imageInfo.size,
mediaProvider: context.mediaProvider) { mediaProvider: context.mediaProvider) {
placeholder placeholder
} }
.timelineMediaFrame(height: timelineItem.content.thumbnailSize?.height ?? timelineItem.content.size?.height, .timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo ?? timelineItem.content.imageInfo)
aspectRatio: timelineItem.content.thumbnailAspectRatio ?? timelineItem.content.aspectRatio)
} }
} }
@@ -87,8 +85,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isThreaded: false, isThreaded: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(filename: "image.jpg", content: .init(filename: "image.jpg",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/jpg"), imageInfo: .mockImage,
thumbnailSource: nil))) thumbnailInfo: nil)))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent, ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
@@ -98,8 +96,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isThreaded: false, isThreaded: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(filename: "other.png", content: .init(filename: "other.png",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), imageInfo: .mockImage,
thumbnailSource: nil))) thumbnailInfo: nil)))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent, ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
@@ -109,9 +107,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isThreaded: false, isThreaded: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(filename: "Blurhashed.jpg", content: .init(filename: "Blurhashed.jpg",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), imageInfo: .mockImage,
aspectRatio: 0.7, thumbnailInfo: nil,
thumbnailSource: nil,
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW", blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW",
contentType: .gif))) contentType: .gif)))
@@ -124,10 +121,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(filename: "Blurhashed.jpg", content: .init(filename: "Blurhashed.jpg",
caption: "This is a great image 😎", caption: "This is a great image 😎",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), imageInfo: .mockImage,
size: .init(width: 50, height: 50), thumbnailInfo: .mockThumbnail,
aspectRatio: 1,
thumbnailSource: nil,
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW", blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW",
contentType: .gif))) contentType: .gif)))
} }
@@ -14,15 +14,14 @@ struct StickerRoomTimelineView: View {
var body: some View { var body: some View {
TimelineStyler(timelineItem: timelineItem) { TimelineStyler(timelineItem: timelineItem) {
LoadableImage(url: timelineItem.imageURL, LoadableImage(mediaSource: timelineItem.imageInfo.source,
mediaType: .timelineItem, mediaType: .timelineItem,
blurhash: timelineItem.blurhash, blurhash: timelineItem.blurhash,
size: timelineItem.size, size: timelineItem.imageInfo.size,
mediaProvider: context.mediaProvider) { mediaProvider: context.mediaProvider) {
placeholder placeholder
} }
.timelineMediaFrame(height: timelineItem.size?.height, .timelineMediaFrame(imageInfo: timelineItem.imageInfo)
aspectRatio: timelineItem.aspectRatio)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel("\(L10n.commonSticker), \(timelineItem.body)") .accessibilityLabel("\(L10n.commonSticker), \(timelineItem.body)")
} }
@@ -51,7 +50,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isEditable: false, isEditable: false,
canBeRepliedTo: true, canBeRepliedTo: true,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory)) imageInfo: .mockImage))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent, StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent,
body: "Some other image", body: "Some other image",
@@ -60,7 +59,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isEditable: false, isEditable: false,
canBeRepliedTo: true, canBeRepliedTo: true,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory)) imageInfo: .mockImage))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent, StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent,
body: "Blurhashed image", body: "Blurhashed image",
@@ -69,8 +68,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isEditable: false, isEditable: false,
canBeRepliedTo: true, canBeRepliedTo: true,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory, imageInfo: .mockImage,
aspectRatio: 0.7,
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW")) blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
} }
} }
@@ -18,8 +18,7 @@ struct VideoRoomTimelineView: View {
TimelineStyler(timelineItem: timelineItem) { TimelineStyler(timelineItem: timelineItem) {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
thumbnail thumbnail
.timelineMediaFrame(height: timelineItem.content.thumbnailSize?.height, .timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo)
aspectRatio: timelineItem.content.aspectRatio)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel(L10n.commonVideo) .accessibilityLabel(L10n.commonVideo)
// This clip shape is distinct from the one in the styler as that one // This clip shape is distinct from the one in the styler as that one
@@ -41,11 +40,11 @@ struct VideoRoomTimelineView: View {
@ViewBuilder @ViewBuilder
var thumbnail: some View { var thumbnail: some View {
if let thumbnailSource = timelineItem.content.thumbnailSource { if let thumbnailSource = timelineItem.content.thumbnailInfo?.source {
LoadableImage(mediaSource: thumbnailSource, LoadableImage(mediaSource: thumbnailSource,
mediaType: .timelineItem, mediaType: .timelineItem,
blurhash: timelineItem.content.blurhash, blurhash: timelineItem.content.blurhash,
size: timelineItem.content.thumbnailSize, size: timelineItem.content.thumbnailInfo?.size,
mediaProvider: context.mediaProvider) { imageView in mediaProvider: context.mediaProvider) { imageView in
imageView imageView
.overlay { playIcon } .overlay { playIcon }
@@ -89,9 +88,8 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isThreaded: false, isThreaded: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(filename: "video.mp4", content: .init(filename: "video.mp4",
duration: 21, videoInfo: .mockVideo,
source: nil, thumbnailInfo: nil)))
thumbnailSource: nil)))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent, VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
@@ -101,9 +99,8 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isThreaded: false, isThreaded: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(filename: "other.mp4", content: .init(filename: "other.mp4",
duration: 22, videoInfo: .mockVideo,
source: nil, thumbnailInfo: nil)))
thumbnailSource: nil)))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent, VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
@@ -113,10 +110,8 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isThreaded: false, isThreaded: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(filename: "Blurhashed.mp4", content: .init(filename: "Blurhashed.mp4",
duration: 23, videoInfo: .mockVideo,
source: nil, thumbnailInfo: nil,
aspectRatio: 0.7,
thumbnailSource: nil,
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))) blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW")))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent, VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent,
@@ -128,9 +123,8 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(filename: "video.mp4", content: .init(filename: "video.mp4",
caption: "This is a caption", caption: "This is a caption",
duration: 21, videoInfo: .mockVideo,
source: nil, thumbnailInfo: nil)))
thumbnailSource: nil)))
} }
} }
} }
@@ -258,11 +258,8 @@ enum RoomTimelineItemFixtures {
isThreaded: false, isThreaded: false,
sender: .init(id: ""), sender: .init(id: ""),
content: .init(filename: "video.mp4", content: .init(filename: "video.mp4",
duration: 100, videoInfo: .mockVideo,
source: .init(url: .picturesDirectory, mimeType: nil), thumbnailInfo: .mockThumbnail,
size: .init(width: 1920, height: 1080),
aspectRatio: 1.78,
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil),
blurhash: "KtI~70X5V?yss9oyrYs:t6")), blurhash: "KtI~70X5V?yss9oyrYs:t6")),
ImageRoomTimelineItem(id: .randomEvent, ImageRoomTimelineItem(id: .randomEvent,
timestamp: "10:47 am", timestamp: "10:47 am",
@@ -272,10 +269,8 @@ enum RoomTimelineItemFixtures {
isThreaded: false, isThreaded: false,
sender: .init(id: ""), sender: .init(id: ""),
content: .init(filename: "image.jpg", content: .init(filename: "image.jpg",
source: .init(url: .picturesDirectory, mimeType: nil), imageInfo: .mockImage,
size: .init(width: 5120, height: 3412), thumbnailInfo: nil,
aspectRatio: 1.5,
thumbnailSource: nil,
blurhash: "KpE4oyayR5|GbHb];3j@of")) blurhash: "KpE4oyayR5|GbHb];3j@of"))
] ]
} }
@@ -215,11 +215,111 @@ struct SendHandleProxy: Hashable {
} }
} }
extension Receipt { struct VideoInfoProxy: Hashable {
var dateTimestamp: Date? { let source: MediaSourceProxy
guard let timestamp else { private(set) var duration: TimeInterval
return nil private(set) var size: CGSize?
} private(set) var aspectRatio: CGFloat?
return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000)) private(set) var mimeType: String?
init(source: MediaSource, duration: TimeInterval, width: UInt64?, height: UInt64?, mimeType: String?) {
self.source = MediaSourceProxy(source: source, mimeType: mimeType)
self.duration = duration
let mediaInfo = MediaInfoProxy(width: width, height: height, mimeType: mimeType)
size = mediaInfo.size
aspectRatio = mediaInfo.aspectRatio
self.mimeType = mediaInfo.mimeType
}
// MARK: - Mocks
private init(source: MediaSourceProxy, duration: TimeInterval, size: CGSize?, aspectRatio: CGFloat?, mimeType: String?) {
self.source = source
self.duration = duration
self.size = size
self.aspectRatio = aspectRatio
self.mimeType = mimeType
}
static var mockVideo: VideoInfoProxy {
.init(source: .init(url: .picturesDirectory, mimeType: nil),
duration: 100,
size: .init(width: 1920, height: 1080),
aspectRatio: 1.78,
mimeType: nil)
}
}
struct ImageInfoProxy: Hashable {
let source: MediaSourceProxy
private(set) var size: CGSize?
private(set) var aspectRatio: CGFloat?
private(set) var mimeType: String?
init?(source: MediaSource?, width: UInt64?, height: UInt64?, mimeType: String?) {
guard let source else {
return nil
}
self.init(source: .init(source: source, mimeType: mimeType), width: width, height: height, mimeType: mimeType)
}
init(source: MediaSource, width: UInt64?, height: UInt64?, mimeType: String?) {
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
let mediaInfo = MediaInfoProxy(width: width, height: height, mimeType: mimeType)
size = mediaInfo.size
aspectRatio = mediaInfo.aspectRatio
self.mimeType = mediaInfo.mimeType
}
// MARK: - Mocks
private init(source: MediaSourceProxy, size: CGSize?, aspectRatio: CGFloat?, mimeType: String?) {
self.source = source
self.size = size
self.aspectRatio = aspectRatio
self.mimeType = mimeType
}
static var mockImage: ImageInfoProxy {
.init(source: .init(url: .picturesDirectory, mimeType: "image/png"),
size: .init(width: 100, height: 100),
aspectRatio: 1,
mimeType: "image/png")
}
static var mockThumbnail: ImageInfoProxy {
.init(source: .init(url: .picturesDirectory, mimeType: nil),
size: nil,
aspectRatio: nil,
mimeType: nil)
}
}
struct MediaInfoProxy: Hashable {
private(set) var size: CGSize?
private(set) var mimeType: String?
private(set) var aspectRatio: CGFloat?
init(width: UInt64?, height: UInt64?, mimeType: String?) {
if let width, let height {
size = .init(width: CGFloat(width), height: CGFloat(height))
if width > 0, height > 0 {
aspectRatio = CGFloat(width) / CGFloat(height)
}
}
self.mimeType = mimeType
} }
} }
@@ -14,13 +14,9 @@ struct ImageRoomTimelineItemContent: Hashable {
var formattedCaption: AttributedString? var formattedCaption: AttributedString?
/// The original textual representation of the formatted caption directly from the event (usually HTML code) /// The original textual representation of the formatted caption directly from the event (usually HTML code)
var formattedCaptionHTMLString: String? var formattedCaptionHTMLString: String?
let source: MediaSourceProxy
var size: CGSize?
var aspectRatio: CGFloat?
let thumbnailSource: MediaSourceProxy? let imageInfo: ImageInfoProxy
var thumbnailSize: CGSize? let thumbnailInfo: ImageInfoProxy?
var thumbnailAspectRatio: CGFloat?
var blurhash: String? var blurhash: String?
var contentType: UTType? var contentType: UTType?
@@ -14,15 +14,9 @@ struct VideoRoomTimelineItemContent: Hashable {
var formattedCaption: AttributedString? var formattedCaption: AttributedString?
/// The original textual representation of the formatted caption directly from the event (usually HTML code) /// The original textual representation of the formatted caption directly from the event (usually HTML code)
var formattedCaptionHTMLString: String? var formattedCaptionHTMLString: String?
let duration: TimeInterval
let source: MediaSourceProxy? let videoInfo: VideoInfoProxy
var size: CGSize? let thumbnailInfo: ImageInfoProxy?
var aspectRatio: CGFloat?
let thumbnailSource: MediaSourceProxy?
var thumbnailSize: CGSize?
var thumbnailAspectRatio: CGFloat?
var blurhash: String? var blurhash: String?
var contentType: UTType? var contentType: UTType?
@@ -17,10 +17,8 @@ struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let sender: TimelineItemSender let sender: TimelineItemSender
let imageURL: URL let imageInfo: ImageInfoProxy
var size: CGSize?
var aspectRatio: CGFloat?
var blurhash: String? var blurhash: String?
var properties = RoomTimelineItemProperties() var properties = RoomTimelineItemProperties()
@@ -118,20 +118,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildStickerTimelineItem(_ eventItemProxy: EventTimelineItemProxy, private func buildStickerTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ body: String, _ body: String,
_ imageInfo: ImageInfo, _ info: MatrixRustSDK.ImageInfo,
_ imageURL: URL, _ imageURL: URL,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol { _ isOutgoing: Bool) -> RoomTimelineItemProtocol {
var imageSize: CGSize? let imageInfo = ImageInfoProxy(url: imageURL, width: info.width, height: info.height, mimeType: info.mimetype)
var imageAspectRatio: CGFloat?
if let height = imageInfo.height,
let width = imageInfo.width {
imageSize = .init(width: CGFloat(width), height: CGFloat(height))
if width > 0, height > 0 {
imageAspectRatio = CGFloat(width) / CGFloat(height)
}
}
return StickerRoomTimelineItem(id: eventItemProxy.id, return StickerRoomTimelineItem(id: eventItemProxy.id,
body: body, body: body,
@@ -140,10 +130,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
isEditable: eventItemProxy.isEditable, isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo, canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender, sender: eventItemProxy.sender,
imageURL: imageURL, imageInfo: imageInfo,
size: imageSize, blurhash: info.blurhash,
aspectRatio: imageAspectRatio,
blurhash: imageInfo.blurhash,
properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions), properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus, deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
@@ -522,91 +510,47 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
let htmlCaption = messageContent.formattedCaption?.format == .html ? messageContent.formattedCaption?.body : nil let htmlCaption = messageContent.formattedCaption?.format == .html ? messageContent.formattedCaption?.body : nil
let formattedCaption = htmlCaption != nil ? attributedStringBuilder.fromHTML(htmlCaption) : attributedStringBuilder.fromPlain(messageContent.caption) let formattedCaption = htmlCaption != nil ? attributedStringBuilder.fromHTML(htmlCaption) : attributedStringBuilder.fromPlain(messageContent.caption)
let thumbnailSource = messageContent.info?.thumbnailSource.map { MediaSourceProxy(source: $0, mimeType: messageContent.info?.thumbnailInfo?.mimetype) } let thumbnailInfo = ImageInfoProxy(source: messageContent.info?.thumbnailSource,
width: messageContent.info?.thumbnailInfo?.width,
height: messageContent.info?.thumbnailInfo?.height,
mimeType: messageContent.info?.thumbnailInfo?.mimetype)
var thumbnailSize: CGSize? let imageInfo = ImageInfoProxy(source: messageContent.source,
var thumbnailAspectRatio: CGFloat? width: messageContent.info?.width,
height: messageContent.info?.height,
if let thumbnailInfo = messageContent.info?.thumbnailInfo, mimeType: messageContent.info?.mimetype)
let height = thumbnailInfo.height,
let width = thumbnailInfo.width {
thumbnailSize = .init(width: CGFloat(width), height: CGFloat(height))
if width > 0, height > 0 {
thumbnailAspectRatio = CGFloat(width) / CGFloat(height)
}
}
var imageSize: CGSize?
var imageAspectRatio: CGFloat?
if let imageInfo = messageContent.info,
let height = imageInfo.height,
let width = imageInfo.width {
imageSize = .init(width: CGFloat(width), height: CGFloat(height))
if width > 0, height > 0 {
imageAspectRatio = CGFloat(width) / CGFloat(height)
}
}
return .init(filename: messageContent.filename, return .init(filename: messageContent.filename,
caption: messageContent.caption, caption: messageContent.caption,
formattedCaption: formattedCaption, formattedCaption: formattedCaption,
formattedCaptionHTMLString: htmlCaption, formattedCaptionHTMLString: htmlCaption,
source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype), imageInfo: imageInfo,
size: imageSize, thumbnailInfo: thumbnailInfo,
aspectRatio: imageAspectRatio,
thumbnailSource: thumbnailSource,
thumbnailSize: thumbnailSize,
thumbnailAspectRatio: thumbnailAspectRatio,
blurhash: messageContent.info?.blurhash, blurhash: messageContent.info?.blurhash,
contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename)) contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename))
} }
private func buildVideoTimelineItemContent(_ messageContent: VideoMessageContent) -> VideoRoomTimelineItemContent { private func buildVideoTimelineItemContent(_ messageContent: VideoMessageContent) -> VideoRoomTimelineItemContent {
let htmlCaption = messageContent.formattedCaption?.format == .html ? messageContent.formattedCaption?.body : nil let htmlCaption = messageContent.formattedCaption?.format == .html ? messageContent.formattedCaption?.body : nil
let formattedCaption = htmlCaption != nil ? attributedStringBuilder.fromHTML(htmlCaption) : attributedStringBuilder.fromPlain(messageContent.caption) let formattedCaption = htmlCaption != nil ? attributedStringBuilder.fromHTML(htmlCaption) : attributedStringBuilder.fromPlain(messageContent.caption)
let thumbnailSource = messageContent.info?.thumbnailSource.map { MediaSourceProxy(source: $0, mimeType: messageContent.info?.thumbnailInfo?.mimetype) } let thumbnailInfo = ImageInfoProxy(source: messageContent.info?.thumbnailSource,
width: messageContent.info?.thumbnailInfo?.width,
var thumbnailSize: CGSize? height: messageContent.info?.thumbnailInfo?.height,
var thumbnailAspectRatio: CGFloat? mimeType: messageContent.info?.thumbnailInfo?.mimetype)
if let thumbnailInfo = messageContent.info?.thumbnailInfo, let videoInfo = VideoInfoProxy(source: messageContent.source,
let height = thumbnailInfo.height, duration: messageContent.info?.duration ?? 0,
let width = thumbnailInfo.width { width: messageContent.info?.width,
thumbnailSize = .init(width: CGFloat(width), height: CGFloat(height)) height: messageContent.info?.height,
mimeType: messageContent.info?.mimetype)
if width > 0, height > 0 {
thumbnailAspectRatio = CGFloat(width) / CGFloat(height)
}
}
var videoSize: CGSize?
var videoAspectRatio: CGFloat?
if let videoInfo = messageContent.info,
let height = videoInfo.height,
let width = videoInfo.width {
videoSize = .init(width: CGFloat(width), height: CGFloat(height))
if width > 0, height > 0 {
videoAspectRatio = CGFloat(width) / CGFloat(height)
}
}
return .init(filename: messageContent.filename, return .init(filename: messageContent.filename,
caption: messageContent.caption, caption: messageContent.caption,
formattedCaption: formattedCaption, formattedCaption: formattedCaption,
formattedCaptionHTMLString: htmlCaption, formattedCaptionHTMLString: htmlCaption,
duration: messageContent.info?.duration ?? 0, videoInfo: videoInfo,
source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype), thumbnailInfo: thumbnailInfo,
size: videoSize,
aspectRatio: videoAspectRatio,
thumbnailSource: thumbnailSource,
thumbnailSize: thumbnailSize,
thumbnailAspectRatio: thumbnailAspectRatio,
blurhash: messageContent.info?.blurhash, blurhash: messageContent.info?.blurhash,
contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename)) contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename))
} }
@@ -821,3 +765,12 @@ private extension RepliedToEventDetails {
} }
} }
} }
private extension Receipt {
var dateTimestamp: Date? {
guard let timestamp else {
return nil
}
return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
}
}
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e96e572bd736c19bb9f3bd37216bd464276c9a1ebbd715fdae27973f9b6f36cc oid sha256:82ce96aa44eb950fb131e85d6ceeab485a5985fb48c30b4d954c65602e77686a
size 333847 size 281497
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e96e572bd736c19bb9f3bd37216bd464276c9a1ebbd715fdae27973f9b6f36cc oid sha256:82ce96aa44eb950fb131e85d6ceeab485a5985fb48c30b4d954c65602e77686a
size 333847 size 281497
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:13f6dfd3e07222ae3757076e453cf99695424db88c8cc85fede448ea64e70519 oid sha256:d08539366199fb29c4f2b01b6b399426b928a0eaae94306441d0a6cf34be4a13
size 276862 size 225250
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:13f6dfd3e07222ae3757076e453cf99695424db88c8cc85fede448ea64e70519 oid sha256:d08539366199fb29c4f2b01b6b399426b928a0eaae94306441d0a6cf34be4a13
size 276862 size 225250
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bd110afc7d00be27146e58474b810415af9dc7d5ed6faf1536419f01f41f9a3a oid sha256:26f2e07fa87d8ae6a910f30e992cf45e528715093663f40739e085c1349a6b31
size 404161 size 218285
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bd110afc7d00be27146e58474b810415af9dc7d5ed6faf1536419f01f41f9a3a oid sha256:26f2e07fa87d8ae6a910f30e992cf45e528715093663f40739e085c1349a6b31
size 404161 size 218285
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ef76e5d6f293ae298df0e70744ffed330e1fcb4a1e1157fbd7578e59bbe52fd4 oid sha256:23b75089dd40fc4c0c8cbaeb97f9659b19c22621133442e09ec7fc962cbae091
size 336667 size 167471
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ef76e5d6f293ae298df0e70744ffed330e1fcb4a1e1157fbd7578e59bbe52fd4 oid sha256:23b75089dd40fc4c0c8cbaeb97f9659b19c22621133442e09ec7fc962cbae091
size 336667 size 167471
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f0f9e472dd0a7675c25bc5856aca8f3e8ed94b157aa2dea3683e8a7cf17b588c oid sha256:ba0a7cbad0cae44f11230bfd240445eeca207ddf22d6e3b5882e69d448cf2a9b
size 94892 size 95167
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f0f9e472dd0a7675c25bc5856aca8f3e8ed94b157aa2dea3683e8a7cf17b588c oid sha256:ba0a7cbad0cae44f11230bfd240445eeca207ddf22d6e3b5882e69d448cf2a9b
size 94892 size 95167
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8e4e25d87b4335854f8cd67072ef0e8e89a50a9a56dbc10b6cf01ee725731767 oid sha256:d74e45a7d9ceb6b72d3c64aa300fbc122f18aef4c3b6cef630850c3cd242a8c4
size 52197 size 52016
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8e4e25d87b4335854f8cd67072ef0e8e89a50a9a56dbc10b6cf01ee725731767 oid sha256:d74e45a7d9ceb6b72d3c64aa300fbc122f18aef4c3b6cef630850c3cd242a8c4
size 52197 size 52016
+4 -5
View File
@@ -150,8 +150,8 @@ class LoggingTests: XCTestCase {
sender: .init(id: "sender"), sender: .init(id: "sender"),
content: .init(filename: "ImageString", content: .init(filename: "ImageString",
caption: "ImageString", caption: "ImageString",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), imageInfo: .mockImage,
thumbnailSource: nil)) thumbnailInfo: nil))
let videoMessage = VideoRoomTimelineItem(id: .randomEvent, let videoMessage = VideoRoomTimelineItem(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
@@ -161,9 +161,8 @@ class LoggingTests: XCTestCase {
sender: .init(id: "sender"), sender: .init(id: "sender"),
content: .init(filename: "VideoString", content: .init(filename: "VideoString",
caption: "VideoString", caption: "VideoString",
duration: 0, videoInfo: .mockVideo,
source: nil, thumbnailInfo: nil))
thumbnailSource: nil))
let fileMessage = FileRoomTimelineItem(id: .randomEvent, let fileMessage = FileRoomTimelineItem(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,