mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Detect the timeline start/end when swiping through media files. (#3714)
This commit is contained in:
parent
df997ad251
commit
d412c10352
@ -414,6 +414,8 @@
|
|||||||
"screen_knock_requests_list_title" = "Requests to join";
|
"screen_knock_requests_list_title" = "Requests to join";
|
||||||
"screen_media_details_file_format" = "File format";
|
"screen_media_details_file_format" = "File format";
|
||||||
"screen_media_details_filename" = "File name";
|
"screen_media_details_filename" = "File name";
|
||||||
|
"screen_media_details_no_more_files_to_show" = "No more files to show";
|
||||||
|
"screen_media_details_no_more_media_to_show" = "No more media to show";
|
||||||
"screen_media_details_redact_confirmation_message" = "This file will be removed from the room and members won’t have access to it.";
|
"screen_media_details_redact_confirmation_message" = "This file will be removed from the room and members won’t have access to it.";
|
||||||
"screen_media_details_redact_confirmation_title" = "Delete file?";
|
"screen_media_details_redact_confirmation_title" = "Delete file?";
|
||||||
"screen_media_details_uploaded_by" = "Uploaded by";
|
"screen_media_details_uploaded_by" = "Uploaded by";
|
||||||
@ -478,8 +480,6 @@
|
|||||||
"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed.";
|
"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed.";
|
||||||
"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices.";
|
"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices.";
|
||||||
"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices.";
|
"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices.";
|
||||||
"screen.media_details.no_more_files_to_show" = "No more files to show";
|
|
||||||
"screen.media_details.no_more_media_to_show" = "No more media to show";
|
|
||||||
"screen_account_provider_form_hint" = "Homeserver address";
|
"screen_account_provider_form_hint" = "Homeserver address";
|
||||||
"screen_account_provider_form_notice" = "Enter a search term or a domain address.";
|
"screen_account_provider_form_notice" = "Enter a search term or a domain address.";
|
||||||
"screen_account_provider_form_subtitle" = "Search for a company, community, or private server.";
|
"screen_account_provider_form_subtitle" = "Search for a company, community, or private server.";
|
||||||
|
@ -414,6 +414,8 @@
|
|||||||
"screen_knock_requests_list_title" = "Requests to join";
|
"screen_knock_requests_list_title" = "Requests to join";
|
||||||
"screen_media_details_file_format" = "File format";
|
"screen_media_details_file_format" = "File format";
|
||||||
"screen_media_details_filename" = "File name";
|
"screen_media_details_filename" = "File name";
|
||||||
|
"screen_media_details_no_more_files_to_show" = "No more files to show";
|
||||||
|
"screen_media_details_no_more_media_to_show" = "No more media to show";
|
||||||
"screen_media_details_redact_confirmation_message" = "This file will be removed from the room and members won’t have access to it.";
|
"screen_media_details_redact_confirmation_message" = "This file will be removed from the room and members won’t have access to it.";
|
||||||
"screen_media_details_redact_confirmation_title" = "Delete file?";
|
"screen_media_details_redact_confirmation_title" = "Delete file?";
|
||||||
"screen_media_details_uploaded_by" = "Uploaded by";
|
"screen_media_details_uploaded_by" = "Uploaded by";
|
||||||
@ -478,8 +480,6 @@
|
|||||||
"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed.";
|
"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed.";
|
||||||
"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices.";
|
"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices.";
|
||||||
"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices.";
|
"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices.";
|
||||||
"screen.media_details.no_more_files_to_show" = "No more files to show";
|
|
||||||
"screen.media_details.no_more_media_to_show" = "No more media to show";
|
|
||||||
"screen_account_provider_form_hint" = "Homeserver address";
|
"screen_account_provider_form_hint" = "Homeserver address";
|
||||||
"screen_account_provider_form_notice" = "Enter a search term or a domain address.";
|
"screen_account_provider_form_notice" = "Enter a search term or a domain address.";
|
||||||
"screen_account_provider_form_subtitle" = "Search for a company, community, or private server.";
|
"screen_account_provider_form_subtitle" = "Search for a company, community, or private server.";
|
||||||
|
@ -1458,6 +1458,10 @@ internal enum L10n {
|
|||||||
internal static var screenMediaDetailsFileFormat: String { return L10n.tr("Localizable", "screen_media_details_file_format") }
|
internal static var screenMediaDetailsFileFormat: String { return L10n.tr("Localizable", "screen_media_details_file_format") }
|
||||||
/// File name
|
/// File name
|
||||||
internal static var screenMediaDetailsFilename: String { return L10n.tr("Localizable", "screen_media_details_filename") }
|
internal static var screenMediaDetailsFilename: String { return L10n.tr("Localizable", "screen_media_details_filename") }
|
||||||
|
/// No more files to show
|
||||||
|
internal static var screenMediaDetailsNoMoreFilesToShow: String { return L10n.tr("Localizable", "screen_media_details_no_more_files_to_show") }
|
||||||
|
/// No more media to show
|
||||||
|
internal static var screenMediaDetailsNoMoreMediaToShow: String { return L10n.tr("Localizable", "screen_media_details_no_more_media_to_show") }
|
||||||
/// This file will be removed from the room and members won’t have access to it.
|
/// This file will be removed from the room and members won’t have access to it.
|
||||||
internal static var screenMediaDetailsRedactConfirmationMessage: String { return L10n.tr("Localizable", "screen_media_details_redact_confirmation_message") }
|
internal static var screenMediaDetailsRedactConfirmationMessage: String { return L10n.tr("Localizable", "screen_media_details_redact_confirmation_message") }
|
||||||
/// Delete file?
|
/// Delete file?
|
||||||
@ -2804,15 +2808,6 @@ internal enum L10n {
|
|||||||
/// You
|
/// You
|
||||||
internal static var you: String { return L10n.tr("Localizable", "common.you") }
|
internal static var you: String { return L10n.tr("Localizable", "common.you") }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum Screen {
|
|
||||||
internal enum MediaDetails {
|
|
||||||
/// No more files to show
|
|
||||||
internal static var noMoreFilesToShow: String { return L10n.tr("Localizable", "screen.media_details.no_more_files_to_show") }
|
|
||||||
/// No more media to show
|
|
||||||
internal static var noMoreMediaToShow: String { return L10n.tr("Localizable", "screen.media_details.no_more_media_to_show") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
|
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
|
||||||
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
|
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||||
|
@ -19,7 +19,7 @@ import QuickLook
|
|||||||
/// in the data.
|
/// in the data.
|
||||||
class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
||||||
/// All of the items in the timeline that can be previewed.
|
/// All of the items in the timeline that can be previewed.
|
||||||
private(set) var previewItems: [TimelineMediaPreviewItem]
|
private(set) var previewItems: [TimelineMediaPreviewItem.Media]
|
||||||
let previewItemsPaginationPublisher = PassthroughSubject<Void, Never>()
|
let previewItemsPaginationPublisher = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
private let initialItem: EventBasedMessageTimelineItemProtocol
|
private let initialItem: EventBasedMessageTimelineItemProtocol
|
||||||
@ -27,30 +27,37 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
|||||||
let initialItemIndex: Int
|
let initialItemIndex: Int
|
||||||
|
|
||||||
/// The media item that is currently being previewed.
|
/// The media item that is currently being previewed.
|
||||||
private(set) var currentItem: TimelineMediaPreviewItem?
|
private(set) var currentItem: TimelineMediaPreviewItem
|
||||||
|
|
||||||
private var backwardPadding: Int
|
private var backwardPadding: Int
|
||||||
private var forwardPadding: Int
|
private var forwardPadding: Int
|
||||||
|
|
||||||
init(itemViewStates: [RoomTimelineItemViewState], initialItem: EventBasedMessageTimelineItemProtocol, initialPadding: Int = 100) {
|
var paginationState: PaginationState
|
||||||
previewItems = itemViewStates.compactMap(TimelineMediaPreviewItem.init)
|
|
||||||
|
init(itemViewStates: [RoomTimelineItemViewState],
|
||||||
|
initialItem: EventBasedMessageTimelineItemProtocol,
|
||||||
|
initialPadding: Int = 100,
|
||||||
|
paginationState: PaginationState) {
|
||||||
|
previewItems = itemViewStates.compactMap(TimelineMediaPreviewItem.Media.init)
|
||||||
self.initialItem = initialItem
|
self.initialItem = initialItem
|
||||||
|
|
||||||
let initialItemArrayIndex = previewItems.firstIndex { $0.id == initialItem.id } ?? 0
|
let initialItemArrayIndex = previewItems.firstIndex { $0.id == initialItem.id } ?? 0
|
||||||
initialItemIndex = initialItemArrayIndex + initialPadding
|
initialItemIndex = initialItemArrayIndex + initialPadding
|
||||||
currentItem = previewItems[initialItemArrayIndex]
|
currentItem = .media(previewItems[initialItemArrayIndex])
|
||||||
|
|
||||||
backwardPadding = initialPadding
|
backwardPadding = initialPadding
|
||||||
forwardPadding = initialPadding
|
forwardPadding = initialPadding
|
||||||
|
|
||||||
|
self.paginationState = paginationState
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateCurrentItem(_ item: TimelineMediaPreviewItem?) {
|
func updateCurrentItem(_ item: TimelineMediaPreviewItem) {
|
||||||
currentItem = item
|
currentItem = item
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePreviewItems(itemViewStates: [RoomTimelineItemViewState]) {
|
func updatePreviewItems(itemViewStates: [RoomTimelineItemViewState]) {
|
||||||
let newItems: [TimelineMediaPreviewItem] = itemViewStates.compactMap { itemViewState in
|
let newItems: [TimelineMediaPreviewItem.Media] = itemViewStates.compactMap { itemViewState in
|
||||||
guard let newItem = TimelineMediaPreviewItem(roomTimelineItemViewState: itemViewState) else { return nil }
|
guard let newItem = TimelineMediaPreviewItem.Media(roomTimelineItemViewState: itemViewState) else { return nil }
|
||||||
|
|
||||||
// If an item already exists use that instead to preserve the file handle, download error etc.
|
// If an item already exists use that instead to preserve the file handle, download error etc.
|
||||||
if let oldItem = previewItems.first(where: { $0.id == newItem.id }) {
|
if let oldItem = previewItems.first(where: { $0.id == newItem.id }) {
|
||||||
@ -91,6 +98,9 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
|||||||
|
|
||||||
// MARK: - QLPreviewControllerDataSource
|
// MARK: - QLPreviewControllerDataSource
|
||||||
|
|
||||||
|
var firstPreviewItemIndex: Int { backwardPadding }
|
||||||
|
var lastPreviewItemIndex: Int { backwardPadding + previewItems.count - 1 }
|
||||||
|
|
||||||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||||
previewItems.count + backwardPadding + forwardPadding
|
previewItems.count + backwardPadding + forwardPadding
|
||||||
}
|
}
|
||||||
@ -98,18 +108,24 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource {
|
|||||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> any QLPreviewItem {
|
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> any QLPreviewItem {
|
||||||
let arrayIndex = index - backwardPadding
|
let arrayIndex = index - backwardPadding
|
||||||
|
|
||||||
if arrayIndex >= 0, arrayIndex < previewItems.count {
|
if index < firstPreviewItemIndex {
|
||||||
return previewItems[arrayIndex]
|
return paginationState.backward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineStart : .paginating
|
||||||
|
} else if index > lastPreviewItemIndex {
|
||||||
|
return paginationState.forward == .timelineEndReached ? TimelineMediaPreviewItem.Loading.timelineEnd : .paginating
|
||||||
} else {
|
} else {
|
||||||
return TimelineMediaPreviewLoadingItem.shared
|
return previewItems[arrayIndex]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - TimelineMediaPreviewItem
|
// MARK: - TimelineMediaPreviewItem
|
||||||
|
|
||||||
/// Wraps a media file and title to be previewed with QuickLook.
|
enum TimelineMediaPreviewItem: Equatable {
|
||||||
class TimelineMediaPreviewItem: NSObject, QLPreviewItem, Identifiable {
|
case media(Media)
|
||||||
|
case loading(Loading)
|
||||||
|
|
||||||
|
/// Wraps a media file and title to be previewed with QuickLook.
|
||||||
|
class Media: NSObject, QLPreviewItem, Identifiable {
|
||||||
fileprivate(set) var timelineItem: EventBasedMessageTimelineItemProtocol
|
fileprivate(set) var timelineItem: EventBasedMessageTimelineItemProtocol
|
||||||
var fileHandle: MediaFileHandleProxy?
|
var fileHandle: MediaFileHandleProxy?
|
||||||
var downloadError: Error?
|
var downloadError: Error?
|
||||||
@ -254,11 +270,21 @@ class TimelineMediaPreviewItem: NSObject, QLPreviewItem, Identifiable {
|
|||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineMediaPreviewLoadingItem: NSObject, QLPreviewItem {
|
class Loading: NSObject, QLPreviewItem {
|
||||||
static let shared = TimelineMediaPreviewLoadingItem()
|
static let paginating = Loading(state: .paginating)
|
||||||
|
static let timelineStart = Loading(state: .timelineStart)
|
||||||
|
static let timelineEnd = Loading(state: .timelineEnd)
|
||||||
|
|
||||||
|
enum State { case paginating, timelineStart, timelineEnd }
|
||||||
|
let state: State
|
||||||
|
|
||||||
let previewItemURL: URL? = nil
|
let previewItemURL: URL? = nil
|
||||||
let previewItemTitle: String? = "" // Empty to force QLPreviewController to not show any text.
|
let previewItemTitle: String? = "" // Empty to force QLPreviewController to not show any text.
|
||||||
|
|
||||||
|
init(state: State) {
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ struct TimelineMediaPreviewViewState: BindableState {
|
|||||||
var dataSource: TimelineMediaPreviewDataSource
|
var dataSource: TimelineMediaPreviewDataSource
|
||||||
|
|
||||||
/// The media item that is currently being previewed.
|
/// The media item that is currently being previewed.
|
||||||
var currentItem: TimelineMediaPreviewItem? { dataSource.currentItem }
|
var currentItem: TimelineMediaPreviewItem { dataSource.currentItem }
|
||||||
/// All of the available actions for the current item.
|
/// All of the available actions for the current item.
|
||||||
var currentItemActions: TimelineItemMenuActions?
|
var currentItemActions: TimelineItemMenuActions?
|
||||||
|
|
||||||
@ -32,9 +32,9 @@ struct TimelineMediaPreviewViewState: BindableState {
|
|||||||
|
|
||||||
struct TimelineMediaPreviewViewStateBindings {
|
struct TimelineMediaPreviewViewStateBindings {
|
||||||
/// A binding that will present the Details view for the specified item.
|
/// A binding that will present the Details view for the specified item.
|
||||||
var mediaDetailsItem: TimelineMediaPreviewItem?
|
var mediaDetailsItem: TimelineMediaPreviewItem.Media?
|
||||||
/// A binding that will present a confirmation to redact the specified item.
|
/// A binding that will present a confirmation to redact the specified item.
|
||||||
var redactConfirmationItem: TimelineMediaPreviewItem?
|
var redactConfirmationItem: TimelineMediaPreviewItem.Media?
|
||||||
/// A binding that will present a document picker to export the specified file.
|
/// A binding that will present a document picker to export the specified file.
|
||||||
var fileToExport: TimelineMediaPreviewFileExportPicker.File?
|
var fileToExport: TimelineMediaPreviewFileExportPicker.File?
|
||||||
|
|
||||||
@ -46,9 +46,10 @@ enum TimelineMediaPreviewAlertType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum TimelineMediaPreviewViewAction {
|
enum TimelineMediaPreviewViewAction {
|
||||||
case updateCurrentItem(TimelineMediaPreviewItem?)
|
case updateCurrentItem(TimelineMediaPreviewItem)
|
||||||
case showCurrentItemDetails
|
case showItemDetails(TimelineMediaPreviewItem.Media)
|
||||||
case menuAction(TimelineItemMenuAction, item: TimelineMediaPreviewItem)
|
case menuAction(TimelineItemMenuAction, item: TimelineMediaPreviewItem.Media)
|
||||||
case redactConfirmation(item: TimelineMediaPreviewItem)
|
case redactConfirmation(item: TimelineMediaPreviewItem.Media)
|
||||||
|
case timelineEndReached
|
||||||
case dismiss
|
case dismiss
|
||||||
}
|
}
|
||||||
|
@ -35,8 +35,11 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.appMediator = appMediator
|
self.appMediator = appMediator
|
||||||
|
|
||||||
super.init(initialViewState: TimelineMediaPreviewViewState(dataSource: .init(itemViewStates: timelineViewModel.context.viewState.timelineState.itemViewStates,
|
let timelineState = timelineViewModel.context.viewState.timelineState
|
||||||
initialItem: context.item),
|
|
||||||
|
super.init(initialViewState: TimelineMediaPreviewViewState(dataSource: .init(itemViewStates: timelineState.itemViewStates,
|
||||||
|
initialItem: context.item,
|
||||||
|
paginationState: timelineState.paginationState),
|
||||||
transitionNamespace: context.namespace),
|
transitionNamespace: context.namespace),
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider)
|
||||||
|
|
||||||
@ -57,14 +60,19 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
self?.state.dataSource.updatePreviewItems(itemViewStates: itemViewStates)
|
self?.state.dataSource.updatePreviewItems(itemViewStates: itemViewStates)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
timelineViewModel.context.$viewState.map(\.timelineState.paginationState)
|
||||||
|
.removeDuplicates()
|
||||||
|
.weakAssign(to: \.state.dataSource.paginationState, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func process(viewAction: TimelineMediaPreviewViewAction) {
|
override func process(viewAction: TimelineMediaPreviewViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .updateCurrentItem(let item):
|
case .updateCurrentItem(let item):
|
||||||
Task { await updateCurrentItem(item) }
|
Task { await updateCurrentItem(item) }
|
||||||
case .showCurrentItemDetails:
|
case .showItemDetails(let mediaItem):
|
||||||
state.bindings.mediaDetailsItem = state.currentItem
|
state.bindings.mediaDetailsItem = mediaItem
|
||||||
case .menuAction(let action, let item):
|
case .menuAction(let action, let item):
|
||||||
switch action {
|
switch action {
|
||||||
case .viewInRoomTimeline:
|
case .viewInRoomTimeline:
|
||||||
@ -78,28 +86,32 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
}
|
}
|
||||||
case .redactConfirmation(let item):
|
case .redactConfirmation(let item):
|
||||||
redactItem(item)
|
redactItem(item)
|
||||||
|
case .timelineEndReached:
|
||||||
|
showTimelineEndIndicator()
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
actionsSubject.send(.dismiss)
|
actionsSubject.send(.dismiss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateCurrentItem(_ previewItem: TimelineMediaPreviewItem?) async {
|
private func updateCurrentItem(_ previewItem: TimelineMediaPreviewItem) async {
|
||||||
previewItem?.downloadError = nil // Clear any existing error.
|
if case let .media(item) = previewItem {
|
||||||
|
item.downloadError = nil // Clear any existing error.
|
||||||
|
}
|
||||||
state.dataSource.updateCurrentItem(previewItem)
|
state.dataSource.updateCurrentItem(previewItem)
|
||||||
rebuildCurrentItemActions()
|
rebuildCurrentItemActions()
|
||||||
|
|
||||||
if let previewItem {
|
if case let .media(mediaItem) = previewItem {
|
||||||
currentItemIDHandler?(previewItem.id)
|
currentItemIDHandler?(mediaItem.id)
|
||||||
|
|
||||||
if previewItem.fileHandle == nil, let source = previewItem.mediaSource {
|
if mediaItem.fileHandle == nil, let source = mediaItem.mediaSource {
|
||||||
switch await mediaProvider.loadFileFromSource(source, filename: previewItem.filename) {
|
switch await mediaProvider.loadFileFromSource(source, filename: mediaItem.filename) {
|
||||||
case .success(let handle):
|
case .success(let handle):
|
||||||
previewItem.fileHandle = handle
|
mediaItem.fileHandle = handle
|
||||||
state.fileLoadedPublisher.send(previewItem.id)
|
state.fileLoadedPublisher.send(mediaItem.id)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
MXLog.error("Failed loading media: \(error)")
|
MXLog.error("Failed loading media: \(error)")
|
||||||
context.objectWillChange.send() // Manually trigger the SwiftUI view update.
|
context.objectWillChange.send() // Manually trigger the SwiftUI view update.
|
||||||
previewItem.downloadError = error
|
mediaItem.downloadError = error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,8 +119,9 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
|
|
||||||
private func rebuildCurrentItemActions() {
|
private func rebuildCurrentItemActions() {
|
||||||
let timelineContext = timelineViewModel.context
|
let timelineContext = timelineViewModel.context
|
||||||
state.currentItemActions = if let currentItem = state.currentItem {
|
state.currentItemActions = switch state.currentItem {
|
||||||
TimelineItemMenuActionProvider(timelineItem: currentItem.timelineItem,
|
case .media(let mediaItem):
|
||||||
|
TimelineItemMenuActionProvider(timelineItem: mediaItem.timelineItem,
|
||||||
canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf,
|
canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf,
|
||||||
canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers,
|
canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers,
|
||||||
canCurrentUserPin: timelineContext.viewState.canCurrentUserPin,
|
canCurrentUserPin: timelineContext.viewState.canCurrentUserPin,
|
||||||
@ -118,13 +131,13 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
timelineKind: timelineContext.viewState.timelineKind,
|
timelineKind: timelineContext.viewState.timelineKind,
|
||||||
emojiProvider: timelineContext.viewState.emojiProvider)
|
emojiProvider: timelineContext.viewState.emojiProvider)
|
||||||
.makeActions()
|
.makeActions()
|
||||||
} else {
|
case .loading:
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveCurrentItem() async {
|
private func saveCurrentItem() async {
|
||||||
guard let currentItem = state.currentItem, let fileURL = currentItem.fileHandle?.url else {
|
guard case let .media(mediaItem) = state.currentItem, let fileURL = mediaItem.fileHandle?.url else {
|
||||||
MXLog.error("Unable to save an item without a URL, the button shouldn't be visible.")
|
MXLog.error("Unable to save an item without a URL, the button shouldn't be visible.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -133,7 +146,7 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
state.bindings.mediaDetailsItem = nil
|
state.bindings.mediaDetailsItem = nil
|
||||||
|
|
||||||
do {
|
do {
|
||||||
switch currentItem.timelineItem {
|
switch mediaItem.timelineItem {
|
||||||
case is AudioRoomTimelineItem, is FileRoomTimelineItem:
|
case is AudioRoomTimelineItem, is FileRoomTimelineItem:
|
||||||
state.bindings.fileToExport = .init(url: fileURL)
|
state.bindings.fileToExport = .init(url: fileURL)
|
||||||
return // Don't show the indicator.
|
return // Don't show the indicator.
|
||||||
@ -158,7 +171,7 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func redactItem(_ item: TimelineMediaPreviewItem) {
|
private func redactItem(_ item: TimelineMediaPreviewItem.Media) {
|
||||||
timelineViewModel.context.send(viewAction: .handleTimelineItemMenuAction(itemID: item.id, action: .redact))
|
timelineViewModel.context.send(viewAction: .handleTimelineItemMenuAction(itemID: item.id, action: .redact))
|
||||||
state.bindings.redactConfirmationItem = nil
|
state.bindings.redactConfirmationItem = nil
|
||||||
state.bindings.mediaDetailsItem = nil
|
state.bindings.mediaDetailsItem = nil
|
||||||
@ -189,5 +202,11 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
|||||||
iconName: "xmark"))
|
iconName: "xmark"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showTimelineEndIndicator() {
|
||||||
|
userIndicatorController.submitIndicator(UserIndicator(id: statusIndicatorID,
|
||||||
|
type: .toast,
|
||||||
|
title: L10n.screenMediaDetailsNoMoreMediaToShow))
|
||||||
|
}
|
||||||
|
|
||||||
private var statusIndicatorID: String { "\(Self.self)-Status" }
|
private var statusIndicatorID: String { "\(Self.self)-Status" }
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import Compound
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TimelineMediaPreviewDetailsView: View {
|
struct TimelineMediaPreviewDetailsView: View {
|
||||||
let item: TimelineMediaPreviewItem
|
let item: TimelineMediaPreviewItem.Media
|
||||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||||
|
|
||||||
@State private var sheetHeight: CGFloat = .zero
|
@State private var sheetHeight: CGFloat = .zero
|
||||||
@ -132,7 +132,7 @@ struct TimelineMediaPreviewDetailsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct ActionButton: View {
|
private struct ActionButton: View {
|
||||||
let item: TimelineMediaPreviewItem
|
let item: TimelineMediaPreviewItem.Media
|
||||||
let action: TimelineItemMenuAction
|
let action: TimelineItemMenuAction
|
||||||
let context: TimelineMediaPreviewViewModel.Context
|
let context: TimelineMediaPreviewViewModel.Context
|
||||||
|
|
||||||
@ -177,29 +177,31 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
|
|||||||
static let presentedOnRoomViewModel = makeViewModel(isPresentedOnRoomScreen: true)
|
static let presentedOnRoomViewModel = makeViewModel(isPresentedOnRoomScreen: true)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
// swiftlint:disable force_unwrapping
|
if case let .media(mediaItem) = viewModel.state.currentItem {
|
||||||
TimelineMediaPreviewDetailsView(item: viewModel.state.currentItem!,
|
TimelineMediaPreviewDetailsView(item: mediaItem, context: viewModel.context)
|
||||||
context: viewModel.context)
|
|
||||||
.previewDisplayName("Image")
|
.previewDisplayName("Image")
|
||||||
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
|
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
|
||||||
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
|
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
TimelineMediaPreviewDetailsView(item: loadingViewModel.state.currentItem!,
|
if case let .media(mediaItem) = loadingViewModel.state.currentItem {
|
||||||
context: loadingViewModel.context)
|
TimelineMediaPreviewDetailsView(item: mediaItem, context: loadingViewModel.context)
|
||||||
.previewDisplayName("Loading")
|
.previewDisplayName("Loading")
|
||||||
.snapshotPreferences(expect: loadingViewModel.context.$viewState.map { state in
|
.snapshotPreferences(expect: loadingViewModel.context.$viewState.map { state in
|
||||||
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
|
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
TimelineMediaPreviewDetailsView(item: unknownTypeViewModel.state.currentItem!,
|
if case let .media(mediaItem) = unknownTypeViewModel.state.currentItem {
|
||||||
context: unknownTypeViewModel.context)
|
TimelineMediaPreviewDetailsView(item: mediaItem, context: unknownTypeViewModel.context)
|
||||||
.previewDisplayName("Unknown type")
|
.previewDisplayName("Unknown type")
|
||||||
|
}
|
||||||
|
|
||||||
TimelineMediaPreviewDetailsView(item: presentedOnRoomViewModel.state.currentItem!,
|
if case let .media(mediaItem) = presentedOnRoomViewModel.state.currentItem {
|
||||||
context: presentedOnRoomViewModel.context)
|
TimelineMediaPreviewDetailsView(item: mediaItem, context: presentedOnRoomViewModel.context)
|
||||||
.previewDisplayName("Incoming on Room")
|
.previewDisplayName("Incoming on Room")
|
||||||
// swiftlint:enable force_unwrapping
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeViewModel(contentType: UTType? = nil,
|
static func makeViewModel(contentType: UTType? = nil,
|
||||||
|
@ -11,7 +11,7 @@ import SwiftUI
|
|||||||
struct TimelineMediaPreviewRedactConfirmationView: View {
|
struct TimelineMediaPreviewRedactConfirmationView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
let item: TimelineMediaPreviewItem
|
let item: TimelineMediaPreviewItem.Media
|
||||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||||
|
|
||||||
@State private var sheetHeight: CGFloat = .zero
|
@State private var sheetHeight: CGFloat = .zero
|
||||||
@ -125,8 +125,9 @@ struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, Tes
|
|||||||
static let viewModel = makeViewModel(contentType: .jpeg)
|
static let viewModel = makeViewModel(contentType: .jpeg)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
// swiftlint:disable:next force_unwrapping
|
if case let .media(mediaItem) = viewModel.state.currentItem {
|
||||||
TimelineMediaPreviewRedactConfirmationView(item: viewModel.state.currentItem!, context: viewModel.context)
|
TimelineMediaPreviewRedactConfirmationView(item: mediaItem, context: viewModel.context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeViewModel(contentType: UTType? = nil) -> TimelineMediaPreviewViewModel {
|
static func makeViewModel(contentType: UTType? = nil) -> TimelineMediaPreviewViewModel {
|
||||||
|
@ -17,7 +17,18 @@ struct TimelineMediaPreviewScreen: View {
|
|||||||
@State private var isFullScreen = false
|
@State private var isFullScreen = false
|
||||||
private var toolbarVisibility: Visibility { isFullScreen ? .hidden : .visible }
|
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 {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@ -40,7 +51,7 @@ struct TimelineMediaPreviewScreen: View {
|
|||||||
.onDisappear {
|
.onDisappear {
|
||||||
itemIDHandler?(nil)
|
itemIDHandler?(nil)
|
||||||
}
|
}
|
||||||
.zoomTransition(sourceID: currentItem?.id, in: context.viewState.transitionNamespace)
|
.zoomTransition(sourceID: currentItemID, in: context.viewState.transitionNamespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
var quickLookPreview: some View {
|
var quickLookPreview: some View {
|
||||||
@ -57,7 +68,7 @@ struct TimelineMediaPreviewScreen: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var fullScreenButton: some View {
|
private var fullScreenButton: some View {
|
||||||
if currentItem != nil {
|
if case .media = currentItem {
|
||||||
Button {
|
Button {
|
||||||
withAnimation { isFullScreen.toggle() }
|
withAnimation { isFullScreen.toggle() }
|
||||||
} label: {
|
} label: {
|
||||||
@ -73,7 +84,7 @@ struct TimelineMediaPreviewScreen: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var downloadStatusIndicator: some View {
|
private var downloadStatusIndicator: some View {
|
||||||
if currentItem?.downloadError != nil {
|
if case let .media(mediaItem) = currentItem, mediaItem.downloadError != nil {
|
||||||
VStack(spacing: 24) {
|
VStack(spacing: 24) {
|
||||||
CompoundIcon(\.error, size: .custom(48), relativeTo: .compound.headingLG)
|
CompoundIcon(\.error, size: .custom(48), relativeTo: .compound.headingLG)
|
||||||
.foregroundStyle(.compound.iconCriticalPrimary)
|
.foregroundStyle(.compound.iconCriticalPrimary)
|
||||||
@ -94,7 +105,7 @@ struct TimelineMediaPreviewScreen: View {
|
|||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
.padding(.vertical, 40)
|
.padding(.vertical, 40)
|
||||||
.background(.compound.bgSubtlePrimary, in: RoundedRectangle(cornerRadius: 14))
|
.background(.compound.bgSubtlePrimary, in: RoundedRectangle(cornerRadius: 14))
|
||||||
} else if currentItem?.fileHandle == nil {
|
} else if shouldShowDownloadIndicator {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
.tint(.compound.iconPrimary)
|
.tint(.compound.iconPrimary)
|
||||||
@ -103,7 +114,7 @@ struct TimelineMediaPreviewScreen: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var caption: some View {
|
private var caption: some View {
|
||||||
if let caption = currentItem?.caption, !isFullScreen {
|
if case let .media(mediaItem) = currentItem, let caption = mediaItem.caption, !isFullScreen {
|
||||||
Text(caption)
|
Text(caption)
|
||||||
.font(.compound.bodyLG)
|
.font(.compound.bodyLG)
|
||||||
.foregroundStyle(.compound.textPrimary)
|
.foregroundStyle(.compound.textPrimary)
|
||||||
@ -133,9 +144,9 @@ struct TimelineMediaPreviewScreen: View {
|
|||||||
toolbarHeader
|
toolbarHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentItem != nil {
|
if case let .media(mediaItem) = currentItem {
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
Button { context.send(viewAction: .showCurrentItemDetails) } label: {
|
Button { context.send(viewAction: .showItemDetails(mediaItem)) } label: {
|
||||||
CompoundIcon(\.info)
|
CompoundIcon(\.info)
|
||||||
}
|
}
|
||||||
.tint(.compound.textActionPrimary)
|
.tint(.compound.textActionPrimary)
|
||||||
@ -145,17 +156,18 @@ struct TimelineMediaPreviewScreen: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var toolbarHeader: some View {
|
private var toolbarHeader: some View {
|
||||||
if let currentItem {
|
switch currentItem {
|
||||||
|
case .media(let mediaItem):
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Text(currentItem.sender.displayName ?? currentItem.sender.id)
|
Text(mediaItem.sender.displayName ?? mediaItem.sender.id)
|
||||||
.font(.compound.bodySMSemibold)
|
.font(.compound.bodySMSemibold)
|
||||||
.foregroundStyle(.compound.textPrimary)
|
.foregroundStyle(.compound.textPrimary)
|
||||||
Text(currentItem.timestamp.formatted(date: .abbreviated, time: .omitted))
|
Text(mediaItem.timestamp.formatted(date: .abbreviated, time: .omitted))
|
||||||
.font(.compound.bodyXS)
|
.font(.compound.bodyXS)
|
||||||
.foregroundStyle(.compound.textPrimary)
|
.foregroundStyle(.compound.textPrimary)
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
}
|
}
|
||||||
} else {
|
case .loading:
|
||||||
Text(L10n.commonLoadingMore)
|
Text(L10n.commonLoadingMore)
|
||||||
.font(.compound.bodySMSemibold)
|
.font(.compound.bodySMSemibold)
|
||||||
.foregroundStyle(.compound.textPrimary)
|
.foregroundStyle(.compound.textPrimary)
|
||||||
@ -215,20 +227,41 @@ private struct QuickLookView: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func loadCurrentItem() {
|
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() {
|
private func handleUpdatedItems() {
|
||||||
if previewController.currentPreviewItem is TimelineMediaPreviewLoadingItem {
|
if previewController.currentPreviewItem is TimelineMediaPreviewItem.Loading {
|
||||||
let dataSource = viewModelContext.viewState.dataSource
|
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.
|
previewController.refreshCurrentPreviewItem() // This will trigger loadCurrentItem automatically.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleFileLoaded(itemID: TimelineItemIdentifier) {
|
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()
|
previewController.refreshCurrentPreviewItem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -586,10 +586,13 @@ final class TimelineProxy: TimelineProxyProtocol {
|
|||||||
MXLog.error("Failed to subscribe to back pagination status with error: \(error)")
|
MXLog.error("Failed to subscribe to back pagination status with error: \(error)")
|
||||||
}
|
}
|
||||||
forwardPaginationStatusSubject.send(.timelineEndReached)
|
forwardPaginationStatusSubject.send(.timelineEndReached)
|
||||||
case .detached, .media:
|
case .detached:
|
||||||
// Detached timelines don't support observation, set the initial state ourself.
|
// Detached timelines don't support observation, set the initial state ourself.
|
||||||
backPaginationStatusSubject.send(.idle)
|
backPaginationStatusSubject.send(.idle)
|
||||||
forwardPaginationStatusSubject.send(.idle)
|
forwardPaginationStatusSubject.send(.idle)
|
||||||
|
case .media(let presentation):
|
||||||
|
backPaginationStatusSubject.send(.idle)
|
||||||
|
forwardPaginationStatusSubject.send(presentation == .mediaFilesScreen ? .timelineEndReached : .idle)
|
||||||
case .pinned:
|
case .pinned:
|
||||||
backPaginationStatusSubject.send(.timelineEndReached)
|
backPaginationStatusSubject.send(.timelineEndReached)
|
||||||
forwardPaginationStatusSubject.send(.timelineEndReached)
|
forwardPaginationStatusSubject.send(.timelineEndReached)
|
||||||
|
@ -27,16 +27,17 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
// Given a data source built with the initial items.
|
// Given a data source built with the initial items.
|
||||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
||||||
initialItem: initialMediaItems[initialItemIndex],
|
initialItem: initialMediaItems[initialItemIndex],
|
||||||
initialPadding: initialPadding)
|
initialPadding: initialPadding,
|
||||||
|
paginationState: .initial)
|
||||||
|
|
||||||
// When the preview controller displays the data.
|
// When the preview controller displays the data.
|
||||||
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||||
let displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem
|
let displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||||
|
|
||||||
// Then the preview controller should be showing the initial item and the data source should reflect this.
|
// Then the preview controller should be showing the initial item and the data source should reflect this.
|
||||||
XCTAssertEqual(dataSource.initialItemIndex, initialItemIndex + initialPadding, "The initial item index should be padded for the preview controller.")
|
XCTAssertEqual(dataSource.initialItemIndex, initialItemIndex + initialPadding, "The initial item index should be padded for the preview controller.")
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should be the initial item.")
|
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should be the initial item.")
|
||||||
XCTAssertEqual(dataSource.currentItem?.id, initialMediaItems[initialItemIndex].id, "The current item should also be the initial item.")
|
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should also be the initial item.")
|
||||||
|
|
||||||
XCTAssertEqual(dataSource.previewItems.count, initialMediaViewStates.count, "The initial count of preview items should be correct.")
|
XCTAssertEqual(dataSource.previewItems.count, initialMediaViewStates.count, "The initial count of preview items should be correct.")
|
||||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
||||||
@ -46,23 +47,29 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
|
|
||||||
func testCurrentUpdateItem() {
|
func testCurrentUpdateItem() {
|
||||||
// Given a data source built with the initial items.
|
// Given a data source built with the initial items.
|
||||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates, initialItem: initialMediaItems[initialItemIndex])
|
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
||||||
|
initialItem: initialMediaItems[initialItemIndex],
|
||||||
|
paginationState: .initial)
|
||||||
|
|
||||||
// When a different item is displayed.
|
// When a different item is displayed.
|
||||||
let previewItem = dataSource.previewController(previewController, previewItemAt: 1 + initialPadding) as? TimelineMediaPreviewItem
|
guard let previewItem = dataSource.previewController(previewController, previewItemAt: 1 + initialPadding) as? TimelineMediaPreviewItem.Media else {
|
||||||
XCTAssertNotNil(previewItem, "A preview item should be found.")
|
XCTFail("A preview item should be found.")
|
||||||
dataSource.updateCurrentItem(previewItem)
|
return
|
||||||
|
}
|
||||||
|
dataSource.updateCurrentItem(.media(previewItem))
|
||||||
|
|
||||||
// Then the data source should reflect the change of item.
|
// Then the data source should reflect the change of item.
|
||||||
XCTAssertEqual(dataSource.currentItem?.id, previewItem?.id, "The displayed item should be the initial item.")
|
XCTAssertEqual(dataSource.currentMediaItemID, previewItem.id, "The displayed item should be the initial item.")
|
||||||
|
|
||||||
// When a loading item is displayed.
|
// When a loading item is displayed.
|
||||||
let loadingItem = dataSource.previewController(previewController, previewItemAt: initialPadding - 1) as? TimelineMediaPreviewLoadingItem
|
guard let loadingItem = dataSource.previewController(previewController, previewItemAt: initialPadding - 1) as? TimelineMediaPreviewItem.Loading else {
|
||||||
XCTAssertNotNil(loadingItem, "A loading item should be be returned.")
|
XCTFail("A loading item should be be returned.")
|
||||||
dataSource.updateCurrentItem(nil)
|
return
|
||||||
|
}
|
||||||
|
dataSource.updateCurrentItem(.loading(loadingItem))
|
||||||
|
|
||||||
// Then the data source should indicate that no item is being displayed.
|
// Then the data source should show a loading item
|
||||||
XCTAssertNil(dataSource.currentItem, "The current item should be nil.")
|
XCTAssertEqual(dataSource.currentItem, .loading(loadingItem), "The displayed item should be the loading item.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUpdatedItems() async throws {
|
func testUpdatedItems() async throws {
|
||||||
@ -77,9 +84,9 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||||
let displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem
|
let displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
||||||
XCTAssertEqual(dataSource.currentItem?.id, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
||||||
|
|
||||||
XCTAssertEqual(dataSource.previewItems.count, initialMediaViewStates.count, "The number of items should not change.")
|
XCTAssertEqual(dataSource.previewItems.count, initialMediaViewStates.count, "The number of items should not change.")
|
||||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The padded number of items should not change.")
|
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The padded number of items should not change.")
|
||||||
@ -100,9 +107,9 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||||
|
|
||||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||||
var displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem
|
var displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
||||||
XCTAssertEqual(dataSource.currentItem?.id, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
||||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||||
|
|
||||||
// When more items are loaded in a forward pagination or sync.
|
// When more items are loaded in a forward pagination or sync.
|
||||||
@ -116,9 +123,9 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||||
|
|
||||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||||
displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem
|
displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
||||||
XCTAssertEqual(dataSource.currentItem?.id, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
||||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,9 +146,9 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||||
|
|
||||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||||
var displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem
|
var displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
||||||
XCTAssertEqual(dataSource.currentItem?.id, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
||||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||||
|
|
||||||
// When paginating forwards by more than the available padding.
|
// When paginating forwards by more than the available padding.
|
||||||
@ -155,9 +162,9 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||||
|
|
||||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||||
displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem
|
displayedItem = dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media
|
||||||
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
XCTAssertEqual(displayedItem?.id, initialMediaItems[initialItemIndex].id, "The displayed item should not change.")
|
||||||
XCTAssertEqual(dataSource.currentItem?.id, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id, "The current item should not change.")
|
||||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,3 +176,12 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
|||||||
.filter(\.supportsMediaCaption) // Voice messages can't be previewed (and don't support captions).
|
.filter(\.supportsMediaCaption) // Voice messages can't be previewed (and don't support captions).
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension TimelineMediaPreviewDataSource {
|
||||||
|
var currentMediaItemID: TimelineItemIdentifier? {
|
||||||
|
switch currentItem {
|
||||||
|
case .media(let mediaItem): mediaItem.id
|
||||||
|
case .loading: nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -25,7 +25,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
// Given a fresh view model.
|
// Given a fresh view model.
|
||||||
setupViewModel()
|
setupViewModel()
|
||||||
XCTAssertFalse(mediaProvider.loadFileFromSourceFilenameCalled)
|
XCTAssertFalse(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||||
XCTAssertEqual(context.viewState.currentItem, context.viewState.dataSource.previewItems[0])
|
XCTAssertEqual(context.viewState.currentItem, .media(context.viewState.dataSource.previewItems[0]))
|
||||||
XCTAssertNotNil(context.viewState.currentItemActions)
|
XCTAssertNotNil(context.viewState.currentItemActions)
|
||||||
|
|
||||||
// When the preview controller sets the current item.
|
// When the preview controller sets the current item.
|
||||||
@ -33,32 +33,32 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
// Then the view model should load the item and update its view state.
|
// Then the view model should load the item and update its view state.
|
||||||
XCTAssertTrue(mediaProvider.loadFileFromSourceFilenameCalled)
|
XCTAssertTrue(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||||
XCTAssertEqual(context.viewState.currentItem, context.viewState.dataSource.previewItems[0])
|
XCTAssertEqual(context.viewState.currentItem, .media(context.viewState.dataSource.previewItems[0]))
|
||||||
XCTAssertNotNil(context.viewState.currentItemActions)
|
XCTAssertNotNil(context.viewState.currentItemActions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLoadingItemFailure() async throws {
|
func testLoadingItemFailure() async throws {
|
||||||
// Given a fresh view model.
|
// Given a fresh view model.
|
||||||
setupViewModel()
|
setupViewModel()
|
||||||
guard let currentItem = context.viewState.currentItem else {
|
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||||
XCTFail("There should be a current item")
|
XCTFail("There should be a current item")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
XCTAssertFalse(mediaProvider.loadFileFromSourceFilenameCalled)
|
XCTAssertFalse(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||||
XCTAssertEqual(currentItem, context.viewState.dataSource.previewItems[0])
|
XCTAssertEqual(mediaItem, context.viewState.dataSource.previewItems[0])
|
||||||
XCTAssertNil(currentItem.downloadError)
|
XCTAssertNil(mediaItem.downloadError)
|
||||||
|
|
||||||
// When the preview controller sets an item that fails to load.
|
// When the preview controller sets an item that fails to load.
|
||||||
mediaProvider.loadFileFromSourceFilenameClosure = { _, _ in .failure(.failedRetrievingFile) }
|
mediaProvider.loadFileFromSourceFilenameClosure = { _, _ in .failure(.failedRetrievingFile) }
|
||||||
let failure = deferFailure(viewModel.state.fileLoadedPublisher, timeout: 1) { _ in true }
|
let failure = deferFailure(viewModel.state.fileLoadedPublisher, timeout: 1) { _ in true }
|
||||||
context.send(viewAction: .updateCurrentItem(context.viewState.dataSource.previewItems[0]))
|
context.send(viewAction: .updateCurrentItem(.media(context.viewState.dataSource.previewItems[0])))
|
||||||
try await failure.fulfill()
|
try await failure.fulfill()
|
||||||
|
|
||||||
// Then the view model should load the item and update its view state.
|
// Then the view model should load the item and update its view state.
|
||||||
XCTAssertTrue(mediaProvider.loadFileFromSourceFilenameCalled)
|
XCTAssertTrue(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||||
XCTAssertEqual(currentItem, context.viewState.dataSource.previewItems[0])
|
XCTAssertEqual(mediaItem, context.viewState.dataSource.previewItems[0])
|
||||||
XCTAssertNotNil(currentItem.downloadError)
|
XCTAssertNotNil(mediaItem.downloadError)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSwipingBetweenItems() async throws {
|
func testSwipingBetweenItems() async throws {
|
||||||
@ -67,21 +67,21 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
// When swiping to another item.
|
// When swiping to another item.
|
||||||
let deferred = deferFulfillment(viewModel.state.fileLoadedPublisher) { _ in true }
|
let deferred = deferFulfillment(viewModel.state.fileLoadedPublisher) { _ in true }
|
||||||
context.send(viewAction: .updateCurrentItem(context.viewState.dataSource.previewItems[1]))
|
context.send(viewAction: .updateCurrentItem(.media(context.viewState.dataSource.previewItems[1])))
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
// Then the view model should load the item and update its view state.
|
// Then the view model should load the item and update its view state.
|
||||||
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 2)
|
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 2)
|
||||||
XCTAssertEqual(context.viewState.currentItem, context.viewState.dataSource.previewItems[1])
|
XCTAssertEqual(context.viewState.currentItem, .media(context.viewState.dataSource.previewItems[1]))
|
||||||
|
|
||||||
// When swiping back to the first item.
|
// When swiping back to the first item.
|
||||||
let failure = deferFailure(viewModel.state.fileLoadedPublisher, timeout: 1) { _ in true }
|
let failure = deferFailure(viewModel.state.fileLoadedPublisher, timeout: 1) { _ in true }
|
||||||
context.send(viewAction: .updateCurrentItem(context.viewState.dataSource.previewItems[0]))
|
context.send(viewAction: .updateCurrentItem(.media(context.viewState.dataSource.previewItems[0])))
|
||||||
try await failure.fulfill()
|
try await failure.fulfill()
|
||||||
|
|
||||||
// Then the view model should not need to load the item, but should still update its view state.
|
// Then the view model should not need to load the item, but should still update its view state.
|
||||||
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 2)
|
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 2)
|
||||||
XCTAssertEqual(context.viewState.currentItem, context.viewState.dataSource.previewItems[0])
|
XCTAssertEqual(context.viewState.currentItem, .media(context.viewState.dataSource.previewItems[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLoadingMoreItem() async throws {
|
func testLoadingMoreItem() async throws {
|
||||||
@ -90,12 +90,12 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
// When swiping to a "loading more" item.
|
// When swiping to a "loading more" item.
|
||||||
let deferred = deferFailure(viewModel.state.fileLoadedPublisher, timeout: 1) { _ in true }
|
let deferred = deferFailure(viewModel.state.fileLoadedPublisher, timeout: 1) { _ in true }
|
||||||
context.send(viewAction: .updateCurrentItem(nil))
|
context.send(viewAction: .updateCurrentItem(.loading(.paginating)))
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
// Then there should no longer be a media preview and no attempt should be made to load one.
|
// Then there should no longer be a media preview and no attempt should be made to load one.
|
||||||
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 1)
|
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 1)
|
||||||
XCTAssertNil(context.viewState.currentItem)
|
XCTAssertEqual(context.viewState.currentItem, .loading(.paginating))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPagination() async throws {
|
func testPagination() async throws {
|
||||||
@ -112,7 +112,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
// And the preview controller attempts to update the current item (now at a new index in the array but it hasn't changed in the data source).
|
// And the preview controller attempts to update the current item (now at a new index in the array but it hasn't changed in the data source).
|
||||||
mediaProvider.loadFileFromSourceFilenameClosure = { _, _ in .failure(.failedRetrievingFile) }
|
mediaProvider.loadFileFromSourceFilenameClosure = { _, _ in .failure(.failedRetrievingFile) }
|
||||||
let failure = deferFailure(viewModel.state.fileLoadedPublisher, timeout: 1) { _ in true }
|
let failure = deferFailure(viewModel.state.fileLoadedPublisher, timeout: 1) { _ in true }
|
||||||
context.send(viewAction: .updateCurrentItem(context.viewState.dataSource.previewItems[3]))
|
context.send(viewAction: .updateCurrentItem(.media(context.viewState.dataSource.previewItems[3])))
|
||||||
try await failure.fulfill()
|
try await failure.fulfill()
|
||||||
|
|
||||||
// Then the current item shouldn't need to be reloaded.
|
// Then the current item shouldn't need to be reloaded.
|
||||||
@ -125,13 +125,13 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
try await testLoadingItem()
|
try await testLoadingItem()
|
||||||
|
|
||||||
// When choosing to view the current item in the timeline.
|
// When choosing to view the current item in the timeline.
|
||||||
guard let item = context.viewState.currentItem else {
|
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||||
XCTFail("There should be a current item.")
|
XCTFail("There should be a current item.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .viewInRoomTimeline(item.id) }
|
let deferred = deferFulfillment(viewModel.actions) { $0 == .viewInRoomTimeline(mediaItem.id) }
|
||||||
context.send(viewAction: .menuAction(.viewInRoomTimeline, item: item))
|
context.send(viewAction: .menuAction(.viewInRoomTimeline, item: mediaItem))
|
||||||
|
|
||||||
// Then the action should be sent upwards to make this happen.
|
// Then the action should be sent upwards to make this happen.
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
@ -142,27 +142,31 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
try await testLoadingItem()
|
try await testLoadingItem()
|
||||||
XCTAssertNil(context.redactConfirmationItem)
|
XCTAssertNil(context.redactConfirmationItem)
|
||||||
XCTAssertFalse(timelineController.redactCalled)
|
XCTAssertFalse(timelineController.redactCalled)
|
||||||
|
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||||
|
XCTFail("There should be a current item.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// When choosing to show the item details.
|
// When choosing to show the item details.
|
||||||
context.send(viewAction: .showCurrentItemDetails)
|
context.send(viewAction: .showItemDetails(mediaItem))
|
||||||
|
|
||||||
// Then the details sheet should be presented.
|
// Then the details sheet should be presented.
|
||||||
guard let item = context.mediaDetailsItem else {
|
guard let mediaDetailsItem = context.mediaDetailsItem else {
|
||||||
XCTFail("The default of the current item should be presented")
|
XCTFail("The default of the current item should be presented")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
XCTAssertEqual(context.mediaDetailsItem, context.viewState.currentItem)
|
XCTAssertEqual(.media(mediaDetailsItem), context.viewState.currentItem)
|
||||||
|
|
||||||
// When choosing to redact the item.
|
// When choosing to redact the item.
|
||||||
context.send(viewAction: .menuAction(.redact, item: item))
|
context.send(viewAction: .menuAction(.redact, item: mediaDetailsItem))
|
||||||
|
|
||||||
// Then the confirmation sheet should be presented.
|
// Then the confirmation sheet should be presented.
|
||||||
XCTAssertEqual(context.redactConfirmationItem, item)
|
XCTAssertEqual(context.redactConfirmationItem, mediaDetailsItem)
|
||||||
XCTAssertFalse(timelineController.redactCalled)
|
XCTAssertFalse(timelineController.redactCalled)
|
||||||
|
|
||||||
// When confirming the redaction.
|
// When confirming the redaction.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
||||||
context.send(viewAction: .redactConfirmation(item: item))
|
context.send(viewAction: .redactConfirmation(item: mediaDetailsItem))
|
||||||
|
|
||||||
// Then the item should be redacted and the view should be dismissed.
|
// Then the item should be redacted and the view should be dismissed.
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
@ -172,35 +176,35 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
func testSaveImage() async throws {
|
func testSaveImage() async throws {
|
||||||
// Given a view model with a loaded image.
|
// Given a view model with a loaded image.
|
||||||
try await testLoadingItem()
|
try await testLoadingItem()
|
||||||
guard let currentItem = context.viewState.currentItem else {
|
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||||
XCTFail("There should be a current item")
|
XCTFail("There should be a current item")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
XCTAssertEqual(currentItem.contentType, "JPEG image")
|
XCTAssertEqual(mediaItem.contentType, "JPEG image")
|
||||||
|
|
||||||
// When choosing to save the image.
|
// When choosing to save the image.
|
||||||
context.send(viewAction: .menuAction(.save, item: currentItem))
|
context.send(viewAction: .menuAction(.save, item: mediaItem))
|
||||||
try await Task.sleep(for: .seconds(0.5))
|
try await Task.sleep(for: .seconds(0.5))
|
||||||
|
|
||||||
// Then the image should be saved as a photo to the user's photo library.
|
// Then the image should be saved as a photo to the user's photo library.
|
||||||
XCTAssertTrue(photoLibraryManager.addResourceAtCalled)
|
XCTAssertTrue(photoLibraryManager.addResourceAtCalled)
|
||||||
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.type, .photo)
|
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.type, .photo)
|
||||||
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.url, currentItem.fileHandle?.url)
|
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.url, mediaItem.fileHandle?.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSaveImageWithoutAuthorization() async throws {
|
func testSaveImageWithoutAuthorization() async throws {
|
||||||
// Given a view model with a loaded image where the user has denied access to the photo library.
|
// Given a view model with a loaded image where the user has denied access to the photo library.
|
||||||
setupViewModel(photoLibraryAuthorizationDenied: true)
|
setupViewModel(photoLibraryAuthorizationDenied: true)
|
||||||
try await loadInitialItem()
|
try await loadInitialItem()
|
||||||
guard let currentItem = context.viewState.currentItem else {
|
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||||
XCTFail("There should be a current item")
|
XCTFail("There should be a current item")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
XCTAssertEqual(currentItem.contentType, "JPEG image")
|
XCTAssertEqual(mediaItem.contentType, "JPEG image")
|
||||||
|
|
||||||
// When choosing to save the image.
|
// When choosing to save the image.
|
||||||
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||||
context.send(viewAction: .menuAction(.save, item: currentItem))
|
context.send(viewAction: .menuAction(.save, item: mediaItem))
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
// Then the user should be prompted to allow access.
|
// Then the user should be prompted to allow access.
|
||||||
@ -212,40 +216,40 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
// Given a view model with a loaded video.
|
// Given a view model with a loaded video.
|
||||||
setupViewModel(initialItemIndex: 1)
|
setupViewModel(initialItemIndex: 1)
|
||||||
try await loadInitialItem()
|
try await loadInitialItem()
|
||||||
guard let currentItem = context.viewState.currentItem else {
|
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||||
XCTFail("There should be a current item")
|
XCTFail("There should be a current item")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
XCTAssertEqual(currentItem.contentType, "MPEG-4 movie")
|
XCTAssertEqual(mediaItem.contentType, "MPEG-4 movie")
|
||||||
|
|
||||||
// When choosing to save the video.
|
// When choosing to save the video.
|
||||||
context.send(viewAction: .menuAction(.save, item: currentItem))
|
context.send(viewAction: .menuAction(.save, item: mediaItem))
|
||||||
try await Task.sleep(for: .seconds(0.5))
|
try await Task.sleep(for: .seconds(0.5))
|
||||||
|
|
||||||
// Then the video should be saved as a video in the user's photo library.
|
// Then the video should be saved as a video in the user's photo library.
|
||||||
XCTAssertTrue(photoLibraryManager.addResourceAtCalled)
|
XCTAssertTrue(photoLibraryManager.addResourceAtCalled)
|
||||||
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.type, .video)
|
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.type, .video)
|
||||||
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.url, currentItem.fileHandle?.url)
|
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.url, mediaItem.fileHandle?.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSaveFile() async throws {
|
func testSaveFile() async throws {
|
||||||
// Given a view model with a loaded file.
|
// Given a view model with a loaded file.
|
||||||
setupViewModel(initialItemIndex: 2)
|
setupViewModel(initialItemIndex: 2)
|
||||||
try await loadInitialItem()
|
try await loadInitialItem()
|
||||||
guard let currentItem = context.viewState.currentItem else {
|
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||||
XCTFail("There should be a current item")
|
XCTFail("There should be a current item")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
XCTAssertEqual(currentItem.contentType, "PDF document")
|
XCTAssertEqual(mediaItem.contentType, "PDF document")
|
||||||
|
|
||||||
// When choosing to save the file.
|
// When choosing to save the file.
|
||||||
context.send(viewAction: .menuAction(.save, item: currentItem))
|
context.send(viewAction: .menuAction(.save, item: mediaItem))
|
||||||
try await Task.sleep(for: .seconds(0.5))
|
try await Task.sleep(for: .seconds(0.5))
|
||||||
|
|
||||||
// Then the binding should be set for the user to export the file to their specified location.
|
// Then the binding should be set for the user to export the file to their specified location.
|
||||||
XCTAssertFalse(photoLibraryManager.addResourceAtCalled)
|
XCTAssertFalse(photoLibraryManager.addResourceAtCalled)
|
||||||
XCTAssertNotNil(context.fileToExport)
|
XCTAssertNotNil(context.fileToExport)
|
||||||
XCTAssertEqual(context.fileToExport?.url, currentItem.fileHandle?.url)
|
XCTAssertEqual(context.fileToExport?.url, mediaItem.fileHandle?.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDismiss() async throws {
|
func testDismiss() async throws {
|
||||||
@ -266,11 +270,11 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
|||||||
let deferred = deferFulfillment(viewModel.state.fileLoadedPublisher) { _ in true }
|
let deferred = deferFulfillment(viewModel.state.fileLoadedPublisher) { _ in true }
|
||||||
let initialItem = context.viewState.dataSource.previewController(QLPreviewController(),
|
let initialItem = context.viewState.dataSource.previewController(QLPreviewController(),
|
||||||
previewItemAt: context.viewState.dataSource.initialItemIndex)
|
previewItemAt: context.viewState.dataSource.initialItemIndex)
|
||||||
guard let initialPreviewItem = initialItem as? TimelineMediaPreviewItem else {
|
guard let initialPreviewItem = initialItem as? TimelineMediaPreviewItem.Media else {
|
||||||
XCTFail("1234")
|
XCTFail("The initial item should be a media preview.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
context.send(viewAction: .updateCurrentItem(initialPreviewItem))
|
context.send(viewAction: .updateCurrentItem(.media(initialPreviewItem)))
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user