Update the SDK and use media filename and caption internally. (#3375)

Doesn't render captions (other than in fallback places).
This commit is contained in:
Doug
2024-10-08 11:00:58 +01:00
committed by GitHub
parent 794d0eead1
commit 6e51969d27
42 changed files with 581 additions and 358 deletions
@@ -532,30 +532,35 @@ class TimelineInteractionHandler {
private func displayMediaActionIfPossible(timelineItem: RoomTimelineItemProtocol) async -> RoomTimelineControllerAction {
var source: MediaSourceProxy?
var body: String
var filename: String
var caption: String?
switch timelineItem {
case let item as ImageRoomTimelineItem:
source = item.content.source
body = item.content.body
filename = item.content.filename
caption = item.content.caption
case let item as VideoRoomTimelineItem:
source = item.content.source
body = item.content.body
filename = item.content.filename
caption = item.content.caption
case let item as FileRoomTimelineItem:
source = item.content.source
body = item.content.body
filename = item.content.filename
caption = item.content.caption
case let item as AudioRoomTimelineItem:
// For now we are just displaying audio messages with the File preview until we create a timeline player for them.
source = item.content.source
body = item.content.body
filename = item.content.filename
caption = item.content.caption
default:
return .none
}
guard let source else { return .none }
switch await mediaProvider.loadFileFromSource(source, body: body) {
switch await mediaProvider.loadFileFromSource(source, filename: filename) {
case .success(let file):
return .displayMediaFile(file: file, title: body)
return .displayMediaFile(file: file, title: caption ?? filename)
case .failure:
return .none
}
@@ -26,8 +26,8 @@ struct TimelineReplyView: View {
switch content {
case .audio(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
plainBody: content.caption ?? content.filename,
formattedBody: content.formattedCaption,
icon: .init(kind: .systemIcon("waveform"), cornerRadii: iconCornerRadii))
case .emote(let content):
ReplyView(sender: sender,
@@ -35,13 +35,13 @@ struct TimelineReplyView: View {
formattedBody: content.formattedBody)
case .file(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
plainBody: content.caption ?? content.filename,
formattedBody: content.formattedCaption,
icon: .init(kind: .icon(\.document), cornerRadii: iconCornerRadii))
case .image(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
plainBody: content.caption ?? content.filename,
formattedBody: content.formattedCaption,
icon: .init(kind: .mediaSource(content.thumbnailSource ?? content.source), cornerRadii: iconCornerRadii))
case .notice(let content):
ReplyView(sender: sender,
@@ -53,8 +53,8 @@ struct TimelineReplyView: View {
formattedBody: content.formattedBody)
case .video(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
plainBody: content.caption ?? content.filename,
formattedBody: content.formattedCaption,
icon: content.thumbnailSource.map { .init(kind: .mediaSource($0), cornerRadii: iconCornerRadii) })
case .voice:
ReplyView(sender: sender,
@@ -247,7 +247,8 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.audio(.init(body: "Some audio",
eventContent: .message(.audio(.init(filename: "audio.m4a",
caption: "Some audio",
duration: 0,
waveform: nil,
source: nil,
@@ -256,7 +257,8 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.file(.init(body: "Some file",
eventContent: .message(.file(.init(filename: "file.txt",
caption: "Some file",
source: nil,
thumbnailSource: nil,
contentType: nil))))),
@@ -264,14 +266,16 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.image(.init(body: "Some image",
eventContent: .message(.image(.init(filename: "image.jpg",
caption: "Some image",
source: imageSource,
thumbnailSource: imageSource))))),
TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.video(.init(body: "Some video",
eventContent: .message(.video(.init(filename: "video.mp4",
caption: "Some video",
duration: 0,
source: nil,
thumbnailSource: imageSource))))),
@@ -283,7 +287,8 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.voice(.init(body: "Some voice message",
eventContent: .message(.voice(.init(filename: "voice-message.ogg",
caption: "Some voice message",
duration: 0,
waveform: nil,
source: nil,
@@ -355,7 +355,7 @@ private extension View {
struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock
static let viewModelWithPins: TimelineViewModel = {
let roomProxy = JoinedRoomProxyMock(.init(name: "Preview Room", pinnedEventIDs: [""]))
let roomProxy = JoinedRoomProxyMock(.init(name: "Preview Room", pinnedEventIDs: ["pinned"]))
return TimelineViewModel(roomProxy: roomProxy,
focussedEventID: nil,
timelineController: MockRoomTimelineController(),
@@ -385,113 +385,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
.snapshotPreferences(delay: 2.0)
}
// These always include a reply
static var threads: some View {
ScrollView {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short"))))),
groupStyle: .single))
AudioRoomTimelineView(timelineItem: .init(id: .init(uniqueID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: ""),
content: .init(body: "audio.ogg",
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
contentType: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short"))))))
FileRoomTimelineView(timelineItem: .init(id: .init(uniqueID: ""),
timestamp: "10:42",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: ""),
content: .init(body: "File",
source: nil,
thumbnailSource: nil,
contentType: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short"))))))
ImageRoomTimelineView(timelineItem: .init(id: .init(uniqueID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: ""),
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short"))))))
LocationRoomTimelineView(timelineItem: .init(id: .random,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782,
longitude: 12.496366),
description: "Location description description description description description description description description"),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short"))))))
LocationRoomTimelineView(timelineItem: .init(id: .random,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short"))))))
VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(uniqueID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: ""),
content: .init(body: "audio.ogg",
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
contentType: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short"))))),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.random),
title: L10n.commonVoiceMessage,
duration: 10,
waveform: EstimatedWaveform.mockWaveform))
}
.environmentObject(viewModel.context)
}
static var mockTimeline: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
@@ -502,7 +395,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
}
.environmentObject(viewModel.context)
}
static var replies: some View {
VStack(spacing: 0) {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""),
@@ -533,7 +426,21 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
}
.environmentObject(viewModel.context)
}
static var threads: some View {
ScrollView {
MockTimelineContent(isThreaded: true)
}
.environmentObject(viewModel.context)
}
static var pinned: some View {
ScrollView {
MockTimelineContent(isPinned: true)
}
.environmentObject(viewModelWithPins.context)
}
static var encryptionAuthenticity: some View {
VStack(spacing: 0) {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""),
@@ -588,7 +495,9 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Some other image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
content: .init(filename: "other.png",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"),
thumbnailSource: nil),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))))
@@ -599,7 +508,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: ""),
content: .init(body: "audio.ogg",
content: .init(filename: "audio.ogg",
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
@@ -612,96 +521,114 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
}
.environmentObject(viewModel.context)
}
static var pinned: some View {
ScrollView {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "")),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
replyDetails: nil),
groupStyle: .single))
}
AudioRoomTimelineView(timelineItem: .init(id: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "")),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: "audio.ogg",
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
contentType: nil),
replyDetails: nil))
FileRoomTimelineView(timelineItem: .init(id: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "")),
timestamp: "10:42",
private struct MockTimelineContent: View {
var isThreaded = false
var isPinned = false
var body: some View {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: makeItemIdentifier(),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
replyDetails: replyDetails),
groupStyle: .single))
AudioRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "audio.ogg",
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
contentType: nil),
replyDetails: replyDetails))
FileRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: "10:42",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "file.txt",
caption: "File",
source: nil,
thumbnailSource: nil,
contentType: nil),
replyDetails: replyDetails))
ImageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: "10:42",
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "image.jpg",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"),
thumbnailSource: nil),
replyDetails: replyDetails))
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: "File",
source: nil,
thumbnailSource: nil,
contentType: nil),
replyDetails: nil))
ImageRoomTimelineView(timelineItem: .init(id: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "")),
timestamp: "10:42",
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
replyDetails: nil))
LocationRoomTimelineView(timelineItem: .init(id: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "")),
timestamp: "Now",
isOutgoing: false,
isThreaded: isThreaded,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782,
longitude: 12.496366),
description: "Location description description description description description description description description"),
replyDetails: replyDetails))
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
replyDetails: replyDetails))
VoiceMessageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782,
longitude: 12.496366),
description: "Location description description description description description description description description"),
replyDetails: nil))
LocationRoomTimelineView(timelineItem: .init(id: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "")),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
replyDetails: nil))
VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "")),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: "audio.ogg",
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
contentType: nil),
replyDetails: nil),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.random),
title: L10n.commonVoiceMessage,
duration: 10,
waveform: EstimatedWaveform.mockWaveform))
}
.environmentObject(viewModelWithPins.context)
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "audio.ogg",
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
contentType: nil),
replyDetails: replyDetails),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.random),
title: L10n.commonVoiceMessage,
duration: 10,
waveform: EstimatedWaveform.mockWaveform))
}
func makeItemIdentifier() -> TimelineItemIdentifier {
isPinned ? .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "pinned")) : .random
}
var replyDetails: TimelineItemReplyDetails? {
isThreaded ? .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short")))) : nil
}
}
@@ -41,6 +41,10 @@ struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "audio.ogg", duration: 300, waveform: nil, source: nil, contentType: nil)))
content: .init(filename: "audio.ogg",
duration: 300,
waveform: nil,
source: nil,
contentType: nil)))
}
}
@@ -42,7 +42,10 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "document.pdf", source: nil, thumbnailSource: nil, contentType: nil)))
content: .init(filename: "document.pdf",
source: nil,
thumbnailSource: nil,
contentType: nil)))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random,
timestamp: "Now",
@@ -51,7 +54,10 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "document.docx", source: nil, thumbnailSource: nil, contentType: nil)))
content: .init(filename: "document.docx",
source: nil,
thumbnailSource: nil,
contentType: nil)))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random,
timestamp: "Now",
@@ -60,7 +66,10 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "document.txt", source: nil, thumbnailSource: nil, contentType: nil)))
content: .init(filename: "document.txt",
source: nil,
thumbnailSource: nil,
contentType: nil)))
}
}
}
@@ -59,7 +59,9 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil)))
content: .init(filename: "image.jpg",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/jpg"),
thumbnailSource: nil)))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random,
timestamp: "Now",
@@ -68,7 +70,9 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Some other image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil)))
content: .init(filename: "other.png",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"),
thumbnailSource: nil)))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random,
timestamp: "Now",
@@ -77,7 +81,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Blurhashed image",
content: .init(filename: "Blurhashed.jpg",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"),
thumbnailSource: nil,
aspectRatio: 0.7,
@@ -70,7 +70,10 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Some video", duration: 21, source: nil, thumbnailSource: nil)))
content: .init(filename: "video.mp4",
duration: 21,
source: nil,
thumbnailSource: nil)))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random,
timestamp: "Now",
@@ -79,7 +82,10 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Some other video", duration: 22, source: nil, thumbnailSource: nil)))
content: .init(filename: "other.mp4",
duration: 22,
source: nil,
thumbnailSource: nil)))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random,
timestamp: "Now",
@@ -88,7 +94,12 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Blurhashed video", duration: 23, source: nil, thumbnailSource: nil, aspectRatio: 0.7, blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW")))
content: .init(filename: "Blurhashed.mp4",
duration: 23,
source: nil,
thumbnailSource: nil,
aspectRatio: 0.7,
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW")))
}
}
}