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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 144 additions and 56 deletions

View File

@ -1012,6 +1012,7 @@
E0FB26262689F04D66A949D7 /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E227F34BE43B08E098796E /* TestablePreview.swift */; };
E14E469CD97550D0FC58F3CA /* CancellableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */; };
E184FFAD32342D3D6E2F89AA /* PinnedEventsTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D53754227CEBD06358956D7 /* PinnedEventsTimelineScreenCoordinator.swift */; };
E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8558D41DD4B553A752C868A /* StackedAvatarsView.swift */; };
E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; };
E21FE4C5B614F311C0955859 /* UserProfileProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C454AE59914B551A6D02C0 /* UserProfileProxy.swift */; };
E27C4D1A1F8BB77CA790B403 /* InviteUsersScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */; };
@ -1946,6 +1947,7 @@
A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceProtocol.swift; sourceTree = "<group>"; };
A84D413BF49F0E980F010A6B /* LogViewerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenCoordinator.swift; sourceTree = "<group>"; };
A8558D41DD4B553A752C868A /* StackedAvatarsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackedAvatarsView.swift; sourceTree = "<group>"; };
A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = "<group>"; };
A8DF55467ED4CE76B7AE9A33 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/InfoPlist.strings; sourceTree = "<group>"; };
A9873374E72AA53260AE90A2 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -2989,6 +2991,7 @@
7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */,
839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */,
AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */,
A8558D41DD4B553A752C868A /* StackedAvatarsView.swift */,
E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */,
AD529C89924EE32CE307F36F /* VisualListItem.swift */,
);
@ -6952,6 +6955,7 @@
F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */,
CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */,
DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */,
E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */,
3DAF325D8AE461F7CDB282BD /* StartChatScreen.swift in Sources */,
6CD61FAF03E8986523C2ABB8 /* StartChatScreenCoordinator.swift in Sources */,
C051475DFF4C8EBDDF4DC8E4 /* StartChatScreenModels.swift in Sources */,

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

View File

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

View File

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

View File

@ -785,6 +785,12 @@ extension PreviewTests {
}
}
func test_stackedAvatarsView() {
for preview in StackedAvatarsView_Previews._allPreviews {
assertSnapshots(matching: preview)
}
}
func test_startChatScreen() {
for preview in StartChatScreen_Previews._allPreviews {
assertSnapshots(matching: preview)