Add support for Space avatars. (#4362)

This commit is contained in:
Doug
2025-07-29 12:20:13 +01:00
committed by GitHub
parent 75bc26d20b
commit 01033cf53e
10 changed files with 97 additions and 44 deletions
@@ -11,20 +11,24 @@ struct LoadableAvatarImage: View {
private let url: URL?
private let name: String?
private let contentID: String?
private let isSpace: Bool
private let avatarSize: Avatars.Size
private let mediaProvider: MediaProviderProtocol?
private let onTap: ((URL) -> Void)?
@ScaledMetric private var frameSize: CGFloat
init(url: URL?, name: String?,
init(url: URL?,
name: String?,
contentID: String?,
isSpace: Bool = false,
avatarSize: Avatars.Size,
mediaProvider: MediaProviderProtocol?,
onTap: ((URL) -> Void)? = nil) {
self.url = url
self.name = name
self.contentID = contentID
self.isSpace = isSpace
self.avatarSize = avatarSize
self.mediaProvider = mediaProvider
self.onTap = onTap
@@ -49,10 +53,14 @@ struct LoadableAvatarImage: View {
avatar
.frame(width: frameSize, height: frameSize)
.background(Color.compound.bgCanvasDefault)
.clipShape(Circle())
.clipShape(avatarShape)
.environment(\.shouldAutomaticallyLoadImages, true) // We always load avatars.
}
private var avatarShape: some Shape {
isSpace ? AnyShape(RoundedRectangle(cornerRadius: frameSize / 4)) : AnyShape(Circle())
}
@ViewBuilder
private var avatar: some View {
if let url {
@@ -9,10 +9,12 @@ import SwiftUI
/// Information about a room avatar such as it's URL or the heroes to use as a fallback.
enum RoomAvatar: Equatable {
/// An avatar generated from the room's details.
/// An avatar generated from a Room's details.
case room(id: String, name: String?, avatarURL: URL?)
/// An avatar generated from the room's heroes.
/// An avatar generated from a collection of room heroes.
case heroes([UserProfileProxy])
/// An avatar generated from a Space's details.
case space(id: String, name: String?, avatarURL: URL?)
/// A static avatar for a tombstoned room.
case tombstoned
@@ -22,6 +24,8 @@ enum RoomAvatar: Equatable {
.room(id: id, name: name, avatarURL: nil)
case let .heroes(users):
.heroes(users.map { .init(userID: $0.userID, displayName: $0.displayName, avatarURL: nil) })
case .space(let id, let name, _):
.space(id: id, name: name, avatarURL: nil)
case .tombstoned:
.tombstoned
}
@@ -29,7 +33,8 @@ enum RoomAvatar: Equatable {
var hasURL: Bool {
switch self {
case let .room(_, _, url):
case let .room(_, _, url),
let .space(_, _, url):
return url != nil
case let .heroes(heroes):
return heroes.first?.avatarURL != nil
@@ -106,6 +111,14 @@ struct RoomAvatarImage: View {
mediaProvider: mediaProvider,
onTap: onAvatarTap)
}
case .space(let id, let name, let avatarURL):
LoadableAvatarImage(url: avatarURL,
name: name,
contentID: id,
isSpace: true,
avatarSize: avatarSize,
mediaProvider: mediaProvider,
onTap: onAvatarTap)
case .tombstoned:
TombstonedAvatarImage(avatarSize: avatarSize)
}
@@ -114,37 +127,53 @@ struct RoomAvatarImage: View {
struct RoomAvatarImage_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
HStack(spacing: 8) {
RoomAvatarImage(avatar: .room(id: "!1:server.com",
name: "Room",
avatarURL: nil),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
VStack(spacing: 20) {
HStack(spacing: 12) {
RoomAvatarImage(avatar: .room(id: "!1:server.com",
name: "Room",
avatarURL: nil),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .room(id: "!2:server.com",
name: "Room",
avatarURL: .mockMXCAvatar),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .space(id: "!space:server.com",
name: "Room",
avatarURL: nil),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .space(id: "!otherspace:server.com",
name: "Room",
avatarURL: .mockMXCAvatar),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .tombstoned, avatarSize: .room(on: .home), mediaProvider: MediaProviderMock(configuration: .init()))
}
RoomAvatarImage(avatar: .room(id: "!2:server.com",
name: "Room",
avatarURL: .mockMXCAvatar),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
displayName: "User",
avatarURL: nil)]),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
displayName: "User",
avatarURL: .mockMXCAvatar)]),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .heroes([.init(userID: "@alice:server.com", displayName: "Alice", avatarURL: nil),
.init(userID: "@bob:server.net", displayName: "Bob", avatarURL: nil)]),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .tombstoned, avatarSize: .room(on: .home), mediaProvider: MediaProviderMock(configuration: .init()))
HStack(spacing: 12) {
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
displayName: "User",
avatarURL: nil)]),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
displayName: "User",
avatarURL: .mockMXCAvatar)]),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .heroes([.init(userID: "@alice:server.com", displayName: "Alice", avatarURL: nil),
.init(userID: "@bob:server.net", displayName: "Bob", avatarURL: nil)]),
avatarSize: .room(on: .home),
mediaProvider: MediaProviderMock(configuration: .init()))
}
}
}
}
@@ -69,6 +69,10 @@ extension BaseRoomInfoProxyProtocol {
return .tombstoned
}
if isSpace {
return .space(id: id, name: displayName, avatarURL: avatarURL)
}
if isDirect, avatarURL == nil, heroes.count == 1 {
return .heroes(heroes.map(UserProfileProxy.init))
}
@@ -134,6 +134,11 @@ extension RoomSummary {
return .tombstoned
}
// NOTE: The check for isSpace isn't implemented yet, waiting to see
// whether we end up with a different type for spaces in the room list.
//
// Don't forget to add a test when you remove this comment 😄
if isDirect, avatarURL == nil, heroes.count == 1 {
return .heroes(heroes)
} else {
@@ -570,7 +570,8 @@ class TimelineController: TimelineControllerProtocol {
attachments: nil)
let avatarURL: URL? = switch roomProxy.details.avatar {
case .room(_, _, let avatarURL):
case .room(_, _, let avatarURL),
.space(_, _, let avatarURL):
avatarURL
case .heroes(let userProfiles):
userProfiles.first?.avatarURL
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1896e5328d738494310900bf5d0f4a7441bb70f91103a20bce20ca064b6e6d5e
size 103499
oid sha256:e28d36588f155e868a9b24d78f7688c64fac91e57a81b835dd111687c265428b
size 132520
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1896e5328d738494310900bf5d0f4a7441bb70f91103a20bce20ca064b6e6d5e
size 103499
oid sha256:e28d36588f155e868a9b24d78f7688c64fac91e57a81b835dd111687c265428b
size 132520
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d84daf5a9f2f6caed69fca648e8f677b3a1055f238d482bbfd7ede5769605bb6
size 58978
oid sha256:4d5acb74a871dc05d9112d6c5ee88d26f2ed2a489995bf9800f9c644236576f9
size 85999
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d84daf5a9f2f6caed69fca648e8f677b3a1055f238d482bbfd7ede5769605bb6
size 58978
oid sha256:4d5acb74a871dc05d9112d6c5ee88d26f2ed2a489995bf9800f9c644236576f9
size 85999
+6
View File
@@ -24,6 +24,8 @@ class RoomSummaryTests: XCTestCase {
XCTAssertEqual(avatarURL, roomDetails.avatarURL)
case .heroes:
XCTFail("A room shouldn't use the heroes for its avatar.")
case .space:
XCTFail("A room shouldn't use a space avatar.")
case .tombstoned:
XCTFail("A room shouldn't use the tombstone for its avatar.")
}
@@ -39,6 +41,8 @@ class RoomSummaryTests: XCTestCase {
XCTAssertEqual(avatarURL, roomDetails.avatarURL)
case .heroes:
XCTFail("A DM with an avatar set shouldn't use the heroes instead.")
case .space:
XCTFail("A DM shouldn't use a space avatar.")
case .tombstoned:
XCTFail("A room shouldn't use the tombstone for its avatar.")
}
@@ -52,6 +56,8 @@ class RoomSummaryTests: XCTestCase {
XCTFail("A DM without an avatar should defer to the hero for the correct placeholder tint colour.")
case .heroes(let heroes):
XCTAssertEqual(heroes, self.heroes)
case .space:
XCTFail("A DM shouldn't use a space avatar.")
case .tombstoned:
XCTFail("A room shouldn't use the tombstone for its avatar.")
}