diff --git a/MailCore/Models/Message.swift b/MailCore/Models/Message.swift index a42e00849b..826aeb8d9a 100644 --- a/MailCore/Models/Message.swift +++ b/MailCore/Models/Message.swift @@ -250,7 +250,7 @@ public final class Message: Object, Decodable, ObjectKeyIdentifiable { } public var formattedSubject: String { - return subject ?? MailResourcesStrings.Localizable.noSubjectTitle + return SubjectFormatter.cleanNotNullOrEmptySubject.format(subject) } public var displayDate: DisplayDate { diff --git a/MailCore/Models/Thread.swift b/MailCore/Models/Thread.swift index ab73e359ac..41b651a82e 100644 --- a/MailCore/Models/Thread.swift +++ b/MailCore/Models/Thread.swift @@ -18,7 +18,6 @@ import Foundation import InfomaniakDI -import MailResources import OrderedCollections import RealmSwift @@ -125,10 +124,7 @@ public class Thread: Object, Decodable, Identifiable { } public var formattedSubject: String { - guard let subject, !subject.isEmpty else { - return MailResourcesStrings.Localizable.noSubjectTitle - } - return subject + return SubjectFormatter.cleanNotNullOrEmptySubject.format(subject) } public var shouldPresentAsDraft: Bool { diff --git a/MailCore/Utils/NotificationsHelper.swift b/MailCore/Utils/NotificationsHelper.swift index 8015b64bb4..d603e0c13d 100644 --- a/MailCore/Utils/NotificationsHelper.swift +++ b/MailCore/Utils/NotificationsHelper.swift @@ -325,6 +325,7 @@ public enum NotificationsHelper { guard let liveMessage = realm.object(ofType: Message.self, forPrimaryKey: message.uid) else { return } + liveMessage.preview = String(preview.prefix(512)) } } diff --git a/MailCore/Utils/SubjectFormatter.swift b/MailCore/Utils/SubjectFormatter.swift new file mode 100644 index 0000000000..1827b6928f --- /dev/null +++ b/MailCore/Utils/SubjectFormatter.swift @@ -0,0 +1,78 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2025 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 MailResources + +public struct SubjectFormatter: FormatStyle { + private func clean(_ value: String) -> String { + let normalizedSubject = value.precomposedStringWithCompatibilityMapping + + let cleanedSubject = String(normalizedSubject.unicodeScalars.map { scalar -> Character in + if scalar.isIllegalInSubject { + return " " + } else { + return Character(scalar) + } + }) + + return cleanedSubject + } + + private func cleanNotNullOrEmpty(_ value: String?) -> String { + guard let value, !value.isEmpty else { + return MailResourcesStrings.Localizable.noSubjectTitle + } + + return clean(value) + } + + public func format(_ value: String?) -> String { + return cleanNotNullOrEmpty(value) + } +} + +public extension FormatStyle where Self == SubjectFormatter { + static var cleanNotNullOrEmptySubject: SubjectFormatter { + return SubjectFormatter() + } +} + +extension UnicodeScalar { + var isInvisible: Bool { + properties.generalCategory == .format || + properties.generalCategory == .control + } + + var isBidiControl: Bool { + switch value { + case 0x202A ... 0x202E, 0x2066 ... 0x2069: + return true + default: + return false + } + } + + var isIllegalInSubject: Bool { + if isInvisible || properties.isJoinControl || isBidiControl { + return true + } + + return false + } +}