// // NotificationGroup.swift // Pachyderm // // Created by Shadowfacts on 9/5/19. // Copyright © 2019 Shadowfacts. All rights reserved. // import Foundation import WebURL public struct NotificationGroup: Identifiable, Hashable, Sendable { public private(set) var notifications: [Notification] public let id: String public let kind: Kind public init?(notifications: [Notification], kind: Kind) { guard !notifications.isEmpty else { return nil } self.notifications = notifications self.id = notifications.first!.id self.kind = kind } public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool { guard lhs.notifications.count == rhs.notifications.count else { return false } for (a, b) in zip(lhs.notifications, rhs.notifications) where a.id != b.id { return false } return true } public func hash(into hasher: inout Hasher) { for notification in notifications { hasher.combine(notification.id) } } private mutating func append(_ notification: Notification) { notifications.append(notification) } private mutating func append(group: NotificationGroup) { notifications.append(contentsOf: group.notifications) } private static func groupKind(for notification: Notification) -> Kind { switch notification.kind { case .mention: return .mention case .reblog: return .reblog case .favourite: return .favourite case .follow: return .follow case .followRequest: return .followRequest case .poll: return .poll case .update: return .update case .status: return .status case .emojiReaction: if let emoji = notification.emoji { return .emojiReaction(emoji, notification.emojiURL) } else { return .unknown } case .unknown: return .unknown } } @MainActor public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] { var groups = [NotificationGroup]() for notification in notifications { let groupKind = groupKind(for: notification) if allowedTypes.contains(notification.kind) { if let lastGroup = groups.last, canMerge(notification: notification, kind: groupKind, into: lastGroup) { groups[groups.count - 1].append(notification) continue } else if groups.count >= 2 { let secondToLastGroup = groups[groups.count - 2] if allowedTypes.contains(notification.kind), canMerge(notification: notification, kind: groupKind, into: secondToLastGroup) { groups[groups.count - 2].append(notification) continue } } } groups.append(NotificationGroup(notifications: [notification], kind: groupKind)!) } return groups } private static func canMerge(notification: Notification, kind: Kind, into group: NotificationGroup) -> Bool { return kind == group.kind && notification.status?.id == group.notifications.first!.status?.id } public static func mergeGroups(first: [NotificationGroup], second: [NotificationGroup], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] { guard !first.isEmpty else { return second } guard !second.isEmpty else { return first } var merged = first var second = second merged.reserveCapacity(second.count) while let firstGroupFromSecond = second.first, allowedTypes.contains(firstGroupFromSecond.kind.notificationKind) { second.removeFirst() guard let lastGroup = merged.last, allowedTypes.contains(lastGroup.kind.notificationKind) else { merged.append(firstGroupFromSecond) break } if canMerge(notification: firstGroupFromSecond.notifications.first!, kind: firstGroupFromSecond.kind, into: lastGroup) { merged[merged.count - 1].append(group: firstGroupFromSecond) } else if merged.count >= 2 { let secondToLastGroup = merged[merged.count - 2] if allowedTypes.contains(secondToLastGroup.kind.notificationKind), canMerge(notification: firstGroupFromSecond.notifications.first!, kind: firstGroupFromSecond.kind, into: secondToLastGroup) { merged[merged.count - 2].append(group: firstGroupFromSecond) } else { merged.append(firstGroupFromSecond) } } else { merged.append(firstGroupFromSecond) } } merged.append(contentsOf: second) return merged } public enum Kind: Sendable, Equatable { case mention case reblog case favourite case follow case followRequest case poll case update case status case emojiReaction(String, WebURL?) case unknown var notificationKind: Notification.Kind { switch self { case .mention: .mention case .reblog: .reblog case .favourite: .favourite case .follow: .follow case .followRequest: .followRequest case .poll: .poll case .update: .update case .status: .status case .emojiReaction(_, _): .emojiReaction case .unknown: .unknown } } } }