Render replies to gallery items (#5942)

This commit is contained in:
Mauro
2026-07-29 16:28:46 +02:00
committed by GitHub
parent 58fb7c4fcb
commit 704b3dbb29
5 changed files with 69 additions and 14 deletions
@@ -60,6 +60,8 @@ struct TimelineReplyView: View {
case .image(let content): return content.imageInfo.source
case .video(let content): return content.videoInfo.source
case .voice(let content): return content.source
// Only the first item is scanned as it's the only one the reply previews.
case .gallery(let content): return content.items.first?.mediaSource
default: return nil
}
}
@@ -75,6 +77,7 @@ struct TimelineReplyView: View {
case .file(let content): return content.thumbnailSource
case .image(let content): return content.thumbnailInfo?.source
case .video(let content): return content.thumbnailInfo?.source
case .gallery(let content): return content.items.first?.thumbnailSource
default: return nil
}
}
@@ -130,12 +133,7 @@ struct TimelineReplyView: View {
formattedBody: nil,
icon: .init(kind: .icon(\.locationPin)))
case .gallery(let content):
ReplyView(sender: sender,
plainBody: content.caption ?? content.body,
formattedBody: content.formattedCaption,
icon: content.items.first?.thumbnailSource.map { .init(kind: .mediaSource($0)) }
?? content.items.first?.mediaSource.map { .init(kind: .mediaSource($0)) }
?? .init(kind: .icon(\.attachment)))
GalleryReplyView(sender: sender, content: content)
}
case .poll(let question):
ReplyView(sender: sender,
@@ -159,6 +157,47 @@ struct TimelineReplyView: View {
}
}
/// A gallery previews its first attachment as though it had been sent on its own, counting its
/// media when they're all images/videos and its attachments otherwise.
private struct GalleryReplyView: View {
let sender: TimelineItemSender
let content: GalleryRoomTimelineItemContent
private var caption: String? {
content.caption?.isBlank == false ? content.caption : nil
}
private var isMediaGallery: Bool {
content.items.allSatisfy { $0.isImage || $0.isVideo }
}
private var placeholder: String {
if isMediaGallery {
L10n.commonGalleryReplyMediaItems(content.items.count)
} else {
L10n.commonGalleryReplyAttachments(content.items.count)
}
}
private var icon: ReplyView.Icon? {
guard let item = content.items.first else { return nil }
return switch item {
case .image(_, let itemContent): .init(kind: .mediaSource(itemContent.thumbnailInfo?.source ?? itemContent.imageInfo.source))
case .video(_, let itemContent): itemContent.thumbnailInfo.map { .init(kind: .mediaSource($0.source)) }
case .audio: .init(kind: .icon(\.audio))
case .file, .other: .init(kind: .icon(\.attachment))
}
}
var body: some View {
ReplyView(sender: sender,
plainBody: caption ?? placeholder,
formattedBody: caption == nil ? nil : content.formattedCaption,
icon: icon)
}
}
private struct LoadingReplyView: View {
var body: some View {
ReplyView(sender: .init(id: "@alice:matrix.org"), plainBody: "Hello world", formattedBody: nil)
@@ -402,6 +441,22 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
eventID: "123",
eventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithEventOnRoomAliasMention))))),
TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.gallery(.init(body: "Gallery",
items: [.mockImage(index: 0),
.mockVideo(index: 1)]))))),
TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.gallery(.init(body: "Gallery",
caption: "A trip to remember 🌅",
items: [.mockImage(index: 0),
.mockVideo(index: 1),
.mockImage(index: 2)]))))),
TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
eventID: "123",