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 44712d8c62
commit da0fce5123
5 changed files with 34 additions and 26 deletions

View File

@@ -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))
}
}
}

View File

@@ -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)
}

View File

@@ -116,7 +116,7 @@ struct RoomAvatarImage: View {
LoadableAvatarImage(url: avatarURL,
name: name,
contentID: id,
isSpace: true,
shape: .roundedRect,
avatarSize: avatarSize,
mediaProvider: mediaProvider,
onTap: onAvatarTap)

View File

@@ -60,7 +60,7 @@ struct RoomDetailsEditScreen: View {
url: context.viewState.avatarURL,
name: context.viewState.initialName,
contentID: context.viewState.roomID,
isSpace: context.viewState.isSpace,
shape: context.viewState.isSpace ? .roundedRect : .circle,
avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider)
.accessibilityLabel(L10n.a11yEditAvatar)

View File

@@ -64,7 +64,7 @@ struct UserDetailsEditScreen: View {
url: context.viewState.selectedAvatarURL,
name: context.viewState.currentDisplayName,
contentID: context.viewState.userID,
isSpace: false,
shape: .circle,
avatarSize: .user(on: .editUserDetails),
mediaProvider: context.mediaProvider)
.overlay(alignment: .bottomTrailing) {