From f378e6e92654b877801e637e5d32614c67493db3 Mon Sep 17 00:00:00 2001 From: bxdxnn <267911624+bxdxnn@users.noreply.github.com> Date: Sat, 11 Apr 2026 20:02:20 +0000 Subject: [PATCH] Make media captions scrollable --- .../TimelineMediaPreviewDataSource.swift | 17 ++++- .../View/TimelineMediaPreviewController.swift | 74 ++++++++++++++++--- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift index 0f7da106c..c5b15518a 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift @@ -272,7 +272,22 @@ enum TimelineMediaPreviewItem: Equatable { var caption: String? { timelineItem.mediaCaption } - + + var formattedCaption: AttributedString? { + switch timelineItem { + case let audioItem as AudioRoomTimelineItem: + audioItem.content.formattedCaption + case let fileItem as FileRoomTimelineItem: + fileItem.content.formattedCaption + case let imageItem as ImageRoomTimelineItem: + imageItem.content.formattedCaption + case let videoItem as VideoRoomTimelineItem: + videoItem.content.formattedCaption + default: + nil + } + } + var contentType: String? { switch timelineItem { case let audioItem as AudioRoomTimelineItem: diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewController.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewController.swift index ce10aadf7..e0a7b7ace 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewController.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewController.swift @@ -316,25 +316,77 @@ private struct CaptionView: View { private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem } - + + private let maxCaptionHeight: CGFloat = 120 + var body: some View { - if case let .media(mediaItem) = currentItem, let caption = mediaItem.caption { + if case let .media(mediaItem) = currentItem, mediaItem.caption != nil { + CaptionScrollView(mediaItem: mediaItem, maxHeight: maxCaptionHeight) + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + } +} + +private struct CaptionScrollView: View { + let mediaItem: TimelineMediaPreviewItem.Media + let maxHeight: CGFloat + + @State private var contentHeight: CGFloat = 0 + @State private var isScrollable: Bool = false + + private var shouldShowFade: Bool { + contentHeight > maxHeight + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + ScrollView(.vertical, showsIndicators: false) { + captionContent + .background(SizePreferenceKey()) + } + .onPreferenceChange(SizePreferenceKey.self) { size in + contentHeight = size.height + isScrollable = size.height > maxHeight + } + .frame(height: maxHeight) + .clipped() + + if shouldShowFade { + LinearGradient(stops: [.init(color: .clear, location: 0.0), + .init(color: .black.opacity(0.5), location: 1.0)], + startPoint: .top, + endPoint: .bottom) + .frame(height: 40) + .allowsHitTesting(false) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(16) + .background { + BlurEffectView(style: .systemChromeMaterial) + .ignoresSafeArea() + } + } + + @ViewBuilder + private var captionContent: some View { + if let formattedCaption = mediaItem.formattedCaption { + FormattedBodyText(attributedString: formattedCaption) + } else if let caption = mediaItem.caption { Text(caption) .font(.compound.bodyLG) .foregroundStyle(.compound.textPrimary) - .lineLimit(5) - .frame(maxWidth: .infinity, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) - .padding(16) - .background { - BlurEffectView(style: .systemChromeMaterial) // Darkest material available, matches the bottom bar when content is beneath. - .ignoresSafeArea() - } - .transition(.move(edge: .bottom).combined(with: .opacity)) } } } +private struct SizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = nextValue() + } +} + private struct DownloadIndicatorView: View { @ObservedObject var context: TimelineMediaPreviewViewModel.Context private var currentItem: TimelineMediaPreviewItem {