From e5848f45d8542fed66bd4f45d204f2ec940851e3 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Fri, 24 Nov 2023 11:50:35 +0100 Subject: [PATCH 1/4] feat: Refactor SwiftSoupUtils --- Mail/Views/AI Writer/AIModel.swift | 2 +- .../WebView/WebViewModel+SwiftSoup.swift | 4 +-- MailCore/Cache/DraftContentManager.swift | 2 +- MailCore/Models/Draft.swift | 2 +- MailCore/Utils/MessageWebViewUtils.swift | 19 ----------- MailCore/Utils/SwiftSoupUtils.swift | 33 +++++++++++++++---- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/Mail/Views/AI Writer/AIModel.swift b/Mail/Views/AI Writer/AIModel.swift index ee077e480..51aaf7ade 100644 --- a/Mail/Views/AI Writer/AIModel.swift +++ b/Mail/Views/AI Writer/AIModel.swift @@ -151,7 +151,7 @@ extension AIModel { replyingString = replyingBody.value } else if let value = replyingBody.value { let splitReply = await MessageBodyUtils.splitBodyAndQuote(messageBody: value) - replyingString = try await SwiftSoupUtils.extractText(from: splitReply.messageBody) + replyingString = try await SwiftSoupUtils(from: splitReply.messageBody).extractText() } guard let replyingString else { return } diff --git a/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift b/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift index 090c893c0..7df5c197c 100644 --- a/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift +++ b/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift @@ -33,10 +33,10 @@ extension WebViewModel { } func loadHTMLString(value: String?, blockRemoteContent: Bool) async -> LoadResult { - guard let rawHtml = value else { return .errorEmptyInputValue } + guard let rawHTML = value else { return .errorEmptyInputValue } do { - guard let safeDocument = MessageWebViewUtils.cleanHTMLContent(rawHTML: rawHtml) + guard let safeDocument = try? SwiftSoupUtils(from: rawHTML).cleanDocument() else { return .errorCleanHTMLContent } try updateHeadContent(of: safeDocument) diff --git a/MailCore/Cache/DraftContentManager.swift b/MailCore/Cache/DraftContentManager.swift index 930dcb77f..64fb147b7 100644 --- a/MailCore/Cache/DraftContentManager.swift +++ b/MailCore/Cache/DraftContentManager.swift @@ -143,7 +143,7 @@ public class DraftContentManager: ObservableObject { var extractedElements = "" for itemToExtract in Draft.appendedHTMLElements { - if let element = try? SwiftSoupUtils.extractHTML(from: parsedMessage, ".\(itemToExtract)") { + if let element = try? SwiftSoupUtils(document: parsedMessage).extractHTML(".\(itemToExtract)") { extractedElements.append(element) } } diff --git a/MailCore/Models/Draft.swift b/MailCore/Models/Draft.swift index 4eba2ed32..61ac620b6 100644 --- a/MailCore/Models/Draft.swift +++ b/MailCore/Models/Draft.swift @@ -234,7 +234,7 @@ public final class Draft: Object, Codable, Identifiable { unsafeQuote = Constants.forwardQuote(message: message) } - let quote = (try? MessageWebViewUtils.cleanHTMLContent(rawHTML: unsafeQuote)?.outerHtml()) ?? "" + let quote = (try? SwiftSoupUtils(from: unsafeQuote).cleanDocument().outerHtml()) ?? "" return "

" + quote } diff --git a/MailCore/Utils/MessageWebViewUtils.swift b/MailCore/Utils/MessageWebViewUtils.swift index 79bf7909f..32031cce7 100644 --- a/MailCore/Utils/MessageWebViewUtils.swift +++ b/MailCore/Utils/MessageWebViewUtils.swift @@ -47,23 +47,4 @@ public enum MessageWebViewUtils { return resources } - - public static func cleanHTMLContent(rawHTML: String) -> Document? { - do { - let dirtyDocument = try SwiftSoup.parse(rawHTML) - let cleanedDocument = try SwiftSoup.Cleaner(headWhitelist: .headWhitelist, bodyWhitelist: .extendedBodyWhitelist) - .clean(dirtyDocument) - - // We need to remove the tag - let metaRefreshTags = try cleanedDocument.select("meta[http-equiv='refresh']") - for metaRefreshTag in metaRefreshTags { - try metaRefreshTag.parent()?.removeChild(metaRefreshTag) - } - - return cleanedDocument - } catch { - DDLogError("An error occurred while parsing body \(error)") - return nil - } - } } diff --git a/MailCore/Utils/SwiftSoupUtils.swift b/MailCore/Utils/SwiftSoupUtils.swift index cb3249ea4..d26cf2a37 100644 --- a/MailCore/Utils/SwiftSoupUtils.swift +++ b/MailCore/Utils/SwiftSoupUtils.swift @@ -18,17 +18,38 @@ import SwiftSoup -public enum SwiftSoupUtils { - public static func extractHTML(from document: Document, _ cssQuery: String) throws -> String { - guard let foundElement = try document.select(cssQuery).first() else { - throw SwiftSoupError.elementNotFound +public struct SwiftSoupUtils { + private let document: Document + + public init(document: Document) { + self.document = document + } + + public init(from html: String) throws { + document = try SwiftSoup.parse(html) + } + + public func cleanDocument() throws -> Document { + let cleanedDocument = try SwiftSoup.Cleaner(headWhitelist: .headWhitelist, bodyWhitelist: .extendedBodyWhitelist) + .clean(document) + + // We need to remove the tag + let metaRefreshTags = try cleanedDocument.select("meta[http-equiv='refresh']") + for metaRefreshTag in metaRefreshTags { + try metaRefreshTag.parent()?.removeChild(metaRefreshTag) } + + return cleanedDocument + } + + public func extractHTML(_ cssQuery: String) throws -> String { + guard let foundElement = try document.select(cssQuery).first() else { throw SwiftSoupError.elementNotFound } + let htmlContent = try foundElement.outerHtml() return htmlContent } - public static func extractText(from html: String) async throws -> String? { - let document = try await SwiftSoup.parse(html) + public func extractText() async throws -> String? { return try document.body()?.text() } } From 08b8ac0ee26ebc0693c60ca03f711da51173cc00 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Fri, 24 Nov 2023 11:59:36 +0100 Subject: [PATCH 2/4] feat(NotificationsHelper): Only extract text from body --- Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift | 2 +- MailCore/Models/Draft.swift | 2 +- MailCore/Utils/NotificationsHelper.swift | 6 +++--- MailCore/Utils/SwiftSoupUtils.swift | 7 ++++++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift b/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift index 7df5c197c..279c33fe2 100644 --- a/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift +++ b/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift @@ -36,7 +36,7 @@ extension WebViewModel { guard let rawHTML = value else { return .errorEmptyInputValue } do { - guard let safeDocument = try? SwiftSoupUtils(from: rawHTML).cleanDocument() + guard let safeDocument = try? SwiftSoupUtils(from: rawHTML).cleanCompleteDocument() else { return .errorCleanHTMLContent } try updateHeadContent(of: safeDocument) diff --git a/MailCore/Models/Draft.swift b/MailCore/Models/Draft.swift index 61ac620b6..e360511c2 100644 --- a/MailCore/Models/Draft.swift +++ b/MailCore/Models/Draft.swift @@ -234,7 +234,7 @@ public final class Draft: Object, Codable, Identifiable { unsafeQuote = Constants.forwardQuote(message: message) } - let quote = (try? SwiftSoupUtils(from: unsafeQuote).cleanDocument().outerHtml()) ?? "" + let quote = (try? SwiftSoupUtils(from: unsafeQuote).cleanCompleteDocument().outerHtml()) ?? "" return "

" + quote } diff --git a/MailCore/Utils/NotificationsHelper.swift b/MailCore/Utils/NotificationsHelper.swift index d86fbe7ab..86f77f073 100644 --- a/MailCore/Utils/NotificationsHelper.swift +++ b/MailCore/Utils/NotificationsHelper.swift @@ -201,10 +201,10 @@ public enum NotificationsHelper { } do { - let basicHtml = try SwiftSoup.clean(body, Whitelist.basic())! - let parsedBody = try await SwiftSoup.parse(basicHtml) + let cleanedDocument = try SwiftSoupUtils(from: body).cleanBody() + guard let extractedBody = cleanedDocument.body() else { return message.preview } - let rawText = try parsedBody.text(trimAndNormaliseWhitespace: false) + let rawText = try extractedBody.text(trimAndNormaliseWhitespace: false) return rawText.trimmingCharacters(in: .whitespacesAndNewlines) } catch { return message.preview diff --git a/MailCore/Utils/SwiftSoupUtils.swift b/MailCore/Utils/SwiftSoupUtils.swift index d26cf2a37..dad3fd657 100644 --- a/MailCore/Utils/SwiftSoupUtils.swift +++ b/MailCore/Utils/SwiftSoupUtils.swift @@ -29,7 +29,12 @@ public struct SwiftSoupUtils { document = try SwiftSoup.parse(html) } - public func cleanDocument() throws -> Document { + public func cleanBody() throws -> Document { + let cleanedDocument = try SwiftSoup.Cleaner(headWhitelist: nil, bodyWhitelist: .extendedBodyWhitelist).clean(document) + return cleanedDocument + } + + public func cleanCompleteDocument() throws -> Document { let cleanedDocument = try SwiftSoup.Cleaner(headWhitelist: .headWhitelist, bodyWhitelist: .extendedBodyWhitelist) .clean(document) From 0373497c3758eb157fb74edbecd43546bc0e74eb Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Tue, 5 Dec 2023 09:04:24 +0100 Subject: [PATCH 3/4] feat: Make SwiftSoupUtils functions async --- Mail/Views/AI Writer/AIModel.swift | 12 +++++------ .../Proposition/AIPropositionView.swift | 20 ++++++++++++------- .../WebView/WebViewModel+SwiftSoup.swift | 2 +- MailCore/Cache/DraftContentManager.swift | 8 ++++---- MailCore/Models/Draft.swift | 4 ++-- MailCore/Utils/NotificationsHelper.swift | 2 +- MailCore/Utils/SwiftSoupUtils.swift | 10 +++++----- 7 files changed, 32 insertions(+), 26 deletions(-) diff --git a/Mail/Views/AI Writer/AIModel.swift b/Mail/Views/AI Writer/AIModel.swift index 51aaf7ade..5184de74a 100644 --- a/Mail/Views/AI Writer/AIModel.swift +++ b/Mail/Views/AI Writer/AIModel.swift @@ -219,32 +219,32 @@ extension AIModel { // MARK: - Insert result extension AIModel { - func didTapInsert() { + func didTapInsert() async { let shouldReplaceBody = shouldOverrideBody() guard !shouldReplaceBody || UserDefaults.shared.doNotShowAIReplaceMessageAgain else { isShowingReplaceBodyAlert = true return } - splitPropositionAndInsert(shouldReplaceBody: shouldReplaceBody) + await splitPropositionAndInsert(shouldReplaceBody: shouldReplaceBody) } - func splitPropositionAndInsert(shouldReplaceBody: Bool) { + func splitPropositionAndInsert(shouldReplaceBody: Bool) async { let (subject, body) = splitSubjectAndBody() if let subject, !subject.isEmpty && shouldOverrideSubject() { isShowingReplaceSubjectAlert = AIProposition(subject: subject, body: body, shouldReplaceContent: shouldReplaceBody) } else { - insertProposition(subject: subject, body: body, shouldReplaceBody: shouldReplaceBody) + await insertProposition(subject: subject, body: body, shouldReplaceBody: shouldReplaceBody) } } - func insertProposition(subject: String?, body: String, shouldReplaceBody: Bool) { + func insertProposition(subject: String?, body: String, shouldReplaceBody: Bool) async { matomo.track( eventWithCategory: .aiWriter, action: .data, name: shouldReplaceBody ? "replaceProposition" : "insertProposition" ) - draftContentManager.replaceContent(subject: subject, body: body) + await draftContentManager.replaceContent(subject: subject, body: body) withAnimation { isShowingProposition = false } diff --git a/Mail/Views/AI Writer/Proposition/AIPropositionView.swift b/Mail/Views/AI Writer/Proposition/AIPropositionView.swift index 5084374b1..9e21398ad 100644 --- a/Mail/Views/AI Writer/Proposition/AIPropositionView.swift +++ b/Mail/Views/AI Writer/Proposition/AIPropositionView.swift @@ -97,7 +97,9 @@ struct AIPropositionView: View { AIProgressView() case .standard, .error: Button { - aiModel.didTapInsert() + Task { + await aiModel.didTapInsert() + } } label: { Label { Text(MailResourcesStrings.Localizable.aiButtonInsert) } icon: { IKIcon(MailResourcesAsset.plus) @@ -123,16 +125,20 @@ struct AIPropositionView: View { } .customAlert(isPresented: $aiModel.isShowingReplaceBodyAlert) { ReplaceMessageBodyView { - aiModel.splitPropositionAndInsert(shouldReplaceBody: true) + Task { + await aiModel.splitPropositionAndInsert(shouldReplaceBody: true) + } } } .customAlert(item: $aiModel.isShowingReplaceSubjectAlert) { proposition in ReplaceMessageSubjectView(subject: proposition.subject) { shouldReplaceSubject in - aiModel.insertProposition( - subject: shouldReplaceSubject ? proposition.subject : nil, - body: proposition.body, - shouldReplaceBody: proposition.shouldReplaceContent - ) + Task { + await aiModel.insertProposition( + subject: shouldReplaceSubject ? proposition.subject : nil, + body: proposition.body, + shouldReplaceBody: proposition.shouldReplaceContent + ) + } } } .ikButtonPrimaryStyle(MailResourcesAsset.aiColor.swiftUIColor) diff --git a/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift b/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift index 279c33fe2..171152d1d 100644 --- a/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift +++ b/Mail/Views/Thread/WebView/WebViewModel+SwiftSoup.swift @@ -36,7 +36,7 @@ extension WebViewModel { guard let rawHTML = value else { return .errorEmptyInputValue } do { - guard let safeDocument = try? SwiftSoupUtils(from: rawHTML).cleanCompleteDocument() + guard let safeDocument = try? await SwiftSoupUtils(from: rawHTML).cleanCompleteDocument() else { return .errorCleanHTMLContent } try updateHeadContent(of: safeDocument) diff --git a/MailCore/Cache/DraftContentManager.swift b/MailCore/Cache/DraftContentManager.swift index 64fb147b7..3e8b4c94a 100644 --- a/MailCore/Cache/DraftContentManager.swift +++ b/MailCore/Cache/DraftContentManager.swift @@ -137,13 +137,13 @@ public class DraftContentManager: ObservableObject { return try await loadReplyingMessage(messageReply.message, replyMode: messageReply.replyMode).body?.freezeIfNeeded() } - public func replaceContent(subject: String? = nil, body: String) { + public func replaceContent(subject: String? = nil, body: String) async { guard let liveDraft = try? getLiveDraft() else { return } - guard let parsedMessage = try? SwiftSoup.parse(liveDraft.body) else { return } + guard let parsedMessage = try? await SwiftSoup.parse(liveDraft.body) else { return } var extractedElements = "" for itemToExtract in Draft.appendedHTMLElements { - if let element = try? SwiftSoupUtils(document: parsedMessage).extractHTML(".\(itemToExtract)") { + if let element = try? await SwiftSoupUtils(document: parsedMessage).extractHTML(".\(itemToExtract)") { extractedElements.append(element) } } @@ -313,7 +313,7 @@ public class DraftContentManager: ObservableObject { private func loadReplyingMessageAndFormat(_ message: Message, replyMode: ReplyMode) async throws -> String { let replyingMessage = try await loadReplyingMessage(message, replyMode: replyMode) - return Draft.replyingBody(message: replyingMessage, replyMode: replyMode) + return await Draft.replyingBody(message: replyingMessage, replyMode: replyMode) } private func loadReplyingAttachments(message: Message, replyMode: ReplyMode) async throws -> [Attachment] { diff --git a/MailCore/Models/Draft.swift b/MailCore/Models/Draft.swift index e360511c2..87fc42a63 100644 --- a/MailCore/Models/Draft.swift +++ b/MailCore/Models/Draft.swift @@ -225,7 +225,7 @@ public final class Draft: Object, Codable, Identifiable { return Draft(to: [recipient.detached()]) } - public static func replyingBody(message: Message, replyMode: ReplyMode) -> String { + public static func replyingBody(message: Message, replyMode: ReplyMode) async -> String { let unsafeQuote: String switch replyMode { case .reply, .replyAll: @@ -234,7 +234,7 @@ public final class Draft: Object, Codable, Identifiable { unsafeQuote = Constants.forwardQuote(message: message) } - let quote = (try? SwiftSoupUtils(from: unsafeQuote).cleanCompleteDocument().outerHtml()) ?? "" + let quote = await (try? SwiftSoupUtils(from: unsafeQuote).cleanCompleteDocument().outerHtml()) ?? "" return "

" + quote } diff --git a/MailCore/Utils/NotificationsHelper.swift b/MailCore/Utils/NotificationsHelper.swift index 86f77f073..80bc6bfb3 100644 --- a/MailCore/Utils/NotificationsHelper.swift +++ b/MailCore/Utils/NotificationsHelper.swift @@ -201,7 +201,7 @@ public enum NotificationsHelper { } do { - let cleanedDocument = try SwiftSoupUtils(from: body).cleanBody() + let cleanedDocument = try await SwiftSoupUtils(from: body).cleanBody() guard let extractedBody = cleanedDocument.body() else { return message.preview } let rawText = try extractedBody.text(trimAndNormaliseWhitespace: false) diff --git a/MailCore/Utils/SwiftSoupUtils.swift b/MailCore/Utils/SwiftSoupUtils.swift index dad3fd657..d8b96392c 100644 --- a/MailCore/Utils/SwiftSoupUtils.swift +++ b/MailCore/Utils/SwiftSoupUtils.swift @@ -29,17 +29,17 @@ public struct SwiftSoupUtils { document = try SwiftSoup.parse(html) } - public func cleanBody() throws -> Document { + public func cleanBody() async throws -> Document { let cleanedDocument = try SwiftSoup.Cleaner(headWhitelist: nil, bodyWhitelist: .extendedBodyWhitelist).clean(document) return cleanedDocument } - public func cleanCompleteDocument() throws -> Document { + public func cleanCompleteDocument() async throws -> Document { let cleanedDocument = try SwiftSoup.Cleaner(headWhitelist: .headWhitelist, bodyWhitelist: .extendedBodyWhitelist) .clean(document) // We need to remove the tag - let metaRefreshTags = try cleanedDocument.select("meta[http-equiv='refresh']") + let metaRefreshTags = try await cleanedDocument.select("meta[http-equiv='refresh']") for metaRefreshTag in metaRefreshTags { try metaRefreshTag.parent()?.removeChild(metaRefreshTag) } @@ -47,8 +47,8 @@ public struct SwiftSoupUtils { return cleanedDocument } - public func extractHTML(_ cssQuery: String) throws -> String { - guard let foundElement = try document.select(cssQuery).first() else { throw SwiftSoupError.elementNotFound } + public func extractHTML(_ cssQuery: String) async throws -> String { + guard let foundElement = try await document.select(cssQuery).first() else { throw SwiftSoupError.elementNotFound } let htmlContent = try foundElement.outerHtml() return htmlContent From 3754f4e2bdea13ce02aec39a7534b3b3f8d4c1f0 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Tue, 5 Dec 2023 10:57:57 +0100 Subject: [PATCH 4/4] fix: Ensure realm objects are frozen --- MailCore/Cache/DraftContentManager.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/MailCore/Cache/DraftContentManager.swift b/MailCore/Cache/DraftContentManager.swift index 3e8b4c94a..85d4cc534 100644 --- a/MailCore/Cache/DraftContentManager.swift +++ b/MailCore/Cache/DraftContentManager.swift @@ -138,8 +138,8 @@ public class DraftContentManager: ObservableObject { } public func replaceContent(subject: String? = nil, body: String) async { - guard let liveDraft = try? getLiveDraft() else { return } - guard let parsedMessage = try? await SwiftSoup.parse(liveDraft.body) else { return } + guard let draft = try? getFrozenDraft() else { return } + guard let parsedMessage = try? await SwiftSoup.parse(draft.body) else { return } var extractedElements = "" for itemToExtract in Draft.appendedHTMLElements { @@ -149,6 +149,7 @@ public class DraftContentManager: ObservableObject { } let realm = mailboxManager.getRealm() + guard let liveDraft = draft.thaw() else { return } try? realm.write { if let subject { liveDraft.subject = subject @@ -166,6 +167,10 @@ public class DraftContentManager: ObservableObject { return liveDraft } + private func getFrozenDraft() throws -> Draft { + return try getLiveDraft().freezeIfNeeded() + } + private func writeCompleteDraft( completeBody: String, signature: Signature, @@ -313,7 +318,7 @@ public class DraftContentManager: ObservableObject { private func loadReplyingMessageAndFormat(_ message: Message, replyMode: ReplyMode) async throws -> String { let replyingMessage = try await loadReplyingMessage(message, replyMode: replyMode) - return await Draft.replyingBody(message: replyingMessage, replyMode: replyMode) + return await Draft.replyingBody(message: replyingMessage.freezeIfNeeded(), replyMode: replyMode) } private func loadReplyingAttachments(message: Message, replyMode: ReplyMode) async throws -> [Attachment] {