mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 02:01:02 +00:00
Consolidate new notifications with the latest one (#563)
This commit is contained in:
parent
ed0bfb7d94
commit
f3e21a714c
6 changed files with 149 additions and 76 deletions
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// ConsolidatedNotification.swift
|
||||
//
|
||||
//
|
||||
// Created by Jérôme Danthinne on 31/01/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ConsolidatedNotification: Identifiable {
|
||||
public let notifications: [Notification]
|
||||
public let type: Notification.NotificationType
|
||||
public let createdAt: ServerDate
|
||||
public let accounts: [Account]
|
||||
public let status: Status?
|
||||
|
||||
public var id: String? { notifications.first?.id }
|
||||
|
||||
public init(notifications: [Notification],
|
||||
type: Notification.NotificationType,
|
||||
createdAt: ServerDate,
|
||||
accounts: [Account],
|
||||
status: Status?)
|
||||
{
|
||||
self.notifications = notifications
|
||||
self.type = type
|
||||
self.createdAt = createdAt
|
||||
self.accounts = accounts
|
||||
self.status = status
|
||||
}
|
||||
|
||||
public static func placeholder() -> ConsolidatedNotification {
|
||||
.init(notifications: [Notification.placeholder()],
|
||||
type: .favourite,
|
||||
createdAt: "2022-12-16T10:20:54.000Z",
|
||||
accounts: [.placeholder()],
|
||||
status: .placeholder())
|
||||
}
|
||||
|
||||
public static func placeholders() -> [ConsolidatedNotification] {
|
||||
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
||||
}
|
||||
}
|
|
@ -14,4 +14,12 @@ public struct Notification: Decodable, Identifiable {
|
|||
public var supportedType: NotificationType? {
|
||||
.init(rawValue: type)
|
||||
}
|
||||
|
||||
public static func placeholder() -> Notification {
|
||||
.init(id: UUID().uuidString,
|
||||
type: NotificationType.favourite.rawValue,
|
||||
createdAt: "2022-12-16T10:20:54.000Z",
|
||||
account: .placeholder(),
|
||||
status: .placeholder())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// ConsolidatedNotificationExt.swift
|
||||
//
|
||||
//
|
||||
// Created by Jérôme Danthinne on 31/01/2023.
|
||||
//
|
||||
|
||||
import Models
|
||||
|
||||
extension ConsolidatedNotification {
|
||||
var notificationIds: [String] { notifications.map(\.id) }
|
||||
}
|
||||
|
||||
extension Array where Element == ConsolidatedNotification {
|
||||
var notificationCount: Int {
|
||||
reduce(0) { $0 + ($1.accounts.isEmpty ? 1 : $1.accounts.count) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// Notification+Consolidated.swift
|
||||
//
|
||||
//
|
||||
// Created by Jérôme Danthinne on 31/01/2023.
|
||||
//
|
||||
|
||||
import Models
|
||||
|
||||
extension Array where Element == Notification {
|
||||
func consolidated(selectedType: Notification.NotificationType?) -> [ConsolidatedNotification] {
|
||||
Dictionary(grouping: self) { $0.consolidationId(selectedType: selectedType) }
|
||||
.values
|
||||
.compactMap { notifications in
|
||||
guard let notification = notifications.first,
|
||||
let supportedType = notification.supportedType
|
||||
else { return nil }
|
||||
|
||||
return ConsolidatedNotification(notifications: notifications,
|
||||
type: supportedType,
|
||||
createdAt: notification.createdAt,
|
||||
accounts: notifications.map(\.account),
|
||||
status: notification.status)
|
||||
}
|
||||
.sorted {
|
||||
$0.createdAt > $1.createdAt
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// NotificationExt.swift
|
||||
//
|
||||
//
|
||||
// Created by Jérôme Danthinne on 31/01/2023.
|
||||
//
|
||||
|
||||
import Models
|
||||
|
||||
extension Notification {
|
||||
func consolidationId(selectedType: Models.Notification.NotificationType?) -> String? {
|
||||
guard let supportedType else { return nil }
|
||||
|
||||
switch supportedType {
|
||||
case .follow where selectedType != .follow:
|
||||
// Always group followers, so use the type to group
|
||||
return supportedType.rawValue
|
||||
case .reblog, .favourite:
|
||||
// Group boosts and favourites by status, so use the type + the related status id
|
||||
return "\(supportedType.rawValue)-\(status?.id ?? "")"
|
||||
default:
|
||||
// Never group remaining ones, so use the notification id itself
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
func isConsolidable(selectedType: Models.Notification.NotificationType?) -> Bool {
|
||||
// Notification is consolidable onlt if the consolidation id is not the notication id (unique) itself
|
||||
consolidationId(selectedType: selectedType) != id
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@ class NotificationsViewModel: ObservableObject {
|
|||
var client: Client? {
|
||||
didSet {
|
||||
if oldValue != client {
|
||||
notifications = []
|
||||
consolidatedNotifications = []
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +32,6 @@ class NotificationsViewModel: ObservableObject {
|
|||
@Published var selectedType: Models.Notification.NotificationType? {
|
||||
didSet {
|
||||
if oldValue != selectedType {
|
||||
notifications = []
|
||||
consolidatedNotifications = []
|
||||
Task {
|
||||
await fetchNotifications()
|
||||
|
@ -51,7 +49,6 @@ class NotificationsViewModel: ObservableObject {
|
|||
return nil
|
||||
}
|
||||
|
||||
private var notifications: [Models.Notification] = []
|
||||
private var consolidatedNotifications: [ConsolidatedNotification] = []
|
||||
|
||||
func fetchNotifications() async {
|
||||
|
@ -64,7 +61,6 @@ class NotificationsViewModel: ObservableObject {
|
|||
try await client.get(endpoint: Notifications.notifications(sinceId: nil,
|
||||
maxId: nil,
|
||||
types: queryTypes))
|
||||
self.notifications = notifications
|
||||
consolidatedNotifications = notifications.consolidated(selectedType: selectedType)
|
||||
nextPageState = notifications.count < 15 ? .none : .hasNextPage
|
||||
} else if let first = consolidatedNotifications.first {
|
||||
|
@ -76,7 +72,6 @@ class NotificationsViewModel: ObservableObject {
|
|||
newNotifications = newNotifications.filter { notification in
|
||||
!consolidatedNotifications.contains(where: { $0.id == notification.id })
|
||||
}
|
||||
notifications.append(contentsOf: newNotifications)
|
||||
consolidatedNotifications.insert(
|
||||
contentsOf: newNotifications.consolidated(selectedType: selectedType),
|
||||
at: 0
|
||||
|
@ -101,7 +96,6 @@ class NotificationsViewModel: ObservableObject {
|
|||
maxId: lastId,
|
||||
types: queryTypes))
|
||||
consolidatedNotifications.append(contentsOf: newNotifications.consolidated(selectedType: selectedType))
|
||||
notifications.append(contentsOf: newNotifications)
|
||||
state = .display(notifications: consolidatedNotifications, nextPageState: newNotifications.count < 15 ? .none : .hasNextPage)
|
||||
} catch {
|
||||
state = .error(error: error)
|
||||
|
@ -117,16 +111,29 @@ class NotificationsViewModel: ObservableObject {
|
|||
|
||||
func handleEvent(event: any StreamEvent) {
|
||||
Task {
|
||||
// Check if the event is a notification,
|
||||
// if it is not already in the list,
|
||||
// and if it can be shown (no selected type or the same as the received notification type)
|
||||
if let event = event as? StreamEventNotification,
|
||||
!consolidatedNotifications.contains(where: { $0.id == event.notification.id })
|
||||
!consolidatedNotifications.flatMap(\.notificationIds).contains(event.notification.id),
|
||||
selectedType == nil || selectedType?.rawValue == event.notification.type
|
||||
{
|
||||
if let selectedType, event.notification.type == selectedType.rawValue {
|
||||
notifications.insert(event.notification, at: 0)
|
||||
consolidatedNotifications = notifications.consolidated(selectedType: selectedType)
|
||||
} else if selectedType == nil {
|
||||
notifications.insert(event.notification, at: 0)
|
||||
consolidatedNotifications = notifications.consolidated(selectedType: selectedType)
|
||||
if event.notification.isConsolidable(selectedType: selectedType) {
|
||||
// If the notification type can be consolidated, try to consolidate with the latest row
|
||||
let latestConsolidatedNotification = consolidatedNotifications.removeFirst()
|
||||
consolidatedNotifications.insert(
|
||||
contentsOf: ([event.notification] + latestConsolidatedNotification.notifications)
|
||||
.consolidated(selectedType: selectedType),
|
||||
at: 0
|
||||
)
|
||||
} else {
|
||||
// Otherwise, just insert the new notification
|
||||
consolidatedNotifications.insert(
|
||||
contentsOf: [event.notification].consolidated(selectedType: selectedType),
|
||||
at: 0
|
||||
)
|
||||
}
|
||||
|
||||
withAnimation {
|
||||
state = .display(notifications: consolidatedNotifications, nextPageState: .hasNextPage)
|
||||
}
|
||||
|
@ -134,66 +141,3 @@ class NotificationsViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsolidatedNotification: Identifiable {
|
||||
let notificationIds: [String]
|
||||
let type: Models.Notification.NotificationType
|
||||
let createdAt: ServerDate
|
||||
let accounts: [Account]
|
||||
let status: Status?
|
||||
|
||||
var id: String? { notificationIds.first }
|
||||
|
||||
static func placeholder() -> ConsolidatedNotification {
|
||||
.init(notificationIds: [UUID().uuidString],
|
||||
type: .favourite,
|
||||
createdAt: "2022-12-16T10:20:54.000Z",
|
||||
accounts: [.placeholder()],
|
||||
status: .placeholder())
|
||||
}
|
||||
|
||||
static func placeholders() -> [ConsolidatedNotification] {
|
||||
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Models.Notification {
|
||||
func consolidated(selectedType: Models.Notification.NotificationType?) -> [ConsolidatedNotification] {
|
||||
Dictionary(grouping: self) { notification -> String? in
|
||||
guard let supportedType = notification.supportedType else { return nil }
|
||||
|
||||
switch supportedType {
|
||||
case .follow where selectedType != .follow:
|
||||
// Always group followers
|
||||
return supportedType.rawValue
|
||||
case .reblog, .favourite:
|
||||
// Group boosts and favourites by status
|
||||
return "\(supportedType.rawValue)-\(notification.status?.id ?? "")"
|
||||
default:
|
||||
// Never group remaining ones
|
||||
return notification.id
|
||||
}
|
||||
}
|
||||
.values
|
||||
.compactMap { notifications in
|
||||
guard let notification = notifications.first,
|
||||
let supportedType = notification.supportedType
|
||||
else { return nil }
|
||||
|
||||
return ConsolidatedNotification(notificationIds: notifications.map(\.id),
|
||||
type: supportedType,
|
||||
createdAt: notification.createdAt,
|
||||
accounts: notifications.map(\.account),
|
||||
status: notification.status)
|
||||
}
|
||||
.sorted {
|
||||
$0.createdAt > $1.createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == ConsolidatedNotification {
|
||||
var notificationCount: Int {
|
||||
reduce(0) { $0 + ($1.accounts.isEmpty ? 1 : $1.accounts.count) }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue