diff --git a/.package.resolved b/.package.resolved
index f848d3e41..c4caf1bba 100644
--- a/.package.resolved
+++ b/.package.resolved
@@ -139,8 +139,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke",
"state" : {
- "revision" : "33f7e93be5d4ec027d42af77a8ec4680d1862ad2",
- "version" : "11.6.4"
+ "revision" : "f4d9b95788679d0654c032961f73e7e9c16ca6b4",
+ "version" : "12.1.0"
}
},
{
diff --git a/Mail/Components/AvatarView.swift b/Mail/Components/AvatarView.swift
new file mode 100644
index 000000000..256e99db1
--- /dev/null
+++ b/Mail/Components/AvatarView.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 InfomaniakCore
+import MailCore
+import NukeUI
+import SwiftUI
+
+struct AvatarView: View {
+ let avatarDisplayable: AvatarDisplayable
+ var size: CGFloat = 28
+
+ var body: some View {
+ if let avatarImageRequest = avatarDisplayable.avatarImageRequest {
+ LazyImage(request: avatarImageRequest) { state in
+ if let image = state.image {
+ ContactImage(image: image, size: size)
+ } else {
+ InitialsView(
+ initials: avatarDisplayable.initials,
+ color: avatarDisplayable.initialsBackgroundColor,
+ size: size
+ )
+ }
+ }
+ } else {
+ InitialsView(initials: avatarDisplayable.initials, color: avatarDisplayable.initialsBackgroundColor, size: size)
+ }
+ }
+}
diff --git a/Mail/Components/ContactImage.swift b/Mail/Components/ContactImage.swift
index ffcad4f16..93f48ae85 100644
--- a/Mail/Components/ContactImage.swift
+++ b/Mail/Components/ContactImage.swift
@@ -20,17 +20,15 @@ import MailCore
import SwiftUI
struct ContactImage: View {
- var image: Image
- var size: CGFloat
+ let image: Image
+ let size: CGFloat
var body: some View {
- ZStack {
- image
- .resizable()
- .scaledToFit()
- .frame(width: size, height: size)
- .clipShape(Circle())
- }
+ image
+ .resizable()
+ .scaledToFit()
+ .frame(width: size, height: size)
+ .clipShape(Circle())
}
}
diff --git a/Mail/Components/RecipientAutocompletionCell.swift b/Mail/Components/RecipientAutocompletionCell.swift
index da0b0ecc7..7e1f31c68 100644
--- a/Mail/Components/RecipientAutocompletionCell.swift
+++ b/Mail/Components/RecipientAutocompletionCell.swift
@@ -24,7 +24,7 @@ struct RecipientAutocompletionCell: View {
var body: some View {
HStack {
- RecipientImage(recipient: recipient)
+ AvatarView(avatarDisplayable: recipient, size: 40)
if recipient.name.isEmpty {
Text(recipient.email)
diff --git a/Mail/Components/RecipientImage.swift b/Mail/Components/RecipientImage.swift
deleted file mode 100644
index 9bb0220dd..000000000
--- a/Mail/Components/RecipientImage.swift
+++ /dev/null
@@ -1,59 +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 MailCore
-import MailResources
-import SwiftUI
-
-struct RecipientImage: View {
- var recipient: Recipient
- var size: CGFloat
-
- @State private var image: Image?
-
- init(recipient: Recipient, size: CGFloat = 40) {
- self.image = recipient.cachedAvatarImage
- self.recipient = recipient
- self.size = size
- }
-
- var body: some View {
- if let image = image {
- ContactImage(image: image, size: size)
- } else {
- InitialsView(initials: recipient.initials, color: recipient.color, size: size)
- .task {
- await fetchAvatar()
- }
- }
- }
-
- func fetchAvatar() async {
- if let avatarImage = await recipient.avatarImage {
- withAnimation {
- image = avatarImage
- }
- }
- }
-}
-
-struct RecipientImage_Previews: PreviewProvider {
- static var previews: some View {
- RecipientImage(recipient: PreviewHelper.sampleRecipient1)
- }
-}
diff --git a/Mail/Components/ThreadCell.swift b/Mail/Components/ThreadCell.swift
index d14be98b7..c783460fe 100644
--- a/Mail/Components/ThreadCell.swift
+++ b/Mail/Components/ThreadCell.swift
@@ -123,7 +123,7 @@ struct ThreadCell: View {
Group {
if density == .large, let recipient = dataHolder.recipientToDisplay {
ZStack {
- RecipientImage(recipient: recipient)
+ AvatarView(avatarDisplayable: recipient, size: 40)
CheckboxView(isSelected: isSelected, density: density)
.opacity(isSelected ? 1 : 0)
}
diff --git a/Mail/Views/Bottom sheets/ContactActionsView.swift b/Mail/Views/Bottom sheets/ContactActionsView.swift
index 8ebd2b7a4..0b8ab97c1 100644
--- a/Mail/Views/Bottom sheets/ContactActionsView.swift
+++ b/Mail/Views/Bottom sheets/ContactActionsView.swift
@@ -64,7 +64,7 @@ struct ContactActionsView: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
- RecipientImage(recipient: recipient, size: 32)
+ AvatarView(avatarDisplayable: recipient, size: 32)
VStack(alignment: .leading) {
Text(recipient.contact?.name ?? recipient.formattedName)
.textStyle(.bodyMedium)
diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift
index eeb89dc91..90b7cfd26 100644
--- a/Mail/Views/SplitView.swift
+++ b/Mail/Views/SplitView.swift
@@ -44,7 +44,6 @@ public class SplitViewManager: ObservableObject {
@Published var showSearch = false
@Published var selectedFolder: Folder?
var splitViewController: UISplitViewController?
- @Published var avatarImage = MailResourcesAsset.placeholderAvatar.swiftUIImage
init(folder: Folder?) {
selectedFolder = folder
diff --git a/Mail/Views/Switch User/AccountCellView.swift b/Mail/Views/Switch User/AccountCellView.swift
index 371e37660..f17409259 100644
--- a/Mail/Views/Switch User/AccountCellView.swift
+++ b/Mail/Views/Switch User/AccountCellView.swift
@@ -70,15 +70,9 @@ struct AccountHeaderCell: View {
let account: Account
@Binding var isSelected: Bool
- @State private var avatarImage = MailResourcesAsset.placeholderAvatar.swiftUIImage
-
var body: some View {
HStack(spacing: 8) {
- avatarImage
- .resizable()
- .frame(width: 38, height: 38)
- .clipShape(Circle())
-
+ AvatarView(avatarDisplayable: account.user, size: 38)
VStack(alignment: .leading, spacing: 2) {
Text(account.user.displayName)
.textStyle(.bodyMedium)
@@ -93,9 +87,6 @@ struct AccountHeaderCell: View {
.foregroundColor(.accentColor)
}
}
- .task {
- avatarImage = await account.user.avatarImage
- }
}
}
diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift
index 90f4eb3b0..a293b4783 100644
--- a/Mail/Views/Switch User/AccountView.swift
+++ b/Mail/Views/Switch User/AccountView.swift
@@ -51,7 +51,6 @@ struct AccountView: View {
@LazyInjectService private var matomo: MatomoUtils
- @State private var avatarImage = MailResourcesAsset.placeholderAvatar.swiftUIImage
private let account = AccountManager.instance.currentAccount!
@State private var isShowingLogoutAlert = false
@State private var isShowingDeleteAccount = false
@@ -63,11 +62,7 @@ struct AccountView: View {
NavigationView {
VStack(spacing: 0) {
ScrollView {
- // Header
- avatarImage
- .resizable()
- .frame(width: 104, height: 104)
- .clipShape(Circle())
+ AvatarView(avatarDisplayable: account.user, size: 104)
.padding(.top, 24)
.padding(.bottom, 16)
@@ -127,9 +122,6 @@ struct AccountView: View {
Label(MailResourcesStrings.Localizable.buttonClose, systemImage: "xmark")
})
}
- .task {
- avatarImage = await account.user.avatarImage
- }
.sheet(isPresented: $isShowingDeleteAccount) {
DeleteAccountView(account: account, delegate: delegate)
}
diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift
index bf91ad20c..7abff9309 100644
--- a/Mail/Views/Thread List/ThreadListView.swift
+++ b/Mail/Views/Thread List/ThreadListView.swift
@@ -352,16 +352,7 @@ private struct ThreadListToolbar: ViewModifier {
Button {
isShowingSwitchAccount.toggle()
} label: {
- splitViewManager.avatarImage
- .resizable()
- .scaledToFit()
- .frame(width: 28, height: 28)
- .clipShape(Circle())
- }
- .task {
- if let account = AccountManager.instance.currentAccount {
- splitViewManager.avatarImage = await account.user.avatarImage
- }
+ AvatarView(avatarDisplayable: AccountManager.instance.currentAccount.user)
}
}
}
diff --git a/Mail/Views/Thread/MessageHeaderSummaryView.swift b/Mail/Views/Thread/MessageHeaderSummaryView.swift
index 80df5a55b..3ef8fcbdf 100644
--- a/Mail/Views/Thread/MessageHeaderSummaryView.swift
+++ b/Mail/Views/Thread/MessageHeaderSummaryView.swift
@@ -43,7 +43,7 @@ struct MessageHeaderSummaryView: View {
matomo.track(eventWithCategory: .message, name: "selectAvatar")
recipientTapped(recipient)
} label: {
- RecipientImage(recipient: recipient, size: 40)
+ AvatarView(avatarDisplayable: recipient, size: 40)
}
}
diff --git a/MailCore/Utils/Nuke+Authorization.swift b/MailCore/API/MailApiFetcher+Nuke.swift
similarity index 63%
rename from MailCore/Utils/Nuke+Authorization.swift
rename to MailCore/API/MailApiFetcher+Nuke.swift
index 20f6c9d5c..e81219c59 100644
--- a/MailCore/Utils/Nuke+Authorization.swift
+++ b/MailCore/API/MailApiFetcher+Nuke.swift
@@ -17,16 +17,24 @@
*/
import Foundation
+import InfomaniakCore
import Nuke
-extension ImagePipeline {
- public func imageWithAuthentication(for url: URL, delegate: (any ImageTaskDelegate)? = nil) async throws -> ImageResponse {
+public extension MailApiFetcher {
+ func avatarImageRequestForContact(_ contact: MergedContact) -> ImageRequest? {
+ guard let avatarPath = contact.remote?.avatar else { return nil }
+ let endpoint = Endpoint.resource(avatarPath)
+
+ return authenticatedImageRequest(endpoint.url)
+ }
+
+ func authenticatedImageRequest(_ url: URL) -> ImageRequest? {
var urlRequest = URLRequest(url: url)
urlRequest.addValue(
- "Bearer \(AccountManager.instance.currentAccount.token.accessToken)",
+ "Bearer \(currentToken?.accessToken ?? "")",
forHTTPHeaderField: "Authorization"
)
- return try await image(for: ImageRequest(urlRequest: urlRequest), delegate: delegate)
+ return ImageRequest(urlRequest: urlRequest)
}
}
diff --git a/MailCore/Cache/AccountManager.swift b/MailCore/Cache/AccountManager.swift
index f1827739a..267790310 100644
--- a/MailCore/Cache/AccountManager.swift
+++ b/MailCore/Cache/AccountManager.swift
@@ -59,34 +59,6 @@ public extension InfomaniakNetworkLoginable {
}
}
-public extension InfomaniakUser {
- var cachedAvatarImage: Image? {
- if let avatarURL = URL(string: avatar),
- let avatarUIImage = ImagePipeline.shared.cache[avatarURL]?.image {
- return Image(uiImage: avatarUIImage)
- }
-
- return nil
- }
-
- var avatarImage: Image {
- get async {
- if let avatarURL = URL(string: avatar),
- let avatarImage = try? await ImagePipeline.shared.image(for: avatarURL).image {
- return Image(uiImage: avatarImage)
- } else {
- let backgroundColor = UIColor.backgroundColor(from: id, with: UIConstants.avatarColors)
- let initialsImage = UIImage.getInitialsPlaceholder(
- with: displayName,
- size: CGSize(width: 40, height: 40),
- backgroundColor: backgroundColor
- )
- return Image(uiImage: initialsImage)
- }
- }
- }
-}
-
@globalActor actor AccountActor: GlobalActor {
static let shared = AccountActor()
diff --git a/MailCore/Models/MergedContact.swift b/MailCore/Models/MergedContact.swift
index e61f8e71a..0bd28ff40 100644
--- a/MailCore/Models/MergedContact.swift
+++ b/MailCore/Models/MergedContact.swift
@@ -25,10 +25,13 @@ import SwiftUI
import UIKit
extension CNContact {
- var image: UIImage? {
- guard let imageData = imageData else { return nil }
- let localImage = UIImage(data: imageData)
- return localImage
+ func pngImageData() async -> Data? {
+ // We have to load something that Nuke can cache
+ guard let imageData,
+ let convertedImage = UIImage(data: imageData)?.pngData() else {
+ return nil
+ }
+ return convertedImage
}
}
@@ -63,37 +66,40 @@ public class MergedContact {
return remote != nil
}
- public var hasAvatar: Bool {
- return local?.imageData != nil || remote?.avatar != nil
+ public init(email: String, remote: Contact?, local: CNContact?) {
+ self.email = email
+ self.remote = remote
+ self.local = local
}
+}
- public var avatarImage: Image? {
- get async {
- if let localImage = local?.image {
- return Image(uiImage: localImage)
- } else if let avatarPath = remote?.avatar,
- let avatarUIImage = try? await ImagePipeline.shared.imageWithAuthentication(for: Endpoint.resource(avatarPath).url).image {
- return Image(uiImage: avatarUIImage)
- }
+extension MergedContact: AvatarDisplayable {
+ public var avatarImageRequest: ImageRequest? {
+ if let localContact = local, localContact.imageDataAvailable {
+ var imageRequest = ImageRequest(id: localContact.identifier) {
+ guard let imageData = await localContact.pngImageData() else {
+ throw MailError.unknownError
+ }
- return nil
+ return imageData
+ }
+ imageRequest.options = [.disableDiskCache]
+ return imageRequest
}
- }
- public var cachedAvatarImage: Image? {
- if let localImage = local?.image {
- return Image(uiImage: localImage)
- } else if let avatarPath = remote?.avatar,
- let avatarUIImage = ImagePipeline.shared.cache[Endpoint.resource(avatarPath).url]?.image {
- return Image(uiImage: avatarUIImage)
+ if let remoteAvatar = remote?.avatar {
+ let avatarURL = Endpoint.resource(remoteAvatar).url
+ return AccountManager.instance.currentMailboxManager?.apiFetcher.authenticatedImageRequest(avatarURL)
}
return nil
}
- public init(email: String, remote: Contact?, local: CNContact?) {
- self.email = email
- self.remote = remote
- self.local = local
+ public var initials: String {
+ ""
+ }
+
+ public var initialsBackgroundColor: UIColor {
+ color
}
}
diff --git a/MailCore/Models/Recipient.swift b/MailCore/Models/Recipient.swift
index f4f376813..e43fa3d85 100644
--- a/MailCore/Models/Recipient.swift
+++ b/MailCore/Models/Recipient.swift
@@ -18,6 +18,7 @@
import Foundation
import MailResources
+import Nuke
import RealmSwift
import SwiftUI
@@ -107,30 +108,17 @@ public class Recipient: EmbeddedObject, Codable {
return "\(name) \(emailString)"
}
}
+}
- public var avatarImage: Image? {
- get async {
- if isCurrentUser && isMe {
- return await AccountManager.instance.currentAccount.user.avatarImage
- } else if let contact = contact,
- contact.hasAvatar,
- let avatarImage = await contact.avatarImage {
- return avatarImage
- } else {
- return nil
- }
+extension Recipient: AvatarDisplayable {
+ public var avatarImageRequest: ImageRequest? {
+ guard !(isCurrentUser && isMe) else {
+ return AccountManager.instance.currentAccount.user.avatarImageRequest
}
+ return contact?.avatarImageRequest
}
- public var cachedAvatarImage: Image? {
- if isCurrentUser && isMe {
- return AccountManager.instance.currentAccount.user.cachedAvatarImage
- } else if let contact = contact,
- contact.hasAvatar,
- let avatarImage = contact.cachedAvatarImage {
- return avatarImage
- } else {
- return nil
- }
+ public var initialsBackgroundColor: UIColor {
+ color
}
}
diff --git a/MailCore/Utils/AvatarDisplayable.swift b/MailCore/Utils/AvatarDisplayable.swift
new file mode 100644
index 000000000..12959c7a9
--- /dev/null
+++ b/MailCore/Utils/AvatarDisplayable.swift
@@ -0,0 +1,44 @@
+/*
+ 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
+import SwiftUI
+import UIKit
+
+public protocol AvatarDisplayable {
+ var avatarImageRequest: ImageRequest? { get }
+ var initials: String { get }
+ var initialsBackgroundColor: UIColor { get }
+}
+
+extension UserProfile: AvatarDisplayable {
+ public var avatarImageRequest: ImageRequest? {
+ guard let avatarURL = URL(string: avatar) else { return nil }
+ return ImageRequest(url: avatarURL)
+ }
+
+ public var initials: String {
+ displayName.initials
+ }
+
+ public var initialsBackgroundColor: UIColor {
+ UIColor.backgroundColor(from: id, with: UIConstants.avatarColors)
+ }
+}
diff --git a/MailCore/Utils/LocalContactsHelper.swift b/MailCore/Utils/LocalContactsHelper.swift
index 230c4325d..b73180a59 100644
--- a/MailCore/Utils/LocalContactsHelper.swift
+++ b/MailCore/Utils/LocalContactsHelper.swift
@@ -37,7 +37,14 @@ class LocalContactsHelper {
static let shared = LocalContactsHelper()
let store = CNContactStore()
- let keysToFetch = ([CNContactIdentifierKey, CNContactEmailAddressesKey, CNContactImageDataKey, CNContactNicknameKey, CNContactOrganizationNameKey] as [CNKeyDescriptor]) + [CNContactFormatter.descriptorForRequiredKeys(for: .fullName)]
+ let keysToFetch = ([
+ CNContactIdentifierKey,
+ CNContactEmailAddressesKey,
+ CNContactImageDataAvailableKey,
+ CNContactImageDataKey,
+ CNContactNicknameKey,
+ CNContactOrganizationNameKey
+ ] as [CNKeyDescriptor]) + [CNContactFormatter.descriptorForRequiredKeys(for: .fullName)]
func enumerateContacts(usingBlock: @escaping (CNContact, UnsafeMutablePointer) -> Void) async {
do {
diff --git a/Project.swift b/Project.swift
index 9327e30a3..a774f99bd 100644
--- a/Project.swift
+++ b/Project.swift
@@ -45,7 +45,7 @@ let project = Project(name: "Mail",
.package(url: "https://github.com/markiv/SwiftUI-Shimmer", .upToNextMajor(from: "1.0.1")),
.package(url: "https://github.com/dkk/WrappingHStack", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/SCENEE/FloatingPanel", .upToNextMajor(from: "2.0.0")),
- .package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "11.3.0")),
+ .package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "12.0.0")),
.package(url: "https://github.com/airbnb/lottie-ios", .exact("3.5.0")),
.package(url: "https://github.com/scinfu/SwiftSoup", .upToNextMajor(from: "2.5.3"))
],
@@ -164,6 +164,7 @@ let project = Project(name: "Mail",
.package(product: "RealmSwift"),
.package(product: "SwiftRegex"),
.package(product: "Nuke"),
+ .package(product: "NukeUI"),
.package(product: "SwiftSoup")
],
settings: .settings(base: baseSettings)