diff --git a/ElementX/Sources/Other/Avatars.swift b/ElementX/Sources/Other/Avatars.swift index f90e3cdfc..29711a08c 100644 --- a/ElementX/Sources/Other/Avatars.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -8,7 +8,6 @@ import Foundation import SwiftUI -import UIKit enum Avatars { enum Size { @@ -39,33 +38,6 @@ 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() - // UIImage(data:) decodes at scale 1; pin the renderer to that so the - // output isn't silently multiplied by the device's screen scale. - format.scale = image.scale - - return UIGraphicsImageRenderer(size: CGSize(width: side, height: side), format: format) - .pngData { _ in - // Round the offsets to pixel boundaries so the crop is a straight - // copy rather than resampling (and softening) the whole image. - image.draw(at: CGPoint(x: ((side - image.size.width) / 2).rounded(), - y: ((side - image.size.height) / 2).rounded())) - } - } - @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 7e6058515..1f9735ad1 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -652,7 +652,7 @@ class TimelineController: TimelineControllerProtocol { if let avatarURL, let mediaSource = try? MediaSourceProxy(url: avatarURL, mimeType: nil) { // Request a generous size so the square crop of a non-square avatar keeps enough resolution. if case let .success(avatarData) = await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 256, height: 256)) { - if let squareAvatarData = Avatars.squareAvatarImageData(from: avatarData) { + if let squareAvatarData = Self.squareAvatarImageData(from: avatarData) { sendMessageIntent.setImage(INImage(imageData: squareAvatarData), forParameterNamed: \.speakableGroupName) } else { MXLog.error("Failed processing the room avatar for the send message intent, using a placeholder.") @@ -675,6 +675,35 @@ class TimelineController: TimelineControllerProtocol { } } +private extension TimelineController { + /// Media repo thumbnails are scaled, not cropped, so they keep the source's aspect + /// ratio. The share sheet's conversation suggestions stretch the donated image to fill + /// their circle, so centre-crop it to a square first. + 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() + // UIImage(data:) decodes at scale 1; pin the renderer to that so the + // output isn't silently multiplied by the device's screen scale. + format.scale = image.scale + + return UIGraphicsImageRenderer(size: CGSize(width: side, height: side), format: format) + .pngData { _ in + // Round the offsets to pixel boundaries so the crop is a straight + // copy rather than resampling (and softening) the whole image. + image.draw(at: CGPoint(x: ((side - image.size.width) / 2).rounded(), + y: ((side - image.size.height) / 2).rounded())) + } + } +} + private nonisolated extension TimelineItemProxy { var isItemCollapsible: Bool { if case let .event(eventItem) = self { diff --git a/NSE/Sources/NotificationContentBuilder.swift b/NSE/Sources/NotificationContentBuilder.swift index dc0b79e40..c4ed82f6b 100644 --- a/NSE/Sources/NotificationContentBuilder.swift +++ b/NSE/Sources/NotificationContentBuilder.swift @@ -257,14 +257,9 @@ nonisolated struct NotificationContentBuilder { let image: INImage if !forcePlaceholder, let mediaSource = icon.mediaSource { - // Request a generous size so the square crop of a non-square avatar keeps enough resolution. - switch await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 256, height: 256)) { + switch await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 100, height: 100)) { case .success(let data): - if let squareData = Avatars.squareAvatarImageData(from: data) { - fetchedImage = INImage(imageData: squareData) - } else { - MXLog.error("Couldn't decode the fetched sender icon, using a placeholder.") - } + fetchedImage = INImage(imageData: data) case .failure(let error): MXLog.error("Couldn't add sender icon: \(error)") }