mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Enable the media browser feature 🖼️ (#3642)
* Overlay a progress indicator for downloads instead of using a toast indicator. * Update the SDK. * Remove the feature flag for the media browser. * Remove the media captions feature flag too. * Add unit test cases for download failure and swiping between items. * Snapshots (with the media browser visible in the screen).
This commit is contained in:
@@ -52,6 +52,7 @@ enum TimelineMediaPreviewAlertType {
|
||||
class TimelineMediaPreviewItem: NSObject, QLPreviewItem, Identifiable {
|
||||
let timelineItem: EventBasedMessageTimelineItemProtocol
|
||||
var fileHandle: MediaFileHandleProxy?
|
||||
var downloadError: Error?
|
||||
|
||||
init(timelineItem: EventBasedMessageTimelineItemProtocol) {
|
||||
self.timelineItem = timelineItem
|
||||
|
||||
@@ -80,21 +80,21 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
}
|
||||
|
||||
private func updateCurrentItem(_ previewItem: TimelineMediaPreviewItem) async {
|
||||
previewItem.downloadError = nil // Clear any existing error.
|
||||
state.currentItem = previewItem
|
||||
currentItemIDHandler?(previewItem.id)
|
||||
|
||||
rebuildCurrentItemActions()
|
||||
|
||||
if previewItem.fileHandle == nil, let source = previewItem.mediaSource {
|
||||
showDownloadingIndicator(itemID: previewItem.id)
|
||||
defer { hideDownloadingIndicator(itemID: previewItem.id) }
|
||||
|
||||
switch await mediaProvider.loadFileFromSource(source, filename: previewItem.filename) {
|
||||
case .success(let handle):
|
||||
previewItem.fileHandle = handle
|
||||
state.fileLoadedPublisher.send(previewItem.id)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed loading media: \(error)")
|
||||
showDownloadErrorIndicator()
|
||||
context.objectWillChange.send() // Manually trigger the SwiftUI view update.
|
||||
previewItem.downloadError = error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,6 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
pinnedEventIDs: timelineContext.viewState.pinnedEventIDs,
|
||||
isDM: timelineContext.viewState.isEncryptedOneToOneRoom,
|
||||
isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled,
|
||||
isCreateMediaCaptionsEnabled: timelineContext.viewState.isCreateMediaCaptionsEnabled,
|
||||
timelineKind: timelineContext.viewState.timelineKind,
|
||||
emojiProvider: timelineContext.viewState.emojiProvider)
|
||||
state.currentItemActions = provider.makeActions()
|
||||
@@ -156,39 +155,17 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
|
||||
// MARK: - Indicators
|
||||
|
||||
private func showDownloadingIndicator(itemID: TimelineItemIdentifier) {
|
||||
let indicatorID = makeDownloadIndicatorID(itemID: itemID)
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: indicatorID,
|
||||
type: .toast(progress: .indeterminate),
|
||||
title: L10n.commonDownloading,
|
||||
persistent: true),
|
||||
delay: .seconds(0.1)) // Don't show the indicator when the SDK loads the file from the store.
|
||||
}
|
||||
|
||||
private func hideDownloadingIndicator(itemID: TimelineItemIdentifier) {
|
||||
let indicatorID = makeDownloadIndicatorID(itemID: itemID)
|
||||
userIndicatorController.retractIndicatorWithId(indicatorID)
|
||||
}
|
||||
|
||||
// FIXME: Add the strings and correct indicator types
|
||||
private func showDownloadErrorIndicator() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: downloadErrorIndicatorID,
|
||||
type: .modal,
|
||||
title: L10n.errorUnknown,
|
||||
iconName: "exclamationmark.circle.fill"))
|
||||
}
|
||||
|
||||
private func showRedactedIndicator() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: statusIndicatorID,
|
||||
type: .toast,
|
||||
title: "File deleted",
|
||||
title: L10n.commonFileDeleted,
|
||||
iconName: "checkmark"))
|
||||
}
|
||||
|
||||
private func showSavedIndicator() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: statusIndicatorID,
|
||||
type: .toast,
|
||||
title: "File saved",
|
||||
title: L10n.commonFileSaved,
|
||||
iconName: "checkmark"))
|
||||
}
|
||||
|
||||
@@ -200,10 +177,4 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
}
|
||||
|
||||
private var statusIndicatorID: String { "\(Self.self)-Status" }
|
||||
|
||||
// Separate indicator IDs for downloads as these can be triggered in the background when swiping between items
|
||||
private var downloadErrorIndicatorID: String { "\(Self.self)-DownloadError" }
|
||||
private func makeDownloadIndicatorID(itemID: TimelineItemIdentifier) -> String {
|
||||
"\(Self.self)-Download-\(itemID.uniqueID.id)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ struct TimelineMediaPreviewScreen: View {
|
||||
Color.clear // A completely clear view breaks any SwiftUI gestures (such as drag to dismiss).
|
||||
.background { QuickLookView(viewModelContext: context).ignoresSafeArea() } // Not the root view to stop QL hijacking the toolbar.
|
||||
.overlay(alignment: .topTrailing) { fullScreenButton }
|
||||
.overlay { downloadStatusIndicator }
|
||||
.toolbar { toolbar }
|
||||
.toolbar(toolbarVisibility, for: .navigationBar)
|
||||
.toolbar(toolbarVisibility, for: .bottomBar)
|
||||
@@ -69,6 +70,36 @@ struct TimelineMediaPreviewScreen: View {
|
||||
.padding(.trailing, 14)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var downloadStatusIndicator: some View {
|
||||
if currentItem.downloadError != nil {
|
||||
VStack(spacing: 24) {
|
||||
CompoundIcon(\.error, size: .custom(48), relativeTo: .compound.headingLG)
|
||||
.foregroundStyle(.compound.iconCriticalPrimary)
|
||||
.padding(.vertical, 24.5)
|
||||
.padding(.horizontal, 28.5)
|
||||
|
||||
VStack(spacing: 2) {
|
||||
Text(L10n.commonDownloadFailed)
|
||||
.font(.compound.headingMDBold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
Text(L10n.screenMediaBrowserDownloadErrorMessage)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 40)
|
||||
.background(.compound.bgSubtlePrimary, in: RoundedRectangle(cornerRadius: 14))
|
||||
} else if currentItem.fileHandle == nil {
|
||||
ProgressView()
|
||||
.controlSize(.large)
|
||||
.tint(.compound.iconPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var caption: some View {
|
||||
if let caption = currentItem.caption, !isFullScreen {
|
||||
@@ -220,12 +251,19 @@ struct TimelineMediaPreviewScreen_Previews: PreviewProvider {
|
||||
@Namespace private static var namespace
|
||||
|
||||
static let viewModel = makeViewModel()
|
||||
static let downloadingViewModel = makeViewModel(isDownloading: true)
|
||||
static let downloadErrorViewModel = makeViewModel(isDownloadError: true)
|
||||
|
||||
static var previews: some View {
|
||||
TimelineMediaPreviewScreen(context: viewModel.context)
|
||||
.previewDisplayName("Normal")
|
||||
TimelineMediaPreviewScreen(context: downloadingViewModel.context)
|
||||
.previewDisplayName("Downloading")
|
||||
TimelineMediaPreviewScreen(context: downloadErrorViewModel.context)
|
||||
.previewDisplayName("Download Error")
|
||||
}
|
||||
|
||||
static func makeViewModel() -> TimelineMediaPreviewViewModel {
|
||||
static func makeViewModel(isDownloading: Bool = false, isDownloadError: Bool = false) -> TimelineMediaPreviewViewModel {
|
||||
let item = FileRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
@@ -243,11 +281,22 @@ struct TimelineMediaPreviewScreen_Previews: PreviewProvider {
|
||||
let timelineController = MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen))
|
||||
timelineController.timelineItems = [item]
|
||||
|
||||
let mediaProvider = MediaProviderMock(configuration: .init())
|
||||
|
||||
if isDownloading {
|
||||
mediaProvider.loadFileFromSourceFilenameClosure = { _, _ in
|
||||
try? await Task.sleep(for: .seconds(3600))
|
||||
return .failure(.failedRetrievingFile)
|
||||
}
|
||||
} else if isDownloadError {
|
||||
mediaProvider.loadFileFromSourceFilenameClosure = { _, _ in .failure(.failedRetrievingFile) }
|
||||
}
|
||||
|
||||
return TimelineMediaPreviewViewModel(context: .init(item: item,
|
||||
viewModel: TimelineViewModel.mock(timelineKind: timelineController.timelineKind,
|
||||
timelineController: timelineController),
|
||||
namespace: namespace),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaProvider: mediaProvider,
|
||||
photoLibraryManager: PhotoLibraryManagerMock(.init()),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appMediator: AppMediatorMock())
|
||||
|
||||
Reference in New Issue
Block a user