Use TimelineMediaQuickLook in the MediaEventsTimelineScreen. (#3598)

This commit is contained in:
Doug 2024-12-10 12:56:51 +00:00 committed by GitHub
parent ea0fa6b3b6
commit e59a705459
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 109 additions and 37 deletions

View File

@ -922,6 +922,7 @@
BE8E5985771DF9137C6CE89A /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; };
BEA646DF302711A753F0D420 /* MapTilerStyleBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225EFCA26877E75CDFE7F48D /* MapTilerStyleBuilderProtocol.swift */; };
BEC6DFEA506085D3027E353C /* MediaEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002399C6CB875C4EBB01CBC0 /* MediaEventsTimelineScreen.swift */; };
BFDDAF1A36FBC7CF63DCB7DD /* clear.png in Resources */ = {isa = PBXBuildFile; fileRef = 17F7A723A46DF5C95BE15EBF /* clear.png */; };
BFEB24336DFD5F196E6F3456 /* IntentionalMentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */; };
C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */; };
C022284E2774A5E1EF683B4D /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; };
@ -1408,6 +1409,7 @@
1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = "<group>"; };
1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = "<group>"; };
17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxyMock.swift; sourceTree = "<group>"; };
17F7A723A46DF5C95BE15EBF /* clear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = clear.png; sourceTree = "<group>"; };
18486B87745B1811E7FBD3D2 /* AnalyticsPromptScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenModels.swift; sourceTree = "<group>"; };
184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = "<group>"; };
18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = "<group>"; };
@ -2958,6 +2960,7 @@
isa = PBXGroup;
children = (
01C4C7DB37597D7D8379511A /* Assets.xcassets */,
17F7A723A46DF5C95BE15EBF /* clear.png */,
A0C06C0F6A8621B22BFAEB56 /* Localizations */,
8AEA6A91159FA0D3EAFCCB0D /* Sounds */,
);
@ -6204,6 +6207,7 @@
5FCD8AFA364206EE32B909A3 /* Settings.bundle in Resources */,
CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */,
2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */,
BFDDAF1A36FBC7CF63DCB7DD /* clear.png in Resources */,
147597951DB07123A87AA1D1 /* landscape_test_image.jpg in Resources */,
FDC67E8C0EDCB00ABC66C859 /* landscape_test_video.mov in Resources */,
E67418DACEDBC29E988E6ACD /* message.caf in Resources */,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -86,7 +86,8 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
mediaPlayerProvider: MediaPlayerProvider(),
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
appMediator: appMediator,
emojiProvider: emojiProvider)
emojiProvider: emojiProvider,
userIndicatorController: userIndicatorController)
let coordinator = MediaEventsTimelineScreenCoordinator(parameters: parameters)

View File

@ -28,8 +28,10 @@ class TimelineMediaPreviewController: QLPreviewController, QLPreviewControllerDa
headerHostingController = UIHostingController(rootView: HeaderView(context: viewModel.context))
headerHostingController.view.backgroundColor = .clear
headerHostingController.sizingOptions = .intrinsicContentSize
captionHostingController = UIHostingController(rootView: CaptionView(context: viewModel.context))
captionHostingController.view.backgroundColor = .clear
captionHostingController.sizingOptions = .intrinsicContentSize
detailsHostingController = UIHostingController(rootView: TimelineMediaPreviewDetailsView(context: viewModel.context))
detailsHostingController.view.backgroundColor = .compound.bgCanvasDefault
@ -87,9 +89,7 @@ class TimelineMediaPreviewController: QLPreviewController, QLPreviewControllerDa
navigationBar?.topItem?.titleView = headerHostingController.view
if navigationBar?.topItem?.rightBarButtonItems?.count == 1 {
navigationBar?.topItem?.rightBarButtonItems?.append(UIBarButtonItem(image: UIImage(systemSymbol: .infoCircle), style: .plain, target: self, action: #selector(presentMediaDetails)))
}
updateBarButtons()
}
// MARK: QLPreviewControllerDataSource
@ -111,6 +111,21 @@ class TimelineMediaPreviewController: QLPreviewController, QLPreviewControllerDa
present(detailsHostingController, animated: true)
}
private var detailsButtonIcon: UIImage {
guard let bundle = Bundle(url: Bundle.main.bundleURL.appending(path: "CompoundDesignTokens_CompoundDesignTokens.bundle")) else {
return UIImage(systemSymbol: .infoCircle)
}
return UIImage(named: "info", in: bundle, compatibleWith: nil) ?? UIImage(systemSymbol: .infoCircle)
}
private func updateBarButtons() {
if navigationBar?.topItem?.rightBarButtonItems?.count == 1 {
let button = UIBarButtonItem(image: detailsButtonIcon, style: .plain, target: self, action: #selector(presentMediaDetails))
navigationBar?.topItem?.rightBarButtonItems?.append(button)
}
}
}
// MARK: - Subviews
@ -143,7 +158,21 @@ private struct CaptionView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
.padding(16)
.background(.ultraThinMaterial)
.background {
BlurView(style: .systemChromeMaterial) // Darkest material available, matches the bottom bar when content is beneath.
}
}
}
}
private struct BlurView: UIViewRepresentable {
var style: UIBlurEffect.Style
func makeUIView(context: Context) -> UIVisualEffectView {
UIVisualEffectView(effect: UIBlurEffect(style: style))
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}

View File

@ -31,7 +31,9 @@ class TimelineMediaPreviewItem: NSObject, QLPreviewItem {
// MARK: QLPreviewItem
var previewItemURL: URL? {
fileHandle?.url
// Falling back to a clear image allows the presentation animation to work when
// the item is in the event cache and just needs to be loaded from the store.
fileHandle?.url ?? Bundle.main.url(forResource: "clear", withExtension: "png")
}
var previewItemTitle: String? {

View File

@ -17,6 +17,7 @@ struct MediaEventsTimelineScreenCoordinatorParameters {
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
let appMediator: AppMediatorProtocol
let emojiProvider: EmojiProviderProtocol
let userIndicatorController: UserIndicatorControllerProtocol
}
enum MediaEventsTimelineScreenCoordinatorAction { }
@ -59,7 +60,8 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
viewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: mediaTimelineViewModel,
filesTimelineViewModel: filesTimelineViewModel,
mediaProvider: parameters.mediaProvider)
mediaProvider: parameters.mediaProvider,
userIndicatorController: parameters.userIndicatorController)
}
func toPresentable() -> AnyView {

View File

@ -23,10 +23,12 @@ struct MediaEventsTimelineScreenViewState: BindableState {
struct MediaEventsTimelineScreenViewStateBindings {
var screenMode: MediaEventsTimelineScreenMode
var mediaPreviewViewModel: TimelineMediaPreviewViewModel?
}
enum MediaEventsTimelineScreenViewAction {
case changedScreenMode
case oldestItemDidAppear
case oldestItemDidDisappear
case tappedItem(RoomTimelineItemViewState)
}

View File

@ -13,6 +13,7 @@ typealias MediaEventsTimelineScreenViewModelType = StateStoreViewModel<MediaEven
class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType, MediaEventsTimelineScreenViewModelProtocol {
private let mediaTimelineViewModel: TimelineViewModelProtocol
private let filesTimelineViewModel: TimelineViewModelProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private var isOldestItemVisible = false
@ -33,9 +34,11 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
init(mediaTimelineViewModel: TimelineViewModelProtocol,
filesTimelineViewModel: TimelineViewModelProtocol,
mediaProvider: MediaProviderProtocol,
screenMode: MediaEventsTimelineScreenMode = .media) {
screenMode: MediaEventsTimelineScreenMode = .media,
userIndicatorController: UserIndicatorControllerProtocol) {
self.mediaTimelineViewModel = mediaTimelineViewModel
self.filesTimelineViewModel = filesTimelineViewModel
self.userIndicatorController = userIndicatorController
super.init(initialViewState: .init(bindings: .init(screenMode: screenMode)), mediaProvider: mediaProvider)
@ -73,6 +76,8 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
backPaginateIfNecessary(paginationStatus: activeTimelineViewModel.context.viewState.timelineState.paginationState.backward)
case .oldestItemDidDisappear:
isOldestItemVisible = false
case .tappedItem(let item):
handleItemTapped(item)
}
}
@ -99,4 +104,24 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
activeTimelineViewModel.context.send(viewAction: .paginateBackwards)
}
}
private func handleItemTapped(_ item: RoomTimelineItemViewState) {
let item: EventBasedMessageTimelineItemProtocol? = switch item.type {
case .audio(let audioItem): audioItem
case .file(let fileItem): fileItem
case .image(let imageItem): imageItem
case .video(let videoItem): videoItem
default: nil
}
guard let item, let mediaProvider = context.mediaProvider else {
MXLog.error("Unexpected item type (or the media provider is missing).")
return
}
let viewModel = TimelineMediaPreviewViewModel(previewItems: [item],
mediaProvider: mediaProvider,
userIndicatorController: userIndicatorController)
state.bindings.mediaPreviewViewModel = viewModel
}
}

View File

@ -30,6 +30,7 @@ struct MediaEventsTimelineScreen: View {
.pickerStyle(.segmented)
}
}
.timelineMediaQuickLook(viewModel: $context.mediaPreviewViewModel)
}
@ViewBuilder
@ -39,13 +40,17 @@ struct MediaEventsTimelineScreen: View {
let columns = [GridItem(.adaptive(minimum: 80, maximum: 150), spacing: 1)]
LazyVGrid(columns: columns, alignment: .center, spacing: 1) {
ForEach(context.viewState.items) { item in
Color.clear // Let the image aspect fill in place
.aspectRatio(1, contentMode: .fill)
.overlay {
viewForTimelineItem(item)
}
.clipped()
.scaleEffect(.init(width: 1, height: -1))
Button {
context.send(viewAction: .tappedItem(item))
} label: {
Color.clear // Let the image aspect fill in place
.aspectRatio(1, contentMode: .fill)
.overlay {
viewForTimelineItem(item)
}
.clipped()
.scaleEffect(.init(width: 1, height: -1))
}
}
}
@ -152,12 +157,14 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
static let mediaViewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: timelineViewModel,
filesTimelineViewModel: timelineViewModel,
mediaProvider: MediaProviderMock(configuration: .init()),
screenMode: .media)
screenMode: .media,
userIndicatorController: UserIndicatorControllerMock())
static let filesViewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: timelineViewModel,
filesTimelineViewModel: timelineViewModel,
mediaProvider: MediaProviderMock(configuration: .init()),
screenMode: .files)
screenMode: .files,
userIndicatorController: UserIndicatorControllerMock())
static var previews: some View {
NavigationStack {