Stacked Avatars View (#3504)

* stacked avatars

* fix tests

* remove comment
This commit is contained in:
Mauro
2024-11-12 14:00:51 +01:00
committed by GitHub
parent 2b153306dd
commit f7aeb3ee95
29 changed files with 144 additions and 56 deletions
+3
View File
@@ -46,6 +46,7 @@ enum UserAvatarSizeOnScreen {
case editUserDetails
case suggestions
case blockedUsers
case knockingUsers
var value: CGFloat {
switch self {
@@ -75,6 +76,8 @@ enum UserAvatarSizeOnScreen {
return 96
case .dmDetails:
return 75
case .knockingUsers:
return 28
}
}
}
@@ -0,0 +1,66 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import SwiftUI
struct StackedAvatarInfo {
let url: URL?
let name: String?
let contentID: String
}
struct StackedAvatarsView: View {
let overlap: CGFloat
let lineWidth: CGFloat
var shouldStackFromLast = false
let avatars: [StackedAvatarInfo]
let avatarSize: AvatarSize
let mediaProvider: MediaProviderProtocol?
var body: some View {
HStack(spacing: -overlap) {
ForEach(0..<avatars.count, id: \.self) { index in
LoadableAvatarImage(url: avatars[index].url,
name: avatars[index].name,
contentID: avatars[index].contentID,
avatarSize: avatarSize,
mediaProvider: mediaProvider)
.padding(lineWidth)
.overlay {
Circle()
.strokeBorder(Color.compound.bgCanvasDefault, lineWidth: lineWidth)
}
.zIndex(shouldStackFromLast ? Double(index) : Double(avatars.count - index))
}
}
}
}
struct StackedAvatarsView_Previews: PreviewProvider, TestablePreview {
static let avatars: [StackedAvatarInfo] = [
.init(url: nil, name: "Alice", contentID: "@alice:matrix.org"),
.init(url: nil, name: "Bob", contentID: "@bob:matrix.org"),
.init(url: nil, name: "Charlie", contentID: "@charlie:matrix.org"),
.init(url: nil, name: "Dan", contentID: "@charlie:matrix.org")
]
static var previews: some View {
VStack(spacing: 10) {
StackedAvatarsView(overlap: 16,
lineWidth: 2,
avatars: avatars,
avatarSize: .user(on: .knockingUsers),
mediaProvider: MediaProviderMock())
StackedAvatarsView(overlap: 16,
lineWidth: 2,
shouldStackFromLast: true,
avatars: avatars,
avatarSize: .user(on: .knockingUsers),
mediaProvider: MediaProviderMock())
}
}
}
@@ -11,25 +11,22 @@ struct TimelineReadReceiptsView: View {
let displayNumber = 3
let timelineItem: EventBasedTimelineItemProtocol
@EnvironmentObject private var context: TimelineViewModel.Context
var avatars: [StackedAvatarInfo] {
timelineItem.properties.orderedReadReceipts.prefix(displayNumber).map { receipt in
StackedAvatarInfo(url: context.viewState.members[receipt.userID]?.avatarURL,
name: context.viewState.members[receipt.userID]?.displayName,
contentID: receipt.userID)
}
}
var body: some View {
HStack(spacing: 2) {
HStack(spacing: -4) {
let receiptsToDisplay = timelineItem.properties.orderedReadReceipts.prefix(displayNumber)
ForEach(0..<receiptsToDisplay.count, id: \.self) { index in
let receipt = receiptsToDisplay[index]
LoadableAvatarImage(url: context.viewState.members[receipt.userID]?.avatarURL,
name: context.viewState.members[receipt.userID]?.displayName,
contentID: receipt.userID,
avatarSize: .user(on: .readReceipt),
mediaProvider: context.mediaProvider)
.overlay {
RoundedRectangle(cornerRadius: .infinity)
.stroke(Color.compound.bgCanvasDefault, lineWidth: 1)
}
.zIndex(Double(displayNumber - index))
}
}
StackedAvatarsView(overlap: 6,
lineWidth: 1,
avatars: avatars, avatarSize: .user(on: .readReceipt),
mediaProvider: context.mediaProvider)
.padding(-1)
if timelineItem.properties.orderedReadReceipts.count > displayNumber {
Text("+\(remaining)")
.font(.compound.bodySM)