Grouped notifications
This commit is contained in:
parent
5320597963
commit
bee71071c2
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue