From 42f8097ce6775af7fdd9fe4a0178a8e3a6c4478e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20D=C3=A9glon?= Date: Thu, 19 Sep 2024 09:56:29 +0200 Subject: [PATCH 01/10] feat: Initial commit --- .../Attachments/AttachmentUploadCell.swift | 6 +- .../Attachments/AttachmentsManager.swift | 6 +- .../Message/Attachment/AttachmentsView.swift | 60 +++++++++++++++---- .../MailAPIFetcher+Attachment.swift | 32 +++++++--- .../API/MailApiFetcher/MailApiFetchable.swift | 2 +- ...tUploadTask.swift => AttachmentTask.swift} | 6 +- .../AttachmentsManagerWorker.swift | 22 +++---- .../CircleIndeterminateProgressView.swift | 49 +++++++++++++++ 8 files changed, 142 insertions(+), 41 deletions(-) rename MailCore/Cache/Attachments/AttachmentsManagerWorker/{AttachmentUploadTask.swift => AttachmentTask.swift} (87%) create mode 100644 MailCoreUI/Components/CircleIndeterminateProgressView.swift diff --git a/Mail/Views/New Message/Attachments/AttachmentUploadCell.swift b/Mail/Views/New Message/Attachments/AttachmentUploadCell.swift index bda7839eb..35b59ca88 100644 --- a/Mail/Views/New Message/Attachments/AttachmentUploadCell.swift +++ b/Mail/Views/New Message/Attachments/AttachmentUploadCell.swift @@ -30,11 +30,11 @@ struct AttachmentUploadCell: View { private let attachment: Attachment private let attachmentRemoved: ((Attachment) -> Void)? - @ObservedObject var uploadTask: AttachmentUploadTask + @ObservedObject var uploadTask: AttachmentTask @ModalState(wrappedValue: nil, context: ContextKeys.compose) private var previewedAttachment: Attachment? - init(uploadTask: AttachmentUploadTask, attachment: Attachment, attachmentRemoved: ((Attachment) -> Void)?) { + init(uploadTask: AttachmentTask, attachment: Attachment, attachmentRemoved: ((Attachment) -> Void)?) { self.uploadTask = uploadTask self.attachment = attachment self.attachmentRemoved = attachmentRemoved @@ -83,7 +83,7 @@ struct AttachmentUploadCell: View { } #Preview { - AttachmentUploadCell(uploadTask: AttachmentUploadTask(), attachment: PreviewHelper.sampleAttachment) { _ in + AttachmentUploadCell(uploadTask: AttachmentTask(), attachment: PreviewHelper.sampleAttachment) { _ in /* Preview */ } } diff --git a/Mail/Views/New Message/Attachments/AttachmentsManager.swift b/Mail/Views/New Message/Attachments/AttachmentsManager.swift index cb884a9f4..aed325655 100644 --- a/Mail/Views/New Message/Attachments/AttachmentsManager.swift +++ b/Mail/Views/New Message/Attachments/AttachmentsManager.swift @@ -46,8 +46,8 @@ import SwiftUI func completeUploadedAttachments() async /// Lookup and return _or_ new object representing a finished task instead. - func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask - + func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentTask +// /// Removes an attachment for a specific primary key /// - Parameter attachmentUUID: primary key of the object func removeAttachment(_ attachmentUUID: String) @@ -101,7 +101,7 @@ import SwiftUI await worker.processHTMLAttachments(attachments) } - func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask { + func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentTask { worker.attachmentUploadTaskOrFinishedTask(for: uuid) } diff --git a/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift b/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift index 4ccc5c575..e9744b7e0 100644 --- a/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift +++ b/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift @@ -28,14 +28,15 @@ import SwiftModalPresentation import SwiftUI struct AttachmentsView: View { - @ModalState private var previewedAttachment: Attachment? - @State private var downloadInProgress = false - @ModalState private var attachmentsURL: AttachmentsURL? + @LazyInjectService private var matomo: MatomoUtils @EnvironmentObject var mailboxManager: MailboxManager @ObservedRealmObject var message: Message - @LazyInjectService private var matomo: MatomoUtils + @ModalState private var previewedAttachment: Attachment? + @ModalState private var attachmentsURL: AttachmentsURL? + @State private var downloadInProgress = false + @State private var downloadProgressState: [String: Double] = [:] private var attachments: [Attachment] { return message.attachments.filter { $0.disposition == .attachment || $0.contentId == nil } @@ -64,18 +65,34 @@ struct AttachmentsView: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: IKPadding.small) { ForEach(attachments) { attachment in + let progress = downloadProgressState[attachment.uuid] ?? 0 Button { openAttachment(attachment) } label: { - AttachmentView(attachment: attachment) + AttachmentView( + attachment: attachment + ) { + if progress > 0 && progress < 1 { + CircleIndeterminateProgressView(progress: progress) + } else { + nil + } + } } } if let swissTransferAttachment = message.swissTransferAttachment { ForEach(swissTransferAttachment.files) { file in + let progress = downloadProgressState[file.uuid] ?? 0 Button { downloadSwissTransferAttachment(stUuid: swissTransferAttachment.uuid, fileUuid: file.uuid) } label: { - AttachmentView(swissTransferFile: file) + AttachmentView(swissTransferFile: file) { + if progress > 0 && progress < 1 { + CircleIndeterminateProgressView(progress: progress) + } else { + nil + } + } } } } @@ -94,11 +111,21 @@ struct AttachmentsView: View { .textStyle(.bodySmallSecondary) .frame(maxWidth: .infinity, alignment: .leading) .multilineTextAlignment(.leading) - - Button(MailResourcesStrings.Localizable.buttonDownloadAll, action: downloadAllAttachments) + + HStack{ + let progress = downloadProgressState[message.swissTransferAttachment?.uuid ?? ""] ?? 0 + CircleIndeterminateProgressView(progress: progress) + .opacity(progress == 1 ? 0 : 1) + Button { + downloadAllAttachments() + } label: { + Text(MailResourcesStrings.Localizable.buttonDownloadAll) + + } .buttonStyle(.ikBorderless(isInlined: true)) .controlSize(.small) - .ikButtonLoading(downloadInProgress) + } + } } .padding(.horizontal, value: .medium) @@ -132,9 +159,12 @@ struct AttachmentsView: View { Task { await tryOrDisplayError { let attachmentURL = try await mailboxManager.apiFetcher.downloadSwissTransferAttachment( - stUuid: stUuid, - fileUuid: fileUuid - ) + stUuid: stUuid, fileUuid: fileUuid + ) { progress in + Task { @MainActor in + downloadProgressState[stUuid] = progress + } + } attachmentsURL = AttachmentsURL(urls: [attachmentURL]) } } @@ -154,7 +184,11 @@ struct AttachmentsView: View { if let swissTransferAttachment = message.swissTransferAttachment { group.addTask { try await mailboxManager.apiFetcher - .downloadAllSwissTransferAttachment(stUuid: swissTransferAttachment.uuid) + .downloadAllSwissTransferAttachment(stUuid: swissTransferAttachment.uuid) { progress in + Task { @MainActor in + downloadProgressState[swissTransferAttachment.uuid] = progress + } + } } } for try await url in group { diff --git a/MailCore/API/MailApiFetcher/MailAPIFetcher+Attachment.swift b/MailCore/API/MailApiFetcher/MailAPIFetcher+Attachment.swift index b288ab3e3..04b2954e6 100644 --- a/MailCore/API/MailApiFetcher/MailAPIFetcher+Attachment.swift +++ b/MailCore/API/MailApiFetcher/MailAPIFetcher+Attachment.swift @@ -29,23 +29,33 @@ public extension MailApiFetcher { return try await request.serializingData().value } - func downloadAttachments(message: Message) async throws -> URL { - try await downloadResource(endpoint: .downloadAttachments(messageResource: message.resource)) + func downloadAttachments(message: Message, progressObserver: ((Double) -> Void)? = nil) async throws -> URL { + try await downloadResource( + endpoint: .downloadAttachments(messageResource: message.resource), + progressObserver: progressObserver + ) } func swissTransferAttachment(stUuid: String) async throws -> SwissTransferAttachment { try await perform(request: authenticatedRequest(.swissTransfer(stUuid: stUuid))) } - func downloadSwissTransferAttachment(stUuid: String, fileUuid: String) async throws -> URL { - try await downloadResource(endpoint: .downloadSwissTransferAttachment(stUuid: stUuid, fileUuid: fileUuid)) + func downloadSwissTransferAttachment(stUuid: String, fileUuid: String, + progressObserver: ((Double) -> Void)? = nil) async throws -> URL { + try await downloadResource( + endpoint: .downloadSwissTransferAttachment(stUuid: stUuid, fileUuid: fileUuid), + progressObserver: progressObserver + ) } - func downloadAllSwissTransferAttachment(stUuid: String) async throws -> URL { - try await downloadResource(endpoint: .downloadAllSwissTransferAttachments(stUuid: stUuid)) + func downloadAllSwissTransferAttachment(stUuid: String, progressObserver: ((Double) -> Void)? = nil) async throws -> URL { + try await downloadResource( + endpoint: .downloadAllSwissTransferAttachments(stUuid: stUuid), + progressObserver: progressObserver + ) } - private func downloadResource(endpoint: Endpoint) async throws -> URL { + private func downloadResource(endpoint: Endpoint, progressObserver: ((Double) -> Void)? = nil) async throws -> URL { let destination = DownloadRequest.suggestedDownloadDestination(options: [ .createIntermediateDirectories, .removePreviousFile @@ -54,6 +64,14 @@ public extension MailApiFetcher { endpoint.url, to: destination ) + if let progressObserver { + Task { + for await progress in download.downloadProgress() { + progressObserver(progress.fractionCompleted) + print(progress.fractionCompleted) + } + } + } return try await download.serializingDownloadedFileURL().value } } diff --git a/MailCore/API/MailApiFetcher/MailApiFetchable.swift b/MailCore/API/MailApiFetcher/MailApiFetchable.swift index 7f758294e..a3bb4ff9e 100644 --- a/MailCore/API/MailApiFetcher/MailApiFetchable.swift +++ b/MailCore/API/MailApiFetcher/MailApiFetchable.swift @@ -58,7 +58,7 @@ public protocol MailApiCommonFetchable { func unstar(mailbox: Mailbox, messages: [Message]) async throws -> MessageActionResult - func downloadAttachments(message: Message) async throws -> URL + func downloadAttachments(message: Message, progressObserver: ((Double) -> Void)?) async throws -> URL func blockSender(message: Message) async throws -> NullableResponse diff --git a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentUploadTask.swift b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentTask.swift similarity index 87% rename from MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentUploadTask.swift rename to MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentTask.swift index 1982a5f7c..fbb07a36a 100644 --- a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentUploadTask.swift +++ b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentTask.swift @@ -18,12 +18,12 @@ import Foundation -/// The progress of uploading an Attachment -@MainActor public final class AttachmentUploadTask: ObservableObject { +/// The progress of a task about an Attachment +@MainActor public final class AttachmentTask: ObservableObject { @Published public var progress: Double = 0 var task: Task? @Published public var error: MailError? - public var uploadDone: Bool { + public var attachmentTask: Bool { return progress >= 1 } diff --git a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift index e8ff25db6..8e879fb92 100644 --- a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift +++ b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift @@ -58,7 +58,7 @@ public protocol AttachmentsManagerWorkable { func importAttachments(attachments: [Attachable], draft: Draft, disposition: AttachmentDisposition) async /// Lookup and return _or_ new object representing a finished task instead. - @MainActor func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask + @MainActor func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentTask } /// Transactionable @@ -80,7 +80,7 @@ public final class AttachmentsManagerWorker { return formatter }() - var attachmentUploadTasks = SendableDictionary() + var attachmentUploadTasks = SendableDictionary() var detachedAttachments: [Attachment] { liveAttachments.map { $0.detached() } @@ -120,7 +120,7 @@ public final class AttachmentsManagerWorker { } func addLocalAttachment(attachment: Attachment) async -> Attachment? { - attachmentUploadTasks[attachment.uuid] = await AttachmentUploadTask() + attachmentUploadTasks[attachment.uuid] = await AttachmentTask() var detached: Attachment? try? writeTransaction { writableRealm in @@ -259,9 +259,9 @@ public final class AttachmentsManagerWorker { return remoteAttachment } - func attachmentUploadTaskOrCreate(for uuid: String) async -> AttachmentUploadTask { + func attachmentUploadTaskOrCreate(for uuid: String) async -> AttachmentTask { guard let attachment = attachmentUploadTask(for: uuid) else { - let newTask = await AttachmentUploadTask() + let newTask = await AttachmentTask() attachmentUploadTasks[uuid] = newTask return newTask } @@ -269,7 +269,7 @@ public final class AttachmentsManagerWorker { return attachment } - func attachmentUploadTask(for uuid: String) -> AttachmentUploadTask? { + func attachmentUploadTask(for uuid: String) -> AttachmentTask? { guard let attachment = attachmentUploadTasks[uuid] else { return nil } @@ -277,11 +277,11 @@ public final class AttachmentsManagerWorker { return attachment } - @MainActor private func updateAttachmentUploadTask(_ uploadTask: AttachmentUploadTask, task: Task?) { + @MainActor private func updateAttachmentUploadTask(_ uploadTask: AttachmentTask, task: Task?) { uploadTask.task = task } - @MainActor private func updateAttachmentUploadTaskProgress(_ uploadTask: AttachmentUploadTask, progress: Double) { + @MainActor private func updateAttachmentUploadTaskProgress(_ uploadTask: AttachmentTask, progress: Double) { uploadTask.progress = progress } @@ -318,7 +318,7 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { } public var allAttachmentsUploaded: Bool { - return attachmentUploadTasks.values.allSatisfy(\.uploadDone) + return attachmentUploadTasks.values.allSatisfy(\.attachmentTask) } public func setUpdateDelegate(_ updateDelegate: AttachmentsContentUpdatable) { @@ -427,9 +427,9 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { return allSanitizedHtml } - @MainActor public func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask { + @MainActor public func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentTask { guard let attachment = attachmentUploadTask(for: uuid) else { - let finishedTask = AttachmentUploadTask() + let finishedTask = AttachmentTask() updateAttachmentUploadTaskProgress(finishedTask, progress: 1) attachmentUploadTasks[uuid] = finishedTask return finishedTask diff --git a/MailCoreUI/Components/CircleIndeterminateProgressView.swift b/MailCoreUI/Components/CircleIndeterminateProgressView.swift new file mode 100644 index 000000000..c9f1cf1c4 --- /dev/null +++ b/MailCoreUI/Components/CircleIndeterminateProgressView.swift @@ -0,0 +1,49 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import SwiftUI + +public struct CircleIndeterminateProgressView: View { + let progress: Double + + public init(progress: Double) { + self.progress = progress + } + + public var body: some View { + ZStack { + Circle() + .stroke(lineWidth: 5) + .opacity(0.1) + .foregroundColor(Color.gray) + .frame(height: 20) + + Circle() + .trim(from: 0.0, to: min(progress, 1.0)) + .stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round)) + .foregroundColor(Color.accentColor) + .rotationEffect(Angle(degrees: 270.0)) + .animation(.linear, value: progress) + .frame(height: 20) + } + } +} + +#Preview { + CircleIndeterminateProgressView(progress: 1) +} From b3ff6511b3c0fec8565045f186a0f79fd8c45cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20D=C3=A9glon?= Date: Thu, 19 Sep 2024 14:34:58 +0200 Subject: [PATCH 02/10] feat: Add the progress bar --- .../Message/Attachment/AttachmentsView.swift | 50 +++++++++++-------- MailCoreUI/Components/AttachmentView.swift | 25 ++++++++-- .../CircleIndeterminateProgressView.swift | 8 +-- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift b/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift index e9744b7e0..3dd65d81c 100644 --- a/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift +++ b/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift @@ -37,6 +37,8 @@ struct AttachmentsView: View { @ModalState private var attachmentsURL: AttachmentsURL? @State private var downloadInProgress = false @State private var downloadProgressState: [String: Double] = [:] + @State private var isDownloadDisabled = false + @State private var showProgressCircle = false private var attachments: [Attachment] { return message.attachments.filter { $0.disposition == .attachment || $0.contentId == nil } @@ -66,34 +68,32 @@ struct AttachmentsView: View { HStack(spacing: IKPadding.small) { ForEach(attachments) { attachment in let progress = downloadProgressState[attachment.uuid] ?? 0 + Button { openAttachment(attachment) } label: { AttachmentView( - attachment: attachment - ) { - if progress > 0 && progress < 1 { - CircleIndeterminateProgressView(progress: progress) - } else { - nil - } - } + attachment: attachment, + isDownloading: progress > 0 && progress < 1, + downloadProgress: progress + ) } + .disabled(isDownloadDisabled) } if let swissTransferAttachment = message.swissTransferAttachment { ForEach(swissTransferAttachment.files) { file in let progress = downloadProgressState[file.uuid] ?? 0 + Button { downloadSwissTransferAttachment(stUuid: swissTransferAttachment.uuid, fileUuid: file.uuid) } label: { - AttachmentView(swissTransferFile: file) { - if progress > 0 && progress < 1 { - CircleIndeterminateProgressView(progress: progress) - } else { - nil - } - } + AttachmentView( + swissTransferFile: file, + isDownloading: progress > 0 && progress < 1, + downloadProgress: progress + ) } + .disabled(isDownloadDisabled) } } } @@ -111,21 +111,24 @@ struct AttachmentsView: View { .textStyle(.bodySmallSecondary) .frame(maxWidth: .infinity, alignment: .leading) .multilineTextAlignment(.leading) - - HStack{ + + HStack { let progress = downloadProgressState[message.swissTransferAttachment?.uuid ?? ""] ?? 0 - CircleIndeterminateProgressView(progress: progress) + if showProgressCircle { + CircleIndeterminateProgressView(progress: progress) .opacity(progress == 1 ? 0 : 1) + } Button { + showProgressCircle = true + isDownloadDisabled = true downloadAllAttachments() } label: { Text(MailResourcesStrings.Localizable.buttonDownloadAll) - } .buttonStyle(.ikBorderless(isInlined: true)) .controlSize(.small) + .disabled(isDownloadDisabled) } - } } .padding(.horizontal, value: .medium) @@ -147,8 +150,10 @@ struct AttachmentsView: View { matomo.track(eventWithCategory: .attachmentActions, name: "open") previewedAttachment = attachment if !FileManager.default.fileExists(atPath: attachment.getLocalURL(mailboxManager: mailboxManager).path) { - Task { + downloadProgressState[attachment.uuid] = 0.0 + Task { @MainActor in await mailboxManager.saveAttachmentLocally(attachment: attachment) + downloadProgressState[attachment.uuid] = 1.0 } } } @@ -172,6 +177,7 @@ struct AttachmentsView: View { private func downloadAllAttachments() { downloadInProgress = true + isDownloadDisabled = true Task { await tryOrDisplayError { matomo.track(eventWithCategory: .message, name: "downloadAll") @@ -198,6 +204,8 @@ struct AttachmentsView: View { } } downloadInProgress = false + isDownloadDisabled = false + showProgressCircle = false } } } diff --git a/MailCoreUI/Components/AttachmentView.swift b/MailCoreUI/Components/AttachmentView.swift index 5819776be..6e1b6abba 100644 --- a/MailCoreUI/Components/AttachmentView.swift +++ b/MailCoreUI/Components/AttachmentView.swift @@ -27,38 +27,53 @@ public struct AttachmentView: View { private let icon: MailResourcesImages @ViewBuilder let accessory: () -> Content? + let isDownloading: Bool + let downloadProgress: Double public init( title: String, subtitle: String, icon: MailResourcesImages, + isDownloading: Bool = false, + downloadProgress: Double = 0, accessory: @escaping () -> Content? = { EmptyView() } ) { self.title = title self.subtitle = subtitle self.icon = icon + self.isDownloading = isDownloading + self.downloadProgress = downloadProgress self.accessory = accessory } - public init(attachment: Attachment, accessory: @escaping () -> Content? = { EmptyView() }) { + public init(attachment: Attachment, isDownloading: Bool = false, downloadProgress: Double = 0, accessory: @escaping () -> Content? = { EmptyView() }) { title = attachment.name subtitle = attachment.size.formatted(.defaultByteCount) icon = attachment.icon + self.isDownloading = isDownloading + self.downloadProgress = downloadProgress self.accessory = accessory } - public init(swissTransferFile: File, accessory: @escaping () -> Content? = { EmptyView() }) { + public init(swissTransferFile: File, isDownloading: Bool = false, downloadProgress: Double = 0, accessory: @escaping () -> Content? = { EmptyView() }) { title = swissTransferFile.name subtitle = swissTransferFile.size.formatted(.defaultByteCount) icon = swissTransferFile.icon + self.isDownloading = isDownloading + self.downloadProgress = downloadProgress self.accessory = accessory } public var body: some View { HStack(spacing: IKPadding.small) { - icon - .iconSize(.large) - .foregroundStyle(MailResourcesAsset.textSecondaryColor) + if isDownloading { + CircleIndeterminateProgressView(progress: 0.5) + .frame(height: 40) + } else { + icon + .iconSize(.large) + .foregroundStyle(MailResourcesAsset.textSecondaryColor) + } VStack(alignment: .leading, spacing: 0) { Text(title) diff --git a/MailCoreUI/Components/CircleIndeterminateProgressView.swift b/MailCoreUI/Components/CircleIndeterminateProgressView.swift index c9f1cf1c4..4e55ddbd5 100644 --- a/MailCoreUI/Components/CircleIndeterminateProgressView.swift +++ b/MailCoreUI/Components/CircleIndeterminateProgressView.swift @@ -28,18 +28,18 @@ public struct CircleIndeterminateProgressView: View { public var body: some View { ZStack { Circle() - .stroke(lineWidth: 5) + .stroke(lineWidth: 2) .opacity(0.1) .foregroundColor(Color.gray) - .frame(height: 20) + .frame(height: 15) Circle() .trim(from: 0.0, to: min(progress, 1.0)) - .stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round)) + .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) .foregroundColor(Color.accentColor) .rotationEffect(Angle(degrees: 270.0)) .animation(.linear, value: progress) - .frame(height: 20) + .frame(height: 15) } } } From 5ba8ffe93acc572239878659fe84625d03a5542f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20D=C3=A9glon?= Date: Thu, 19 Sep 2024 14:39:51 +0200 Subject: [PATCH 03/10] fix: Swiftformat --- MailCoreUI/Components/AttachmentView.swift | 14 +++++++-- .../CircleIndeterminateProgressView.swift | 30 +++++++++---------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/MailCoreUI/Components/AttachmentView.swift b/MailCoreUI/Components/AttachmentView.swift index 6e1b6abba..1fd66461e 100644 --- a/MailCoreUI/Components/AttachmentView.swift +++ b/MailCoreUI/Components/AttachmentView.swift @@ -46,7 +46,12 @@ public struct AttachmentView: View { self.accessory = accessory } - public init(attachment: Attachment, isDownloading: Bool = false, downloadProgress: Double = 0, accessory: @escaping () -> Content? = { EmptyView() }) { + public init( + attachment: Attachment, + isDownloading: Bool = false, + downloadProgress: Double = 0, + accessory: @escaping () -> Content? = { EmptyView() } + ) { title = attachment.name subtitle = attachment.size.formatted(.defaultByteCount) icon = attachment.icon @@ -55,7 +60,12 @@ public struct AttachmentView: View { self.accessory = accessory } - public init(swissTransferFile: File, isDownloading: Bool = false, downloadProgress: Double = 0, accessory: @escaping () -> Content? = { EmptyView() }) { + public init( + swissTransferFile: File, + isDownloading: Bool = false, + downloadProgress: Double = 0, + accessory: @escaping () -> Content? = { EmptyView() } + ) { title = swissTransferFile.name subtitle = swissTransferFile.size.formatted(.defaultByteCount) icon = swissTransferFile.icon diff --git a/MailCoreUI/Components/CircleIndeterminateProgressView.swift b/MailCoreUI/Components/CircleIndeterminateProgressView.swift index 4e55ddbd5..50e6f22f5 100644 --- a/MailCoreUI/Components/CircleIndeterminateProgressView.swift +++ b/MailCoreUI/Components/CircleIndeterminateProgressView.swift @@ -26,22 +26,22 @@ public struct CircleIndeterminateProgressView: View { } public var body: some View { - ZStack { - Circle() - .stroke(lineWidth: 2) - .opacity(0.1) - .foregroundColor(Color.gray) - .frame(height: 15) - - Circle() - .trim(from: 0.0, to: min(progress, 1.0)) - .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) - .foregroundColor(Color.accentColor) - .rotationEffect(Angle(degrees: 270.0)) - .animation(.linear, value: progress) - .frame(height: 15) + ZStack { + Circle() + .stroke(lineWidth: 2) + .opacity(0.1) + .foregroundColor(Color.gray) + .frame(height: 15) + + Circle() + .trim(from: 0.0, to: min(progress, 1.0)) + .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) + .foregroundColor(Color.accentColor) + .rotationEffect(Angle(degrees: 270.0)) + .animation(.linear, value: progress) + .frame(height: 15) + } } - } } #Preview { From b911e38eefef496fb3f16ac635e9d1fd180c0548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20D=C3=A9glon?= Date: Fri, 20 Sep 2024 09:09:45 +0200 Subject: [PATCH 04/10] fix: Add progress in AttachmentView --- .../Attachments/AttachmentUploadCell.swift | 2 +- .../Message/Attachment/AttachmentsView.swift | 24 ++++++++++++++----- .../Thread/Message/BodyImageProcessor.swift | 2 +- .../MailAPIFetcher+Attachment.swift | 12 ++++++++-- .../API/MailApiFetcher/MailApiFetchable.swift | 2 +- .../MailboxManager/MailboxManageable.swift | 4 ++-- .../MailboxManager+Message.swift | 8 +++---- MailCoreUI/Components/AttachmentView.swift | 4 ++-- 8 files changed, 39 insertions(+), 19 deletions(-) diff --git a/Mail/Views/New Message/Attachments/AttachmentUploadCell.swift b/Mail/Views/New Message/Attachments/AttachmentUploadCell.swift index 35b59ca88..1be3d1579 100644 --- a/Mail/Views/New Message/Attachments/AttachmentUploadCell.swift +++ b/Mail/Views/New Message/Attachments/AttachmentUploadCell.swift @@ -76,7 +76,7 @@ struct AttachmentUploadCell: View { previewedAttachment = attachment if !FileManager.default.fileExists(atPath: attachment.getLocalURL(mailboxManager: mailboxManager).path) { Task { - await mailboxManager.saveAttachmentLocally(attachment: attachment) + await mailboxManager.saveAttachmentLocally(attachment: attachment, progressObserver: nil) } } } diff --git a/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift b/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift index 3dd65d81c..14cd9c051 100644 --- a/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift +++ b/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift @@ -37,6 +37,7 @@ struct AttachmentsView: View { @ModalState private var attachmentsURL: AttachmentsURL? @State private var downloadInProgress = false @State private var downloadProgressState: [String: Double] = [:] + @State private var trackDownloadTask: [String: Task] = [:] @State private var isDownloadDisabled = false @State private var showProgressCircle = false @@ -119,12 +120,11 @@ struct AttachmentsView: View { .opacity(progress == 1 ? 0 : 1) } Button { - showProgressCircle = true - isDownloadDisabled = true downloadAllAttachments() } label: { Text(MailResourcesStrings.Localizable.buttonDownloadAll) } + .disabled(isDownloadDisabled) .buttonStyle(.ikBorderless(isInlined: true)) .controlSize(.small) .disabled(isDownloadDisabled) @@ -144,6 +144,11 @@ struct AttachmentsView: View { DocumentPicker(pickerType: .exportContent(attachmentsURL.urls)) .ignoresSafeArea() } + .onDisappear { + for (_, task) in trackDownloadTask { + task.cancel() + } + } } private func openAttachment(_ attachment: Attachment) { @@ -151,33 +156,40 @@ struct AttachmentsView: View { previewedAttachment = attachment if !FileManager.default.fileExists(atPath: attachment.getLocalURL(mailboxManager: mailboxManager).path) { downloadProgressState[attachment.uuid] = 0.0 - Task { @MainActor in - await mailboxManager.saveAttachmentLocally(attachment: attachment) + trackDownloadTask[attachment.uuid] = Task { @MainActor in + await mailboxManager.saveAttachmentLocally(attachment: attachment) { progress in + Task { @MainActor in + downloadProgressState[attachment.uuid] = progress + } + } downloadProgressState[attachment.uuid] = 1.0 } } } private func downloadSwissTransferAttachment(stUuid: String, fileUuid: String) { + isDownloadDisabled = true matomo.track(eventWithCategory: .attachmentActions, name: "openSwissTransfer") - Task { + trackDownloadTask[fileUuid] = Task { await tryOrDisplayError { let attachmentURL = try await mailboxManager.apiFetcher.downloadSwissTransferAttachment( stUuid: stUuid, fileUuid: fileUuid ) { progress in Task { @MainActor in - downloadProgressState[stUuid] = progress + downloadProgressState[fileUuid] = progress } } attachmentsURL = AttachmentsURL(urls: [attachmentURL]) } } + isDownloadDisabled = false } private func downloadAllAttachments() { downloadInProgress = true isDownloadDisabled = true + showProgressCircle = true Task { await tryOrDisplayError { matomo.track(eventWithCategory: .message, name: "downloadAll") diff --git a/Mail/Views/Thread/Message/BodyImageProcessor.swift b/Mail/Views/Thread/Message/BodyImageProcessor.swift index 428d2dc7f..02bc4fc13 100644 --- a/Mail/Views/Thread/Message/BodyImageProcessor.swift +++ b/Mail/Views/Thread/Message/BodyImageProcessor.swift @@ -33,7 +33,7 @@ struct BodyImageProcessor { let base64Images: [ImageBase64AndMime?] = await attachments .concurrentMap(customConcurrency: Constants.concurrentNetworkCalls) { attachment in do { - let attachmentData = try await mailboxManager.attachmentData(attachment) + let attachmentData = try await mailboxManager.attachmentData(attachment, progressObserver: nil) // Skip compression on non static images types or already heic sources guard attachment.mimeType.contains("jpg") diff --git a/MailCore/API/MailApiFetcher/MailAPIFetcher+Attachment.swift b/MailCore/API/MailApiFetcher/MailAPIFetcher+Attachment.swift index 04b2954e6..f4716c1a8 100644 --- a/MailCore/API/MailApiFetcher/MailAPIFetcher+Attachment.swift +++ b/MailCore/API/MailApiFetcher/MailAPIFetcher+Attachment.swift @@ -21,11 +21,19 @@ import Foundation import InfomaniakCore public extension MailApiFetcher { - func attachment(attachment: Attachment) async throws -> Data { + func attachment(attachment: Attachment, progressObserver: ((Double) -> Void)? = nil) async throws -> Data { guard let resource = attachment.resource else { throw MailError.resourceError } let request = authenticatedRequest(.resource(resource)) + if let progressObserver { + Task { + for await progress in request.downloadProgress() { + progressObserver(progress.fractionCompleted) + print("attachement", progress.fractionCompleted) + } + } + } return try await request.serializingData().value } @@ -68,7 +76,7 @@ public extension MailApiFetcher { Task { for await progress in download.downloadProgress() { progressObserver(progress.fractionCompleted) - print(progress.fractionCompleted) + print("Download ressource: \(progress.fractionCompleted)") } } } diff --git a/MailCore/API/MailApiFetcher/MailApiFetchable.swift b/MailCore/API/MailApiFetcher/MailApiFetchable.swift index a3bb4ff9e..80272b65a 100644 --- a/MailCore/API/MailApiFetcher/MailApiFetchable.swift +++ b/MailCore/API/MailApiFetcher/MailApiFetchable.swift @@ -112,7 +112,7 @@ public protocol MailApiExtendedFetchable { func delete(mailbox: Mailbox, messages: [Message]) async throws -> [Empty] - func attachment(attachment: Attachment) async throws -> Data + func attachment(attachment: Attachment, progressObserver: ((Double) -> Void)?) async throws -> Data func draft(mailbox: Mailbox, draftUuid: String) async throws -> Draft diff --git a/MailCore/Cache/MailboxManager/MailboxManageable.swift b/MailCore/Cache/MailboxManager/MailboxManageable.swift index 85580230d..43f7c2113 100644 --- a/MailCore/Cache/MailboxManager/MailboxManageable.swift +++ b/MailCore/Cache/MailboxManager/MailboxManageable.swift @@ -42,8 +42,8 @@ public protocol MailboxManagerMessageable { func fetchOneNewPage(folder: Folder) async throws -> Bool func fetchOneOldPage(folder: Folder) async throws -> Int? func message(message: Message) async throws - func attachmentData(_ attachment: Attachment) async throws -> Data - func saveAttachmentLocally(attachment: Attachment) async + func attachmentData(_ attachment: Attachment, progressObserver: ((Double) -> Void)?) async throws -> Data + func saveAttachmentLocally(attachment: Attachment, progressObserver: ((Double) -> Void)?) async func markAsSeen(message: Message, seen: Bool) async throws func move(messages: [Message], to folderRole: FolderRole, origin: Folder?) async throws -> UndoAction func move(messages: [Message], to folder: Folder, origin: Folder?) async throws -> UndoAction diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Message.swift b/MailCore/Cache/MailboxManager/MailboxManager+Message.swift index 8d53b3148..e6d10f9dc 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Message.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Message.swift @@ -34,12 +34,12 @@ public extension MailboxManager { } } - func attachmentData(_ attachment: Attachment) async throws -> Data { + func attachmentData(_ attachment: Attachment, progressObserver: ((Double) -> Void)?) async throws -> Data { guard !Task.isCancelled else { throw CancellationError() } - let data = try await apiFetcher.attachment(attachment: attachment) + let data = try await apiFetcher.attachment(attachment: attachment, progressObserver: progressObserver) let safeAttachment = ThreadSafeReference(to: attachment) try? writeTransaction { writableRealm in @@ -53,9 +53,9 @@ public extension MailboxManager { return data } - func saveAttachmentLocally(attachment: Attachment) async { + func saveAttachmentLocally(attachment: Attachment, progressObserver: ((Double) -> Void)?) async { do { - let data = try await attachmentData(attachment) + let data = try await attachmentData(attachment, progressObserver: progressObserver) let url = attachment.getLocalURL(userId: account.userId, mailboxId: mailbox.mailboxId) let parentFolder = url.deletingLastPathComponent() if !FileManager.default.fileExists(atPath: parentFolder.path) { diff --git a/MailCoreUI/Components/AttachmentView.swift b/MailCoreUI/Components/AttachmentView.swift index 1fd66461e..55919ca82 100644 --- a/MailCoreUI/Components/AttachmentView.swift +++ b/MailCoreUI/Components/AttachmentView.swift @@ -77,8 +77,8 @@ public struct AttachmentView: View { public var body: some View { HStack(spacing: IKPadding.small) { if isDownloading { - CircleIndeterminateProgressView(progress: 0.5) - .frame(height: 40) + CircleIndeterminateProgressView(progress: downloadProgress) + .frame(height: IKPadding.large.sign.rawValue.constraintMultiplierTargetValue) } else { icon .iconSize(.large) From b467d3bb8cdebfc1b44011db1e1bf0c9efc736d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20D=C3=A9glon?= Date: Fri, 20 Sep 2024 09:24:39 +0200 Subject: [PATCH 05/10] fix: Feedback --- MailCoreUI/Components/AttachmentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MailCoreUI/Components/AttachmentView.swift b/MailCoreUI/Components/AttachmentView.swift index 55919ca82..693179b19 100644 --- a/MailCoreUI/Components/AttachmentView.swift +++ b/MailCoreUI/Components/AttachmentView.swift @@ -78,7 +78,7 @@ public struct AttachmentView: View { HStack(spacing: IKPadding.small) { if isDownloading { CircleIndeterminateProgressView(progress: downloadProgress) - .frame(height: IKPadding.large.sign.rawValue.constraintMultiplierTargetValue) + .frame(height: IKPadding.large) } else { icon .iconSize(.large) From 6ffdb2c6d20ec3666b36ea782dc013bc81869f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20D=C3=A9glon?= Date: Fri, 20 Sep 2024 10:40:33 +0200 Subject: [PATCH 06/10] fix: CI fix --- MailTests/Search/ITSearchViewModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MailTests/Search/ITSearchViewModel.swift b/MailTests/Search/ITSearchViewModel.swift index a2ca59087..4f2bbb8c2 100644 --- a/MailTests/Search/ITSearchViewModel.swift +++ b/MailTests/Search/ITSearchViewModel.swift @@ -182,10 +182,10 @@ final class MCKMailboxManageable_SearchViewModel: MailboxManageable, MCKTransact func fetchOneOldPage(folder: MailCore.Folder) async throws -> Int? { nil } func message(message: MailCore.Message) async throws {} + + func attachmentData(_ attachment: MailCore.Attachment, progressObserver: ((Double) -> Void)?) async throws -> Data { Data() } - func attachmentData(_ attachment: MailCore.Attachment) async throws -> Data { Data() } - - func saveAttachmentLocally(attachment: MailCore.Attachment) async {} + func saveAttachmentLocally(attachment: MailCore.Attachment, progressObserver: ((Double) -> Void)?) async {} func markAsSeen(message: MailCore.Message, seen: Bool) async throws {} From d1c889c7d05da15566e95ac2b6712db8bff3c634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20D=C3=A9glon?= Date: Fri, 20 Sep 2024 10:41:52 +0200 Subject: [PATCH 07/10] fix: Refactor --- MailTests/Search/ITSearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MailTests/Search/ITSearchViewModel.swift b/MailTests/Search/ITSearchViewModel.swift index 4f2bbb8c2..9cd281d05 100644 --- a/MailTests/Search/ITSearchViewModel.swift +++ b/MailTests/Search/ITSearchViewModel.swift @@ -182,7 +182,7 @@ final class MCKMailboxManageable_SearchViewModel: MailboxManageable, MCKTransact func fetchOneOldPage(folder: MailCore.Folder) async throws -> Int? { nil } func message(message: MailCore.Message) async throws {} - + func attachmentData(_ attachment: MailCore.Attachment, progressObserver: ((Double) -> Void)?) async throws -> Data { Data() } func saveAttachmentLocally(attachment: MailCore.Attachment, progressObserver: ((Double) -> Void)?) async {} From 0b74abdbdae0ca6f0daf26c54436d4f35ef13702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20D=C3=A9glon?= Date: Fri, 20 Sep 2024 11:54:59 +0200 Subject: [PATCH 08/10] refactor: Refactor ProgressView to ProgressStyle --- MailCoreUI/Components/AttachmentView.swift | 4 +-- ...ft => MailCircularProgressViewStyle.swift} | 29 ++++++------------- 2 files changed, 11 insertions(+), 22 deletions(-) rename MailCoreUI/Components/{CircleIndeterminateProgressView.swift => MailCircularProgressViewStyle.swift} (53%) diff --git a/MailCoreUI/Components/AttachmentView.swift b/MailCoreUI/Components/AttachmentView.swift index 693179b19..b864952ac 100644 --- a/MailCoreUI/Components/AttachmentView.swift +++ b/MailCoreUI/Components/AttachmentView.swift @@ -77,8 +77,8 @@ public struct AttachmentView: View { public var body: some View { HStack(spacing: IKPadding.small) { if isDownloading { - CircleIndeterminateProgressView(progress: downloadProgress) - .frame(height: IKPadding.large) + ProgressView(value: downloadProgress) + .progressViewStyle(MailCircularProgressViewStyle()) } else { icon .iconSize(.large) diff --git a/MailCoreUI/Components/CircleIndeterminateProgressView.swift b/MailCoreUI/Components/MailCircularProgressViewStyle.swift similarity index 53% rename from MailCoreUI/Components/CircleIndeterminateProgressView.swift rename to MailCoreUI/Components/MailCircularProgressViewStyle.swift index 50e6f22f5..fd1b5550a 100644 --- a/MailCoreUI/Components/CircleIndeterminateProgressView.swift +++ b/MailCoreUI/Components/MailCircularProgressViewStyle.swift @@ -16,34 +16,23 @@ along with this program. If not, see . */ +import MailResources import SwiftUI -public struct CircleIndeterminateProgressView: View { - let progress: Double +public struct MailCircularProgressViewStyle: ProgressViewStyle { + public init() {} - public init(progress: Double) { - self.progress = progress - } - - public var body: some View { + public func makeBody(configuration: Configuration) -> some View { ZStack { Circle() - .stroke(lineWidth: 2) + .stroke(lineWidth: 3) .opacity(0.1) - .foregroundColor(Color.gray) - .frame(height: 15) Circle() - .trim(from: 0.0, to: min(progress, 1.0)) - .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) - .foregroundColor(Color.accentColor) - .rotationEffect(Angle(degrees: 270.0)) - .animation(.linear, value: progress) - .frame(height: 15) + .trim(from: 0, to: configuration.fractionCompleted ?? 0) + .stroke(Color.accentColor, lineWidth: 3) + .rotationEffect(.degrees(-90)) } + .frame(width: 16) } } - -#Preview { - CircleIndeterminateProgressView(progress: 1) -} From 76d55112499953f4b9c71a2c188330765935ef0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20D=C3=A9glon?= Date: Fri, 20 Sep 2024 11:57:11 +0200 Subject: [PATCH 09/10] refactor: Protocol update --- .../Views/Thread/Message/Attachment/AttachmentsView.swift | 7 ++++--- MailTests/Folders/ITFolderListViewModel.swift | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift b/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift index 14cd9c051..3a498873a 100644 --- a/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift +++ b/Mail/Views/Thread/Message/Attachment/AttachmentsView.swift @@ -116,8 +116,8 @@ struct AttachmentsView: View { HStack { let progress = downloadProgressState[message.swissTransferAttachment?.uuid ?? ""] ?? 0 if showProgressCircle { - CircleIndeterminateProgressView(progress: progress) - .opacity(progress == 1 ? 0 : 1) + ProgressView(value: progress) + .progressViewStyle(MailCircularProgressViewStyle()) } Button { downloadAllAttachments() @@ -127,7 +127,6 @@ struct AttachmentsView: View { .disabled(isDownloadDisabled) .buttonStyle(.ikBorderless(isInlined: true)) .controlSize(.small) - .disabled(isDownloadDisabled) } } } @@ -152,6 +151,7 @@ struct AttachmentsView: View { } private func openAttachment(_ attachment: Attachment) { + isDownloadDisabled = true matomo.track(eventWithCategory: .attachmentActions, name: "open") previewedAttachment = attachment if !FileManager.default.fileExists(atPath: attachment.getLocalURL(mailboxManager: mailboxManager).path) { @@ -165,6 +165,7 @@ struct AttachmentsView: View { downloadProgressState[attachment.uuid] = 1.0 } } + isDownloadDisabled = false } private func downloadSwissTransferAttachment(stUuid: String, fileUuid: String) { diff --git a/MailTests/Folders/ITFolderListViewModel.swift b/MailTests/Folders/ITFolderListViewModel.swift index 34dff7107..32b999110 100644 --- a/MailTests/Folders/ITFolderListViewModel.swift +++ b/MailTests/Folders/ITFolderListViewModel.swift @@ -153,10 +153,10 @@ struct MCKMailboxManageable_FolderListViewModel: MailboxManageable, MCKTransacti func message(message: MailCore.Message) async throws {} - func attachmentData(_ attachment: MailCore.Attachment) async throws -> Data { Data() } - - func saveAttachmentLocally(attachment: MailCore.Attachment) async {} - + func attachmentData(_ attachment: MailCore.Attachment, progressObserver: ((Double) -> Void)?) async throws -> Data { Data() } + + func saveAttachmentLocally(attachment: MailCore.Attachment, progressObserver: ((Double) -> Void)?) async {} + func markAsSeen(message: MailCore.Message, seen: Bool) async throws {} func move(messages: [MailCore.Message], to folderRole: MailCore.FolderRole, origin: Folder?) async throws -> MailCore From d9746f7c18cbb940fb070a2e995b1d46db052e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20D=C3=A9glon?= Date: Fri, 20 Sep 2024 12:05:49 +0200 Subject: [PATCH 10/10] refactor: Remove white space for swiftformat --- MailTests/Folders/ITFolderListViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MailTests/Folders/ITFolderListViewModel.swift b/MailTests/Folders/ITFolderListViewModel.swift index 32b999110..7fa063622 100644 --- a/MailTests/Folders/ITFolderListViewModel.swift +++ b/MailTests/Folders/ITFolderListViewModel.swift @@ -154,9 +154,9 @@ struct MCKMailboxManageable_FolderListViewModel: MailboxManageable, MCKTransacti func message(message: MailCore.Message) async throws {} func attachmentData(_ attachment: MailCore.Attachment, progressObserver: ((Double) -> Void)?) async throws -> Data { Data() } - + func saveAttachmentLocally(attachment: MailCore.Attachment, progressObserver: ((Double) -> Void)?) async {} - + func markAsSeen(message: MailCore.Message, seen: Bool) async throws {} func move(messages: [MailCore.Message], to folderRole: MailCore.FolderRole, origin: Folder?) async throws -> MailCore