mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Use the room heroes when computing a DM avatar. (#2900)
* Use room heroes for DM avatar content ID. * Use RoomAvatar.heroes for the DM Details stack.
This commit is contained in:
@@ -17,28 +17,9 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AvatarHeaderView<Footer: View>: View {
|
||||
private struct AvatarInfo {
|
||||
let id: String
|
||||
let name: String?
|
||||
let avatarURL: URL?
|
||||
|
||||
init(from room: RoomDetails) {
|
||||
id = room.id
|
||||
name = room.name
|
||||
avatarURL = room.avatarURL
|
||||
}
|
||||
|
||||
init(from member: RoomMemberDetails) {
|
||||
id = member.id
|
||||
name = member.isBanned ? nil : member.name
|
||||
avatarURL = member.isBanned ? nil : member.avatarURL
|
||||
}
|
||||
|
||||
init(from user: UserProfileProxy) {
|
||||
id = user.userID
|
||||
name = user.displayName
|
||||
avatarURL = user.avatarURL
|
||||
}
|
||||
private enum AvatarInfo {
|
||||
case room(RoomAvatar)
|
||||
case user(UserProfileProxy)
|
||||
}
|
||||
|
||||
private enum Badge: Hashable {
|
||||
@@ -46,8 +27,8 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
case `public`
|
||||
}
|
||||
|
||||
private let mainAvatarInfo: AvatarInfo
|
||||
private let secondaryAvatarInfo: AvatarInfo?
|
||||
private let avatarInfo: AvatarInfo
|
||||
private let title: String
|
||||
private let subtitle: String?
|
||||
private let badges: [Badge]
|
||||
|
||||
@@ -61,8 +42,8 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
imageProvider: ImageProviderProtocol? = nil,
|
||||
onAvatarTap: (() -> Void)? = nil,
|
||||
@ViewBuilder footer: @escaping () -> Footer) {
|
||||
mainAvatarInfo = .init(from: room)
|
||||
secondaryAvatarInfo = nil
|
||||
avatarInfo = .room(room.avatar)
|
||||
title = room.name ?? room.id
|
||||
subtitle = room.canonicalAlias
|
||||
|
||||
self.avatarSize = avatarSize
|
||||
@@ -83,9 +64,10 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
imageProvider: ImageProviderProtocol? = nil,
|
||||
onAvatarTap: (() -> Void)? = nil,
|
||||
@ViewBuilder footer: @escaping () -> Footer) {
|
||||
mainAvatarInfo = .init(from: dmRecipient)
|
||||
secondaryAvatarInfo = .init(from: accountOwner)
|
||||
subtitle = dmRecipient.isBanned ? nil : dmRecipient.name == nil ? nil : dmRecipient.id
|
||||
let dmRecipientProfile = UserProfileProxy(member: dmRecipient)
|
||||
avatarInfo = .room(.heroes([dmRecipientProfile, UserProfileProxy(member: accountOwner)]))
|
||||
title = dmRecipientProfile.displayName ?? dmRecipientProfile.userID
|
||||
subtitle = dmRecipientProfile.displayName == nil ? nil : dmRecipientProfile.userID
|
||||
|
||||
avatarSize = .user(on: .dmDetails)
|
||||
self.imageProvider = imageProvider
|
||||
@@ -100,15 +82,13 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
imageProvider: ImageProviderProtocol? = nil,
|
||||
onAvatarTap: (() -> Void)? = nil,
|
||||
@ViewBuilder footer: @escaping () -> Footer) {
|
||||
mainAvatarInfo = .init(from: member)
|
||||
secondaryAvatarInfo = nil
|
||||
subtitle = member.isBanned ? nil : member.name == nil ? nil : member.id
|
||||
let profile = UserProfileProxy(member: member)
|
||||
|
||||
self.avatarSize = avatarSize
|
||||
self.imageProvider = imageProvider
|
||||
self.onAvatarTap = onAvatarTap
|
||||
self.footer = footer
|
||||
badges = []
|
||||
self.init(user: profile,
|
||||
avatarSize: avatarSize,
|
||||
imageProvider: imageProvider,
|
||||
onAvatarTap: onAvatarTap,
|
||||
footer: footer)
|
||||
}
|
||||
|
||||
init(user: UserProfileProxy,
|
||||
@@ -116,8 +96,8 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
imageProvider: ImageProviderProtocol? = nil,
|
||||
onAvatarTap: (() -> Void)? = nil,
|
||||
@ViewBuilder footer: @escaping () -> Footer) {
|
||||
mainAvatarInfo = .init(from: user)
|
||||
secondaryAvatarInfo = nil
|
||||
avatarInfo = .user(user)
|
||||
title = user.displayName ?? user.userID
|
||||
subtitle = user.displayName == nil ? nil : user.userID
|
||||
|
||||
self.avatarSize = avatarSize
|
||||
@@ -152,40 +132,15 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var avatar: some View {
|
||||
if let secondaryAvatarInfo {
|
||||
ZStack {
|
||||
LoadableAvatarImage(url: mainAvatarInfo.avatarURL,
|
||||
name: mainAvatarInfo.name,
|
||||
contentID: mainAvatarInfo.id,
|
||||
avatarSize: avatarSize,
|
||||
imageProvider: imageProvider)
|
||||
.scaledFrame(size: 120, alignment: .topTrailing)
|
||||
LoadableAvatarImage(url: secondaryAvatarInfo.avatarURL,
|
||||
name: secondaryAvatarInfo.name,
|
||||
contentID: secondaryAvatarInfo.id,
|
||||
avatarSize: avatarSize,
|
||||
imageProvider: imageProvider)
|
||||
.mask {
|
||||
Rectangle()
|
||||
.fill(Color.white)
|
||||
.overlay {
|
||||
Circle()
|
||||
.inset(by: -4)
|
||||
.fill(Color.black)
|
||||
.scaledOffset(x: 120 - avatarSize.value,
|
||||
y: -120 + avatarSize.value)
|
||||
}
|
||||
.compositingGroup()
|
||||
.luminanceToAlpha()
|
||||
}
|
||||
.scaledFrame(size: 120, alignment: .bottomLeading)
|
||||
}
|
||||
.scaledFrame(size: 120)
|
||||
|
||||
} else {
|
||||
LoadableAvatarImage(url: mainAvatarInfo.avatarURL,
|
||||
name: mainAvatarInfo.name,
|
||||
contentID: mainAvatarInfo.id,
|
||||
switch avatarInfo {
|
||||
case .room(let roomAvatar):
|
||||
RoomAvatarImage(avatar: roomAvatar,
|
||||
avatarSize: avatarSize,
|
||||
imageProvider: imageProvider)
|
||||
case .user(let userProfile):
|
||||
LoadableAvatarImage(url: userProfile.avatarURL,
|
||||
name: userProfile.displayName,
|
||||
contentID: userProfile.userID,
|
||||
avatarSize: avatarSize,
|
||||
imageProvider: imageProvider)
|
||||
}
|
||||
@@ -203,7 +158,7 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
Spacer()
|
||||
.frame(height: 9)
|
||||
|
||||
Text(mainAvatarInfo.name ?? mainAvatarInfo.id)
|
||||
Text(title)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -237,7 +192,9 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
Form {
|
||||
AvatarHeaderView(room: .init(id: "@test:matrix.org",
|
||||
name: "Test Room",
|
||||
avatarURL: URL.picturesDirectory,
|
||||
avatar: .room(id: "@test:matrix.org",
|
||||
name: "Test Room",
|
||||
avatarURL: .picturesDirectory),
|
||||
canonicalAlias: "#test:matrix.org",
|
||||
isEncrypted: true,
|
||||
isPublic: true),
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
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.
|
||||
case room(id: String, name: String?, avatarURL: URL?)
|
||||
/// An avatar generated from the room's heroes.
|
||||
case heroes([UserProfileProxy])
|
||||
}
|
||||
|
||||
/// A view that shows the avatar for a room, or a cluster of heroes if provided.
|
||||
///
|
||||
/// This should be preferred over `LoadableAvatarImage` when displaying a
|
||||
/// room avatar so that DMs have a consistent appearance throughout the app.
|
||||
struct RoomAvatarImage: View {
|
||||
let avatar: RoomAvatar
|
||||
|
||||
let avatarSize: AvatarSize
|
||||
let imageProvider: ImageProviderProtocol?
|
||||
|
||||
var body: some View {
|
||||
switch avatar {
|
||||
case .room(let id, let name, let avatarURL):
|
||||
LoadableAvatarImage(url: avatarURL,
|
||||
name: name,
|
||||
contentID: id,
|
||||
avatarSize: avatarSize,
|
||||
imageProvider: imageProvider)
|
||||
case .heroes(let users):
|
||||
// We will expand upon this with more stack sizes in the future.
|
||||
if users.count == 0 {
|
||||
let _ = assertionFailure("We should never pass empty heroes here.")
|
||||
PlaceholderAvatarImage(name: nil, contentID: nil)
|
||||
} else if users.count == 2 {
|
||||
let clusterSize = avatarSize.value * 1.6
|
||||
ZStack {
|
||||
LoadableAvatarImage(url: users[0].avatarURL,
|
||||
name: users[0].displayName,
|
||||
contentID: users[0].userID,
|
||||
avatarSize: avatarSize,
|
||||
imageProvider: imageProvider)
|
||||
.scaledFrame(size: clusterSize, alignment: .topTrailing)
|
||||
|
||||
LoadableAvatarImage(url: users[1].avatarURL,
|
||||
name: users[1].displayName,
|
||||
contentID: users[1].userID,
|
||||
avatarSize: avatarSize,
|
||||
imageProvider: imageProvider)
|
||||
.mask {
|
||||
Rectangle()
|
||||
.fill(Color.white)
|
||||
.overlay {
|
||||
Circle()
|
||||
.inset(by: -4)
|
||||
.fill(Color.black)
|
||||
.scaledOffset(x: clusterSize - avatarSize.value,
|
||||
y: -clusterSize + avatarSize.value)
|
||||
}
|
||||
.compositingGroup()
|
||||
.luminanceToAlpha()
|
||||
}
|
||||
.scaledFrame(size: clusterSize, alignment: .bottomLeading)
|
||||
}
|
||||
.scaledFrame(size: clusterSize)
|
||||
} else {
|
||||
LoadableAvatarImage(url: users[0].avatarURL,
|
||||
name: users[0].displayName,
|
||||
contentID: users[0].userID,
|
||||
avatarSize: avatarSize,
|
||||
imageProvider: imageProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
imageProvider: MockMediaProvider())
|
||||
|
||||
RoomAvatarImage(avatar: .room(id: "!2:server.com",
|
||||
name: "Room",
|
||||
avatarURL: .picturesDirectory),
|
||||
avatarSize: .room(on: .home),
|
||||
imageProvider: MockMediaProvider())
|
||||
|
||||
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
|
||||
displayName: "User",
|
||||
avatarURL: nil)]),
|
||||
avatarSize: .room(on: .home),
|
||||
imageProvider: MockMediaProvider())
|
||||
|
||||
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
|
||||
displayName: "User",
|
||||
avatarURL: .picturesDirectory)]),
|
||||
avatarSize: .room(on: .home),
|
||||
imageProvider: MockMediaProvider())
|
||||
|
||||
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),
|
||||
imageProvider: MockMediaProvider())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user