Fix broken avatar aspect ratio in the share sheet and notifications

The media repo serves thumbnails with the scale method, so avatars keep
their source aspect ratio. The system stretches whatever INImage we
donate to fill a circle, distorting any non-square avatar in the share
sheet's conversation suggestions (and notifications, which are donated
the same image).

Centre-crop donated avatars to a square before handing them to the
system, in both the timeline's send message intent donation and the
NSE's communication context.
This commit is contained in:
Matthew Hodgson
2026-08-03 22:02:08 +03:00
committed by Mauro
parent a82726a991
commit 39805d080e
3 changed files with 29 additions and 3 deletions
+23
View File
@@ -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)
@@ -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()
}
+3 -1
View File
@@ -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)")
}