Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 108 additions & 9 deletions MactrixLibrary/Sources/UI/Timeline/ReadReciptsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ struct ReadReciptsView<RoomMember: Models.RoomMember>: View {
let imageLoader: ImageLoader?
let roomMembers: [RoomMember]

private let truncatedAvatarLimit = 3
private let fullAvatarLimit = 4
@State private var showPopover: Bool = false

var users: [String] {
receipts
.sorted { a, b in
Expand All @@ -16,6 +20,20 @@ struct ReadReciptsView<RoomMember: Models.RoomMember>: View {
.map { key, _ in key }
}

var visibleUsers: [String] {
let shouldTruncate = users.count > fullAvatarLimit
let visibleAvatarLimit = shouldTruncate ? truncatedAvatarLimit : fullAvatarLimit
return Array(users.suffix(visibleAvatarLimit))
}

var hiddenCount: Int {
users.count - visibleUsers.count
}

var popoverUsers: [String] {
users.reversed()
}

@ViewBuilder
func avatarImage(forUserId userId: String) -> some View {
let user = roomMembers.first(where: { $0.id == userId })
Expand All @@ -32,17 +50,98 @@ struct ReadReciptsView<RoomMember: Models.RoomMember>: View {
return user?.displayName ?? userId
}

func readByTooltip(forUsers userIds: [String]) -> String {
let names = userIds.map { userDisplayName(forUserId: $0) }

switch names.count {
case 0:
return ""
case 1:
return "Read by \(names[0])"
case 2:
return "Read by \(names[0]) and \(names[1])"
case 3:
return "Read by \(names[0]), \(names[1]), and \(names[2])"
default:
return "Read by \(names[0]), \(names[1]), and \(names.count - 2) others"
}
}

func formattedTimestamp(_ date: Date) -> String {
if Calendar.current.isDateInToday(date) {
return date.formatted(.dateTime.hour().minute())
}
return date.formatted(.dateTime.weekday(.abbreviated).hour().minute())
}

var popoverHeader: String {
users.count == 1 ? "Read by 1 person" : "Read by \(users.count) people"
}

@ViewBuilder
var readReceiptsPopover: some View {
VStack(alignment: .leading) {
Text(popoverHeader)
.font(.headline)
.padding(.bottom, 4)

ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(popoverUsers, id: \.self) { userId in
HStack(spacing: 10) {
avatarImage(forUserId: userId)
.frame(width: 28, height: 28)
.clipShape(Circle())

VStack(alignment: .leading, spacing: 2) {
Text(userDisplayName(forUserId: userId))
.font(.body)
.lineLimit(1)

if let timestamp = receipts[userId]?.timestamp {
Text(formattedTimestamp(timestamp))
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.padding(.vertical, 4)
}
}
}
}
.frame(width: 200)
.frame(maxHeight: 250)
.padding()
}

var body: some View {
HStack(spacing: -2) {
ForEach(users, id: \.self) { userId in
avatarImage(forUserId: userId)
.frame(width: 14, height: 14)
.clipShape(Circle())
.background(
Circle().stroke(Color(NSColor.controlBackgroundColor), lineWidth: 3)
)
.help("Read by \(userDisplayName(forUserId: userId))")
Button {
showPopover.toggle()
} label: {
HStack(spacing: -2) {
if hiddenCount > 0 {
Text("+\(hiddenCount)")
.font(.system(.caption2))
.foregroundStyle(.secondary)
.padding(.trailing, 4)
}
ForEach(visibleUsers, id: \.self) { userId in
avatarImage(forUserId: userId)
.frame(width: 14, height: 14)
.clipShape(Circle())
.background(
Circle().stroke(Color(NSColor.controlBackgroundColor), lineWidth: 3)
)
}
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.pointerStyle(.link)
.help(readByTooltip(forUsers: users))
.popover(isPresented: $showPopover) {
readReceiptsPopover
}
}
}