mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Rework the presentation of the media browser quick look view to use SwiftUI. (#3619)
* Embed the media preview quick look inside a full screen cover With a zoom transition on iOS 18. * Use a the representable coordinator properly. * Fix a bug with the toolbar appearance. * Format * Try prevent the zoom transition being upside down. * Fix the snapshot test configuration.
This commit is contained in:
+3
-3
@@ -21,7 +21,7 @@ struct MediaEventsTimelineScreenCoordinatorParameters {
|
||||
}
|
||||
|
||||
enum MediaEventsTimelineScreenCoordinatorAction {
|
||||
case viewInRoomTimeline(TimelineItemIdentifier)
|
||||
case viewItem(TimelineMediaPreviewContext)
|
||||
}
|
||||
|
||||
final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -68,8 +68,8 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
viewModel.actionsPublisher
|
||||
.sink { [weak self] action in
|
||||
switch action {
|
||||
case .viewInRoomTimeline(let itemID):
|
||||
self?.actionsSubject.send(.viewInRoomTimeline(itemID))
|
||||
case .viewItem(let previewContext):
|
||||
self?.actionsSubject.send(.viewItem(previewContext))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
+5
-4
@@ -5,10 +5,10 @@
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum MediaEventsTimelineScreenViewModelAction {
|
||||
case viewInRoomTimeline(TimelineItemIdentifier)
|
||||
case viewItem(TimelineMediaPreviewContext)
|
||||
}
|
||||
|
||||
enum MediaEventsTimelineScreenMode {
|
||||
@@ -29,16 +29,17 @@ struct MediaEventsTimelineScreenViewState: BindableState {
|
||||
var activeTimelineContextProvider: (() -> TimelineViewModel.Context)!
|
||||
|
||||
var bindings: MediaEventsTimelineScreenViewStateBindings
|
||||
|
||||
var currentPreviewItemID: TimelineItemIdentifier?
|
||||
}
|
||||
|
||||
struct MediaEventsTimelineScreenViewStateBindings {
|
||||
var screenMode: MediaEventsTimelineScreenMode
|
||||
var mediaPreviewViewModel: TimelineMediaPreviewViewModel?
|
||||
}
|
||||
|
||||
enum MediaEventsTimelineScreenViewAction {
|
||||
case changedScreenMode
|
||||
case oldestItemDidAppear
|
||||
case oldestItemDidDisappear
|
||||
case tappedItem(RoomTimelineItemViewState)
|
||||
case tappedItem(item: RoomTimelineItemViewState, namespace: Namespace.ID)
|
||||
}
|
||||
|
||||
+13
-24
@@ -84,8 +84,8 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
backPaginateIfNecessary(paginationStatus: activeTimelineViewModel.context.viewState.timelineState.paginationState.backward)
|
||||
case .oldestItemDidDisappear:
|
||||
isOldestItemVisible = false
|
||||
case .tappedItem(let item):
|
||||
handleItemTapped(item)
|
||||
case .tappedItem(let item, let namespace):
|
||||
handleItemTapped(item, namespace: namespace)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
}
|
||||
}
|
||||
|
||||
private func handleItemTapped(_ item: RoomTimelineItemViewState) {
|
||||
private func handleItemTapped(_ item: RoomTimelineItemViewState, namespace: Namespace.ID) {
|
||||
let item: EventBasedMessageTimelineItemProtocol? = switch item.type {
|
||||
case .audio(let audioItem): audioItem
|
||||
case .file(let fileItem): fileItem
|
||||
@@ -149,31 +149,20 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
default: nil
|
||||
}
|
||||
|
||||
guard let item, let mediaProvider = context.mediaProvider else {
|
||||
MXLog.error("Unexpected item type (or the media provider is missing).")
|
||||
guard let item else {
|
||||
MXLog.error("Unexpected item type tapped.")
|
||||
return
|
||||
}
|
||||
|
||||
let viewModel = TimelineMediaPreviewViewModel(initialItem: item,
|
||||
timelineViewModel: activeTimelineViewModel,
|
||||
mediaProvider: mediaProvider,
|
||||
userIndicatorController: userIndicatorController)
|
||||
actionsSubject.send(.viewItem(.init(item: item,
|
||||
viewModel: activeTimelineViewModel,
|
||||
namespace: namespace,
|
||||
completion: { [weak self] in
|
||||
self?.state.currentPreviewItemID = nil
|
||||
})))
|
||||
|
||||
mediaPreviewCancellable = viewModel.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .viewInRoomTimeline(let itemID):
|
||||
state.bindings.mediaPreviewViewModel = nil
|
||||
actionsSubject.send(.viewInRoomTimeline(itemID))
|
||||
case .dismiss:
|
||||
state.bindings.mediaPreviewViewModel = nil
|
||||
case .loadedMediaFile:
|
||||
break // Handled by the preview controller
|
||||
}
|
||||
}
|
||||
|
||||
state.bindings.mediaPreviewViewModel = viewModel
|
||||
// Set the current item in the next run loop so that (hopefully) the presentation will be ready before we flip the thumbnail.
|
||||
Task { state.currentPreviewItemID = item.id }
|
||||
}
|
||||
|
||||
private func titleForDate(_ date: Date) -> String {
|
||||
|
||||
+22
-6
@@ -11,6 +11,8 @@ import SwiftUI
|
||||
struct MediaEventsTimelineScreen: View {
|
||||
@ObservedObject var context: MediaEventsTimelineScreenViewModel.Context
|
||||
|
||||
@Namespace private var zoomTransition
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -30,7 +32,6 @@ struct MediaEventsTimelineScreen: View {
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
}
|
||||
.timelineMediaQuickLook(viewModel: $context.mediaPreviewViewModel)
|
||||
.environmentObject(context.viewState.activeTimelineContextProvider())
|
||||
.environment(\.timelineContext, context.viewState.activeTimelineContextProvider())
|
||||
}
|
||||
@@ -39,7 +40,7 @@ struct MediaEventsTimelineScreen: View {
|
||||
// * flip the scrollView vertically to keep the items
|
||||
// at the bottom and have pagination working properly
|
||||
// * flip the grid vertically to counteract the scroll view
|
||||
// but also horizontally to preserve the corect item order
|
||||
// but also horizontally to preserve the correct item order
|
||||
// * flip the items on both axes have them render correctly
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
@@ -73,7 +74,7 @@ struct MediaEventsTimelineScreen: View {
|
||||
Section {
|
||||
ForEach(group.items) { item in
|
||||
Button {
|
||||
context.send(viewAction: .tappedItem(item))
|
||||
tappedItem(item)
|
||||
} label: {
|
||||
Color.clear // Let the image aspect fill in place
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
@@ -81,8 +82,9 @@ struct MediaEventsTimelineScreen: View {
|
||||
viewForTimelineItem(item)
|
||||
}
|
||||
.clipped()
|
||||
.scaleEffect(.init(width: -1, height: -1))
|
||||
.scaleEffect(scale(for: item, isGridLayout: true))
|
||||
}
|
||||
.zoomTransitionSource(id: item.identifier, in: zoomTransition)
|
||||
}
|
||||
} footer: {
|
||||
// Use a footer as the header because the scrollView is flipped
|
||||
@@ -104,11 +106,12 @@ struct MediaEventsTimelineScreen: View {
|
||||
Divider()
|
||||
|
||||
Button {
|
||||
context.send(viewAction: .tappedItem(item))
|
||||
tappedItem(item)
|
||||
} label: {
|
||||
viewForTimelineItem(item)
|
||||
.scaleEffect(.init(width: 1, height: -1))
|
||||
.scaleEffect(scale(for: item, isGridLayout: false))
|
||||
}
|
||||
.zoomTransitionSource(id: item.identifier, in: zoomTransition)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
@@ -207,6 +210,19 @@ struct MediaEventsTimelineScreen: View {
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
func tappedItem(_ item: RoomTimelineItemViewState) {
|
||||
context.send(viewAction: .tappedItem(item: item, namespace: zoomTransition))
|
||||
}
|
||||
|
||||
func scale(for item: RoomTimelineItemViewState, isGridLayout: Bool) -> CGSize {
|
||||
guard item.identifier != context.viewState.currentPreviewItemID else {
|
||||
// Remove the flip when presenting a preview so that the zoom transition is the right way up 🙃
|
||||
return CGSize(width: 1, height: 1)
|
||||
}
|
||||
|
||||
return CGSize(width: isGridLayout ? -1 : 1, height: -1)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
Reference in New Issue
Block a user