mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Add support for Space avatars. (#4362)
This commit is contained in:
@@ -11,20 +11,24 @@ struct LoadableAvatarImage: View {
|
|||||||
private let url: URL?
|
private let url: URL?
|
||||||
private let name: String?
|
private let name: String?
|
||||||
private let contentID: String?
|
private let contentID: String?
|
||||||
|
private let isSpace: Bool
|
||||||
private let avatarSize: Avatars.Size
|
private let avatarSize: Avatars.Size
|
||||||
private let mediaProvider: MediaProviderProtocol?
|
private let mediaProvider: MediaProviderProtocol?
|
||||||
private let onTap: ((URL) -> Void)?
|
private let onTap: ((URL) -> Void)?
|
||||||
|
|
||||||
@ScaledMetric private var frameSize: CGFloat
|
@ScaledMetric private var frameSize: CGFloat
|
||||||
|
|
||||||
init(url: URL?, name: String?,
|
init(url: URL?,
|
||||||
|
name: String?,
|
||||||
contentID: String?,
|
contentID: String?,
|
||||||
|
isSpace: Bool = false,
|
||||||
avatarSize: Avatars.Size,
|
avatarSize: Avatars.Size,
|
||||||
mediaProvider: MediaProviderProtocol?,
|
mediaProvider: MediaProviderProtocol?,
|
||||||
onTap: ((URL) -> Void)? = nil) {
|
onTap: ((URL) -> Void)? = nil) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.name = name
|
self.name = name
|
||||||
self.contentID = contentID
|
self.contentID = contentID
|
||||||
|
self.isSpace = isSpace
|
||||||
self.avatarSize = avatarSize
|
self.avatarSize = avatarSize
|
||||||
self.mediaProvider = mediaProvider
|
self.mediaProvider = mediaProvider
|
||||||
self.onTap = onTap
|
self.onTap = onTap
|
||||||
@@ -49,10 +53,14 @@ struct LoadableAvatarImage: View {
|
|||||||
avatar
|
avatar
|
||||||
.frame(width: frameSize, height: frameSize)
|
.frame(width: frameSize, height: frameSize)
|
||||||
.background(Color.compound.bgCanvasDefault)
|
.background(Color.compound.bgCanvasDefault)
|
||||||
.clipShape(Circle())
|
.clipShape(avatarShape)
|
||||||
.environment(\.shouldAutomaticallyLoadImages, true) // We always load avatars.
|
.environment(\.shouldAutomaticallyLoadImages, true) // We always load avatars.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var avatarShape: some Shape {
|
||||||
|
isSpace ? AnyShape(RoundedRectangle(cornerRadius: frameSize / 4)) : AnyShape(Circle())
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var avatar: some View {
|
private var avatar: some View {
|
||||||
if let url {
|
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.
|
/// Information about a room avatar such as it's URL or the heroes to use as a fallback.
|
||||||
enum RoomAvatar: Equatable {
|
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?)
|
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])
|
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.
|
/// A static avatar for a tombstoned room.
|
||||||
case tombstoned
|
case tombstoned
|
||||||
|
|
||||||
@@ -22,6 +24,8 @@ enum RoomAvatar: Equatable {
|
|||||||
.room(id: id, name: name, avatarURL: nil)
|
.room(id: id, name: name, avatarURL: nil)
|
||||||
case let .heroes(users):
|
case let .heroes(users):
|
||||||
.heroes(users.map { .init(userID: $0.userID, displayName: $0.displayName, avatarURL: nil) })
|
.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:
|
case .tombstoned:
|
||||||
.tombstoned
|
.tombstoned
|
||||||
}
|
}
|
||||||
@@ -29,7 +33,8 @@ enum RoomAvatar: Equatable {
|
|||||||
|
|
||||||
var hasURL: Bool {
|
var hasURL: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case let .room(_, _, url):
|
case let .room(_, _, url),
|
||||||
|
let .space(_, _, url):
|
||||||
return url != nil
|
return url != nil
|
||||||
case let .heroes(heroes):
|
case let .heroes(heroes):
|
||||||
return heroes.first?.avatarURL != nil
|
return heroes.first?.avatarURL != nil
|
||||||
@@ -106,6 +111,14 @@ struct RoomAvatarImage: View {
|
|||||||
mediaProvider: mediaProvider,
|
mediaProvider: mediaProvider,
|
||||||
onTap: onAvatarTap)
|
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:
|
case .tombstoned:
|
||||||
TombstonedAvatarImage(avatarSize: avatarSize)
|
TombstonedAvatarImage(avatarSize: avatarSize)
|
||||||
}
|
}
|
||||||
@@ -114,37 +127,53 @@ struct RoomAvatarImage: View {
|
|||||||
|
|
||||||
struct RoomAvatarImage_Previews: PreviewProvider, TestablePreview {
|
struct RoomAvatarImage_Previews: PreviewProvider, TestablePreview {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
HStack(spacing: 8) {
|
VStack(spacing: 20) {
|
||||||
RoomAvatarImage(avatar: .room(id: "!1:server.com",
|
HStack(spacing: 12) {
|
||||||
name: "Room",
|
RoomAvatarImage(avatar: .room(id: "!1:server.com",
|
||||||
avatarURL: nil),
|
name: "Room",
|
||||||
avatarSize: .room(on: .home),
|
avatarURL: nil),
|
||||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
avatarSize: .room(on: .home),
|
||||||
|
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||||
|
|
||||||
RoomAvatarImage(avatar: .room(id: "!2:server.com",
|
RoomAvatarImage(avatar: .room(id: "!2:server.com",
|
||||||
name: "Room",
|
name: "Room",
|
||||||
avatarURL: .mockMXCAvatar),
|
avatarURL: .mockMXCAvatar),
|
||||||
avatarSize: .room(on: .home),
|
avatarSize: .room(on: .home),
|
||||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||||
|
|
||||||
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
|
RoomAvatarImage(avatar: .space(id: "!space:server.com",
|
||||||
displayName: "User",
|
name: "Room",
|
||||||
avatarURL: nil)]),
|
avatarURL: nil),
|
||||||
avatarSize: .room(on: .home),
|
avatarSize: .room(on: .home),
|
||||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||||
|
|
||||||
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
|
RoomAvatarImage(avatar: .space(id: "!otherspace:server.com",
|
||||||
displayName: "User",
|
name: "Room",
|
||||||
avatarURL: .mockMXCAvatar)]),
|
avatarURL: .mockMXCAvatar),
|
||||||
avatarSize: .room(on: .home),
|
avatarSize: .room(on: .home),
|
||||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||||
|
|
||||||
RoomAvatarImage(avatar: .heroes([.init(userID: "@alice:server.com", displayName: "Alice", avatarURL: nil),
|
RoomAvatarImage(avatar: .tombstoned, avatarSize: .room(on: .home), mediaProvider: MediaProviderMock(configuration: .init()))
|
||||||
.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
|
return .tombstoned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isSpace {
|
||||||
|
return .space(id: id, name: displayName, avatarURL: avatarURL)
|
||||||
|
}
|
||||||
|
|
||||||
if isDirect, avatarURL == nil, heroes.count == 1 {
|
if isDirect, avatarURL == nil, heroes.count == 1 {
|
||||||
return .heroes(heroes.map(UserProfileProxy.init))
|
return .heroes(heroes.map(UserProfileProxy.init))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,11 @@ extension RoomSummary {
|
|||||||
return .tombstoned
|
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 {
|
if isDirect, avatarURL == nil, heroes.count == 1 {
|
||||||
return .heroes(heroes)
|
return .heroes(heroes)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -570,7 +570,8 @@ class TimelineController: TimelineControllerProtocol {
|
|||||||
attachments: nil)
|
attachments: nil)
|
||||||
|
|
||||||
let avatarURL: URL? = switch roomProxy.details.avatar {
|
let avatarURL: URL? = switch roomProxy.details.avatar {
|
||||||
case .room(_, _, let avatarURL):
|
case .room(_, _, let avatarURL),
|
||||||
|
.space(_, _, let avatarURL):
|
||||||
avatarURL
|
avatarURL
|
||||||
case .heroes(let userProfiles):
|
case .heroes(let userProfiles):
|
||||||
userProfiles.first?.avatarURL
|
userProfiles.first?.avatarURL
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1896e5328d738494310900bf5d0f4a7441bb70f91103a20bce20ca064b6e6d5e
|
oid sha256:e28d36588f155e868a9b24d78f7688c64fac91e57a81b835dd111687c265428b
|
||||||
size 103499
|
size 132520
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1896e5328d738494310900bf5d0f4a7441bb70f91103a20bce20ca064b6e6d5e
|
oid sha256:e28d36588f155e868a9b24d78f7688c64fac91e57a81b835dd111687c265428b
|
||||||
size 103499
|
size 132520
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d84daf5a9f2f6caed69fca648e8f677b3a1055f238d482bbfd7ede5769605bb6
|
oid sha256:4d5acb74a871dc05d9112d6c5ee88d26f2ed2a489995bf9800f9c644236576f9
|
||||||
size 58978
|
size 85999
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d84daf5a9f2f6caed69fca648e8f677b3a1055f238d482bbfd7ede5769605bb6
|
oid sha256:4d5acb74a871dc05d9112d6c5ee88d26f2ed2a489995bf9800f9c644236576f9
|
||||||
size 58978
|
size 85999
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class RoomSummaryTests: XCTestCase {
|
|||||||
XCTAssertEqual(avatarURL, roomDetails.avatarURL)
|
XCTAssertEqual(avatarURL, roomDetails.avatarURL)
|
||||||
case .heroes:
|
case .heroes:
|
||||||
XCTFail("A room shouldn't use the heroes for its avatar.")
|
XCTFail("A room shouldn't use the heroes for its avatar.")
|
||||||
|
case .space:
|
||||||
|
XCTFail("A room shouldn't use a space avatar.")
|
||||||
case .tombstoned:
|
case .tombstoned:
|
||||||
XCTFail("A room shouldn't use the tombstone for its avatar.")
|
XCTFail("A room shouldn't use the tombstone for its avatar.")
|
||||||
}
|
}
|
||||||
@@ -39,6 +41,8 @@ class RoomSummaryTests: XCTestCase {
|
|||||||
XCTAssertEqual(avatarURL, roomDetails.avatarURL)
|
XCTAssertEqual(avatarURL, roomDetails.avatarURL)
|
||||||
case .heroes:
|
case .heroes:
|
||||||
XCTFail("A DM with an avatar set shouldn't use the heroes instead.")
|
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:
|
case .tombstoned:
|
||||||
XCTFail("A room shouldn't use the tombstone for its avatar.")
|
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.")
|
XCTFail("A DM without an avatar should defer to the hero for the correct placeholder tint colour.")
|
||||||
case .heroes(let heroes):
|
case .heroes(let heroes):
|
||||||
XCTAssertEqual(heroes, self.heroes)
|
XCTAssertEqual(heroes, self.heroes)
|
||||||
|
case .space:
|
||||||
|
XCTFail("A DM shouldn't use a space avatar.")
|
||||||
case .tombstoned:
|
case .tombstoned:
|
||||||
XCTFail("A room shouldn't use the tombstone for its avatar.")
|
XCTFail("A room shouldn't use the tombstone for its avatar.")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user