Add a rounded rect border around space avatars

.. and also refactor the LoadableAvatarImage to stop having knowledge of spaces and instead focus on generic shapes.
This commit is contained in:
Stefan Ceriu
2026-02-10 14:12:40 +02:00
committed by Stefan Ceriu
parent 20a84dd896
commit 591ce3975b
5 changed files with 34 additions and 26 deletions
@@ -9,10 +9,15 @@
import SwiftUI
struct LoadableAvatarImage: View {
enum Shape {
case circle
case roundedRect
}
private let url: URL?
private let name: String?
private let contentID: String
private let isSpace: Bool
private let shape: Shape
private let avatarSize: Avatars.Size
private let mediaProvider: MediaProviderProtocol?
private let onTap: ((URL) -> Void)?
@@ -22,14 +27,14 @@ struct LoadableAvatarImage: View {
init(url: URL?,
name: String?,
contentID: String,
isSpace: Bool = false,
shape: LoadableAvatarImage.Shape = .circle,
avatarSize: Avatars.Size,
mediaProvider: MediaProviderProtocol?,
onTap: ((URL) -> Void)? = nil) {
self.url = url
self.name = name
self.contentID = contentID
self.isSpace = isSpace
self.shape = shape
self.avatarSize = avatarSize
self.mediaProvider = mediaProvider
self.onTap = onTap
@@ -54,7 +59,7 @@ struct LoadableAvatarImage: View {
avatar
.frame(width: frameSize, height: frameSize)
.background(Color.compound.bgCanvasDefault)
.clipAvatar(isSpace: isSpace, scaledSize: _frameSize)
.avatarShape(shape, scaledSize: _frameSize)
.environment(\.shouldAutomaticallyLoadImages, true) // We always load avatars.
}
@@ -77,35 +82,38 @@ struct LoadableAvatarImage: View {
}
extension View {
func clipAvatar(isSpace: Bool, size: CGFloat) -> some View {
modifier(ClipAvatarModifier(isSpace: isSpace, size: size))
func avatarShape(_ shape: LoadableAvatarImage.Shape, size: CGFloat) -> some View {
modifier(AvatarShapeModifier(shape: shape, size: size))
}
func clipAvatar(isSpace: Bool, scaledSize: ScaledMetric<CGFloat>) -> some View {
modifier(ClipAvatarModifier(isSpace: isSpace, scaledSize: scaledSize))
func avatarShape(_ shape: LoadableAvatarImage.Shape, scaledSize: ScaledMetric<CGFloat>) -> some View {
modifier(AvatarShapeModifier(shape: shape, scaledSize: scaledSize))
}
}
struct ClipAvatarModifier: ViewModifier {
private let isSpace: Bool
private struct AvatarShapeModifier: ViewModifier {
private let shape: LoadableAvatarImage.Shape
@ScaledMetric private var scaledSize: CGFloat
init(isSpace: Bool, size: CGFloat) {
self.isSpace = isSpace
init(shape: LoadableAvatarImage.Shape, size: CGFloat) {
self.shape = shape
_scaledSize = ScaledMetric(wrappedValue: size)
}
init(isSpace: Bool, scaledSize: ScaledMetric<CGFloat>) {
self.isSpace = isSpace
init(shape: LoadableAvatarImage.Shape, scaledSize: ScaledMetric<CGFloat>) {
self.shape = shape
_scaledSize = scaledSize
}
func body(content: Content) -> some View {
content
.clipShape(avatarShape)
}
private var avatarShape: some Shape {
isSpace ? AnyShape(RoundedRectangle(cornerRadius: scaledSize / 4)) : AnyShape(Circle())
switch shape {
case .circle:
content.clipShape(Circle())
case .roundedRect:
let shape = RoundedRectangle(cornerRadius: scaledSize / 4)
content
.clipShape(shape)
.overlay(shape.stroke(.compound.iconQuaternaryAlpha, lineWidth: 1))
}
}
}
@@ -14,7 +14,7 @@ struct OverridableAvatarImage: View {
let url: URL?
let name: String?
let contentID: String
let isSpace: Bool
let shape: LoadableAvatarImage.Shape
let avatarSize: Avatars.Size
let mediaProvider: MediaProviderProtocol?
@@ -28,12 +28,12 @@ struct OverridableAvatarImage: View {
ProgressView()
}
.scaledFrame(size: avatarSize.value)
.clipAvatar(isSpace: isSpace, size: avatarSize.value)
.avatarShape(shape, size: avatarSize.value)
} else {
LoadableAvatarImage(url: url,
name: name,
contentID: contentID,
isSpace: isSpace,
shape: shape,
avatarSize: avatarSize,
mediaProvider: mediaProvider)
}
@@ -116,7 +116,7 @@ struct RoomAvatarImage: View {
LoadableAvatarImage(url: avatarURL,
name: name,
contentID: id,
isSpace: true,
shape: .roundedRect,
avatarSize: avatarSize,
mediaProvider: mediaProvider,
onTap: onAvatarTap)