diff --git a/ElementX/Sources/Other/Avatars.swift b/ElementX/Sources/Other/Avatars.swift index 29711a08c..39f4857d8 100644 --- a/ElementX/Sources/Other/Avatars.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI +import UIKit enum Avatars { enum Size { @@ -38,6 +39,28 @@ enum Avatars { } } + /// Media repo thumbnails are scaled, not cropped, so they keep the source's aspect + /// ratio. System UI (share sheet suggestions, notifications) stretches non-square + /// images to fill its circle, so centre-crop to a square before donating them. + nonisolated static func squareAvatarImageData(from data: Data) -> Data? { + guard let image = UIImage(data: data) else { return nil } + + let side = min(image.size.width, image.size.height) + guard side > 0 else { return nil } + + if image.size.width == image.size.height { + return data + } + + let format = UIGraphicsImageRendererFormat() + format.scale = image.scale + + return UIGraphicsImageRenderer(size: CGSize(width: side, height: side), format: format) + .pngData { _ in + image.draw(at: CGPoint(x: (side - image.size.width) / 2, y: (side - image.size.height) / 2)) + } + } + @MainActor static func generatePlaceholderAvatarImageData(name: String, id: String, size: CGSize) -> Data? { let image = PlaceholderAvatarImage(name: name, contentID: id) diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index a568d958f..c22dc19ae 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -650,8 +650,9 @@ class TimelineController: TimelineControllerProtocol { } if let avatarURL, let mediaSource = try? MediaSourceProxy(url: avatarURL, mimeType: nil) { - if case let .success(avatarData) = await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 100, height: 100)) { - sendMessageIntent.setImage(INImage(imageData: avatarData), forParameterNamed: \.speakableGroupName) + if case let .success(avatarData) = await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 100, height: 100)), + let squareAvatarData = Avatars.squareAvatarImageData(from: avatarData) { + sendMessageIntent.setImage(INImage(imageData: squareAvatarData), forParameterNamed: \.speakableGroupName) } else { addPlacehoder() } diff --git a/NSE/Sources/NotificationContentBuilder.swift b/NSE/Sources/NotificationContentBuilder.swift index c4ed82f6b..7bb218394 100644 --- a/NSE/Sources/NotificationContentBuilder.swift +++ b/NSE/Sources/NotificationContentBuilder.swift @@ -259,7 +259,9 @@ nonisolated struct NotificationContentBuilder { let mediaSource = icon.mediaSource { switch await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 100, height: 100)) { case .success(let data): - fetchedImage = INImage(imageData: data) + if let squareData = Avatars.squareAvatarImageData(from: data) { + fetchedImage = INImage(imageData: squareData) + } case .failure(let error): MXLog.error("Couldn't add sender icon: \(error)") }