Revert back to UIKit for the presentation of the timeline media preview. (#3719)

* Revert back to UIKit for the presentation of the timeline media preview.

* Fix a presentation issue where the media is clipped until the animation finishes.

* Workaround for the preview controller replacing the info button when swiping.

* Use a self-sizing detent on the media info sheet.
This commit is contained in:
Doug
2025-01-31 17:28:14 +00:00
committed by GitHub
parent ef340fb70f
commit 6cd583d904
25 changed files with 695 additions and 583 deletions
@@ -21,7 +21,7 @@ struct MediaEventsTimelineScreenCoordinatorParameters {
}
enum MediaEventsTimelineScreenCoordinatorAction {
case viewItem(TimelineMediaPreviewContext)
case viewInRoomTimeline(TimelineItemIdentifier)
}
final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
@@ -63,13 +63,14 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
viewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: mediaTimelineViewModel,
filesTimelineViewModel: filesTimelineViewModel,
mediaProvider: parameters.mediaProvider,
userIndicatorController: parameters.userIndicatorController)
userIndicatorController: parameters.userIndicatorController,
appMediator: parameters.appMediator)
viewModel.actionsPublisher
.sink { [weak self] action in
switch action {
case .viewItem(let previewContext):
self?.actionsSubject.send(.viewItem(previewContext))
case .viewInRoomTimeline(let itemID):
self?.actionsSubject.send(.viewInRoomTimeline(itemID))
}
}
.store(in: &cancellables)
@@ -8,7 +8,7 @@
import SwiftUI
enum MediaEventsTimelineScreenViewModelAction {
case viewItem(TimelineMediaPreviewContext)
case viewInRoomTimeline(TimelineItemIdentifier)
}
enum MediaEventsTimelineScreenMode {
@@ -31,17 +31,16 @@ 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(item: RoomTimelineItemViewState, namespace: Namespace.ID)
case tappedItem(item: RoomTimelineItemViewState)
}
@@ -13,7 +13,9 @@ typealias MediaEventsTimelineScreenViewModelType = StateStoreViewModel<MediaEven
class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType, MediaEventsTimelineScreenViewModelProtocol {
private let mediaTimelineViewModel: TimelineViewModelProtocol
private let filesTimelineViewModel: TimelineViewModelProtocol
private let mediaProvider: MediaProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let appMediator: AppMediatorProtocol
private var isOldestItemVisible = false
@@ -37,10 +39,13 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
filesTimelineViewModel: TimelineViewModelProtocol,
initialViewState: MediaEventsTimelineScreenViewState = .init(bindings: .init(screenMode: .media)),
mediaProvider: MediaProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
userIndicatorController: UserIndicatorControllerProtocol,
appMediator: AppMediatorProtocol) {
self.mediaTimelineViewModel = mediaTimelineViewModel
self.filesTimelineViewModel = filesTimelineViewModel
self.mediaProvider = mediaProvider
self.userIndicatorController = userIndicatorController
self.appMediator = appMediator
super.init(initialViewState: initialViewState, mediaProvider: mediaProvider)
@@ -84,8 +89,8 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
backPaginateIfNecessary(paginationStatus: activeTimelineViewModel.context.viewState.timelineState.paginationState.backward)
case .oldestItemDidDisappear:
isOldestItemVisible = false
case .tappedItem(let item, let namespace):
handleItemTapped(item, namespace: namespace)
case .tappedItem(let item):
handleItemTapped(item)
}
}
@@ -141,7 +146,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
}
}
private func handleItemTapped(_ item: RoomTimelineItemViewState, namespace: Namespace.ID) {
private func handleItemTapped(_ item: RoomTimelineItemViewState) {
let item: EventBasedMessageTimelineItemProtocol? = switch item.type {
case .audio(let audioItem): audioItem
case .file(let fileItem): fileItem
@@ -155,14 +160,25 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
return
}
actionsSubject.send(.viewItem(.init(item: item,
viewModel: activeTimelineViewModel,
namespace: namespace) { [weak self] itemID in
self?.state.currentPreviewItemID = itemID
}))
let viewModel = TimelineMediaPreviewViewModel(initialItem: item,
timelineViewModel: activeTimelineViewModel,
mediaProvider: mediaProvider,
photoLibraryManager: PhotoLibraryManager(),
userIndicatorController: userIndicatorController,
appMediator: appMediator)
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
}
}
.store(in: &cancellables)
// 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 }
state.bindings.mediaPreviewViewModel = viewModel
}
private func titleForDate(_ date: Date) -> String {
@@ -11,8 +11,6 @@ import SwiftUI
struct MediaEventsTimelineScreen: View {
@ObservedObject var context: MediaEventsTimelineScreenViewModel.Context
@Namespace private var zoomTransition
var body: some View {
mainContent
.navigationBarTitleDisplayMode(.inline)
@@ -25,6 +23,7 @@ struct MediaEventsTimelineScreen: View {
.onChange(of: context.screenMode) { _, _ in
context.send(viewAction: .changedScreenMode)
}
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
}
// The scale effects do the following:
@@ -65,9 +64,8 @@ struct MediaEventsTimelineScreen: View {
tappedItem(item)
} label: {
viewForTimelineItem(item)
.scaleEffect(scale(for: item, isGridLayout: true))
.scaleEffect(CGSize(width: -1, height: -1))
}
.zoomTransitionSource(id: item.identifier, in: zoomTransition)
}
} footer: {
// Use a footer as the header because the scrollView is flipped
@@ -92,9 +90,8 @@ struct MediaEventsTimelineScreen: View {
tappedItem(item)
} label: {
viewForTimelineItem(item)
.scaleEffect(scale(for: item, isGridLayout: false))
.scaleEffect(CGSize(width: 1, height: -1))
}
.zoomTransitionSource(id: item.identifier, in: zoomTransition)
}
.padding(.horizontal, 16)
}
@@ -216,16 +213,7 @@ struct MediaEventsTimelineScreen: View {
}
func tappedItem(_ item: RoomTimelineItemViewState) {
context.send(viewAction: .tappedItem(item: item, namespace: zoomTransition))
}
func scale(for item: RoomTimelineItemViewState, isGridLayout: Bool) -> CGSize {
if item.identifier == context.viewState.currentPreviewItemID, #available(iOS 18.0, *) {
// Remove the flip when presenting a preview so that the zoom transition is the right way up 🙃
CGSize(width: 1, height: 1)
} else {
CGSize(width: isGridLayout ? -1 : 1, height: -1)
}
context.send(viewAction: .tappedItem(item: item))
}
}
@@ -265,7 +253,8 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
filesTimelineViewModel: makeTimelineViewModel(empty: empty),
initialViewState: .init(bindings: .init(screenMode: screenMode)),
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock())
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock())
}
private static func makeTimelineViewModel(empty: Bool) -> TimelineViewModel {