mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Detect the timeline start/end when swiping through media files. (#3714)
This commit is contained in:
+24
-22
@@ -9,7 +9,7 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineMediaPreviewDetailsView: View {
|
||||
let item: TimelineMediaPreviewItem
|
||||
let item: TimelineMediaPreviewItem.Media
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
@State private var sheetHeight: CGFloat = .zero
|
||||
@@ -132,7 +132,7 @@ struct TimelineMediaPreviewDetailsView: View {
|
||||
}
|
||||
|
||||
private struct ActionButton: View {
|
||||
let item: TimelineMediaPreviewItem
|
||||
let item: TimelineMediaPreviewItem.Media
|
||||
let action: TimelineItemMenuAction
|
||||
let context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
@@ -177,29 +177,31 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
|
||||
static let presentedOnRoomViewModel = makeViewModel(isPresentedOnRoomScreen: true)
|
||||
|
||||
static var previews: some View {
|
||||
// swiftlint:disable force_unwrapping
|
||||
TimelineMediaPreviewDetailsView(item: viewModel.state.currentItem!,
|
||||
context: viewModel.context)
|
||||
.previewDisplayName("Image")
|
||||
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
|
||||
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
|
||||
})
|
||||
if case let .media(mediaItem) = viewModel.state.currentItem {
|
||||
TimelineMediaPreviewDetailsView(item: mediaItem, context: viewModel.context)
|
||||
.previewDisplayName("Image")
|
||||
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
|
||||
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
|
||||
})
|
||||
}
|
||||
|
||||
TimelineMediaPreviewDetailsView(item: loadingViewModel.state.currentItem!,
|
||||
context: loadingViewModel.context)
|
||||
.previewDisplayName("Loading")
|
||||
.snapshotPreferences(expect: loadingViewModel.context.$viewState.map { state in
|
||||
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
|
||||
})
|
||||
if case let .media(mediaItem) = loadingViewModel.state.currentItem {
|
||||
TimelineMediaPreviewDetailsView(item: mediaItem, context: loadingViewModel.context)
|
||||
.previewDisplayName("Loading")
|
||||
.snapshotPreferences(expect: loadingViewModel.context.$viewState.map { state in
|
||||
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
|
||||
})
|
||||
}
|
||||
|
||||
TimelineMediaPreviewDetailsView(item: unknownTypeViewModel.state.currentItem!,
|
||||
context: unknownTypeViewModel.context)
|
||||
.previewDisplayName("Unknown type")
|
||||
if case let .media(mediaItem) = unknownTypeViewModel.state.currentItem {
|
||||
TimelineMediaPreviewDetailsView(item: mediaItem, context: unknownTypeViewModel.context)
|
||||
.previewDisplayName("Unknown type")
|
||||
}
|
||||
|
||||
TimelineMediaPreviewDetailsView(item: presentedOnRoomViewModel.state.currentItem!,
|
||||
context: presentedOnRoomViewModel.context)
|
||||
.previewDisplayName("Incoming on Room")
|
||||
// swiftlint:enable force_unwrapping
|
||||
if case let .media(mediaItem) = presentedOnRoomViewModel.state.currentItem {
|
||||
TimelineMediaPreviewDetailsView(item: mediaItem, context: presentedOnRoomViewModel.context)
|
||||
.previewDisplayName("Incoming on Room")
|
||||
}
|
||||
}
|
||||
|
||||
static func makeViewModel(contentType: UTType? = nil,
|
||||
|
||||
+4
-3
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
struct TimelineMediaPreviewRedactConfirmationView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
let item: TimelineMediaPreviewItem
|
||||
let item: TimelineMediaPreviewItem.Media
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
@State private var sheetHeight: CGFloat = .zero
|
||||
@@ -125,8 +125,9 @@ struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, Tes
|
||||
static let viewModel = makeViewModel(contentType: .jpeg)
|
||||
|
||||
static var previews: some View {
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
TimelineMediaPreviewRedactConfirmationView(item: viewModel.state.currentItem!, context: viewModel.context)
|
||||
if case let .media(mediaItem) = viewModel.state.currentItem {
|
||||
TimelineMediaPreviewRedactConfirmationView(item: mediaItem, context: viewModel.context)
|
||||
}
|
||||
}
|
||||
|
||||
static func makeViewModel(contentType: UTType? = nil) -> TimelineMediaPreviewViewModel {
|
||||
|
||||
@@ -17,7 +17,18 @@ struct TimelineMediaPreviewScreen: View {
|
||||
@State private var isFullScreen = false
|
||||
private var toolbarVisibility: Visibility { isFullScreen ? .hidden : .visible }
|
||||
|
||||
private var currentItem: TimelineMediaPreviewItem? { context.viewState.currentItem }
|
||||
private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem }
|
||||
private var currentItemID: TimelineItemIdentifier? {
|
||||
guard case .media(let mediaItem) = currentItem else { return nil }
|
||||
return mediaItem.id
|
||||
}
|
||||
|
||||
private var shouldShowDownloadIndicator: Bool {
|
||||
switch currentItem {
|
||||
case .media(let mediaItem): mediaItem.fileHandle == nil
|
||||
case .loading(let loadingItem): loadingItem.state == .paginating
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -40,7 +51,7 @@ struct TimelineMediaPreviewScreen: View {
|
||||
.onDisappear {
|
||||
itemIDHandler?(nil)
|
||||
}
|
||||
.zoomTransition(sourceID: currentItem?.id, in: context.viewState.transitionNamespace)
|
||||
.zoomTransition(sourceID: currentItemID, in: context.viewState.transitionNamespace)
|
||||
}
|
||||
|
||||
var quickLookPreview: some View {
|
||||
@@ -57,7 +68,7 @@ struct TimelineMediaPreviewScreen: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var fullScreenButton: some View {
|
||||
if currentItem != nil {
|
||||
if case .media = currentItem {
|
||||
Button {
|
||||
withAnimation { isFullScreen.toggle() }
|
||||
} label: {
|
||||
@@ -73,7 +84,7 @@ struct TimelineMediaPreviewScreen: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var downloadStatusIndicator: some View {
|
||||
if currentItem?.downloadError != nil {
|
||||
if case let .media(mediaItem) = currentItem, mediaItem.downloadError != nil {
|
||||
VStack(spacing: 24) {
|
||||
CompoundIcon(\.error, size: .custom(48), relativeTo: .compound.headingLG)
|
||||
.foregroundStyle(.compound.iconCriticalPrimary)
|
||||
@@ -94,7 +105,7 @@ struct TimelineMediaPreviewScreen: View {
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 40)
|
||||
.background(.compound.bgSubtlePrimary, in: RoundedRectangle(cornerRadius: 14))
|
||||
} else if currentItem?.fileHandle == nil {
|
||||
} else if shouldShowDownloadIndicator {
|
||||
ProgressView()
|
||||
.controlSize(.large)
|
||||
.tint(.compound.iconPrimary)
|
||||
@@ -103,7 +114,7 @@ struct TimelineMediaPreviewScreen: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var caption: some View {
|
||||
if let caption = currentItem?.caption, !isFullScreen {
|
||||
if case let .media(mediaItem) = currentItem, let caption = mediaItem.caption, !isFullScreen {
|
||||
Text(caption)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
@@ -133,9 +144,9 @@ struct TimelineMediaPreviewScreen: View {
|
||||
toolbarHeader
|
||||
}
|
||||
|
||||
if currentItem != nil {
|
||||
if case let .media(mediaItem) = currentItem {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button { context.send(viewAction: .showCurrentItemDetails) } label: {
|
||||
Button { context.send(viewAction: .showItemDetails(mediaItem)) } label: {
|
||||
CompoundIcon(\.info)
|
||||
}
|
||||
.tint(.compound.textActionPrimary)
|
||||
@@ -145,17 +156,18 @@ struct TimelineMediaPreviewScreen: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var toolbarHeader: some View {
|
||||
if let currentItem {
|
||||
switch currentItem {
|
||||
case .media(let mediaItem):
|
||||
VStack(spacing: 0) {
|
||||
Text(currentItem.sender.displayName ?? currentItem.sender.id)
|
||||
Text(mediaItem.sender.displayName ?? mediaItem.sender.id)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
Text(currentItem.timestamp.formatted(date: .abbreviated, time: .omitted))
|
||||
Text(mediaItem.timestamp.formatted(date: .abbreviated, time: .omitted))
|
||||
.font(.compound.bodyXS)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
} else {
|
||||
case .loading:
|
||||
Text(L10n.commonLoadingMore)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
@@ -215,20 +227,41 @@ private struct QuickLookView: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
private func loadCurrentItem() {
|
||||
viewModelContext.send(viewAction: .updateCurrentItem(previewController.currentPreviewItem as? TimelineMediaPreviewItem))
|
||||
if let previewItem = previewController.currentPreviewItem as? TimelineMediaPreviewItem.Media {
|
||||
viewModelContext.send(viewAction: .updateCurrentItem(.media(previewItem)))
|
||||
} else if let loadingItem = previewController.currentPreviewItem as? TimelineMediaPreviewItem.Loading {
|
||||
switch loadingItem.state {
|
||||
case .paginating:
|
||||
viewModelContext.send(viewAction: .updateCurrentItem(.loading(loadingItem)))
|
||||
case .timelineStart:
|
||||
Task { await returnToIndex(viewModelContext.viewState.dataSource.firstPreviewItemIndex) }
|
||||
case .timelineEnd:
|
||||
Task { await returnToIndex(viewModelContext.viewState.dataSource.lastPreviewItemIndex) }
|
||||
}
|
||||
} else {
|
||||
MXLog.error("Unexpected preview item type: \(type(of: previewController.currentPreviewItem))")
|
||||
}
|
||||
}
|
||||
|
||||
private func returnToIndex(_ index: Int) async {
|
||||
// Sleep to fix a bug where the update didn't take effect when the swipe velocity was slow.
|
||||
try? await Task.sleep(for: .seconds(0.1))
|
||||
|
||||
previewController.currentPreviewItemIndex = index
|
||||
viewModelContext.send(viewAction: .timelineEndReached)
|
||||
}
|
||||
|
||||
private func handleUpdatedItems() {
|
||||
if previewController.currentPreviewItem is TimelineMediaPreviewLoadingItem {
|
||||
if previewController.currentPreviewItem is TimelineMediaPreviewItem.Loading {
|
||||
let dataSource = viewModelContext.viewState.dataSource
|
||||
if dataSource.previewController(previewController, previewItemAt: previewController.currentPreviewItemIndex) is TimelineMediaPreviewItem {
|
||||
if dataSource.previewController(previewController, previewItemAt: previewController.currentPreviewItemIndex) is TimelineMediaPreviewItem.Media {
|
||||
previewController.refreshCurrentPreviewItem() // This will trigger loadCurrentItem automatically.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleFileLoaded(itemID: TimelineItemIdentifier) {
|
||||
guard (previewController.currentPreviewItem as? TimelineMediaPreviewItem)?.id == itemID else { return }
|
||||
guard (previewController.currentPreviewItem as? TimelineMediaPreviewItem.Media)?.id == itemID else { return }
|
||||
previewController.refreshCurrentPreviewItem()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user