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 } ]