diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index d8acfaf79..4abc8f39d 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -92,7 +92,7 @@ extension JoinedRoomProxyMock { } canUserInviteUserIDReturnValue = .success(configuration.canUserInvite) canUserRedactOtherUserIDReturnValue = .success(false) - canUserRedactOwnUserIDReturnValue = .success(false) + canUserRedactOwnUserIDReturnValue = .success(true) canUserKickUserIDClosure = { [weak self] userID in .success(self?.membersPublisher.value.first { $0.userID == userID }?.role ?? .user != .user) } diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewController.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewController.swift index f0569d915..b8b48352f 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewController.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewController.swift @@ -132,13 +132,14 @@ class TimelineMediaPreviewController: QLPreviewController, QLPreviewControllerDa private struct HeaderView: View { @ObservedObject var context: TimelineMediaPreviewViewModel.Context + private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem } var body: some View { VStack(spacing: 0) { - Text(context.viewState.currentItem?.sender.displayName ?? context.viewState.currentItem?.sender.id ?? L10n.commonLoading) + Text(currentItem.sender.displayName ?? currentItem.sender.id) .font(.compound.bodySMSemibold) .foregroundStyle(.compound.textPrimary) - Text(context.viewState.currentItem?.timestamp.formatted(date: .abbreviated, time: .omitted) ?? "") + Text(currentItem.timestamp.formatted(date: .abbreviated, time: .omitted)) .font(.compound.bodyXS) .foregroundStyle(.compound.textPrimary) .textCase(.uppercase) @@ -148,9 +149,10 @@ private struct HeaderView: View { private struct CaptionView: View { @ObservedObject var context: TimelineMediaPreviewViewModel.Context + private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem } var body: some View { - if let caption = context.viewState.currentItem?.caption { + if let caption = currentItem.caption { Text(caption) .font(.compound.bodyLG) .foregroundStyle(.compound.textPrimary) diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModels.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModels.swift index 057b28034..607b4d7ab 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModels.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModels.swift @@ -14,12 +14,19 @@ enum TimelineMediaPreviewViewModelAction { struct TimelineMediaPreviewViewState: BindableState { var previewItems: [TimelineMediaPreviewItem] - var currentItem: TimelineMediaPreviewItem? + var currentItem: TimelineMediaPreviewItem + var currentItemActions: TimelineItemMenuActions? + + var bindings = TimelineMediaPreviewViewStateBindings() +} + +struct TimelineMediaPreviewViewStateBindings { + var isPresentingRedactConfirmation = false } /// Wraps a media file and title to be previewed with QuickLook. class TimelineMediaPreviewItem: NSObject, QLPreviewItem { - private let timelineItem: EventBasedMessageTimelineItemProtocol + let timelineItem: EventBasedMessageTimelineItemProtocol var fileHandle: MediaFileHandleProxy? init(timelineItem: EventBasedMessageTimelineItemProtocol) { @@ -159,6 +166,6 @@ class TimelineMediaPreviewItem: NSObject, QLPreviewItem { } enum TimelineMediaPreviewViewAction { - case viewInTimeline - case redact + case menuAction(TimelineItemMenuAction) + case redactConfirmation } diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewViewModel.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewViewModel.swift index 6edf0d7d7..11b406be7 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewViewModel.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewViewModel.swift @@ -11,6 +11,7 @@ import Foundation typealias TimelineMediaPreviewViewModelType = StateStoreViewModel class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType { + private let timelineViewModel: TimelineViewModelProtocol private let mediaProvider: MediaProviderProtocol private let userIndicatorController: UserIndicatorControllerProtocol @@ -19,26 +20,51 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType { actionsSubject.eraseToAnyPublisher() } - init(previewItems: [EventBasedMessageTimelineItemProtocol], mediaProvider: MediaProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol) { + init(initialItem: EventBasedMessageTimelineItemProtocol, + timelineViewModel: TimelineViewModelProtocol, + mediaProvider: MediaProviderProtocol, + userIndicatorController: UserIndicatorControllerProtocol) { + self.timelineViewModel = timelineViewModel self.mediaProvider = mediaProvider // We might not want to inject this, instead creating a new instance with a custom position and colour scheme 🤔 self.userIndicatorController = userIndicatorController - super.init(initialViewState: TimelineMediaPreviewViewState(previewItems: previewItems.map(TimelineMediaPreviewItem.init)), mediaProvider: mediaProvider) + let currentItem = TimelineMediaPreviewItem(timelineItem: initialItem) + + super.init(initialViewState: TimelineMediaPreviewViewState(previewItems: [currentItem], + currentItem: currentItem), + mediaProvider: mediaProvider) + + rebuildCurrentItemActions() + + timelineViewModel.context.$viewState.map(\.canCurrentUserRedactSelf) + .merge(with: timelineViewModel.context.$viewState.map(\.canCurrentUserRedactOthers)) + .sink { [weak self] _ in + self?.rebuildCurrentItemActions() + } + .store(in: &cancellables) } override func process(viewAction: TimelineMediaPreviewViewAction) { switch viewAction { - case .viewInTimeline: - actionsSubject.send(.viewInTimeline) - case .redact: + case .menuAction(let action): + switch action { + case .viewInRoomTimeline: + actionsSubject.send(.viewInTimeline) + case .redact: + state.bindings.isPresentingRedactConfirmation = true + default: + MXLog.error("Received unexpected action: \(action)") + } + case .redactConfirmation: break // Do it here?? } } func updateCurrentItem(_ previewItem: TimelineMediaPreviewItem) async { state.currentItem = previewItem + rebuildCurrentItemActions() if previewItem.fileHandle == nil, let source = previewItem.mediaSource { showDownloadingIndicator(itemID: previewItem.id) @@ -50,11 +76,26 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType { actionsSubject.send(.loadedMediaFile) case .failure(let error): MXLog.error("Failed loading media: \(error)") - #warning("Show the error!") + showDownloadErrorIndicator() } } } + func rebuildCurrentItemActions() { + let timelineContext = timelineViewModel.context + let provider = TimelineItemMenuActionProvider(timelineItem: state.currentItem.timelineItem, + canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf, + canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers, + canCurrentUserPin: timelineContext.viewState.canCurrentUserPin, + 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() + } + private func showDownloadingIndicator(itemID: TimelineItemIdentifier) { let indicatorID = makeDownloadIndicatorID(itemID: itemID) userIndicatorController.submitIndicator(UserIndicator(id: indicatorID, @@ -69,7 +110,16 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType { userIndicatorController.retractIndicatorWithId(indicatorID) } + private func showDownloadErrorIndicator() { + // FIXME: Add the correct string and indicator type?? + userIndicatorController.submitIndicator(UserIndicator(id: downloadErrorIndicatorID, + type: .modal, + title: L10n.errorUnknown, + iconName: "exclamationmark.circle.fill")) + } + + private var downloadErrorIndicatorID: String { "\(Self.self)-DownloadError" } private func makeDownloadIndicatorID(itemID: TimelineItemIdentifier) -> String { - "\(TimelineMediaPreviewViewModel.self)-Download-\(itemID.uniqueID.id)" + "\(Self.self)-Download-\(itemID.uniqueID.id)" } } diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaQuickLook.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaQuickLook.swift index 4a186ec75..5b5c0271b 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaQuickLook.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaQuickLook.swift @@ -134,21 +134,22 @@ struct TimelineMediaQuickLook_Previews: PreviewProvider { } static func makeViewModel() -> TimelineMediaPreviewViewModel { - let previewItem = FileRoomTimelineItem(id: .randomEvent, - timestamp: .mock, - isOutgoing: false, - isEditable: false, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "", displayName: "Sally Sanderson"), - content: .init(filename: "Important document.pdf", - caption: "A caption goes right here.", - source: try? .init(url: .mockMXCFile, mimeType: nil), - fileSize: 3 * 1024 * 1024, - thumbnailSource: nil, - contentType: .pdf)) + let item = FileRoomTimelineItem(id: .randomEvent, + timestamp: .mock, + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "", displayName: "Sally Sanderson"), + content: .init(filename: "Important document.pdf", + caption: "A caption goes right here.", + source: try? .init(url: .mockMXCFile, mimeType: nil), + fileSize: 3 * 1024 * 1024, + thumbnailSource: nil, + contentType: .pdf)) - return TimelineMediaPreviewViewModel(previewItems: [previewItem], + return TimelineMediaPreviewViewModel(initialItem: item, + timelineViewModel: TimelineViewModel.mock, mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: UserIndicatorControllerMock()) } diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift index 09e714545..adef393f7 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift @@ -11,7 +11,7 @@ import SwiftUI struct TimelineMediaPreviewDetailsView: View { @ObservedObject var context: TimelineMediaPreviewViewModel.Context - @State private var isPresentingRedactConfirmation = false + private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem } var body: some View { ScrollView { @@ -19,10 +19,10 @@ struct TimelineMediaPreviewDetailsView: View { details actions } - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity, alignment: .leading) } .padding(.top, 19) // For the drag indicator - .sheet(isPresented: $isPresentingRedactConfirmation) { + .sheet(isPresented: $context.isPresentingRedactConfirmation) { TimelineMediaPreviewRedactConfirmationView(context: context) } } @@ -31,48 +31,42 @@ struct TimelineMediaPreviewDetailsView: View { VStack(alignment: .leading, spacing: 24) { DetailsRow(title: L10n.screenMediaDetailsUploadedBy) { HStack(spacing: 8) { - if let sender = context.viewState.currentItem?.sender { - LoadableAvatarImage(url: sender.avatarURL, - name: sender.displayName, - contentID: sender.id, - avatarSize: .user(on: .mediaPreviewDetails), - mediaProvider: context.mediaProvider) - - VStack(alignment: .leading, spacing: 0) { - if let displayName = sender.displayName { - Text(displayName) - .font(.compound.bodyMDSemibold) - .foregroundStyle(.compound.decorativeColor(for: sender.id).text) - } - - Text(sender.id) - .font(.compound.bodySM) - .foregroundStyle(.compound.textSecondary) + LoadableAvatarImage(url: currentItem.sender.avatarURL, + name: currentItem.sender.displayName, + contentID: currentItem.sender.id, + avatarSize: .user(on: .mediaPreviewDetails), + mediaProvider: context.mediaProvider) + + VStack(alignment: .leading, spacing: 0) { + if let displayName = currentItem.sender.displayName { + Text(displayName) + .font(.compound.bodyMDSemibold) + .foregroundStyle(.compound.decorativeColor(for: currentItem.sender.id).text) } - } else { - Text(L10n.commonLoading) - .font(.compound.bodyMD) - .foregroundStyle(.compound.textPrimary) + + Text(currentItem.sender.id) + .font(.compound.bodySM) + .foregroundStyle(.compound.textSecondary) } } } DetailsRow(title: L10n.screenMediaDetailsUploadedOn) { - Text(context.viewState.currentItem?.timestamp.formatted(date: .abbreviated, time: .shortened) ?? "") + Text(currentItem.timestamp.formatted(date: .abbreviated, time: .shortened)) .font(.compound.bodyMD) .foregroundStyle(.compound.textPrimary) } DetailsRow(title: L10n.screenMediaDetailsFilename) { - Text(context.viewState.currentItem?.filename ?? "") + Text(currentItem.filename ?? "") .font(.compound.bodyMD) .foregroundStyle(.compound.textPrimary) } - if let contentType = context.viewState.currentItem?.contentType { + if let contentType = currentItem.contentType { DetailsRow(title: L10n.screenMediaDetailsFileFormat) { Group { - if let fileSize = context.viewState.currentItem?.fileSize { + if let fileSize = currentItem.fileSize { Text(contentType) + Text(" – ") + Text(UInt(fileSize).formatted(.byteCount(style: .file))) } else { Text(contentType) @@ -88,23 +82,38 @@ struct TimelineMediaPreviewDetailsView: View { .padding(.horizontal, 16) } + @ViewBuilder private var actions: some View { - VStack(spacing: 0) { - Divider() - .background(Color.compound.bgSubtlePrimary) - - Button { context.send(viewAction: .viewInTimeline) } label: { - Label(L10n.actionViewInTimeline, icon: \.visibilityOn) + if let actions = context.viewState.currentItemActions { + VStack(spacing: 0) { + if !actions.actions.isEmpty { + Divider() + .background(Color.compound.bgSubtlePrimary) + } + + ForEach(actions.actions, id: \.self) { action in + Button(role: action.isDestructive ? .destructive : nil) { + context.send(viewAction: .menuAction(action)) + } label: { + action.label + } + .buttonStyle(.menuSheet) + } + + if !actions.secondaryActions.isEmpty { + Divider() + .background(Color.compound.bgSubtlePrimary) + } + + ForEach(actions.secondaryActions, id: \.self) { action in + Button(role: action.isDestructive ? .destructive : nil) { + context.send(viewAction: .menuAction(action)) + } label: { + action.label + } + .buttonStyle(.menuSheet) + } } - .buttonStyle(.menuSheet) - - Divider() - .background(Color.compound.bgSubtlePrimary) - - Button(role: .destructive) { isPresentingRedactConfirmation = true } label: { - Label(L10n.actionRemove, icon: \.delete) - } - .buttonStyle(.menuSheet) } } @@ -130,38 +139,42 @@ struct TimelineMediaPreviewDetailsView: View { import UniformTypeIdentifiers struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePreview { - static let viewModel = makeViewModel(contentType: .jpeg) + static let viewModel = makeViewModel(contentType: .jpeg, isOutgoing: true) static let unknownTypeViewModel = makeViewModel() + static let presentedOnRoomViewModel = makeViewModel(isPresentedOnRoomScreen: true) static var previews: some View { TimelineMediaPreviewDetailsView(context: viewModel.context) .previewDisplayName("Image") + .snapshotPreferences(delay: 0.1) TimelineMediaPreviewDetailsView(context: unknownTypeViewModel.context) .previewDisplayName("Unknown type") + .snapshotPreferences(delay: 0.1) + + TimelineMediaPreviewDetailsView(context: presentedOnRoomViewModel.context) + .previewDisplayName("Incoming on Room") + .snapshotPreferences(delay: 0.1) } - static func makeViewModel(contentType: UTType? = nil) -> TimelineMediaPreviewViewModel { - let previewItems = [ - ImageRoomTimelineItem(id: .randomEvent, - timestamp: .mock, - isOutgoing: false, - isEditable: true, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "@alice:matrix.org", - displayName: "Alice", - avatarURL: .mockMXCUserAvatar), - content: .init(filename: "Amazing Image.jpeg", - imageInfo: .mockImage, - thumbnailInfo: .mockThumbnail, - contentType: contentType)) - ] + static func makeViewModel(contentType: UTType? = nil, isOutgoing: Bool = false, isPresentedOnRoomScreen: Bool = false) -> TimelineMediaPreviewViewModel { + let item = ImageRoomTimelineItem(id: .randomEvent, + timestamp: .mock, + isOutgoing: isOutgoing, + isEditable: true, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "@alice:matrix.org", + displayName: "Alice", + avatarURL: .mockMXCUserAvatar), + content: .init(filename: "Amazing Image.jpeg", + imageInfo: .mockImage, + thumbnailInfo: .mockThumbnail, + contentType: contentType)) - let viewModel = TimelineMediaPreviewViewModel(previewItems: previewItems, - mediaProvider: MediaProviderMock(configuration: .init()), - userIndicatorController: UserIndicatorControllerMock()) - viewModel.state.currentItem = viewModel.state.previewItems.first - - return viewModel + let timelineKind = TimelineKind.media(isPresentedOnRoomScreen ? .roomScreen : .mediaFilesScreen) + return TimelineMediaPreviewViewModel(initialItem: item, + timelineViewModel: TimelineViewModel.mock(timelineKind: timelineKind), + mediaProvider: MediaProviderMock(configuration: .init()), + userIndicatorController: UserIndicatorControllerMock()) } } diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift index 1e000285e..b33f51258 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift @@ -13,6 +13,8 @@ struct TimelineMediaPreviewRedactConfirmationView: View { @ObservedObject var context: TimelineMediaPreviewViewModel.Context + private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem } + var body: some View { ScrollView { VStack(spacing: 0) { @@ -51,51 +53,49 @@ struct TimelineMediaPreviewRedactConfirmationView: View { @ViewBuilder private var preview: some View { - if let currentItem = context.viewState.currentItem { - HStack(spacing: 12) { - if let mediaSource = currentItem.thumbnailMediaSource { - Color.clear - .scaledFrame(size: 40) - .background { - LoadableImage(mediaSource: mediaSource, - mediaType: .timelineItem(uniqueID: currentItem.id.uniqueID.id), - blurhash: currentItem.blurhash, - mediaProvider: context.mediaProvider) { - Color.compound.bgSubtleSecondary - } - .aspectRatio(contentMode: .fill) + HStack(spacing: 12) { + if let mediaSource = currentItem.thumbnailMediaSource { + Color.clear + .scaledFrame(size: 40) + .background { + LoadableImage(mediaSource: mediaSource, + mediaType: .timelineItem(uniqueID: currentItem.id.uniqueID.id), + blurhash: currentItem.blurhash, + mediaProvider: context.mediaProvider) { + Color.compound.bgSubtleSecondary } - .clipShape(RoundedRectangle(cornerRadius: 8)) - } - - VStack(alignment: .leading, spacing: 4) { - Text(currentItem.filename ?? "") - .font(.compound.bodyMD) - .foregroundStyle(.compound.textPrimary) - - if let contentType = currentItem.contentType { - Group { - if let fileSize = currentItem.fileSize { - Text(contentType) + Text(" – ") + Text(UInt(fileSize).formatted(.byteCount(style: .file))) - } else { - Text(contentType) - } - } - .font(.compound.bodySM) - .foregroundStyle(.compound.textSecondary) + .aspectRatio(contentMode: .fill) } + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + + VStack(alignment: .leading, spacing: 4) { + Text(currentItem.filename ?? "") + .font(.compound.bodyMD) + .foregroundStyle(.compound.textPrimary) + + if let contentType = currentItem.contentType { + Group { + if let fileSize = currentItem.fileSize { + Text(contentType) + Text(" – ") + Text(UInt(fileSize).formatted(.byteCount(style: .file))) + } else { + Text(contentType) + } + } + .font(.compound.bodySM) + .foregroundStyle(.compound.textSecondary) } } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 24) - .padding(.bottom, 40) } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + .padding(.bottom, 40) } private var buttons: some View { VStack(spacing: 16) { Button(L10n.actionRemove, role: .destructive) { - context.send(viewAction: .redact) + context.send(viewAction: .redactConfirmation) } .buttonStyle(.compound(.primary)) @@ -124,27 +124,23 @@ struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, Tes } static func makeViewModel(contentType: UTType? = nil) -> TimelineMediaPreviewViewModel { - let previewItems = [ - ImageRoomTimelineItem(id: .randomEvent, - timestamp: .mock, - isOutgoing: false, - isEditable: true, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "@alice:matrix.org", - displayName: "Alice", - avatarURL: .mockMXCUserAvatar), - content: .init(filename: "Amazing Image.jpeg", - imageInfo: .mockImage, - thumbnailInfo: .mockThumbnail, - contentType: contentType)) - ] + let item = ImageRoomTimelineItem(id: .randomEvent, + timestamp: .mock, + isOutgoing: false, + isEditable: true, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "@alice:matrix.org", + displayName: "Alice", + avatarURL: .mockMXCUserAvatar), + content: .init(filename: "Amazing Image.jpeg", + imageInfo: .mockImage, + thumbnailInfo: .mockThumbnail, + contentType: contentType)) - let viewModel = TimelineMediaPreviewViewModel(previewItems: previewItems, - mediaProvider: MediaProviderMock(configuration: .init()), - userIndicatorController: UserIndicatorControllerMock()) - viewModel.state.currentItem = viewModel.state.previewItems.first - - return viewModel + return TimelineMediaPreviewViewModel(initialItem: item, + timelineViewModel: TimelineViewModel.mock, + mediaProvider: MediaProviderMock(configuration: .init()), + userIndicatorController: UserIndicatorControllerMock()) } } diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift index 3a62adb04..eee346b20 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift @@ -144,7 +144,8 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType return } - let viewModel = TimelineMediaPreviewViewModel(previewItems: [item], + let viewModel = TimelineMediaPreviewViewModel(initialItem: item, + timelineViewModel: activeTimelineViewModel, mediaProvider: mediaProvider, userIndicatorController: userIndicatorController) state.bindings.mediaPreviewViewModel = viewModel diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift index 9978baaf6..3ab087f10 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift @@ -164,7 +164,7 @@ extension View { struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { static let timelineViewModel: TimelineViewModel = { - let timelineController = MockRoomTimelineController(timelineKind: .media) + let timelineController = MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen)) return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index 9d2da9473..8df6f8e0f 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -38,7 +38,7 @@ struct PinnedEventsTimelineScreen: View { isDM: timelineContext.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, isCreateMediaCaptionsEnabled: timelineContext.viewState.isCreateMediaCaptionsEnabled, - isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline, + timelineKind: timelineContext.viewState.timelineKind, emojiProvider: timelineContext.viewState.emojiProvider) .makeActions() if let actions { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 595718188..10ed99a2a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -76,7 +76,7 @@ struct RoomScreen: View { isDM: timelineContext.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, isCreateMediaCaptionsEnabled: timelineContext.viewState.isCreateMediaCaptionsEnabled, - isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline, + timelineKind: timelineContext.viewState.timelineKind, emojiProvider: timelineContext.viewState.emojiProvider) .makeActions() if let actions { diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 2f1cf70fb..cff01156b 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -85,7 +85,7 @@ enum TimelineComposerAction { } struct TimelineViewState: BindableState { - let isPinnedEventsTimeline: Bool + let timelineKind: TimelineKind var roomID: String var members: [String: RoomMemberState] = [:] var typingMembers: [String] = [] diff --git a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift index 84fde96d3..eeac0ba9c 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift @@ -316,7 +316,7 @@ class TimelineTableViewController: UIViewController { var snapshot = NSDiffableDataSourceSnapshot() // We don't want to display the typing notification in this timeline - if !coordinator.context.viewState.isPinnedEventsTimeline { + if coordinator.context.viewState.timelineKind != .pinned { snapshot.appendSections([.typingIndicator]) snapshot.appendItems([TimelineUniqueId(id: TimelineTypingIndicatorCell.reuseIdentifier)]) } diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 358fde08d..7b44d7532 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -75,7 +75,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { appSettings: appSettings, analyticsService: analyticsService) - super.init(initialViewState: TimelineViewState(isPinnedEventsTimeline: timelineController.timelineKind == .pinned, + super.init(initialViewState: TimelineViewState(timelineKind: timelineController.timelineKind, roomID: roomProxy.id, isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom, timelineState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), @@ -690,13 +690,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } else { for (index, item) in itemGroup.enumerated() { if index == 0 { - timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .first), + timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.timelineKind == .pinned ? .single : .first), forKey: item.id.uniqueID) } else if index == itemGroup.count - 1 { - timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .last), + timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.timelineKind == .pinned ? .single : .last), forKey: item.id.uniqueID) } else { - timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .middle), + timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.timelineKind == .pinned ? .single : .middle), forKey: item.id.uniqueID) } } @@ -868,29 +868,21 @@ private extension RoomInfoProxy { // MARK: - Mocks extension TimelineViewModel { - static let mock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), - focussedEventID: nil, - timelineController: MockRoomTimelineController(), - mediaProvider: MediaProviderMock(configuration: .init()), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics, - emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) + static let mock = mock(timelineKind: .live) - static let pinnedEventsTimelineMock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), - focussedEventID: nil, - timelineController: MockRoomTimelineController(timelineKind: .pinned), - mediaProvider: MediaProviderMock(configuration: .init()), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics, - emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) + static func mock(timelineKind: TimelineKind = .live) -> TimelineViewModel { + TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), + focussedEventID: nil, + timelineController: MockRoomTimelineController(timelineKind: timelineKind), + mediaProvider: MediaProviderMock(configuration: .init()), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) + } } extension EnvironmentValues { diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift index 175f5c340..7e90df5c7 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift @@ -345,7 +345,7 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { isDM: true, isViewSourceEnabled: true, isCreateMediaCaptionsEnabled: true, - isPinnedEventsTimeline: false, + timelineKind: .live, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) guard let actions = provider.makeActions() else { return nil } diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift index 3a0e88b79..859b6f9bb 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift @@ -82,9 +82,9 @@ enum TimelineItemMenuAction: Identifiable, Hashable { var switchToDefaultComposer: Bool { switch self { case .reply, .edit, .addCaption, .editCaption, .editPoll: - return false + false default: - return true + true } } @@ -92,9 +92,9 @@ enum TimelineItemMenuAction: Identifiable, Hashable { var canAppearInFailedEcho: Bool { switch self { case .copy, .edit, .redact, .viewSource, .editPoll: - return true + true default: - return false + false } } @@ -102,9 +102,9 @@ enum TimelineItemMenuAction: Identifiable, Hashable { var canAppearInRedacted: Bool { switch self { case .viewSource, .unpin, .viewInRoomTimeline: - return true + true default: - return false + false } } @@ -112,18 +112,27 @@ enum TimelineItemMenuAction: Identifiable, Hashable { var isDestructive: Bool { switch self { case .redact, .report, .removeCaption: - return true + true default: - return false + false } } var canAppearInPinnedEventsTimeline: Bool { switch self { case .viewInRoomTimeline, .pin, .unpin, .forward: - return true + true default: - return false + false + } + } + + var canAppearInMediaDetails: Bool { + switch self { + case .viewInRoomTimeline, .redact: + true + default: + false } } diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift index 4d624fb3f..8fd211567 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift @@ -17,7 +17,7 @@ struct TimelineItemMenuActionProvider { let isDM: Bool let isViewSourceEnabled: Bool let isCreateMediaCaptionsEnabled: Bool - let isPinnedEventsTimeline: Bool + let timelineKind: TimelineKind let emojiProvider: EmojiProviderProtocol // swiftlint:disable:next cyclomatic_complexity @@ -38,6 +38,10 @@ struct TimelineItemMenuActionProvider { var actions: [TimelineItemMenuAction] = [] var secondaryActions: [TimelineItemMenuAction] = [] + + if timelineKind == .pinned || timelineKind == .media(.mediaFilesScreen) { + actions.append(.viewInRoomTimeline) + } if item.canBeRepliedTo { if let messageItem = item as? EventBasedMessageTimelineItemProtocol { @@ -99,10 +103,15 @@ struct TimelineItemMenuActionProvider { secondaryActions.append(.redact) } - if isPinnedEventsTimeline { - actions.insert(.viewInRoomTimeline, at: 0) + switch timelineKind { + case .pinned: actions = actions.filter(\.canAppearInPinnedEventsTimeline) secondaryActions = secondaryActions.filter(\.canAppearInPinnedEventsTimeline) + case .media: + actions = actions.filter(\.canAppearInMediaDetails) + secondaryActions = secondaryActions.filter(\.canAppearInMediaDetails) + case .live, .detached: + break // viewInRoomTimeline is the only non-room item and was added conditionally. } if item.hasFailedToSend { @@ -114,11 +123,10 @@ struct TimelineItemMenuActionProvider { actions = actions.filter(\.canAppearInRedacted) secondaryActions = secondaryActions.filter(\.canAppearInRedacted) } + + let isReactable = timelineKind == .live || timelineKind == .detached ? item.isReactable : false - return .init(isReactable: isPinnedEventsTimeline ? false : item.isReactable, - actions: actions, - secondaryActions: secondaryActions, - emojiProvider: emojiProvider) + return .init(isReactable: isReactable, actions: actions, secondaryActions: secondaryActions, emojiProvider: emojiProvider) } private func makeEncryptedItemActions(_ encryptedItem: EncryptedRoomTimelineItem) -> TimelineItemMenuActions? { diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index c20faa2d6..72152cf98 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -20,7 +20,7 @@ struct TimelineItemBubbledStylerView: View { private var isEncryptedOneToOneRoom: Bool { context.viewState.isEncryptedOneToOneRoom } private var isFocussed: Bool { focussedEventID != nil && timelineItem.id.eventID == focussedEventID } private var isPinned: Bool { - guard !context.viewState.isPinnedEventsTimeline, + guard context.viewState.timelineKind != .pinned, let eventID = timelineItem.id.eventID else { return false } @@ -110,7 +110,7 @@ struct TimelineItemBubbledStylerView: View { } // Do not display reactions in the pinned events timeline - if !context.viewState.isPinnedEventsTimeline, + if context.viewState.timelineKind != .pinned, !timelineItem.properties.reactions.isEmpty { TimelineReactionsView(context: context, itemID: timelineItem.id, @@ -150,7 +150,7 @@ struct TimelineItemBubbledStylerView: View { isDM: context.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: context.viewState.isViewSourceEnabled, isCreateMediaCaptionsEnabled: context.viewState.isCreateMediaCaptionsEnabled, - isPinnedEventsTimeline: context.viewState.isPinnedEventsTimeline, + timelineKind: context.viewState.timelineKind, emojiProvider: context.viewState.emojiProvider) TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: action)) diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift index 3a563b977..1a5ed45a2 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift @@ -23,7 +23,7 @@ struct TimelineItemStatusView: View { @ViewBuilder private var mainContent: some View { - if context.viewState.isPinnedEventsTimeline { + if context.viewState.timelineKind == .pinned { // Do not display any status when is a pinned events timeline EmptyView() } else if context.viewState.showReadReceipts, !timelineItem.properties.orderedReadReceipts.isEmpty { diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift index 1f5a7d95d..12aca1688 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift @@ -12,7 +12,7 @@ struct PollRoomTimelineView: View { @EnvironmentObject private var context: TimelineViewModel.Context private var state: PollViewState { - if context.viewState.isPinnedEventsTimeline { + if context.viewState.timelineKind == .pinned { return .preview } else { return .full(isEditable: timelineItem.isEditable) @@ -51,7 +51,7 @@ struct PollRoomTimelineView: View { struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock - static let pinnedEventsTimelineViewModel = TimelineViewModel.pinnedEventsTimelineMock + static let pinnedEventsTimelineViewModel = TimelineViewModel.mock(timelineKind: .pinned) static var previews: some View { PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isOutgoing: false)) diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index 55970a85b..225f67a4d 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -171,7 +171,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { let timeline = try await TimelineProxy(timeline: room.messageFilteredTimeline(internalIdPrefix: nil, allowedMessageTypes: allowedMessageTypes, dateDividerMode: .monthly), - kind: .media) + kind: .media(.mediaFilesScreen)) await timeline.subscribeForUpdates() return .success(timeline) diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index 64b047b07..9620445e5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -9,11 +9,13 @@ import Combine import Foundation import MatrixRustSDK -enum TimelineKind { +enum TimelineKind: Equatable { case live case detached case pinned - case media + + enum MediaPresentation { case roomScreen, mediaFilesScreen } + case media(MediaPresentation) } enum TimelineProxyError: Error { diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Image.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Image.png index 8d7e4fc16..e85193f21 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Image.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f6b9b7eca06b7b1afcd0616f0e79047478581e0fb526dfeae6b8439114b0226 -size 117138 +oid sha256:a30bdebdc1c816e6ca11cc4259749ec3e7f873e89c38e5cc64b23b5b7dcf7ebb +size 129684 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Incoming-on-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Incoming-on-Room.png new file mode 100644 index 000000000..3b6efef5c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Incoming-on-Room.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f6ff841733941f55b065477a234c318de67cc068870ea560d8650093556e385 +size 106118 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Unknown-type.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Unknown-type.png index 8d8042630..80641257d 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Unknown-type.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Unknown-type.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7196cc10b241f834aecd1002c58f4ccb2cb11781e4981b8cb6a6b9470a889cb -size 107373 +oid sha256:a6045b90ca6ad9b362f26505caeda788504d98461a4bdc6003603f79ae25cc45 +size 112315 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Image.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Image.png index 10371a3ee..65629de67 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Image.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49a257b2f3211b9fdfca3c28370c070215fd430df8bbde86627f248422350aa2 -size 125751 +oid sha256:90f32840b85ff12b6de880e8ab1890f4442b873fac71083093324fc02e9700d3 +size 128067 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Incoming-on-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Incoming-on-Room.png new file mode 100644 index 000000000..9812d5391 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Incoming-on-Room.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a1466df7ac398bb7f47bad54aeb8f6a3c6fb7ed29bcfa839feb4c39969afe19 +size 106123 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Unknown-type.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Unknown-type.png index 19b05c085..8c65e32f4 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Unknown-type.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Unknown-type.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95120569dffed5d1fe5be2022dfa5b90c31bb81c67faf7610274142d57e2aee2 -size 114855 +oid sha256:3ac674ce8b1e9f8c508ed259d9dc50a4b58f2304d9a3e2d9903121676502091d +size 111333 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Image.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Image.png index ce2cfabaa..e4d0518d1 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Image.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1104e19b712f200d87642dc62242940a4c568d1fb83e03d47136706db313e950 -size 70169 +oid sha256:339faa15c493a6e36f0c567b89eb6d036010c5637a0be03f15f7915306612320 +size 82212 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Incoming-on-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Incoming-on-Room.png new file mode 100644 index 000000000..cebaf17fa --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Incoming-on-Room.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:444149b32adb1da10c83c3f6a19da8bff11b26be4cda4a88e90d5ed50ac61b3d +size 59813 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Unknown-type.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Unknown-type.png index 6b6001806..23706e04a 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Unknown-type.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Unknown-type.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22c63f9b069c435c5f809d97b0b8c21755b4deb233bae72754fe168e65fef78f -size 62044 +oid sha256:0cd944ccb9183bfa3c39e7a866b38835baa6c158338c74e05f816f99268d892e +size 66083 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Image.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Image.png index ad908051f..009ea838a 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Image.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27efc1726973b2bab72413648b4f4c26037185fb147dc28b9901733804c9c78a -size 77471 +oid sha256:04241b16f28aa03f42de4e365eadf80fca0637494e891eb5c81a337d7693f476 +size 80107 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Incoming-on-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Incoming-on-Room.png new file mode 100644 index 000000000..309a948b6 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Incoming-on-Room.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce1be4e6bf0c06b2a1d0046d3c8eed0830292880365a10b048f02bdd15688246 +size 59774 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Unknown-type.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Unknown-type.png index ac809f280..e0472b548 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Unknown-type.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Unknown-type.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d268bbbaf1996132ff6ed23ba92afcec1b5e6307aec34b17e40edb549754c996 -size 68547 +oid sha256:54f7a21d3defe67d94c06aa226b503fcc30de8717043e5ca13242c76a4272bc4 +size 64878 diff --git a/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift b/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift index f8dd1d153..dade5840e 100644 --- a/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift +++ b/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift @@ -21,9 +21,9 @@ class TimelineMediaPreviewViewModelTests: XCTestCase { // Given a fresh view model. setupViewModel() XCTAssertFalse(mediaProvider.loadFileFromSourceFilenameCalled) - XCTAssertNil(context.viewState.currentItem) + XCTAssertEqual(context.viewState.currentItem, context.viewState.previewItems[0]) - // When setting the current item. + // When the preview controller sets the current item. await viewModel.updateCurrentItem(context.viewState.previewItems[0]) // Then the view model should load the item and update its view state. @@ -34,22 +34,21 @@ class TimelineMediaPreviewViewModelTests: XCTestCase { // MARK: - Helpers private func setupViewModel() { - let previewItems = [ - ImageRoomTimelineItem(id: .randomEvent, - timestamp: .mock, - isOutgoing: false, - isEditable: false, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "", displayName: "Sally Sanderson"), - content: .init(filename: "Amazing image.jpeg", - caption: "A caption goes right here.", - imageInfo: .mockImage, - thumbnailInfo: .mockThumbnail)) - ] + let item = ImageRoomTimelineItem(id: .randomEvent, + timestamp: .mock, + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "", displayName: "Sally Sanderson"), + content: .init(filename: "Amazing image.jpeg", + caption: "A caption goes right here.", + imageInfo: .mockImage, + thumbnailInfo: .mockThumbnail)) mediaProvider = MediaProviderMock(configuration: .init()) - viewModel = TimelineMediaPreviewViewModel(previewItems: previewItems, + viewModel = TimelineMediaPreviewViewModel(initialItem: item, + timelineViewModel: TimelineViewModel.mock, mediaProvider: mediaProvider, userIndicatorController: UserIndicatorControllerMock()) }