diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index e258f17e6..9479ff79c 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -7610,16 +7610,16 @@ projectDirPath = ""; projectRoot = ""; targets = ( - C0C687DE1D270F9895FEE186 /* AccessibilityTests */, C0FAEB81CFD9776CD78CE489 /* ElementX */, - D3DB351B7FBE0F49649171FC /* IntegrationTests */, - 676A87694F0F247B2AF1A142 /* MatrixRustSDKMocks */, FEB53A5BC378C913769656D8 /* NSE */, - F8E276FD6DC43EADB85241BC /* Periphery */, - 7A17BE29BAC81ADBAC6349D9 /* PreviewTests */, 19F0C845D67E9BEA4BE7133E /* ShareExtension */, - 0E28CD62691FDBC63147D5E3 /* UITests */, + 676A87694F0F247B2AF1A142 /* MatrixRustSDKMocks */, 32C23C8D224D46EFE62AFAD0 /* UnitTests */, + 7A17BE29BAC81ADBAC6349D9 /* PreviewTests */, + 0E28CD62691FDBC63147D5E3 /* UITests */, + D3DB351B7FBE0F49649171FC /* IntegrationTests */, + C0C687DE1D270F9895FEE186 /* AccessibilityTests */, + F8E276FD6DC43EADB85241BC /* Periphery */, ); }; /* End PBXProject section */ diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index e904faae4..bdf323e4a 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -416,11 +416,6 @@ final nonisolated class AppSettings: @unchecked Sendable { @UserPreference(defaultValue: false) var linkPreviewsEnabled: Bool - /// Enables *sending* gallery messages (multiple media in a single message). Received galleries - /// are always rendered regardless of this flag, matching Element X Android. - @UserPreference(defaultValue: false) - var galleryEnabled: Bool - @UserPreference(defaultValue: false) var jumpToReadMarkerEnabled: Bool diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 7ae74513b..ab90845b4 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -1123,8 +1123,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: flowParameters.appSettings), timelineController: timelineController, clientProxy: userSession.clientProxy, - userIndicatorController: flowParameters.userIndicatorController, - galleryEnabled: flowParameters.appSettings.galleryEnabled) + userIndicatorController: flowParameters.userIndicatorController) let mediaUploadPreviewScreenCoordinator = MediaUploadPreviewScreenCoordinator(parameters: parameters) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 5337b954c..5df8a7c17 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -12267,48 +12267,6 @@ nonisolated class TimelineControllerMock: TimelineControllerProtocol, @unchecked return sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue } } - //MARK: - sendGallery - - private let sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCountLock = NSLock() - private nonisolated(unsafe) var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingCallsCount = 0 - var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCount: Int { - get { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCountLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingCallsCount } } - set { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCountLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingCallsCount = newValue } } - } - var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCalled: Bool { - return sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCount > 0 - } - private let sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArgumentsLock = NSLock() - private nonisolated(unsafe) var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedArguments: (itemInfos: [GalleryItemInfo], caption: String?, formattedCaption: String?, inReplyToEventID: String?)? - var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArguments: (itemInfos: [GalleryItemInfo], caption: String?, formattedCaption: String?, inReplyToEventID: String?)? { - get { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArgumentsLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedArguments } } - set { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArgumentsLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedArguments = newValue } } - } - private let sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedInvocationsLock = NSLock() - private nonisolated(unsafe) var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedInvocations: [(itemInfos: [GalleryItemInfo], caption: String?, formattedCaption: String?, inReplyToEventID: String?)] = [] - var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedInvocations: [(itemInfos: [GalleryItemInfo], caption: String?, formattedCaption: String?, inReplyToEventID: String?)] { - get { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedInvocationsLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedInvocations } } - set { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedInvocationsLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedInvocations = newValue } } - } - - private let sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReturnValueLock = NSLock() - private nonisolated(unsafe) var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReturnValue: Result! - var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReturnValue: Result! { - get { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReturnValueLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReturnValue } } - set { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReturnValueLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReturnValue = newValue } } - } - nonisolated(unsafe) var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDClosure: (([GalleryItemInfo], String?, String?, String?) async -> Result)? - - @concurrent func sendGallery(itemInfos: [GalleryItemInfo], caption: String?, formattedCaption: String?, inReplyToEventID: String?) async -> Result { - sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCountLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingCallsCount += 1 } - sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArguments = (itemInfos: itemInfos, caption: caption, formattedCaption: formattedCaption, inReplyToEventID: inReplyToEventID) - sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedInvocationsLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedInvocations.append((itemInfos: itemInfos, caption: caption, formattedCaption: formattedCaption, inReplyToEventID: inReplyToEventID)) } - if let sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDClosure = sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDClosure { - return await sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDClosure(itemInfos, caption, formattedCaption, inReplyToEventID) - } else { - return sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReturnValue - } - } //MARK: - createPoll private let createPollQuestionAnswersMaxSelectionsPollKindCallsCountLock = NSLock() @@ -13064,48 +13022,6 @@ nonisolated class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable return sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue } } - //MARK: - sendGallery - - private let sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCountLock = NSLock() - private nonisolated(unsafe) var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingCallsCount = 0 - var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCount: Int { - get { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCountLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingCallsCount } } - set { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCountLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingCallsCount = newValue } } - } - var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCalled: Bool { - return sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCount > 0 - } - private let sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArgumentsLock = NSLock() - private nonisolated(unsafe) var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedArguments: (itemInfos: [GalleryItemInfo], caption: String?, formattedCaption: String?, inReplyToEventID: String?)? - var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArguments: (itemInfos: [GalleryItemInfo], caption: String?, formattedCaption: String?, inReplyToEventID: String?)? { - get { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArgumentsLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedArguments } } - set { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArgumentsLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedArguments = newValue } } - } - private let sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedInvocationsLock = NSLock() - private nonisolated(unsafe) var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedInvocations: [(itemInfos: [GalleryItemInfo], caption: String?, formattedCaption: String?, inReplyToEventID: String?)] = [] - var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedInvocations: [(itemInfos: [GalleryItemInfo], caption: String?, formattedCaption: String?, inReplyToEventID: String?)] { - get { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedInvocationsLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedInvocations } } - set { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedInvocationsLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedInvocations = newValue } } - } - - private let sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReturnValueLock = NSLock() - private nonisolated(unsafe) var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReturnValue: Result! - var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReturnValue: Result! { - get { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReturnValueLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReturnValue } } - set { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReturnValueLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReturnValue = newValue } } - } - nonisolated(unsafe) var sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDClosure: (([GalleryItemInfo], String?, String?, String?) async -> Result)? - - @concurrent func sendGallery(itemInfos: [GalleryItemInfo], caption: String?, formattedCaption: String?, inReplyToEventID: String?) async -> Result { - sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCountLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingCallsCount += 1 } - sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArguments = (itemInfos: itemInfos, caption: caption, formattedCaption: formattedCaption, inReplyToEventID: inReplyToEventID) - sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedInvocationsLock.withLock { sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDUnderlyingReceivedInvocations.append((itemInfos: itemInfos, caption: caption, formattedCaption: formattedCaption, inReplyToEventID: inReplyToEventID)) } - if let sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDClosure = sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDClosure { - return await sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDClosure(itemInfos, caption, formattedCaption, inReplyToEventID) - } else { - return sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReturnValue - } - } //MARK: - sendReadReceipt private let sendReadReceiptForTypeCallsCountLock = NSLock() diff --git a/ElementX/Sources/Mocks/TimelineControllerMock.swift b/ElementX/Sources/Mocks/TimelineControllerMock.swift index 976dfde7e..0a9660b19 100644 --- a/ElementX/Sources/Mocks/TimelineControllerMock.swift +++ b/ElementX/Sources/Mocks/TimelineControllerMock.swift @@ -162,17 +162,6 @@ struct TimelineControllerMockConfiguration { return .success(()) } - sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDClosure = { [weak self, timelineProxy] itemInfos, caption, formattedCaption, inReplyToEventID in - self?.callbacks.send(.messageSentOrEdited) - if let timelineProxy { - return await timelineProxy.sendGallery(itemInfos: itemInfos, - caption: caption, - formattedCaption: formattedCaption, - inReplyToEventID: inReplyToEventID).mapError(TimelineControllerError.timelineProxyError) - } - return .success(()) - } - sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure = { [weak self, timelineProxy] url, audioInfo, waveform, requestHandle in self?.callbacks.send(.messageSentOrEdited) if let timelineProxy { diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift index 888d8a686..c72a03334 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift @@ -18,9 +18,6 @@ struct MediaUploadPreviewScreenCoordinatorParameters { let timelineController: TimelineControllerProtocol let clientProxy: ClientProxyProtocol let userIndicatorController: UserIndicatorControllerProtocol - /// Whether sending as a gallery is enabled. When `false`, multiple attachments are sent as - /// individual messages (the previous behaviour). - let galleryEnabled: Bool } enum MediaUploadPreviewScreenCoordinatorAction { @@ -44,8 +41,7 @@ final class MediaUploadPreviewScreenCoordinator: CoordinatorProtocol { mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor, timelineController: parameters.timelineController, clientProxy: parameters.clientProxy, - userIndicatorController: parameters.userIndicatorController, - galleryEnabled: parameters.galleryEnabled) + userIndicatorController: parameters.userIndicatorController) } func start() { diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift index a2489dacb..6205d3ef8 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift @@ -22,7 +22,6 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, private var processingTask: Task, Never> private var requestHandle: SendAttachmentJoinHandleProtocol? private let clientProxy: ClientProxyProtocol - private let galleryEnabled: Bool private var actionsSubject: PassthroughSubject = .init() @@ -37,14 +36,12 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, mediaUploadingPreprocessor: MediaUploadingPreprocessor, timelineController: TimelineControllerProtocol, clientProxy: ClientProxyProtocol, - userIndicatorController: UserIndicatorControllerProtocol, - galleryEnabled: Bool) { + userIndicatorController: UserIndicatorControllerProtocol) { self.mediaURLs = mediaURLs self.mediaUploadingPreprocessor = mediaUploadingPreprocessor self.timelineController = timelineController self.clientProxy = clientProxy self.userIndicatorController = userIndicatorController - self.galleryEnabled = galleryEnabled // Start processing the media whilst the user is reviewing it/adding a caption. processingTask = Self.processMedia(at: mediaURLs, preprocessor: mediaUploadingPreprocessor, clientProxy: clientProxy) @@ -57,7 +54,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, override func process(viewAction: MediaUploadPreviewScreenViewAction) { // Get the current caption before all the processing starts. - let caption = state.bindings.caption.nonBlankString + var caption = state.bindings.caption.nonBlankString switch viewAction { case .send: @@ -68,25 +65,14 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, switch await processingTask.value { case .success(let mediaInfos): - if galleryEnabled, mediaInfos.count > 1 { - switch await sendGallery(mediaInfos: mediaInfos, caption: caption) { + for mediaInfo in mediaInfos { + switch await sendAttachment(mediaInfo: mediaInfo, caption: caption) { case .success: - break + caption = nil // Set the caption only on the first uploaded file. case .failure(let error): - MXLog.error("Failed sending gallery with error: \(error)") + MXLog.error("Failed sending media with error: \(error)") showError(label: L10n.screenMediaUploadPreviewErrorFailedSending) } - } else { - var perItemCaption = caption - for mediaInfo in mediaInfos { - switch await sendAttachment(mediaInfo: mediaInfo, caption: perItemCaption) { - case .success: - perItemCaption = nil // Set the caption only on the first uploaded file. - case .failure(let error): - MXLog.error("Failed sending media with error: \(error)") - showError(label: L10n.screenMediaUploadPreviewErrorFailedSending) - } - } } actionsSubject.send(.dismiss) @@ -121,40 +107,6 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, } } - private func sendGallery(mediaInfos: [MediaInfo], caption: String?) async -> Result { - let itemInfos: [GalleryItemInfo] = mediaInfos.map { mediaInfo in - switch mediaInfo { - case let .image(imageURL, thumbnailURL, imageInfo): - return .image(imageInfo: imageInfo, - source: .file(filename: imageURL.path(percentEncoded: false)), - caption: nil, - formattedCaption: nil, - thumbnailSource: .file(filename: thumbnailURL.path(percentEncoded: false))) - case let .video(videoURL, thumbnailURL, videoInfo): - return .video(videoInfo: videoInfo, - source: .file(filename: videoURL.path(percentEncoded: false)), - caption: nil, - formattedCaption: nil, - thumbnailSource: .file(filename: thumbnailURL.path(percentEncoded: false))) - case let .audio(audioURL, audioInfo): - return .audio(audioInfo: audioInfo, - source: .file(filename: audioURL.path(percentEncoded: false)), - caption: nil, - formattedCaption: nil) - case let .file(fileURL, fileInfo): - return .file(fileInfo: fileInfo, - source: .file(filename: fileURL.path(percentEncoded: false)), - caption: nil, - formattedCaption: nil) - } - } - - return await timelineController.sendGallery(itemInfos: itemInfos, - caption: caption, - formattedCaption: nil, - inReplyToEventID: nil) - } - func stopProcessing() { processingTask.cancel() } diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift index 62d1dea0c..130dcc9b2 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift @@ -6,7 +6,6 @@ // Please see LICENSE files in the repository root for full details. // -import AVFoundation import Combine import Compound import GameController @@ -36,7 +35,17 @@ struct MediaUploadPreviewScreen: View { mainContent .id(context.viewState.mediaURLs) .ignoresSafeArea(edges: [.horizontal]) - .overlay(alignment: .top) { galleryBadge } + .safeAreaInset(edge: .top) { + if context.viewState.mediaURLs.count > 1 { + Text(L10n.screenMediaUploadPreviewItemCount(currentIndex + 1, context.viewState.mediaURLs.count)) + .font(.compound.bodyMD) + .foregroundColor(.compound.textPrimary) + .padding(.vertical, 4) + .padding(.horizontal, 8) + .background(.compound.bgBadgeDefault) + .clipShape(.capsule) + } + } .safeAreaInset(edge: .bottom, spacing: 0) { composer .padding(.horizontal, 12) @@ -65,20 +74,6 @@ struct MediaUploadPreviewScreen: View { } } - @ViewBuilder - private var galleryBadge: some View { - if context.viewState.mediaURLs.count > 1 { - Text(L10n.screenMediaUploadPreviewItemCount(currentIndex + 1, context.viewState.mediaURLs.count)) - .font(.compound.bodySMSemibold) - .foregroundStyle(.compound.textPrimary) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color.compound.bgCanvasDefault.opacity(0.85), - in: Capsule()) - .padding(.top, 12) - } - } - @ViewBuilder private var mainContent: some View { if ProcessInfo.processInfo.isiOSAppOnMac { @@ -86,9 +81,6 @@ struct MediaUploadPreviewScreen: View { .font(.compound.headingMD) .foregroundColor(.compound.textSecondary) .frame(maxWidth: .infinity, maxHeight: .infinity) - } else if context.viewState.mediaURLs.count > 1 { - UploadMediaPeekCarousel(mediaURLs: context.viewState.mediaURLs, - currentIndex: $currentIndex) } else { PreviewView(mediaURLs: context.viewState.mediaURLs, title: context.viewState.title, @@ -222,133 +214,6 @@ struct MediaUploadPreviewScreen: View { } } -private struct UploadMediaPeekCarousel: View { - let mediaURLs: [URL] - @Binding var currentIndex: Int - - @State private var scrolledID: Int? - - var body: some View { - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(spacing: 8) { - ForEach(Array(mediaURLs.enumerated()), id: \.offset) { index, url in - UploadMediaThumbnail(url: url) - .containerRelativeFrame(.horizontal) - .id(index) - } - } - .scrollTargetLayout() - } - .contentMargins(.horizontal, 24, for: .scrollContent) - .scrollTargetBehavior(.viewAligned) - .scrollPosition(id: $scrolledID) - .onAppear { - if scrolledID == nil { - scrolledID = currentIndex - } - } - .onChange(of: scrolledID) { _, newValue in - if let newValue, newValue != currentIndex { - currentIndex = newValue - } - } - } -} - -private struct UploadMediaThumbnail: View { - let url: URL - - private var contentType: UTType? { - UTType(filenameExtension: url.pathExtension) - } - - private var isImageOrVideo: Bool { - guard let contentType else { return false } - return contentType.conforms(to: .image) || contentType.conforms(to: .movie) || contentType.conforms(to: .audiovisualContent) - } - - var body: some View { - Group { - if isImageOrVideo { - UploadMediaImageThumbnail(url: url) - } else { - UploadMediaFilePreview(url: url, title: url.lastPathComponent) - } - } - .clipShape(RoundedRectangle(cornerRadius: 8)) - } -} - -private struct UploadMediaImageThumbnail: View { - let url: URL - @State private var image: UIImage? - - var body: some View { - ZStack { - Color.black - if let image { - Image(uiImage: image) - .resizable() - .scaledToFit() - } else { - ProgressView().tint(.white) - } - } - .task(id: url) { await load() } - } - - private func load() async { - if let image = UIImage(contentsOfFile: url.path(percentEncoded: false)) { - self.image = image - return - } - - // Fall back to a video frame thumbnail. - let asset = AVURLAsset(url: url) - let generator = AVAssetImageGenerator(asset: asset) - generator.appliesPreferredTrackTransform = true - do { - let cgImage = try await generator.image(at: .zero).image - image = UIImage(cgImage: cgImage) - } catch { - image = nil - } - } -} - -private struct UploadMediaFilePreview: UIViewControllerRepresentable { - let url: URL - let title: String - - func makeUIViewController(context: Context) -> QLPreviewController { - let controller = QLPreviewController() - controller.dataSource = context.coordinator - return controller - } - - func updateUIViewController(_ uiViewController: QLPreviewController, context: Context) { } - - func makeCoordinator() -> Coordinator { - Coordinator(url: url, title: title) - } - - final class Coordinator: NSObject, QLPreviewControllerDataSource { - private let item: PreviewItem - - init(url: URL, title: String) { - item = PreviewItem(previewItemURL: url, previewItemTitle: title) - } - - func numberOfPreviewItems(in controller: QLPreviewController) -> Int { - 1 - } - - func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { - item - } - } -} - private struct PreviewView: UIViewControllerRepresentable { let mediaURLs: [URL] let title: String? @@ -531,8 +396,7 @@ struct MediaUploadPreviewScreen_Previews: PreviewProvider, TestablePreview { mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: .volatile()), timelineController: TimelineControllerMock(.init()), clientProxy: ClientProxyMock(.init()), - userIndicatorController: UserIndicatorControllerMock(), - galleryEnabled: true) + userIndicatorController: UserIndicatorControllerMock()) static var previews: some View { ElementNavigationStack { MediaUploadPreviewScreen(context: viewModel.context) diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 0993cd990..c5ddd12a6 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -67,8 +67,6 @@ protocol DeveloperOptionsProtocol: AnyObject { var linkPreviewsEnabled: Bool { get set } - var galleryEnabled: Bool { get set } - var jumpToReadMarkerEnabled: Bool { get set } var linkNewDeviceEnabled: Bool { get set } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 9627d68bb..121c1829b 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -97,11 +97,6 @@ struct DeveloperOptionsScreen: View { .foregroundStyle(.compound.textCriticalPrimary) } - Toggle(isOn: $context.galleryEnabled) { - Text("Gallery messages") - Text("Allows sending multiple media in a single message. Received galleries always render regardless of this setting.") - } - Toggle(isOn: $context.jumpToReadMarkerEnabled) { Text("Jump to unread") Text("Adds a button to jump to the read marker, plus a presence dot on the scroll-to-bottom button when new messages arrive while scrolled away.") diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index 4f6d5bd95..7e1baceda 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -402,16 +402,6 @@ class TimelineController: TimelineControllerProtocol { } } - func sendGallery(itemInfos: [GalleryItemInfo], - caption: String?, - formattedCaption: String?, - inReplyToEventID: String?) async -> Result { - await activeTimeline.sendGallery(itemInfos: itemInfos, - caption: caption, - formattedCaption: formattedCaption, - inReplyToEventID: inReplyToEventID).mapError(TimelineControllerError.timelineProxyError) - } - // MARK: - Polls func createPoll(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result { diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift index 74025f1d4..139a13dcc 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift @@ -134,11 +134,6 @@ protocol TimelineControllerProtocol: Sendable { waveform: [Float], requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result - func sendGallery(itemInfos: [GalleryItemInfo], - caption: String?, - formattedCaption: String?, - inReplyToEventID: String?) async -> Result - // MARK: - Poll func createPoll(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index a27875380..31785952f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -374,30 +374,6 @@ final class TimelineProxy: TimelineProxyProtocol { return .success(()) } - func sendGallery(itemInfos: [GalleryItemInfo], - caption: String?, - formattedCaption: String?, - inReplyToEventID: String?) async -> Result { - MXLog.info("Sending gallery with \(itemInfos.count) items") - - do { - let formatted = formattedCaption.map { FormattedBody(format: .html, body: $0) } - let params = GalleryUploadParameters(caption: caption, - formattedCaption: formatted, - mentions: nil, - inReplyTo: inReplyToEventID) - - let handle = try timeline.sendGallery(params: params, itemInfos: itemInfos) - try await handle.join() - MXLog.info("Finished sending gallery") - } catch { - MXLog.error("Failed sending gallery with error: \(error)") - return .failure(.sdkError(error)) - } - - return .success(()) - } - func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [Float], diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index 55991d731..3cef9a6ac 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -110,11 +110,6 @@ protocol TimelineProxyProtocol: Sendable { waveform: [Float], requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result - func sendGallery(itemInfos: [GalleryItemInfo], - caption: String?, - formattedCaption: String?, - inReplyToEventID: String?) async -> Result - func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result func markAsRead(receiptType: ReceiptType) async -> Result diff --git a/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift b/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift index 4169fa1df..fe525e686 100644 --- a/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift +++ b/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift @@ -190,12 +190,10 @@ final class MediaUploadPreviewScreenViewModelTests { #expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.") #expect(userIndicatorController.submitIndicatorDelayCallsCount == 1) // Loading indicator - // Then the screen should be dismissed once the gallery has been sent in a single request. + // Then the screen should be dismissed once all of the files have been sent. try await deferredDismiss.fulfill() - #expect(timelineProxy.sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCount == 1) - #expect(timelineProxy.sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDReceivedArguments?.itemInfos.count == 3) - #expect(timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount == 0) - #expect(timelineProxy.sendFileUrlFileInfoCaptionRequestHandleCallsCount == 0) + #expect(timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount == 1) + #expect(timelineProxy.sendFileUrlFileInfoCaptionRequestHandleCallsCount == 2) #expect(userIndicatorController.submitIndicatorDelayCallsCount == 1, "Only a loading indicator should be shown.") } @@ -220,8 +218,8 @@ final class MediaUploadPreviewScreenViewModelTests { @Test func multipleFilesWithSendFailure() async throws { - // Given an upload screen with multiple media files where the gallery send fails. - setUpViewModel(urls: [fileURL, imageURL, imageURL, fileURL], expectedCaption: nil, simulateGallerySendFailure: true) + // Given an upload screen with multiple media files where one of the files will fail to send. + setUpViewModel(urls: [fileURL, imageURL, imageURL, fileURL], expectedCaption: nil, simulateImageSendFailures: true) #expect(!context.viewState.shouldDisableInteraction) #expect(userIndicatorController.submitIndicatorDelayCallsCount == 0) @@ -232,10 +230,11 @@ final class MediaUploadPreviewScreenViewModelTests { #expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.") #expect(userIndicatorController.submitIndicatorDelayCallsCount == 1) // Loading indicator - // Then the screen should still be dismissed and an error indicator surfaced. + // Then the screen should be dismissed so the user can see which files made it into the timeline. try await deferredDismiss.fulfill() - #expect(timelineProxy.sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDCallsCount == 1) - #expect(userIndicatorController.submitIndicatorDelayCallsCount == 2, "An error indicator should be shown.") + #expect(timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount == 2) + #expect(timelineProxy.sendFileUrlFileInfoCaptionRequestHandleCallsCount == 2) + #expect(userIndicatorController.submitIndicatorDelayCallsCount == 3, "Error indicators for each failure should be shown.") } // MARK: - Helpers @@ -269,8 +268,7 @@ final class MediaUploadPreviewScreenViewModelTests { private func setUpViewModel(urls: [URL], expectedCaption: String?, maxUploadSizeResult: Result? = nil, - simulateGallerySendFailure: Bool = false, - galleryEnabled: Bool = true) { + simulateImageSendFailures: Bool = false) { timelineProxy = TimelineProxyMock(.init()) timelineProxy.sendAudioUrlAudioInfoCaptionRequestHandleClosure = { [weak self] _, _, caption, _ in self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) @@ -279,15 +277,12 @@ final class MediaUploadPreviewScreenViewModelTests { self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) } timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure = { [weak self] _, _, _, caption, _ in - self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) + guard !simulateImageSendFailures else { return .failure(.sdkError(TestError.unknown)) } + return self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) } timelineProxy.sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure = { [weak self] _, _, _, caption, _ in self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) } - timelineProxy.sendGalleryItemInfosCaptionFormattedCaptionInReplyToEventIDClosure = { [weak self] _, caption, _, _ in - guard !simulateGallerySendFailure else { return .failure(.sdkError(TestError.unknown)) } - return self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) - } clientProxy = ClientProxyMock(.init()) if let maxUploadSizeResult { @@ -301,8 +296,7 @@ final class MediaUploadPreviewScreenViewModelTests { mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings), timelineController: TimelineControllerMock(.init(timelineProxy: timelineProxy)), clientProxy: clientProxy, - userIndicatorController: userIndicatorController, - galleryEnabled: galleryEnabled) + userIndicatorController: userIndicatorController) } private func verifyCaption(_ caption: String?, expectedCaption: String?) -> Result {