diff --git a/Mail/Components/AvatarView.swift b/Mail/Components/AvatarView.swift
index 9ae142fbd..b355b4e8e 100644
--- a/Mail/Components/AvatarView.swift
+++ b/Mail/Components/AvatarView.swift
@@ -22,16 +22,40 @@ import MailResources
import NukeUI
import SwiftUI
-struct AvatarView: View, Equatable {
+extension AvatarView: Equatable {
+ static func == (lhs: AvatarView, rhs: AvatarView) -> Bool {
+ return lhs.mailboxManager == rhs.mailboxManager
+ && lhs.size == rhs.size
+ && lhs.contactConfiguration.id == rhs.contactConfiguration.id
+ }
+}
+
+/// A view that displays an avatar linked to a Contact.
+struct AvatarView: View {
+ /// A view model for async loading of contacts
+ @ObservedObject private var viewModel: AvatarViewModel
+
/// Optional as this view can be displayed from a context without a mailboxManager available
- let mailboxManager: MailboxManager?
+ private let mailboxManager: MailboxManager?
- let displayablePerson: CommonContact
+ /// The size of the avatar view
+ private let size: CGFloat
- var size: CGFloat = 28
+ /// The configuration associated to this view
+ private let contactConfiguration: ContactConfiguration
+
+ init(mailboxManager: MailboxManager?, contactConfiguration: ContactConfiguration, size: CGFloat = 28) {
+ self.mailboxManager = mailboxManager
+ self.size = size
+ self.contactConfiguration = contactConfiguration
+
+ // We use an ObservedObject instead of a StateObject because SwiftUI doesn't want to respect Equatable
+ _viewModel = ObservedObject(wrappedValue: AvatarViewModel(contactConfiguration: contactConfiguration))
+ }
var body: some View {
Group {
+ let displayablePerson = viewModel.displayablePerson
if let mailboxManager,
let currentToken = mailboxManager.apiFetcher.currentToken,
let avatarImageRequest = displayablePerson.avatarImageRequest.authenticatedRequestIfNeeded(token:
diff --git a/Mail/Components/Buttons/AccountButton.swift b/Mail/Components/Buttons/AccountButton.swift
index 1ce80b88e..fd413475c 100644
--- a/Mail/Components/Buttons/AccountButton.swift
+++ b/Mail/Components/Buttons/AccountButton.swift
@@ -31,10 +31,8 @@ struct AccountButton: View {
presentedCurrentAccount = mailboxManager.account
} label: {
if let currentAccountUser = mailboxManager.account.user {
- AvatarView(
- mailboxManager: mailboxManager,
- displayablePerson: CommonContact(user: currentAccountUser)
- )
+ AvatarView(mailboxManager: mailboxManager,
+ contactConfiguration: .user(user: currentAccountUser))
}
}
.sheet(item: $presentedCurrentAccount) { account in
diff --git a/Mail/Components/RecipientCell.swift b/Mail/Components/RecipientCell.swift
index 88fdbaee2..0442a5108 100644
--- a/Mail/Components/RecipientCell.swift
+++ b/Mail/Components/RecipientCell.swift
@@ -45,10 +45,7 @@ struct RecipientCell: View {
HStack(spacing: UIPadding.small) {
AvatarView(
mailboxManager: mailboxManager,
- displayablePerson: CommonContact(
- recipient: recipient,
- contextMailboxManager: mailboxManager
- ),
+ contactConfiguration: .recipient(recipient: recipient, contextMailboxManager: mailboxManager),
size: 40
)
.accessibilityHidden(true)
diff --git a/Mail/Components/ThreadCell.swift b/Mail/Components/ThreadCell.swift
index 8a569dee0..00e0f035a 100644
--- a/Mail/Components/ThreadCell.swift
+++ b/Mail/Components/ThreadCell.swift
@@ -71,11 +71,11 @@ struct ThreadCellDataHolder {
}
}
- func commonContact(contextMailboxManager: MailboxManager) -> CommonContact {
+ func contactConfiguration(contextMailboxManager: MailboxManager) -> ContactConfiguration {
if let recipientToDisplay {
- return CommonContact(recipient: recipientToDisplay, contextMailboxManager: contextMailboxManager)
+ return .recipient(recipient: recipientToDisplay, contextMailboxManager: contextMailboxManager)
} else {
- return CommonContact.emptyContact(contextMailboxManager: contextMailboxManager)
+ return .emptyContact
}
}
}
@@ -149,7 +149,7 @@ struct ThreadCell: View {
ZStack {
AvatarView(
mailboxManager: mailboxManager,
- displayablePerson: dataHolder.commonContact(contextMailboxManager: mailboxManager),
+ contactConfiguration: dataHolder.contactConfiguration(contextMailboxManager: mailboxManager),
size: 40
)
.opacity(isSelected ? 0 : 1)
diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift
index f538ec9b7..7e6df11d3 100644
--- a/Mail/Helpers/AppAssembly.swift
+++ b/Mail/Helpers/AppAssembly.swift
@@ -124,6 +124,14 @@ enum ApplicationAssembly {
},
Factory(type: AppLaunchCounter.self) { _, _ in
AppLaunchCounter()
+ },
+ Factory(type: ContactCache.self) { _, _ in
+ let contactCache = ContactCache()
+ if Bundle.main.isExtension {
+ // Limit the cache size in extension mode, not strictly needed, but coherent.
+ contactCache.countLimit = Constants.contactCacheExtensionMaxCount
+ }
+ return contactCache
}
]
diff --git a/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsHeaderView.swift b/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsHeaderView.swift
index 634e90ac2..e22d35bf7 100644
--- a/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsHeaderView.swift
+++ b/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsHeaderView.swift
@@ -26,7 +26,7 @@ struct ContactActionsHeaderView: View {
var body: some View {
HStack {
- AvatarView(mailboxManager: mailboxManager, displayablePerson: displayablePerson, size: 40)
+ AvatarView(mailboxManager: mailboxManager, contactConfiguration: .contact(contact: displayablePerson), size: 40)
.accessibilityHidden(true)
VStack(alignment: .leading) {
Text(displayablePerson, format: .displayablePerson())
diff --git a/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsView.swift b/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsView.swift
index 44ee7ade3..2ddb8517e 100644
--- a/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsView.swift
+++ b/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsView.swift
@@ -41,10 +41,9 @@ struct ContactActionsView: View {
var body: some View {
VStack(alignment: .leading, spacing: UIPadding.small) {
- ContactActionsHeaderView(displayablePerson: CommonContact(
- recipient: recipient,
- contextMailboxManager: mailboxManager
- ))
+ let contactConfiguration = ContactConfiguration.recipient(recipient: recipient, contextMailboxManager: mailboxManager)
+ let contact = CommonContactCache.getOrCreateContact(contactConfiguration: contactConfiguration)
+ ContactActionsHeaderView(displayablePerson: contact)
VStack(alignment: .leading, spacing: 0) {
ForEach(actions) { action in
diff --git a/Mail/Views/Switch User/AccountCellView.swift b/Mail/Views/Switch User/AccountCellView.swift
index df89b0ef1..1ca455f28 100644
--- a/Mail/Views/Switch User/AccountCellView.swift
+++ b/Mail/Views/Switch User/AccountCellView.swift
@@ -77,8 +77,7 @@ struct AccountHeaderCell: View {
var body: some View {
HStack(spacing: UIPadding.small) {
- AvatarView(mailboxManager: mailboxManager, displayablePerson: CommonContact(user: account.user), size: 40)
-
+ AvatarView(mailboxManager: mailboxManager, contactConfiguration: .user(user: account.user), size: 40)
VStack(alignment: .leading, spacing: 0) {
Text(account.user.displayName)
.textStyle(.bodyMedium)
diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift
index 7cc5f15e7..c72b29873 100644
--- a/Mail/Views/Switch User/AccountView.swift
+++ b/Mail/Views/Switch User/AccountView.swift
@@ -68,23 +68,25 @@ struct AccountView: View {
var body: some View {
VStack(spacing: 0) {
ScrollView {
- AvatarView(mailboxManager: mailboxManager,
- displayablePerson: CommonContact(user: account.user),
- size: AccountView.avatarViewSize)
- .padding(.bottom, value: .regular)
- .padding(.top, value: .medium)
- .background {
- if EasterEgg.halloween.shouldTrigger() {
- LottieView(configuration: LottieConfiguration(id: 1, filename: "illu_easter_egg_halloween"),
- isVisible: $isLottieAnimationVisible)
- .offset(y: AccountView.avatarViewSize)
- .allowsHitTesting(false)
- .onAppear {
- EasterEgg.halloween.onTrigger()
- }
- }
+ AvatarView(
+ mailboxManager: mailboxManager,
+ contactConfiguration: .user(user: account.user),
+ size: AccountView.avatarViewSize
+ )
+ .padding(.bottom, value: .regular)
+ .padding(.top, value: .medium)
+ .background {
+ if EasterEgg.halloween.shouldTrigger() {
+ LottieView(configuration: LottieConfiguration(id: 1, filename: "illu_easter_egg_halloween"),
+ isVisible: $isLottieAnimationVisible)
+ .offset(y: AccountView.avatarViewSize)
+ .allowsHitTesting(false)
+ .onAppear {
+ EasterEgg.halloween.onTrigger()
+ }
}
- .zIndex(1)
+ }
+ .zIndex(1)
VStack(spacing: 0) {
Text(account.user.displayName)
diff --git a/Mail/Views/Thread/MessageHeaderSummaryView.swift b/Mail/Views/Thread/MessageHeaderSummaryView.swift
index 9c614059a..52ac03444 100644
--- a/Mail/Views/Thread/MessageHeaderSummaryView.swift
+++ b/Mail/Views/Thread/MessageHeaderSummaryView.swift
@@ -50,10 +50,8 @@ struct MessageHeaderSummaryView: View {
} label: {
AvatarView(
mailboxManager: mailboxManager,
- displayablePerson: CommonContact(
- recipient: recipient,
- contextMailboxManager: mailboxManager
- ),
+ contactConfiguration: .recipient(recipient: recipient,
+ contextMailboxManager: mailboxManager),
size: 40
)
}
@@ -70,7 +68,14 @@ struct MessageHeaderSummaryView: View {
HStack(alignment: .firstTextBaseline, spacing: UIPadding.small) {
VStack {
ForEach(message.from) { recipient in
- Text(CommonContact(recipient: recipient, contextMailboxManager: mailboxManager),
+ let contactConfiguration = ContactConfiguration.recipient(
+ recipient: recipient,
+ contextMailboxManager: mailboxManager
+ )
+ let contact = CommonContactCache
+ .getOrCreateContact(contactConfiguration: contactConfiguration)
+
+ Text(contact,
format: .displayablePerson())
.lineLimit(1)
.textStyle(.bodyMedium)
@@ -87,7 +92,13 @@ struct MessageHeaderSummaryView: View {
HStack {
Text(
message.recipients.map {
- CommonContact(recipient: $0, contextMailboxManager: mailboxManager).formatted()
+ let contactConfiguration = ContactConfiguration.recipient(
+ recipient: $0,
+ contextMailboxManager: mailboxManager
+ )
+ let contact = CommonContactCache
+ .getOrCreateContact(contactConfiguration: contactConfiguration)
+ return contact.formatted()
},
format: .list(type: .and)
)
diff --git a/MailCore/Models/Contact/AvatarImageRequest.swift b/MailCore/Models/Contact/AvatarImageRequest.swift
new file mode 100644
index 000000000..19764bc1d
--- /dev/null
+++ b/MailCore/Models/Contact/AvatarImageRequest.swift
@@ -0,0 +1,45 @@
+/*
+ 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 Foundation
+import InfomaniakCore
+import Nuke
+
+public struct AvatarImageRequest {
+ let imageRequest: ImageRequest?
+ let shouldAuthenticate: Bool
+
+ public func authenticatedRequestIfNeeded(token: ApiToken) -> ImageRequest? {
+ guard let unauthenticatedImageRequest = imageRequest,
+ let unauthenticatedUrlRequest = unauthenticatedImageRequest.urlRequest else {
+ return nil
+ }
+
+ guard shouldAuthenticate else {
+ return unauthenticatedImageRequest
+ }
+
+ var authenticatedUrlRequest = unauthenticatedUrlRequest
+ authenticatedUrlRequest.addValue(
+ "Bearer \(token.accessToken)",
+ forHTTPHeaderField: "Authorization"
+ )
+
+ return ImageRequest(urlRequest: authenticatedUrlRequest)
+ }
+}
diff --git a/MailCore/Models/CommonContact.swift b/MailCore/Models/Contact/CommonContact.swift
similarity index 69%
rename from MailCore/Models/CommonContact.swift
rename to MailCore/Models/Contact/CommonContact.swift
index 03b3b8c7e..07c208091 100644
--- a/MailCore/Models/CommonContact.swift
+++ b/MailCore/Models/Contact/CommonContact.swift
@@ -22,38 +22,44 @@ import MailResources
import Nuke
import UIKit
-public struct AvatarImageRequest {
- let imageRequest: ImageRequest?
- let shouldAuthenticate: Bool
+public final class CommonContact: Identifiable {
+ /// Empty contact is a singleton
+ public static let emptyContact = CommonContact()
- public func authenticatedRequestIfNeeded(token: ApiToken) -> ImageRequest? {
- guard let unauthenticatedImageRequest = imageRequest,
- let unauthenticatedUrlRequest = unauthenticatedImageRequest.urlRequest else {
- return nil
- }
-
- guard shouldAuthenticate else {
- return unauthenticatedImageRequest
- }
-
- var authenticatedUrlRequest = unauthenticatedUrlRequest
- authenticatedUrlRequest.addValue(
- "Bearer \(token.accessToken)",
- forHTTPHeaderField: "Authorization"
- )
+ public let id: Int
- return ImageRequest(urlRequest: authenticatedUrlRequest)
- }
-}
-
-public struct CommonContact {
public let fullName: String
public let email: String
public let avatarImageRequest: AvatarImageRequest
public let color: UIColor
- public init(recipient: Recipient, contextMailboxManager: MailboxManager) {
+ static func from(contactConfiguration: ContactConfiguration) -> CommonContact {
+ switch contactConfiguration {
+ case .recipient(let recipient, let contextMailboxManager):
+ CommonContact(recipient: recipient, contextMailboxManager: contextMailboxManager)
+ case .user(let user):
+ CommonContact(user: user)
+ case .contact(let contact):
+ contact
+ case .emptyContact:
+ emptyContact
+ }
+ }
+
+ /// Empty contact
+ private init() {
+ let recipient = Recipient(email: "", name: "")
email = recipient.email
+ fullName = recipient.name
+ id = recipient.id.hashValue
+ color = UIColor.backgroundColor(from: recipient.hash, with: UIConstants.avatarColors)
+ avatarImageRequest = AvatarImageRequest(imageRequest: nil, shouldAuthenticate: true)
+ }
+
+ /// Init form a `Recipient` in the context of a mailbox
+ init(recipient: Recipient, contextMailboxManager: MailboxManager) {
+ email = recipient.email
+ id = recipient.id.hashValue
if recipient.isMe(currentMailboxEmail: contextMailboxManager.mailbox.email) {
fullName = MailResourcesStrings.Localizable.contactMe
@@ -65,7 +71,7 @@ public struct CommonContact {
avatarImageRequest = AvatarImageRequest(imageRequest: nil, shouldAuthenticate: false)
}
} else {
- let mainViewRealm = contextMailboxManager.contactManager.viewRealm
+ let mainViewRealm = contextMailboxManager.contactManager.getRealm()
let contact = contextMailboxManager.contactManager.getContact(for: recipient, realm: mainViewRealm)
fullName = contact?.name ?? (recipient.name.isEmpty ? recipient.email : recipient.name)
color = contact?.color ?? UIColor.backgroundColor(from: email.hash, with: UIConstants.avatarColors)
@@ -73,7 +79,9 @@ public struct CommonContact {
}
}
- public init(user: UserProfile) {
+ /// Init form a `UserProfile`
+ init(user: UserProfile) {
+ id = user.id
fullName = user.displayName
email = user.email
color = UIColor.backgroundColor(from: user.id, with: UIConstants.avatarColors)
@@ -83,11 +91,6 @@ public struct CommonContact {
avatarImageRequest = AvatarImageRequest(imageRequest: nil, shouldAuthenticate: false)
}
}
-
- /// A `CommonContact` that represents no-one.
- public static func emptyContact(contextMailboxManager: MailboxManager) -> CommonContact {
- CommonContact(recipient: Recipient(email: "", name: ""), contextMailboxManager: contextMailboxManager)
- }
}
extension CommonContact: Equatable {
diff --git a/MailCore/Models/Contact/CommonContactCache.swift b/MailCore/Models/Contact/CommonContactCache.swift
new file mode 100644
index 000000000..ec90a4b53
--- /dev/null
+++ b/MailCore/Models/Contact/CommonContactCache.swift
@@ -0,0 +1,68 @@
+/*
+ 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 Foundation
+import InfomaniakCore
+import InfomaniakDI
+
+/// A standard cache for `CommonContact`, used by DI
+public typealias ContactCache = NSCache
+
+/// Creating a `CommonContact` is expensive, relying on a cache to reduce hangs
+public enum CommonContactCache {
+ /// Set to false for testing without cache
+ private static let cacheEnabled = true
+
+ /// The underlying standard cache
+ @LazyInjectService private static var cache: ContactCache
+
+ /// Get a contact from cache if any or nil
+ public static func getContactFromCache(contactConfiguration: ContactConfiguration) -> CommonContact? {
+ /// cache enabled check
+ guard cacheEnabled else {
+ return nil
+ }
+
+ return cache.object(forKey: contactConfiguration.cacheKey)
+ }
+
+ /// Get a contact from cache or build it
+ public static func getOrCreateContact(contactConfiguration: ContactConfiguration) -> CommonContact {
+ /// Try to fetch the entry from cache
+ if let cachedContact = getContactFromCache(contactConfiguration: contactConfiguration) {
+ return cachedContact
+ }
+
+ let contact: CommonContact
+ switch contactConfiguration {
+ case .recipient(let recipient, let contextMailboxManager):
+ contact = CommonContact(recipient: recipient, contextMailboxManager: contextMailboxManager)
+ case .user(let user):
+ contact = CommonContact(user: user)
+ case .contact(let wrappedContact):
+ contact = wrappedContact
+ case .emptyContact:
+ contact = CommonContact.emptyContact
+ }
+
+ // Store the object in cache
+ cache.setObject(contact, forKey: contactConfiguration.cacheKey)
+
+ return contact
+ }
+}
diff --git a/MailCore/Models/Contact.swift b/MailCore/Models/Contact/Contact.swift
similarity index 100%
rename from MailCore/Models/Contact.swift
rename to MailCore/Models/Contact/Contact.swift
diff --git a/MailCore/Models/Contact/ContactConfiguration.swift b/MailCore/Models/Contact/ContactConfiguration.swift
new file mode 100644
index 000000000..00354f636
--- /dev/null
+++ b/MailCore/Models/Contact/ContactConfiguration.swift
@@ -0,0 +1,64 @@
+/*
+ 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 Foundation
+import InfomaniakCore
+
+public enum ContactConfiguration: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ switch self {
+ case .recipient(let recipient, _):
+ return ".recipient:\(recipient.name) \(recipient.email)"
+ case .user(let user):
+ return ".user:\(user.displayName) \(user.email)"
+ case .contact(let contact):
+ return ".contact:\(contact.fullName) \(contact.email)"
+ case .emptyContact:
+ return ".emptyContact"
+ }
+ }
+
+ case recipient(recipient: Recipient, contextMailboxManager: MailboxManager)
+ case user(user: UserProfile)
+ case contact(contact: CommonContact)
+ case emptyContact
+}
+
+extension ContactConfiguration {
+ /// A stable key to be used with NSCache
+ var cacheKey: NSNumber {
+ return NSNumber(value: id)
+ }
+}
+
+extension ContactConfiguration: Identifiable {
+ public var id: Int {
+ switch self {
+ case .recipient(let recipient, let contextMailboxManager):
+ // One cache entry per recipient per mailbox
+ let hash = recipient.id.hash ^ contextMailboxManager.mailbox.id.hash
+ return hash
+ case .user(let user):
+ return user.id
+ case .contact(let wrappedContact):
+ return wrappedContact.id
+ case .emptyContact:
+ return CommonContact.emptyContact.id
+ }
+ }
+}
diff --git a/MailCore/Utils/Constants.swift b/MailCore/Utils/Constants.swift
index f0f65ab06..81c23e768 100644
--- a/MailCore/Utils/Constants.swift
+++ b/MailCore/Utils/Constants.swift
@@ -192,4 +192,7 @@ public enum Constants {
public static let openingBeforeReview = 50
public static let minimumOpeningBeforeSync = 5
+
+ /// A count limit for the Contact cache in Extension mode, where we have strong memory constraints.
+ public static let contactCacheExtensionMaxCount = 50
}
diff --git a/MailCore/Utils/ThreadRecipientsFormatter.swift b/MailCore/Utils/ThreadRecipientsFormatter.swift
index 6f4a3f8ef..b6835adbe 100644
--- a/MailCore/Utils/ThreadRecipientsFormatter.swift
+++ b/MailCore/Utils/ThreadRecipientsFormatter.swift
@@ -60,13 +60,22 @@ public extension Thread {
case 0:
return MailResourcesStrings.Localizable.unknownRecipientTitle
case 1:
- return CommonContact(recipient: fromArray[0], contextMailboxManager: contextMailboxManager).formatted()
+ let contactConfiguration = ContactConfiguration.recipient(
+ recipient: fromArray[0],
+ contextMailboxManager: contextMailboxManager
+ )
+ let contact = CommonContactCache.getOrCreateContact(contactConfiguration: contactConfiguration)
+ return contact.formatted()
default:
let fromCount = min(fromArray.count, Constants.threadCellMaxRecipients)
return fromArray[0 ..< fromCount]
.map {
- CommonContact(recipient: $0, contextMailboxManager: contextMailboxManager)
- .formatted(style: .shortName)
+ let contactConfiguration = ContactConfiguration.recipient(
+ recipient: $0,
+ contextMailboxManager: contextMailboxManager
+ )
+ let contact = CommonContactCache.getOrCreateContact(contactConfiguration: contactConfiguration)
+ return contact.formatted(style: .shortName)
}
.joined(separator: ", ")
}
@@ -74,7 +83,11 @@ public extension Thread {
private func formattedTo(thread: Thread) -> String {
guard let to = thread.to.first else { return MailResourcesStrings.Localizable.unknownRecipientTitle }
- return CommonContact(recipient: to, contextMailboxManager: contextMailboxManager).formatted()
+ let contact = CommonContactCache.getOrCreateContact(contactConfiguration: .recipient(
+ recipient: to,
+ contextMailboxManager: contextMailboxManager
+ ))
+ return contact.formatted()
}
public func format(_ value: Thread) -> String {
diff --git a/MailCore/ViewModel/AvatarViewModel.swift b/MailCore/ViewModel/AvatarViewModel.swift
new file mode 100644
index 000000000..df1d3a97b
--- /dev/null
+++ b/MailCore/ViewModel/AvatarViewModel.swift
@@ -0,0 +1,63 @@
+/*
+ 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 Foundation
+import SwiftUI
+
+/// Something that can asynchronously load a contact, and update views accordingly
+@MainActor
+public final class AvatarViewModel: ObservableObject {
+ @Published public var displayablePerson: CommonContact
+
+ /// Something not using the MainActor
+ private struct AvatarLoader {
+ var contactConfiguration: ContactConfiguration
+
+ /// Async get contact method
+ func getContact() async -> CommonContact {
+ return CommonContact.from(contactConfiguration: contactConfiguration)
+ }
+ }
+
+ public init(contactConfiguration: ContactConfiguration) {
+ // early exit on empty value
+ if case .emptyContact = contactConfiguration {
+ displayablePerson = CommonContact.emptyContact
+ return
+ }
+
+ // early exit on wrapped value
+ if case .contact(let wrappedContact) = contactConfiguration {
+ displayablePerson = wrappedContact
+ return
+ }
+
+ // early exit on contact cached
+ if let cached = CommonContactCache.getContactFromCache(contactConfiguration: contactConfiguration) {
+ displayablePerson = cached
+ return
+ }
+
+ // Load contact in background, empty contact in the meantime
+ displayablePerson = CommonContact.emptyContact
+ Task {
+ let loader = AvatarLoader(contactConfiguration: contactConfiguration)
+ self.displayablePerson = await loader.getContact()
+ }
+ }
+}
diff --git a/MailNotificationServiceExtension/NotificationServiceAssembly.swift b/MailNotificationServiceExtension/NotificationServiceAssembly.swift
index 1d32883a8..7eae03ba7 100644
--- a/MailNotificationServiceExtension/NotificationServiceAssembly.swift
+++ b/MailNotificationServiceExtension/NotificationServiceAssembly.swift
@@ -99,6 +99,12 @@ enum NotificationServiceAssembly {
},
Factory(type: IKSnackBarAvoider.self) { _, _ in
IKSnackBarAvoider()
+ },
+ Factory(type: ContactCache.self) { _, _ in
+ let contactCache = ContactCache()
+ // Limit the cache size in extension mode, not needed, but coherent.
+ contactCache.countLimit = Constants.contactCacheExtensionMaxCount
+ return contactCache
}
]