Grouped notifications

This commit is contained in:
Lumaa 2024-02-29 13:33:17 +01:00
parent 5320597963
commit bee71071c2
3 changed files with 205 additions and 33 deletions

View File

@ -5,24 +5,43 @@ import SwiftUI
struct NotificationRow: View {
@EnvironmentObject private var navigator: Navigator
var notif: Notification = .placeholder()
@State private var multiPeopleSheet: Bool = false
var notif: GroupedNotification
var showIcon: Bool = true
var body: some View {
VStack {
HStack(spacing: 5) {
ProfilePicture(url: notif.account.avatar, size: 60)
.padding(.trailing)
.overlay(alignment: .bottomTrailing) {
if showIcon {
notifIcon()
.offset(x: -5, y: 5)
}
}
.padding(.horizontal, 10)
.onTapGesture {
navigator.navigate(to: .account(acc: notif.account))
if let acc = notif.accounts.first {
if notif.accounts.count == 1 {
ProfilePicture(url: acc.avatar, size: 60)
.padding(.trailing)
.overlay(alignment: .bottomTrailing) {
if showIcon {
notifIcon()
.offset(x: -5, y: 5)
}
}
.padding(.horizontal, 10)
.onTapGesture {
navigator.navigate(to: .account(acc: acc))
}
} else {
accountCount()
.padding(.trailing)
.overlay(alignment: .bottomTrailing) {
if showIcon {
notifIcon()
.offset(x: -5, y: 5)
}
}
.padding(.horizontal, 10)
.onTapGesture {
multiPeopleSheet.toggle()
}
}
}
VStack(alignment: .leading) {
Text(localizedString())
@ -37,33 +56,89 @@ struct NotificationRow: View {
.foregroundStyle(Color.gray)
.lineLimit(3, reservesSpace: true)
if notif.status!.mediaAttachments.count > 0 {
Label("activity.status.attachments-\(notif.status!.mediaAttachments.count)", systemImage: "photo.on.rectangle.angled")
HStack(spacing: 7.5) {
if notif.status!.mediaAttachments.count > 0 {
Label("activity.status.attachments-\(notif.status!.mediaAttachments.count)", systemImage: notif.status!.mediaAttachments.count > 1 ? "photo.on.rectangle.angled" : "photo")
.multilineTextAlignment(.leading)
.font(.caption)
.foregroundStyle(Color.gray)
.lineLimit(1, reservesSpace: false)
}
Spacer()
Text(notif.notifications[0].createdAt.relativeFormatted)
.multilineTextAlignment(.leading)
.font(.caption)
.foregroundStyle(Color.gray)
.lineLimit(1, reservesSpace: false)
}
} else {
TextEmoji(notif.account.note, emojis: notif.account.emojis)
.multilineTextAlignment(.leading)
.font(.caption)
.foregroundStyle(Color.gray)
.lineLimit(3, reservesSpace: true)
if let acc = notif.accounts.first {
TextEmoji(acc.note, emojis: acc.emojis)
.multilineTextAlignment(.leading)
.font(.caption)
.foregroundStyle(Color.gray)
.lineLimit(3, reservesSpace: true)
}
}
}
.contentShape(Rectangle())
.onTapGesture {
navigator.navigate(to: notif.status == nil ? .account(acc: notif.account) : .post(status: notif.status!))
navigator.navigate(to: notif.status == nil ? .account(acc: notif.accounts.first!) : .post(status: notif.status!))
}
}
.padding(.horizontal)
.sheet(isPresented: $multiPeopleSheet) {
users
.presentationDetents([.height(200), .medium])
.presentationDragIndicator(.visible)
.presentationCornerRadius(25)
.scrollBounceBehavior(.basedOnSize)
}
}
}
var users: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .listRowSeparatorLeading) {
ForEach(notif.accounts) { acc in
HStack {
ProfilePicture(url: acc.avatar, size: 60)
.padding(.horizontal)
VStack(alignment: .leading) {
Text(acc.displayName ?? acc.username)
.font(.body.bold())
Text("@\(acc.acct)")
.font(.caption)
.foregroundStyle(Color.gray)
.lineLimit(1)
}
Spacer()
}
.padding(.vertical)
.onTapGesture {
multiPeopleSheet.toggle()
navigator.navigate(to: .account(acc: acc))
}
}
}
.padding(.top)
}
}
private func localizedString() -> LocalizedStringKey {
let nameStr = "@\(notif.account.username)"
switch (notif.supportedType) {
var nameStr: String = ""
if notif.accounts.count > 1, let acc = notif.accounts.first {
nameStr = String(localized: "activity.group.\("@" + acc.username)-\(notif.accounts.count - 1)")
} else if let acc = notif.accounts.first {
nameStr = "@\(acc.username)"
}
switch (notif.type) {
case .favourite:
return "activity.favorite.\(nameStr)"
case .follow:
@ -74,13 +149,15 @@ struct NotificationRow: View {
return "activity.reblogged.\(nameStr)"
case .status:
return "activity.status.\(nameStr)"
case .update:
return "activity.update.\(nameStr)"
default:
return "activity.unknown" // follow requests & polls
}
}
private func notifColor() -> Color {
switch (notif.supportedType) {
switch (notif.type) {
case .favourite:
return Color.red
case .follow:
@ -89,7 +166,7 @@ struct NotificationRow: View {
return Color.blue
case .reblog:
return Color.orange
case .status:
case .status, .update: // update and post are techn. the same
return Color.yellow
default:
return Color.gray
@ -101,7 +178,7 @@ struct NotificationRow: View {
let size: CGFloat = 60.0 / 4.0
ZStack {
switch (notif.supportedType) {
switch (notif.type) {
case .favourite:
Image(systemName: "heart.fill")
.resizable()
@ -127,6 +204,11 @@ struct NotificationRow: View {
.resizable()
.scaledToFit()
.frame(width: size, height: size)
case .update:
Image(systemName: "pencil.and.scribble")
.resizable()
.scaledToFit()
.frame(width: size, height: size)
default:
Image(systemName: "questionmark")
.resizable()
@ -144,8 +226,25 @@ struct NotificationRow: View {
}
.fixedSize()
}
}
#Preview {
NotificationRow()
@ViewBuilder
private func accountCount() -> some View {
ZStack {
Text(String("+\(notif.accounts.count - 1)"))
.font(.body)
.scaledToFit()
.lineLimit(1)
.minimumScaleFactor(0.5)
.frame(width: 30, height: 30)
}
.frame(width: 40, height: 40)
.padding(7)
.background(Color.gray)
.clipShape(.circle)
.overlay {
Circle()
.stroke(Color.appBackground, lineWidth: 3)
}
.fixedSize()
}
}

View File

@ -28,3 +28,76 @@ public struct Notification: Decodable, Identifiable, Equatable {
extension Notification: Sendable {}
extension Notification.NotificationType: Sendable {}
extension [Notification] {
public func toGrouped() -> [GroupedNotification] {
var groupedNotifications: [GroupedNotification] = []
for notification in self {
if let existingIndex = groupedNotifications.firstIndex(where: { $0.type == notification.supportedType && $0.timeDifference(with: notification) >= GroupedNotification.groupHours && $0.status == notification.status }) {
// If a group with the same type exists, add the notification to that group
groupedNotifications[existingIndex].notifications.append(notification)
groupedNotifications[existingIndex].accounts.append(notification.account)
groupedNotifications[existingIndex].accounts = groupedNotifications[existingIndex].accounts.uniqued()
} else {
// If no group with the same type exists, create a new group
groupedNotifications.append(GroupedNotification(notifications: [notification], type: notification.supportedType ?? .favourite))
}
}
return groupedNotifications
}
}
public struct GroupedNotification: Identifiable, Hashable {
public var id: String? { notifications.first?.id }
public var notifications: [Notification]
public let type: Notification.NotificationType
public var accounts: [Account]
public let status: Status?
public static let groupHours: Int = -5 // all notifications 5 hours away from first
init(notifications: [Notification], type: Notification.NotificationType, accounts: [Account], status: Status?) {
self.notifications = notifications
self.type = type
self.accounts = accounts
self.status = status
}
init(notifications: [Notification], type: Notification.NotificationType) {
if let firstNotif = notifications.first {
if let maxType = Calendar.current.date(byAdding: .hour, value: GroupedNotification.groupHours, to: firstNotif.createdAt.asDate) {
let filtered = notifications.filter({ $0.supportedType == type && $0.createdAt.asDate >= maxType })
let timed = filtered.sorted(by: { $0.createdAt.asDate > $1.createdAt.asDate })
self.notifications = timed
self.accounts = timed.map({ $0.account })
self.status = firstNotif.status
} else {
self.notifications = []
self.accounts = []
self.status = nil
}
} else {
self.notifications = []
self.accounts = []
self.status = nil
}
self.type = type
}
func timeDifference(with notification: Notification) -> Int {
guard let firstNotificationDate = notifications.first?.createdAt.asDate else {
return 0
}
let timeDifference = Calendar.current.dateComponents([.hour, .minute], from: firstNotificationDate, to: notification.createdAt.asDate)
return timeDifference.hour ?? 0 // Get the time difference in hours
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

View File

@ -7,7 +7,7 @@ struct NotificationsView: View {
@Environment(AccountManager.self) private var accountManager
@State private var navigator: Navigator = Navigator()
@State private var notifications: [Notification] = []
@State private var notifications: [GroupedNotification] = []
@State private var loadingNotifs: Bool = true
@State private var lastId: Int? = nil
private let notifLimit = 50
@ -110,15 +110,15 @@ struct NotificationsView: View {
}
do {
var notifs: [Notification] = try await client.get(endpoint: Notifications.notifications(minId: notifications.last?.id, maxId: nil, types: nil, limit: lastId != nil ? notifLimit : 30))
var notifs: [Notification] = try await client.get(endpoint: Notifications.notifications(minId: nil, maxId: notifications.last?.id, types: nil, limit: lastId != nil ? notifLimit : 30))
guard !notifs.isEmpty else { return }
notifs = notifs.filter({ $0.supportedType != .mention && $0.status?.visibility != .direct })
if notifications.isEmpty {
notifications = notifs
notifications = notifs.toGrouped()
} else {
notifications.append(contentsOf: notifs)
notifications.append(contentsOf: notifs.toGrouped())
}
await getBadge()