diff --git a/Mail/Components/ThreadCell/ThreadCell.swift b/Mail/Components/ThreadCell/ThreadCell.swift index 10dd63ee8..09252c0e2 100644 --- a/Mail/Components/ThreadCell/ThreadCell.swift +++ b/Mail/Components/ThreadCell/ThreadCell.swift @@ -54,6 +54,7 @@ struct ThreadCellDataHolder { let isInWrittenByMeFolder: Bool init(thread: Thread) { + // swiftlint:disable:next last_where let lastMessageNotFromSent = thread.messages.filter(Self.lastMessageNotFromSentPredicate).last ?? thread.messages.last date = thread.date.formatted(.thread(.list)) diff --git a/Mail/Views/AI Writer/Proposition/AIDismissibleErrorView.swift b/Mail/Views/AI Writer/Proposition/AIDismissibleErrorView.swift index 02ecb8425..cc3935b1a 100644 --- a/Mail/Views/AI Writer/Proposition/AIDismissibleErrorView.swift +++ b/Mail/Views/AI Writer/Proposition/AIDismissibleErrorView.swift @@ -36,14 +36,15 @@ struct AIDismissibleErrorView: View { InformationBlockView( icon: MailResourcesAsset.warningFill.swiftUIImage, message: error?.localizedDescription ?? "", - iconColor: MailResourcesAsset.orangeColor.swiftUIColor - ) { - matomo.track(eventWithCategory: .aiWriter, name: "dismissError") + iconColor: MailResourcesAsset.orangeColor.swiftUIColor, + dismissHandler: { // swiftlint:disable:this trailing_closure + matomo.track(eventWithCategory: .aiWriter, name: "dismissError") - withAnimation { - isShowingError = false + withAnimation { + isShowingError = false + } } - } + ) } } .onChange(of: error) { newError in diff --git a/Mail/Views/Bottom sheets/Actions/HeaderCloseButtonView.swift b/Mail/Views/Bottom sheets/Actions/HeaderCloseButtonView.swift index a1bcb670a..6f554bad6 100644 --- a/Mail/Views/Bottom sheets/Actions/HeaderCloseButtonView.swift +++ b/Mail/Views/Bottom sheets/Actions/HeaderCloseButtonView.swift @@ -39,7 +39,7 @@ struct HeaderCloseButtonView: View { } } -#Preview { +@available(iOS 17.0, *) +#Preview(traits: .sizeThatFitsLayout) { HeaderCloseButtonView(title: "View") {} - .previewLayout(.sizeThatFits) } diff --git a/Mail/Views/Menu Drawer/Actions/HelpView.swift b/Mail/Views/Menu Drawer/Actions/HelpView.swift index a8e5ea1e6..011bfb4ca 100644 --- a/Mail/Views/Menu Drawer/Actions/HelpView.swift +++ b/Mail/Views/Menu Drawer/Actions/HelpView.swift @@ -87,6 +87,7 @@ struct HelpView: View { .background(MailResourcesAsset.backgroundColor.swiftUIColor) .navigationBarTitle(MailResourcesStrings.Localizable.buttonHelp, displayMode: .inline) .customAlert(item: $updateVersionAlert) { action in + // swiftlint:disable:next trailing_closure UpdateVersionAlertView(onLaterPressed: { openURL(action.destination) }) diff --git a/Mail/Views/Menu Drawer/Folders/FolderCell.swift b/Mail/Views/Menu Drawer/Folders/FolderCell.swift index 208b07597..867c8145d 100644 --- a/Mail/Views/Menu Drawer/Folders/FolderCell.swift +++ b/Mail/Views/Menu Drawer/Folders/FolderCell.swift @@ -26,7 +26,7 @@ import SwiftUI import SwiftUIMacros extension EnvironmentValues { - @EnvironmentValue + @EnvironmentKey var folderCellType = FolderCell.CellType.menuDrawer } diff --git a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift index abe71128d..7f7d083b5 100644 --- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift +++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift @@ -27,7 +27,7 @@ import SwiftUI import SwiftUIMacros extension EnvironmentValues { - @EnvironmentValue + @EnvironmentKey var mailboxCellStyle = MailboxCell.Style.menuDrawer } diff --git a/Mail/Views/Settings/Data & Privacy/SettingsAccountManagementView.swift b/Mail/Views/Settings/Data & Privacy/SettingsAccountManagementView.swift index ffad878fc..2986e0796 100644 --- a/Mail/Views/Settings/Data & Privacy/SettingsAccountManagementView.swift +++ b/Mail/Views/Settings/Data & Privacy/SettingsAccountManagementView.swift @@ -105,7 +105,7 @@ struct SettingsAccountManagementView: View { } } -extension ApiToken: Identifiable { +extension ApiToken: @retroactive Identifiable { public var id: String { return "\(userId)\(accessToken)" } diff --git a/Mail/Views/Settings/General/SettingsNotificationsView.swift b/Mail/Views/Settings/General/SettingsNotificationsView.swift index 915986854..7777fd7e1 100644 --- a/Mail/Views/Settings/General/SettingsNotificationsView.swift +++ b/Mail/Views/Settings/General/SettingsNotificationsView.swift @@ -113,6 +113,7 @@ struct SettingsNotificationsView: View { } } } + // swiftlint:disable:next trailing_closure .sceneLifecycle(willEnterForeground: { settingsNotificationEnabled { enabled in showWarning = !enabled diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index 90d80f73e..f139f5216 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -153,6 +153,7 @@ struct SplitView: View { SafariWebView(url: safariContent.url) .ignoresSafeArea() } + // swiftlint:disable:next trailing_closure .sceneLifecycle(willEnterForeground: { Task { // We need to write in Task instead of async let to avoid being cancelled to early diff --git a/Mail/Views/Sync Profile/SyncInstallProfileTutorialView.swift b/Mail/Views/Sync Profile/SyncInstallProfileTutorialView.swift index e92f17a8f..0eb40a8a2 100644 --- a/Mail/Views/Sync Profile/SyncInstallProfileTutorialView.swift +++ b/Mail/Views/Sync Profile/SyncInstallProfileTutorialView.swift @@ -24,7 +24,7 @@ import MailCoreUI import MailResources import SwiftUI -extension MailResourcesImages: Identifiable { +extension MailResourcesImages: @retroactive Identifiable { public var id: String { name } diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift index 943f2bd3a..82077ee8e 100644 --- a/Mail/Views/Thread List/ThreadListView.swift +++ b/Mail/Views/Thread List/ThreadListView.swift @@ -197,6 +197,7 @@ struct ThreadListView: View { .onChange(of: multipleSelectionViewModel.isEnabled) { isEnabled in scrollObserver.shouldObserve = !isEnabled } + // swiftlint:disable:next trailing_closure .sceneLifecycle(willEnterForeground: { updateFetchingTask() }) @@ -210,6 +211,7 @@ struct ThreadListView: View { FlushFolderAlertView(flushAlert: item, folder: viewModel.frozenFolder) } .customAlert(isPresented: $isShowingUpdateAlert) { + // swiftlint:disable:next trailing_closure UpdateVersionAlertView(onDismiss: { hasDismissedUpdateVersionView = true }) diff --git a/Mail/Views/Thread/Message/BodyImageProcessor.swift b/Mail/Views/Thread/Message/BodyImageProcessor.swift new file mode 100644 index 000000000..428d2dc7f --- /dev/null +++ b/Mail/Views/Thread/Message/BodyImageProcessor.swift @@ -0,0 +1,130 @@ +/* + 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 Foundation +import MailCore +import OSLog +import UIKit + +struct BodyImageProcessor { + private let bodyImageMutator = BodyImageMutator() + + /// Download and encode all images for the current chunk in parallel. + public func fetchBase64Images( + _ attachments: ArraySlice, + mailboxManager: MailboxManager + ) async -> [ImageBase64AndMime?] { + // Force a fixed max concurrency to be a nice citizen to the network. + let base64Images: [ImageBase64AndMime?] = await attachments + .concurrentMap(customConcurrency: Constants.concurrentNetworkCalls) { attachment in + do { + let attachmentData = try await mailboxManager.attachmentData(attachment) + + // Skip compression on non static images types or already heic sources + guard attachment.mimeType.contains("jpg") + || attachment.mimeType.contains("jpeg") + || attachment.mimeType.contains("png") else { + let base64String = attachmentData.base64EncodedString() + return ImageBase64AndMime(base64String, attachment.mimeType) + } + + // Skip compression with lockdown mode enables as images can glitch + let isLockdownModeEnabled = (UserDefaults.standard.object(forKey: "LDMGlobalEnabled") as? Bool) ?? false + guard !isLockdownModeEnabled else { + let base64String = attachmentData.base64EncodedString() + return ImageBase64AndMime(base64String, attachment.mimeType) + } + + let compressedImage = compressedBase64ImageAndMime( + attachmentData: attachmentData, + attachmentMime: attachment.mimeType + ) + return compressedImage + } catch { + Logger.general.error("Error \(error) : Failed to fetch data for attachment: \(attachment)") + return nil + } + } + + assert(base64Images.count == attachments.count, "Arrays count should match") + return base64Images + } + + /// Try to compress the attachment with the best matched algorithm. Trade CPU cycles to reduce render time and memory usage. + private func compressedBase64ImageAndMime(attachmentData: Data, attachmentMime: String) -> ImageBase64AndMime { + guard #available(iOS 17.0, *) else { + let base64String = attachmentData.base64EncodedString() + return ImageBase64AndMime(base64String, attachmentMime) + } + + // On iOS17 Safari and iOS has support for heic. Quality is unchanged. Size is halved. + let image = UIImage(data: attachmentData) + guard let imageCompressed = image?.heicData(), imageCompressed.count < attachmentData.count else { + let base64String = attachmentData.base64EncodedString() + return ImageBase64AndMime(base64String, attachmentMime) + } + + let base64String = imageCompressed.base64EncodedString() + return ImageBase64AndMime(base64String, "image/heic") + } + + /// Inject base64 images in a body + public func injectImagesInBody( + body: String?, + attachments: ArraySlice, + base64Images: [ImageBase64AndMime?] + ) async -> String? { + guard let body, !body.isEmpty else { + return nil + } + + var workingBody = body + for (index, attachment) in attachments.enumerated() { + guard !Task.isCancelled else { + break + } + + guard let contentId = attachment.contentId, + let base64Image = base64Images[safe: index] as? ImageBase64AndMime else { + continue + } + + bodyImageMutator.replaceContentIdForBase64Image( + in: &workingBody, + contentId: contentId, + mimeType: base64Image.mimeType, + contentBase64Encoded: base64Image.imageEncoded + ) + } + return workingBody + } +} + +struct BodyImageMutator { + func replaceContentIdForBase64Image( + in body: inout String, + contentId: String, + mimeType: String, + contentBase64Encoded: String + ) { + body = body.replacingOccurrences( + of: "cid:\(contentId)", + with: "data:\(mimeType);base64,\(contentBase64Encoded)" + ) + } +} diff --git a/Mail/Views/Thread/Message/InlineAttachmentWorker.swift b/Mail/Views/Thread/Message/InlineAttachmentWorker.swift deleted file mode 100644 index 726666281..000000000 --- a/Mail/Views/Thread/Message/InlineAttachmentWorker.swift +++ /dev/null @@ -1,305 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 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 Algorithms -import CocoaLumberjackSwift -import Foundation -import InfomaniakConcurrency -import InfomaniakCore -import MailCore -import SwiftUI - -/// Something to process the Attachments outside of the mainActor -/// -/// Call `start()` to begin processing, call `stop` to make sure internal Task is cancelled. -final class InlineAttachmentWorker: ObservableObject { - private let bodyImageProcessor = BodyImageProcessor() - - /// The presentableBody with the current pre-processing (partial or done) - @Published var presentableBody: PresentableBody - - /// Set to true when done processing - @Published var isMessagePreprocessed: Bool - - var mailboxManager: MailboxManager? - - private let messageUid: String - - /// Tracking the preprocessing Task tree - private var processing: Task? - - public init(messageUid: String) { - self.messageUid = messageUid - isMessagePreprocessed = false - presentableBody = PresentableBody() - } - - deinit { - stop() - } - - func stop() { - processing?.cancel() - processing = nil - } - - func start(mailboxManager: MailboxManager) { - // Content was processed or is processing - guard !isMessagePreprocessed else { - return - } - - self.mailboxManager = mailboxManager - processing = Task { [weak self] in - guard let message = mailboxManager.transactionExecutor.fetchObject(ofType: Message.self, forPrimaryKey: messageUid)? - .freeze() else { - return - } - - await self?.prepareBody(frozenMessage: message) - - guard !Task.isCancelled else { - return - } - - await self?.insertInlineAttachments(frozenMessage: message) - - guard !Task.isCancelled else { - return - } - - await self?.processingCompleted() - } - } - - private func prepareBody(frozenMessage: Message) async { - guard !Task.isCancelled else { - return - } - guard let updatedPresentableBody = await MessageBodyUtils.prepareWithPrintOption(message: frozenMessage) else { return } - - // Mutate DOM if task is active - guard !Task.isCancelled else { - return - } - await setPresentableBody(updatedPresentableBody) - } - - private func insertInlineAttachments(frozenMessage: Message) async { - guard !Task.isCancelled else { - return - } - - // Since mutation of the DOM is costly, I batch the processing of images, then mutate the DOM. - let attachmentsArray = frozenMessage.attachments.filter { $0.contentId != nil }.toArray() - - guard !attachmentsArray.isEmpty else { - return - } - - // Chunking, and processing each chunk. Opportunity to yield between each batch. - let chunks = attachmentsArray.chunks(ofCount: Constants.inlineAttachmentBatchSize) - for attachments in chunks { - guard !Task.isCancelled else { - return - } - - // Run each batch in a `Task` to get an `autoreleasepool` behaviour - let batchTask = Task { - await processInlineAttachments(attachments) - } - await batchTask.finish() - await Task.yield() - } - } - - private func processInlineAttachments(_ attachments: ArraySlice) async { - guard !Task.isCancelled else { - return - } - - guard let mailboxManager else { - Logger.general.error("processInlineAttachments will fail without a mailboxManager") - return - } - - let base64Images = await bodyImageProcessor.fetchBase64Images(attachments, mailboxManager: mailboxManager) - - guard !Task.isCancelled else { - return - } - - // Read the DOM once - let bodyParameters = await readPresentableBody() - let detachedBody = bodyParameters.detachedBody - - // process compact and base body in parallel - async let mailBody = bodyImageProcessor.injectImagesInBody(body: bodyParameters.bodyString, - attachments: attachments, - base64Images: base64Images) - - async let compactBody = bodyImageProcessor.injectImagesInBody(body: bodyParameters.compactBody, - attachments: attachments, - base64Images: base64Images) - - let bodyValue = await mailBody - let compactBodyCopy = await compactBody - detachedBody?.value = bodyValue - - let updatedPresentableBody = PresentableBody( - body: detachedBody, - compactBody: compactBodyCopy, - quotes: presentableBody.quotes - ) - - // Mutate DOM if task is still active - guard !Task.isCancelled else { - return - } - - await setPresentableBody(updatedPresentableBody) - } - - @MainActor private func setPresentableBody(_ body: PresentableBody) { - presentableBody = body - } - - @MainActor func processingCompleted() { - isMessagePreprocessed = true - } - - typealias BodyParts = (bodyString: String?, compactBody: String?, detachedBody: Body?) - @MainActor private func readPresentableBody() -> BodyParts { - let mailBody = presentableBody.body?.value - let compactBody = presentableBody.compactBody - let detachedBody = presentableBody.body?.detached() - - return (mailBody, compactBody, detachedBody) - } -} - -/// Something to package a base64 encoded image and its mime type -typealias ImageBase64AndMime = (imageEncoded: String, mimeType: String) - -/// Download compress and format images into a mail body -struct BodyImageProcessor { - private let bodyImageMutator = BodyImageMutator() - - /// Download and encode all images for the current chunk in parallel. - public func fetchBase64Images(_ attachments: ArraySlice, - mailboxManager: MailboxManager) async -> [ImageBase64AndMime?] { - // Force a fixed max concurrency to be a nice citizen to the network. - let base64Images: [ImageBase64AndMime?] = await attachments - .concurrentMap(customConcurrency: Constants.concurrentNetworkCalls) { attachment in - do { - let attachmentData = try await mailboxManager.attachmentData(attachment) - - // Skip compression on non static images types or already heic sources - guard attachment.mimeType.contains("jpg") - || attachment.mimeType.contains("jpeg") - || attachment.mimeType.contains("png") else { - let base64String = attachmentData.base64EncodedString() - return ImageBase64AndMime(base64String, attachment.mimeType) - } - - // Skip compression with lockdown mode enables as images can glitch - let isLockdownModeEnabled = (UserDefaults.standard.object(forKey: "LDMGlobalEnabled") as? Bool) ?? false - guard !isLockdownModeEnabled else { - let base64String = attachmentData.base64EncodedString() - return ImageBase64AndMime(base64String, attachment.mimeType) - } - - let compressedImage = compressedBase64ImageAndMime( - attachmentData: attachmentData, - attachmentMime: attachment.mimeType - ) - return compressedImage - - } catch { - Logger.general.error("Error \(error) : Failed to fetch data for attachment: \(attachment)") - return nil - } - } - - assert(base64Images.count == attachments.count, "Arrays count should match") - return base64Images - } - - /// Try to compress the attachment with the best matched algorithm. Trade CPU cycles to reduce render time and memory usage. - private func compressedBase64ImageAndMime(attachmentData: Data, attachmentMime: String) -> ImageBase64AndMime { - guard #available(iOS 17.0, *) else { - let base64String = attachmentData.base64EncodedString() - return ImageBase64AndMime(base64String, attachmentMime) - } - - // On iOS17 Safari _and_ iOS has support for heic. Quality is unchanged. Size is halved. - let image = UIImage(data: attachmentData) - guard let imageCompressed = image?.heicData(), - imageCompressed.count < attachmentData.count else { - let base64String = attachmentData.base64EncodedString() - return ImageBase64AndMime(base64String, attachmentMime) - } - - let base64String = imageCompressed.base64EncodedString() - return ImageBase64AndMime(base64String, "image/heic") - } - - /// Inject base64 images in a body - public func injectImagesInBody(body: String?, - attachments: ArraySlice, - base64Images: [ImageBase64AndMime?]) async -> String? { - guard let body, !body.isEmpty else { - return nil - } - - var workingBody = body - for (index, attachment) in attachments.enumerated() { - guard !Task.isCancelled else { - break - } - - guard let contentId = attachment.contentId, - let base64Image = base64Images[safe: index] as? ImageBase64AndMime else { - continue - } - - bodyImageMutator.replaceContentIdForBase64Image( - in: &workingBody, - contentId: contentId, - mimeType: base64Image.mimeType, - contentBase64Encoded: base64Image.imageEncoded - ) - } - return workingBody - } -} - -/// Something to insert base64 image into a mail body. Easily testable. -struct BodyImageMutator { - func replaceContentIdForBase64Image( - in body: inout String, - contentId: String, - mimeType: String, - contentBase64Encoded: String - ) { - body = body.replacingOccurrences( - of: "cid:\(contentId)", - with: "data:\(mimeType);base64,\(contentBase64Encoded)" - ) - } -} diff --git a/Mail/Views/Thread/Message/MessageBodyView.swift b/Mail/Views/Thread/Message/MessageBody/MessageBodyContentView.swift similarity index 73% rename from Mail/Views/Thread/Message/MessageBodyView.swift rename to Mail/Views/Thread/Message/MessageBody/MessageBodyContentView.swift index 49ea4a5ea..ed3f71bca 100644 --- a/Mail/Views/Thread/Message/MessageBodyView.swift +++ b/Mail/Views/Thread/Message/MessageBody/MessageBodyContentView.swift @@ -20,60 +20,47 @@ import InfomaniakCoreUI import InfomaniakDI import MailCore import MailCoreUI -import MailResources import RealmSwift import SwiftSoup import SwiftUI -struct MessageBodyView: View { +struct MessageBodyContentView: View { @LazyInjectService private var matomo: MatomoUtils @LazyInjectService private var snackbarPresenter: SnackBarPresentable @StateObject private var model = WebViewModel() - let presentableBody: PresentableBody - let isMessagePreprocessed: Bool - var blockRemoteContent: Bool - let messageUid: String - @Binding var displayContentBlockedActionView: Bool + let presentableBody: PresentableBody? + let blockRemoteContent: Bool + let messageUid: String + private let printNotificationPublisher = NotificationCenter.default.publisher(for: Notification.Name.printNotification) var body: some View { ZStack { VStack { - if presentableBody.body != nil { + if let presentableBody, presentableBody.body != nil { WebView(webView: model.webView, messageUid: messageUid) { - loadBody(blockRemoteContent: blockRemoteContent) + loadBody(blockRemoteContent: blockRemoteContent, presentableBody: presentableBody) } .frame(height: model.webViewHeight) .onAppear { - loadBody(blockRemoteContent: blockRemoteContent) - } - .onChange(of: presentableBody) { _ in - loadBody(blockRemoteContent: blockRemoteContent) + loadBody(blockRemoteContent: blockRemoteContent, presentableBody: presentableBody) } - .onChange(of: isMessagePreprocessed) { _ in - loadBody(blockRemoteContent: blockRemoteContent) + .onChange(of: presentableBody) { newValue in + loadBody(blockRemoteContent: blockRemoteContent, presentableBody: newValue) } .onChange(of: model.showBlockQuote) { _ in - loadBody(blockRemoteContent: blockRemoteContent) + loadBody(blockRemoteContent: blockRemoteContent, presentableBody: presentableBody) } .onChange(of: blockRemoteContent) { newValue in - loadBody(blockRemoteContent: newValue) + loadBody(blockRemoteContent: newValue, presentableBody: presentableBody) } if !presentableBody.quotes.isEmpty { - Button(model.showBlockQuote - ? MailResourcesStrings.Localizable.messageHideQuotedText - : MailResourcesStrings.Localizable.messageShowQuotedText) { - model.showBlockQuote.toggle() - } - .buttonStyle(.ikBorderless(isInlined: true)) - .controlSize(.small) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, value: .medium) + ShowBlockquoteButton(showBlockquote: $model.showBlockQuote) } } } @@ -108,7 +95,9 @@ struct MessageBodyView: View { } } - private func loadBody(blockRemoteContent: Bool) { + private func loadBody(blockRemoteContent: Bool, presentableBody: PresentableBody?) { + guard let presentableBody else { return } + Task { let loadResult = try await model.loadBody( presentableBody: presentableBody, @@ -122,11 +111,10 @@ struct MessageBodyView: View { } #Preview { - MessageBodyView( + MessageBodyContentView( + displayContentBlockedActionView: .constant(false), presentableBody: PreviewHelper.samplePresentableBody, - isMessagePreprocessed: true, blockRemoteContent: false, - messageUid: "message_uid", - displayContentBlockedActionView: .constant(false) + messageUid: "message_uid" ) } diff --git a/Mail/Views/Thread/Message/MessageBody/MessageBodyView.swift b/Mail/Views/Thread/Message/MessageBody/MessageBodyView.swift new file mode 100644 index 000000000..eae8c83a8 --- /dev/null +++ b/Mail/Views/Thread/Message/MessageBody/MessageBodyView.swift @@ -0,0 +1,68 @@ +/* + 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 MailCore +import MailCoreUI +import MailResources +import SwiftUI + +struct MessageBodyView: View { + @EnvironmentObject private var messagesWorker: MessagesWorker + + @State private var isShowingLoadingError = false + + @Binding var displayContentBlockedActionView: Bool + + let isRemoteContentBlocked: Bool + let messageUid: String + + var body: some View { + ZStack { + if isShowingLoadingError { + Text(MailResourcesStrings.Localizable.errorLoadingMessage) + .textStyle(.bodySmallItalicSecondary) + .padding(value: .medium) + .frame(maxWidth: .infinity, alignment: .leading) + } else { + MessageBodyContentView( + displayContentBlockedActionView: $displayContentBlockedActionView, + presentableBody: messagesWorker.presentableBodies[messageUid], + blockRemoteContent: isRemoteContentBlocked, + messageUid: messageUid + ) + } + } + .task { + await tryOrDisplayError { + do { + try await messagesWorker.fetchAndProcessIfNeeded(messageUid: messageUid) + } catch is MessagesWorker.WorkerError { + isShowingLoadingError = true + } + } + } + } +} + +#Preview { + MessageBodyView( + displayContentBlockedActionView: .constant(false), + isRemoteContentBlocked: false, + messageUid: PreviewHelper.sampleMessage.uid + ) +} diff --git a/Mail/Views/Thread/Message/MessageBody/ShowBlockquoteButton.swift b/Mail/Views/Thread/Message/MessageBody/ShowBlockquoteButton.swift new file mode 100644 index 000000000..e57f41754 --- /dev/null +++ b/Mail/Views/Thread/Message/MessageBody/ShowBlockquoteButton.swift @@ -0,0 +1,47 @@ +/* + 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 MailCoreUI +import MailResources +import SwiftUI + +struct ShowBlockquoteButton: View { + @Binding var showBlockquote: Bool + + private var label: String { + if showBlockquote { + return MailResourcesStrings.Localizable.messageHideQuotedText + } else { + return MailResourcesStrings.Localizable.messageShowQuotedText + } + } + + var body: some View { + Button(label) { + showBlockquote.toggle() + } + .buttonStyle(.ikBorderless(isInlined: true)) + .controlSize(.small) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(value: .medium) + } +} + +#Preview { + ShowBlockquoteButton(showBlockquote: .constant(true)) +} diff --git a/Mail/Views/Thread/Message/MessageHeader/MessageHeaderSummaryView.swift b/Mail/Views/Thread/Message/MessageHeader/MessageHeaderSummaryView.swift index c0c486d2a..5f10852dc 100644 --- a/Mail/Views/Thread/Message/MessageHeader/MessageHeaderSummaryView.swift +++ b/Mail/Views/Thread/Message/MessageHeader/MessageHeaderSummaryView.swift @@ -154,22 +154,22 @@ struct MessageHeaderSummaryView: View { } } -#Preview("Message collapsed") { - MessageHeaderSummaryView(message: PreviewHelper.sampleMessage, - isMessageExpanded: .constant(false), - isHeaderExpanded: .constant(false)) { - // Preview - } - .environmentObject(PreviewHelper.sampleMailboxManager) - .previewLayout(.sizeThatFits) +@available(iOS 17.0, *) +#Preview("Message collapsed", traits: .sizeThatFitsLayout) { + MessageHeaderSummaryView( + message: PreviewHelper.sampleMessage, + isMessageExpanded: .constant(false), + isHeaderExpanded: .constant(false) + ) {} + .environmentObject(PreviewHelper.sampleMailboxManager) } -#Preview("Message expanded") { - MessageHeaderSummaryView(message: PreviewHelper.sampleMessage, - isMessageExpanded: .constant(true), - isHeaderExpanded: .constant(false)) { - // Preview - } - .environmentObject(PreviewHelper.sampleMailboxManager) - .previewLayout(.sizeThatFits) +@available(iOS 17.0, *) +#Preview("Message expanded", traits: .sizeThatFitsLayout) { + MessageHeaderSummaryView( + message: PreviewHelper.sampleMessage, + isMessageExpanded: .constant(true), + isHeaderExpanded: .constant(false) + ) {} + .environmentObject(PreviewHelper.sampleMailboxManager) } diff --git a/Mail/Views/Thread/Message/MessageView.swift b/Mail/Views/Thread/Message/MessageView.swift index d55080bba..cdd4c11a1 100644 --- a/Mail/Views/Thread/Message/MessageView.swift +++ b/Mail/Views/Thread/Message/MessageView.swift @@ -34,15 +34,9 @@ extension EnvironmentValues { /// Something that can display an email struct MessageView: View { - @LazyInjectService private var snackbarPresenter: SnackBarPresentable - @Environment(\.isMessageInteractive) private var isMessageInteractive - @EnvironmentObject private var mailboxManager: MailboxManager - - @State private var isShowingErrorLoading = false @State private var displayContentBlockedActionView = false - @StateObject private var inlineAttachmentWorker: InlineAttachmentWorker @Binding var threadForcedExpansion: [String: MessageExpansionType] @@ -57,12 +51,6 @@ struct MessageView: View { threadForcedExpansion[message.uid] == .expanded } - init(message: Message, threadForcedExpansion: Binding<[String: MessageExpansionType]>) { - self.message = message - _threadForcedExpansion = threadForcedExpansion - _inlineAttachmentWorker = StateObject(wrappedValue: InlineAttachmentWorker(messageUid: message.uid)) - } - var body: some View { VStack(spacing: 0) { MessageHeaderView( @@ -83,41 +71,14 @@ struct MessageView: View { ) } - if isShowingErrorLoading { - Text(MailResourcesStrings.Localizable.errorLoadingMessage) - .textStyle(.bodySmallItalicSecondary) - .padding(.horizontal, value: .medium) - .frame(maxWidth: .infinity, alignment: .leading) - } else { - MessageBodyView( - presentableBody: inlineAttachmentWorker.presentableBody, - isMessagePreprocessed: inlineAttachmentWorker.isMessagePreprocessed, - blockRemoteContent: isRemoteContentBlocked, - messageUid: message.uid, - displayContentBlockedActionView: $displayContentBlockedActionView - ) - } + MessageBodyView( + displayContentBlockedActionView: $displayContentBlockedActionView, + isRemoteContentBlocked: isRemoteContentBlocked, + messageUid: message.uid + ) } } } - .onAppear { - prepareBodyIfNeeded() - } - .task { - await fetchMessageAndEventCalendar() - } - .task(id: isMessageExpanded) { - await fetchMessageAndEventCalendar() - } - .onDisappear { - inlineAttachmentWorker.stop() - } - .onChange(of: message.fullyDownloaded) { _ in - prepareBodyIfNeeded() - } - .onChange(of: isMessageExpanded) { _ in - prepareBodyIfNeeded() - } .accessibilityAction(named: MailResourcesStrings.Localizable.expandMessage) { guard isMessageInteractive else { return } withAnimation { @@ -125,65 +86,20 @@ struct MessageView: View { } } } - - private func fetchMessageAndEventCalendar() async { - guard isMessageExpanded else { return } - - async let fetchMessageResult: Void = fetchMessage() - - async let fetchEventCalendar: Void = fetchEventCalendar() - - await fetchMessageResult - await fetchEventCalendar - } - - private func fetchMessage() async { - guard message.shouldComplete else { return } - - await tryOrDisplayError { - do { - try await mailboxManager.message(message: message) - } catch let error as MailApiError where error == .apiMessageNotFound { - snackbarPresenter.show(message: error.errorDescription ?? "") - try await mailboxManager.refreshFolder(from: [message], additionalFolder: nil) - } catch let error as AFErrorWithContext where error.afError.isExplicitlyCancelledError { - isShowingErrorLoading = false - } catch { - isShowingErrorLoading = true - } - } - } - - private func fetchEventCalendar() async { - try? await mailboxManager.calendarEvent(from: message.uid) - } -} - -/// MessageView code related to pre-processing -extension MessageView { - func prepareBodyIfNeeded() { - guard message.fullyDownloaded, isMessageExpanded else { - return - } - - inlineAttachmentWorker.start(mailboxManager: mailboxManager) - } } -#Preview("Message collapsed") { +@available(iOS 17.0, *) +#Preview("Message collapsed", traits: .sizeThatFitsLayout) { MessageView( - message: PreviewHelper.sampleMessage, - threadForcedExpansion: .constant([PreviewHelper.sampleMessage.uid: .collapsed]) + threadForcedExpansion: .constant([PreviewHelper.sampleMessage.uid: .collapsed]), + message: PreviewHelper.sampleMessage ) - .environmentObject(PreviewHelper.sampleMailboxManager) - .previewLayout(.sizeThatFits) } -#Preview("Message expanded") { +@available(iOS 17.0, *) +#Preview("Message expanded", traits: .sizeThatFitsLayout) { MessageView( - message: PreviewHelper.sampleMessage, - threadForcedExpansion: .constant([PreviewHelper.sampleMessage.uid: .expanded]) + threadForcedExpansion: .constant([PreviewHelper.sampleMessage.uid: .expanded]), + message: PreviewHelper.sampleMessage ) - .environmentObject(PreviewHelper.sampleMailboxManager) - .previewLayout(.sizeThatFits) } diff --git a/Mail/Views/Thread/MessageListView.swift b/Mail/Views/Thread/MessageListView.swift index a2fa99f91..483334577 100644 --- a/Mail/Views/Thread/MessageListView.swift +++ b/Mail/Views/Thread/MessageListView.swift @@ -28,6 +28,7 @@ enum MessageExpansionType: Equatable { } struct MessageListView: View { + @StateObject private var messagesWorker = MessagesWorker() @State private var messageExpansion = [String: MessageExpansionType]() let messages: [Message] @@ -43,10 +44,7 @@ struct MessageListView: View { } } else if messageExpansion[message.uid] != .superCollapsed { VStack(spacing: 0) { - MessageView( - message: message, - threadForcedExpansion: $messageExpansion - ) + MessageView(threadForcedExpansion: $messageExpansion, message: message) if divider(for: message) { IKDivider(type: .full) } @@ -58,8 +56,7 @@ struct MessageListView: View { .onAppear { computeExpansion(from: messages) - guard messages.count > 1, - let firstExpandedUid = firstExpanded()?.uid else { + guard messages.count > 1, let firstExpandedUid = firstExpanded()?.uid else { return } @@ -70,6 +67,7 @@ struct MessageListView: View { } } } + .environmentObject(messagesWorker) .id(messages.id) } } diff --git a/Mail/Views/Thread/MessagesWorker.swift b/Mail/Views/Thread/MessagesWorker.swift new file mode 100644 index 000000000..9e501274f --- /dev/null +++ b/Mail/Views/Thread/MessagesWorker.swift @@ -0,0 +1,199 @@ +/* + 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 Foundation +import InfomaniakDI +import MailCore + +typealias ImageBase64AndMime = (imageEncoded: String, mimeType: String) + +extension MessagesWorker { + enum WorkerError: Error { + case cantFetchMessage + } +} + +@MainActor +final class MessagesWorker: ObservableObject { + @LazyInjectService private var accountManager: AccountManager + @LazyInjectService private var snackbarPresenter: SnackBarPresentable + + @Published var presentableBodies = [String: PresentableBody]() + + private var replacedAllAttachments = [String: Bool]() + private let bodyImageProcessor = BodyImageProcessor() + + func fetchAndProcessIfNeeded(messageUid: String) async throws { + guard let mailboxManager = accountManager.currentMailboxManager else { + return + } + + try await fetchMessageAndCalendar(of: messageUid, with: mailboxManager) + guard !Task.isCancelled else { return } + await prepareBodyAndAttachments(of: messageUid, with: mailboxManager) + } +} + +// MARK: - Fetch Message and Calendar Event + +extension MessagesWorker { + private func fetchMessageAndCalendar(of messageUid: String, with mailboxManager: MailboxManager) async throws { + guard let message = getFrozenMessage(uid: messageUid, with: mailboxManager) else { + return + } + + async let fetchMessageResult: Void = fetchMessage(of: message, with: mailboxManager) + async let fetchEventCalendar: Void = fetchEventCalendar(of: message, with: mailboxManager) + + try await fetchMessageResult + await fetchEventCalendar + } + + private func fetchMessage(of message: Message, with mailboxManager: MailboxManager) async throws { + guard message.shouldComplete else { + return + } + + do { + try await mailboxManager.message(message: message) + } catch let error as MailApiError where error == .apiMessageNotFound { + snackbarPresenter.show(message: error.errorDescription ?? "") + try? await mailboxManager.refreshFolder(from: [message], additionalFolder: nil) + } catch { + throw WorkerError.cantFetchMessage + } + } + + private func fetchEventCalendar(of message: Message, with mailboxManager: MailboxManager) async { + try? await mailboxManager.calendarEvent(from: message.uid) + } +} + +// MARK: - Prepare body + +extension MessagesWorker { + private func prepareBodyAndAttachments(of messageUid: String, with mailboxManager: MailboxManager) async { + guard let message = getFrozenMessage(uid: messageUid, with: mailboxManager) else { + return + } + + await prepareBody(of: message) + guard !Task.isCancelled else { return } + await insertInlineAttachments(for: message, with: mailboxManager) + } + + private func prepareBody(of message: Message) async { + guard !Task.isCancelled else { return } + + guard !hasPresentableBody(messageUid: message.uid), + let updatedPresentableBody = await MessageBodyUtils.prepareWithPrintOption(message: message) else { + return + } + + setPresentableBody(updatedPresentableBody, for: message) + } +} + +// MARK: - Inline attachments + +extension MessagesWorker { + private func insertInlineAttachments(for frozenMessage: Message, with mailboxManager: MailboxManager) async { + guard !Task.isCancelled else { return } + + guard !hasPresentableBodyWithAllAttachments(messageUid: frozenMessage.uid) else { + return + } + + let attachmentsArray = frozenMessage.attachments.filter { $0.contentId != nil }.toArray() + guard !attachmentsArray.isEmpty else { + return + } + + let chunks = attachmentsArray.chunks(ofCount: Constants.inlineAttachmentBatchSize) + for attachments in chunks { + guard !Task.isCancelled else { return } + let batchTask = Task { + await processInlineAttachments(attachments, for: frozenMessage, with: mailboxManager) + } + await batchTask.finish() + } + + setReplacedAllAttachments(for: frozenMessage) + } + + private func processInlineAttachments( + _ attachments: ArraySlice, + for frozenMessage: Message, + with mailboxManager: MailboxManager + ) async { + guard !Task.isCancelled else { return } + + guard let presentableBody = presentableBodies[frozenMessage.uid] else { return } + + let base64Images = await bodyImageProcessor.fetchBase64Images(attachments, mailboxManager: mailboxManager) + + async let mailBody = bodyImageProcessor.injectImagesInBody( + body: presentableBody.body?.value, + attachments: attachments, + base64Images: base64Images + ) + async let compactBody = bodyImageProcessor.injectImagesInBody( + body: presentableBody.compactBody, + attachments: attachments, + base64Images: base64Images + ) + + let bodyValue = await mailBody + let compactBodyCopy = await compactBody + + let body = presentableBody.body?.detached() + body?.value = bodyValue + + let updatedPresentableBody = PresentableBody( + body: body, + compactBody: compactBodyCopy, + quotes: presentableBody.quotes + ) + + setPresentableBody(updatedPresentableBody, for: frozenMessage) + } +} + +// MARK: - Utils + +extension MessagesWorker { + private func getFrozenMessage(uid: String, with mailboxManager: MailboxManager) -> Message? { + return mailboxManager.transactionExecutor.fetchObject(ofType: Message.self, forPrimaryKey: uid)?.freeze() + } + + private func setPresentableBody(_ presentableBody: PresentableBody, for message: Message) { + presentableBodies[message.uid] = presentableBody + } + + private func hasPresentableBody(messageUid: String) -> Bool { + return presentableBodies[messageUid] != nil + } + + private func setReplacedAllAttachments(for message: Message) { + replacedAllAttachments[message.uid] = true + } + + private func hasPresentableBodyWithAllAttachments(messageUid: String) -> Bool { + return replacedAllAttachments[messageUid, default: false] + } +} diff --git a/Mail/Views/Thread/SuperCollapsedView.swift b/Mail/Views/Thread/SuperCollapsedView.swift index 978f7e4e1..996b5fb95 100644 --- a/Mail/Views/Thread/SuperCollapsedView.swift +++ b/Mail/Views/Thread/SuperCollapsedView.swift @@ -49,5 +49,5 @@ struct SuperCollapsedView: View { } #Preview { - SuperCollapsedView(count: 5, action: {}) + SuperCollapsedView(count: 5) {} } diff --git a/Mail/Views/Thread/ThreadView.swift b/Mail/Views/Thread/ThreadView.swift index 9f9a217ee..9ebb29e5b 100644 --- a/Mail/Views/Thread/ThreadView.swift +++ b/Mail/Views/Thread/ThreadView.swift @@ -67,12 +67,6 @@ struct ThreadView: View { .task { await markThreadAsReadIfNeeded(thread: thread) } - .onChange(of: thread) { newValue in - guard newValue.uid != thread.uid else { return } - Task { - await markThreadAsReadIfNeeded(thread: newValue) - } - } .navigationTitle(displayNavigationTitle ? thread.formattedSubject : "") .navigationBarThreadViewStyle(appearance: displayNavigationTitle ? BarAppearanceConstants .threadViewNavigationBarScrolledAppearance : BarAppearanceConstants.threadViewNavigationBarAppearance) @@ -82,6 +76,7 @@ struct ThreadView: View { frozenFolder: thread.folder?.freezeIfNeeded(), frozenMessages: thread.messages.freezeIfNeeded().toArray() ) + .id(thread.id) .matomoView(view: [MatomoUtils.View.threadView.displayName, "Main"]) } diff --git a/MailCore/API/MailApiFetcher/MailApiFetcher.swift b/MailCore/API/MailApiFetcher/MailApiFetcher.swift index ca3f5fedf..6747945e1 100644 --- a/MailCore/API/MailApiFetcher/MailApiFetcher.swift +++ b/MailCore/API/MailApiFetcher/MailApiFetcher.swift @@ -59,9 +59,8 @@ public final class MailApiFetcher: ApiFetcher, MailApiFetchable { if case .responseSerializationFailed(let reason) = afError, case .decodingFailed(let error) = reason, let statusCode = request.response?.statusCode, (200 ... 299).contains(statusCode) { var rawJson = "No data" - if let data = request.data, - let stringData = String(data: data, encoding: .utf8) { - rawJson = stringData + if let data = request.data { + rawJson = String(decoding: data, as: UTF8.self) } SentrySDK.capture(error: error) { scope in diff --git a/MailCore/API/MailError.swift b/MailCore/API/MailError.swift index cab92e810..f92e97417 100644 --- a/MailCore/API/MailError.swift +++ b/MailCore/API/MailError.swift @@ -21,7 +21,7 @@ import Foundation import InfomaniakCore import MailResources -extension ApiError: CustomStringConvertible {} +extension ApiError: @retroactive CustomStringConvertible {} public class AFErrorWithContext: MailError, CustomStringConvertible { public let request: DataRequest diff --git a/MailCore/Cache/Actions/ActionsManager.swift b/MailCore/Cache/Actions/ActionsManager.swift index ed6d00b8b..a3c8cc0a7 100644 --- a/MailCore/Cache/Actions/ActionsManager.swift +++ b/MailCore/Cache/Actions/ActionsManager.swift @@ -24,7 +24,7 @@ import MailResources import Sentry import SwiftUI -extension [Message]: Identifiable { +extension [Message]: @retroactive Identifiable { public var id: Int { var hasher = Hasher() forEach { hasher.combine($0.hashValue) } diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index 61242e8cd..60d322234 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -40,7 +40,7 @@ actor DraftQueue { func beginBackgroundTask(withName name: String, for uuid: String) async { let identifier = await UIApplication.shared.beginBackgroundTask(withName: name) { [self] in Task { - endBackgroundTask(uuid: uuid) + await endBackgroundTask(uuid: uuid) } } identifierQueue[uuid] = identifier diff --git a/MailCore/Cache/MailboxManager/MailboxManageable.swift b/MailCore/Cache/MailboxManager/MailboxManageable.swift index 399910670..85580230d 100644 --- a/MailCore/Cache/MailboxManager/MailboxManageable.swift +++ b/MailCore/Cache/MailboxManager/MailboxManageable.swift @@ -107,4 +107,4 @@ public protocol MailboxManagerAttachable { func swissTransferAttachment(message: Message) async throws } -// TODO: write a dedicated protocol for each MailboxManager+<> +// TODO: write a dedicated protocol for each MailboxManager diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift b/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift index a8919b6d1..362b69806 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift @@ -87,7 +87,7 @@ public extension MailboxManager { } func createFolder(name: String, parent: Folder?) async throws -> Folder { - var folder = try await apiFetcher.create(mailbox: mailbox, folder: NewFolder(name: name, path: parent?.path)) + let folder = try await apiFetcher.create(mailbox: mailbox, folder: NewFolder(name: name, path: parent?.path)) try writeTransaction { writableRealm in writableRealm.add(folder) if let parent { diff --git a/MailCore/Models/Body.swift b/MailCore/Models/Body.swift index 5fdeb44cc..2eda1e0bd 100644 --- a/MailCore/Models/Body.swift +++ b/MailCore/Models/Body.swift @@ -106,10 +106,6 @@ final class ProxyBody: Codable { compactBody = nil quotes = [] } - - public init(presentableBody: PresentableBody) { - self.init(body: presentableBody.body, compactBody: presentableBody.compactBody, quotes: presentableBody.quotes) - } } // MARK: - SubBody diff --git a/MailCore/Utils/ContentRuleGenerator.swift b/MailCore/Utils/ContentRuleGenerator.swift index e67773c44..543070d15 100644 --- a/MailCore/Utils/ContentRuleGenerator.swift +++ b/MailCore/Utils/ContentRuleGenerator.swift @@ -58,6 +58,6 @@ public enum ContentRuleGenerator { public static func generateContentRulesJSON(rules: [ContentRule]) -> String? { guard let encodedData = try? jsonEncoder.encode(rules) else { return nil } - return String(data: encodedData, encoding: .utf8) + return String(decoding: encodedData, as: UTF8.self) } } diff --git a/MailCore/Utils/LocalContactsHelper.swift b/MailCore/Utils/LocalContactsHelper.swift index d25c1c81b..a908378a8 100644 --- a/MailCore/Utils/LocalContactsHelper.swift +++ b/MailCore/Utils/LocalContactsHelper.swift @@ -74,7 +74,7 @@ public final class LocalContactsHelper: LocalContactsHelpable { private func checkAuthorization() async throws { switch CNContactStore.authorizationStatus(for: .contacts) { - case .authorized: + case .authorized, .limited: // All ok return case .restricted, .denied: diff --git a/MailCore/Utils/MessageBodyUtils.swift b/MailCore/Utils/MessageBodyUtils.swift index 10fe89dbb..fd50a2208 100644 --- a/MailCore/Utils/MessageBodyUtils.swift +++ b/MailCore/Utils/MessageBodyUtils.swift @@ -18,7 +18,6 @@ import CocoaLumberjackSwift import Foundation -import MailCore import MailResources import SwiftSoup diff --git a/MailCoreUI/Components/InformationBlockView.swift b/MailCoreUI/Components/InformationBlockView.swift index e24a107f1..8cb2e026a 100644 --- a/MailCoreUI/Components/InformationBlockView.swift +++ b/MailCoreUI/Components/InformationBlockView.swift @@ -116,9 +116,9 @@ public struct InformationBlockView: View { message: "Tip", iconColor: .blue, buttonAction: {}, - buttonTitle: "Button title" - ) { - /* Preview */ } + buttonTitle: "Button title", + dismissHandler: {} // swiftlint:disable:this trailing_closure + ) } #Preview("Without Title") { diff --git a/MailCoreUI/Extensions/Environment+Extension.swift b/MailCoreUI/Extensions/Environment+Extension.swift index 3f96f4d73..123889ca3 100644 --- a/MailCoreUI/Extensions/Environment+Extension.swift +++ b/MailCoreUI/Extensions/Environment+Extension.swift @@ -21,6 +21,6 @@ import SwiftUI import SwiftUIMacros public extension EnvironmentValues { - @EnvironmentValue + @EnvironmentKey var isCompactWindow = true } diff --git a/MailCoreUI/ViewModifier/SheetViewModifier.swift b/MailCoreUI/ViewModifier/SheetViewModifier.swift index b64597bc9..902dcf7da 100644 --- a/MailCoreUI/ViewModifier/SheetViewModifier.swift +++ b/MailCoreUI/ViewModifier/SheetViewModifier.swift @@ -24,7 +24,7 @@ import SwiftUIMacros public typealias DismissModalAction = () -> Void public extension EnvironmentValues { - @EnvironmentValue + @EnvironmentKey var dismissModal: DismissModalAction = { /* dismiss nothing by default */ } } diff --git a/MailNotificationContentExtension/NotificationViewController.swift b/MailNotificationContentExtension/NotificationViewController.swift index ffb8fb966..aada4dcc8 100644 --- a/MailNotificationContentExtension/NotificationViewController.swift +++ b/MailNotificationContentExtension/NotificationViewController.swift @@ -89,12 +89,9 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi } let messageView = ScrollView { - MessageView( - message: message, - threadForcedExpansion: .constant([messageUid: .expanded]) - ) - .environment(\.isMessageInteractive, false) - .environmentObject(mailboxManager) + MessageView(threadForcedExpansion: .constant([messageUid: .expanded]), message: message) + .environment(\.isMessageInteractive, false) + .environmentObject(mailboxManager) } let hostingViewController = UIHostingController(rootView: messageView) diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 434b80f22..7f2ee31a7 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -28,6 +28,7 @@ import MobileCoreServices import Social import SwiftUI import UIKit +import UniformTypeIdentifiers import VersionChecker struct ComposeMessageWrapperView: View { @@ -39,7 +40,7 @@ struct ComposeMessageWrapperView: View { let itemProviders: [NSItemProvider] let dismissHandler: SimpleClosure - static let typePropertyList = String(kUTTypePropertyList) + static let typePropertyList = UTType.propertyList.identifier /// All the Attachments that should directly provide URLs and title for a new Email Draft var htmlAttachments: [HTMLAttachable] { diff --git a/MailTests/Contacts/UTContactManager.swift b/MailTests/Contacts/UTContactManager.swift index 369373c2e..157bd0b54 100644 --- a/MailTests/Contacts/UTContactManager.swift +++ b/MailTests/Contacts/UTContactManager.swift @@ -22,7 +22,7 @@ import XCTest final class UTContactManager: XCTestCase { let contactManager = ContactManager(userId: 0, apiFetcher: MailApiFetcher()) - override class func setUp() { + override static func setUp() { super.setUp() MockingHelper.clearRegisteredTypes() diff --git a/MailTests/Folders/ArrayShapeCompare.swift b/MailTests/Folders/ArrayShapeCompare.swift index 943bffa05..8637eebd8 100644 --- a/MailTests/Folders/ArrayShapeCompare.swift +++ b/MailTests/Folders/ArrayShapeCompare.swift @@ -56,7 +56,7 @@ public struct ArrayShapeCompare { /// Testing that the tool `ArrayShapeCompare` is correct final class UTArrayShapeCompare: XCTestCase { - override class func setUp() { + override static func setUp() { super.setUp() MockingHelper.clearRegisteredTypes() diff --git a/MailTests/Folders/FolderStructureGenerator.swift b/MailTests/Folders/FolderStructureGenerator.swift index a6933ff56..6d1290d55 100644 --- a/MailTests/Folders/FolderStructureGenerator.swift +++ b/MailTests/Folders/FolderStructureGenerator.swift @@ -17,6 +17,7 @@ */ // swiftlint:disable all +// swift-format-ignore-file // swiftformat:disable all @@ -219,3 +220,6 @@ final class InMemoryRealmAccessor: RealmAccessible { inMemoryRealm } } + +// swiftlint:enable all +// swiftformat:enable all diff --git a/MailTests/Folders/ITFolderListViewModel.swift b/MailTests/Folders/ITFolderListViewModel.swift index 06c2ce1b8..34dff7107 100644 --- a/MailTests/Folders/ITFolderListViewModel.swift +++ b/MailTests/Folders/ITFolderListViewModel.swift @@ -51,7 +51,7 @@ struct MCKContactManageable_FolderListViewModel: ContactManageable, MCKTransacti } /// A MailboxManageable used to test the FolderListViewModel -struct MCKMailboxManageable_FolderListViewModel: MailboxManageable, MCKTransactionablePassthrough /* , RealmAccessible */ { +struct MCKMailboxManageable_FolderListViewModel: MailboxManageable, MCKTransactionablePassthrough { let mailbox = Mailbox() var contactManager: MailCore.ContactManageable { @@ -207,7 +207,7 @@ struct MCKMailboxManageable_FolderListViewModel: MailboxManageable, MCKTransacti /// Integration tests of the FolderListViewModel final class ITFolderListViewModel: XCTestCase { - override class func setUp() { + override static func setUp() { super.setUp() MockingHelper.clearRegisteredTypes() @@ -261,7 +261,7 @@ final class ITFolderListViewModel: XCTestCase { /// Integration tests of the FolderListViewModelWorker final class ITFolderListViewModelWorker: XCTestCase { - override class func setUp() { + override static func setUp() { super.setUp() MockingHelper.clearRegisteredTypes() diff --git a/MailTests/Folders/ITNestableFolder.swift b/MailTests/Folders/ITNestableFolder.swift index 6f14aebcb..1899cfa57 100644 --- a/MailTests/Folders/ITNestableFolder.swift +++ b/MailTests/Folders/ITNestableFolder.swift @@ -27,7 +27,7 @@ final class ITNestableFolder: XCTestCase { var maxDepth = 0 var maxElementsPerLevel = 0 - override class func setUp() { + override static func setUp() { super.setUp() MockingHelper.clearRegisteredTypes() @@ -36,7 +36,7 @@ final class ITNestableFolder: XCTestCase { // MARK: - TestSuite - override class var defaultTestSuite: XCTestSuite { + override static var defaultTestSuite: XCTestSuite { let testSuite = XCTestSuite(name: NSStringFromClass(self)) // Wide not deep @@ -56,7 +56,7 @@ final class ITNestableFolder: XCTestCase { return testSuite } - class func addNewTest(maxDepth: Int, maxElementsPerLevel: Int, testSuite: XCTestSuite) { + static func addNewTest(maxDepth: Int, maxElementsPerLevel: Int, testSuite: XCTestSuite) { for invocation in testInvocations { let newTestCase = ITNestableFolder(invocation: invocation) newTestCase.maxDepth = maxDepth diff --git a/MailTests/MailApiTests.swift b/MailTests/MailApiTests.swift index bcfde10ce..ec4489496 100644 --- a/MailTests/MailApiTests.swift +++ b/MailTests/MailApiTests.swift @@ -23,7 +23,7 @@ import InfomaniakLogin import XCTest final class MailApiTests: XCTestCase { - override class func setUp() { + override static func setUp() { super.setUp() MockingHelper.clearRegisteredTypes() diff --git a/MailTests/MailBodyImageMutator.swift b/MailTests/MailBodyImageMutator.swift index c51e00c20..724bbcd35 100644 --- a/MailTests/MailBodyImageMutator.swift +++ b/MailTests/MailBodyImageMutator.swift @@ -22,7 +22,7 @@ import MailResources import XCTest final class MailBodyImageMutator: XCTestCase { - override class func setUp() { + override static func setUp() { super.setUp() MockingHelper.clearRegisteredTypes() diff --git a/MailTests/MailboxManagerTests.swift b/MailTests/MailboxManagerTests.swift index c446d4800..b0ecf3cf9 100644 --- a/MailTests/MailboxManagerTests.swift +++ b/MailTests/MailboxManagerTests.swift @@ -26,7 +26,7 @@ import XCTest final class MailboxManagerTests: XCTestCase { static var mailboxManager: MailboxManager! - override class func setUp() { + override static func setUp() { super.setUp() MockingHelper.clearRegisteredTypes() diff --git a/MailTests/Search/ITSearchViewModel.swift b/MailTests/Search/ITSearchViewModel.swift index 2549ff917..a2ca59087 100644 --- a/MailTests/Search/ITSearchViewModel.swift +++ b/MailTests/Search/ITSearchViewModel.swift @@ -242,14 +242,14 @@ final class MCKMailboxManageable_SearchViewModel: MailboxManageable, MCKTransact // MARK: - ITSearchViewModel final class ITSearchViewModel: XCTestCase { - override class func setUp() { + override static func setUp() { super.setUp() MockingHelper.clearRegisteredTypes() MockingHelper.registerConcreteTypes(configuration: .minimal) } - /// Tests removed temporarly + /// Tests removed temporarily /// Uncomment when all MailboxManager are MailboxManageable // @MainActor func testInit() { diff --git a/MailTests/SignatureTests.swift b/MailTests/SignatureTests.swift index ae46a0c88..ef477372b 100644 --- a/MailTests/SignatureTests.swift +++ b/MailTests/SignatureTests.swift @@ -50,7 +50,7 @@ final class SignatureTests: XCTestCase {
Test 

    private var accountManager: AccountManager!


k

infomaniak

Coye de Brunélis Adrien -  Developer


Rue Eugène-Marziano 25, 1227 Genève
Swiss Made | ISO 27001 14001 50001 9001



""" - override class func setUp() { + override static func setUp() { super.setUp() MockingHelper.clearRegisteredTypes() diff --git a/Tuist/Package.swift b/Tuist/Package.swift index bbcc49b4b..a73c9882c 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -8,7 +8,7 @@ import ProjectDescriptionHelpers let packageSettings = PackageSettings( productTypes: [ "RealmSwift": .staticLibrary, - "Realm": .staticLibrary, + "Realm": .staticLibrary ] ) @@ -46,6 +46,6 @@ let package = Package( .package(url: "https://github.com/httpswift/swifter", .upToNextMajor(from: "1.5.0")), .package(url: "https://github.com/Wouter01/SwiftUI-Macros.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/SVGKit/SVGKit.git", branch: "3.x"), - .package(url: "https://github.com/Infomaniak/swift-rich-html-editor.git", .upToNextMajor(from: "1.1.1")), + .package(url: "https://github.com/Infomaniak/swift-rich-html-editor.git", .upToNextMajor(from: "1.1.1")) ] ) diff --git a/Tuist/ProjectDescriptionHelpers/Constants.swift b/Tuist/ProjectDescriptionHelpers/Constants.swift index a3cf6bb08..682f8aff7 100644 --- a/Tuist/ProjectDescriptionHelpers/Constants.swift +++ b/Tuist/ProjectDescriptionHelpers/Constants.swift @@ -36,7 +36,7 @@ public enum Constants { public static let destinations = Set([.iPhone, .iPad, .macCatalyst]) public static let swiftlintScript = TargetScript.post(path: "scripts/lint.sh", name: "Swiftlint") - + public static let stripSymbolsScript = TargetScript.post( path: "scripts/strip_symbols.sh", name: "Strip Symbols (Release)",