forked from shadowfacts/Tusker
Perform grouping with existing notifications when refreshing
Closes #88
This commit is contained in:
parent
7465abe0a9
commit
eb9a5aeb42
|
@ -8,8 +8,8 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class NotificationGroup: Identifiable, Hashable {
|
||||
public let notifications: [Notification]
|
||||
public struct NotificationGroup: Identifiable, Hashable {
|
||||
public private(set) var notifications: [Notification]
|
||||
public let id: String
|
||||
public let kind: Notification.Kind
|
||||
public let statusState: StatusState?
|
||||
|
@ -27,34 +27,90 @@ public class NotificationGroup: Identifiable, Hashable {
|
|||
}
|
||||
|
||||
public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
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) {
|
||||
hasher.combine(id)
|
||||
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)
|
||||
}
|
||||
|
||||
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
||||
var groups = [[Notification]]()
|
||||
var groups = [NotificationGroup]()
|
||||
for notification in notifications {
|
||||
if allowedTypes.contains(notification.kind) {
|
||||
if let lastGroup = groups.last, let firstNotification = lastGroup.first, firstNotification.kind == notification.kind, firstNotification.status?.id == notification.status?.id {
|
||||
if let lastGroup = groups.last, canMerge(notification: notification, into: lastGroup) {
|
||||
groups[groups.count - 1].append(notification)
|
||||
continue
|
||||
} else if groups.count >= 2 {
|
||||
let secondToLastGroup = groups[groups.count - 2]
|
||||
if allowedTypes.contains(groups[groups.count - 1][0].kind), let firstNotification = secondToLastGroup.first, firstNotification.kind == notification.kind, firstNotification.status?.id == notification.status?.id {
|
||||
if allowedTypes.contains(groups[groups.count - 1].kind), canMerge(notification: notification, into: secondToLastGroup) {
|
||||
groups[groups.count - 2].append(notification)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.append([notification])
|
||||
groups.append(NotificationGroup(notifications: [notification])!)
|
||||
}
|
||||
return groups.map {
|
||||
NotificationGroup(notifications: $0)!
|
||||
return groups
|
||||
}
|
||||
|
||||
private static func canMerge(notification: Notification, into group: NotificationGroup) -> Bool {
|
||||
return notification.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) {
|
||||
|
||||
second.removeFirst()
|
||||
|
||||
guard let lastGroup = merged.last,
|
||||
allowedTypes.contains(lastGroup.kind) else {
|
||||
merged.append(firstGroupFromSecond)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
if canMerge(notification: firstGroupFromSecond.notifications.first!, 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), canMerge(notification: firstGroupFromSecond.notifications.first!, into: secondToLastGroup) {
|
||||
merged[merged.count - 2].append(group: firstGroupFromSecond)
|
||||
}
|
||||
} else {
|
||||
merged.append(firstGroupFromSecond)
|
||||
}
|
||||
}
|
||||
merged.append(contentsOf: second)
|
||||
return merged
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
//
|
||||
// NotificationGroupTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Shadowfacts on 4/26/22.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Pachyderm
|
||||
|
||||
class NotificationGroupTests: XCTestCase {
|
||||
|
||||
let decoder: JSONDecoder = {
|
||||
let d = JSONDecoder()
|
||||
d.dateDecodingStrategy = .iso8601
|
||||
return d
|
||||
}()
|
||||
|
||||
let statusA = """
|
||||
{
|
||||
"id": "1",
|
||||
"created_at": "2019-11-23T07:28:34Z",
|
||||
"account": {
|
||||
"id": "2",
|
||||
"username": "bar",
|
||||
"acct": "bar",
|
||||
"display_name": "bar",
|
||||
"locked": false,
|
||||
"created_at": "2019-11-01T01:01:01Z",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"note": "",
|
||||
"url": "https://example.com/@bar",
|
||||
"uri": "https://example.com/@bar",
|
||||
},
|
||||
"url": "https://example.com/@bar/1",
|
||||
"uri": "https://example.com/@bar/1",
|
||||
"content": "",
|
||||
"emojis": [],
|
||||
"reblogs_count": 0,
|
||||
"favourites_count": 0,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"visibility": "public",
|
||||
"media_attachments": [],
|
||||
"mentions": [],
|
||||
"tags": [],
|
||||
}
|
||||
}
|
||||
"""
|
||||
lazy var likeA1Data = """
|
||||
{
|
||||
"id": "1",
|
||||
"type": "favourite",
|
||||
"created_at": "2019-11-23T07:29:18Z",
|
||||
"account": {
|
||||
"id": "1",
|
||||
"username": "foo",
|
||||
"acct": "foo",
|
||||
"display_name": "foo",
|
||||
"locked": false,
|
||||
"created_at": "2019-11-01T01:01:01Z",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"note": "",
|
||||
"url": "https://example.com/@foo",
|
||||
"uri": "https://example.com/@foo",
|
||||
},
|
||||
"status": \(statusA)
|
||||
""".data(using: .utf8)!
|
||||
lazy var likeA1 = try! decoder.decode(Notification.self, from: likeA1Data)
|
||||
lazy var likeA2Data = """
|
||||
{
|
||||
"id": "2",
|
||||
"type": "favourite",
|
||||
"created_at": "2019-11-23T07:30:00Z",
|
||||
"account": {
|
||||
"id": "2",
|
||||
"username": "baz",
|
||||
"acct": "baz",
|
||||
"display_name": "baz",
|
||||
"locked": false,
|
||||
"created_at": "2019-11-01T01:01:01Z",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"note": "",
|
||||
"url": "https://example.com/@baz",
|
||||
"uri": "https://example.com/@baz",
|
||||
},
|
||||
"status": \(statusA)
|
||||
""".data(using: .utf8)!
|
||||
lazy var likeA2 = try! decoder.decode(Notification.self, from: likeA2Data)
|
||||
let statusB = """
|
||||
{
|
||||
"id": "2",
|
||||
"created_at": "2019-11-23T07:28:34Z",
|
||||
"account": {
|
||||
"id": "2",
|
||||
"username": "bar",
|
||||
"acct": "bar",
|
||||
"display_name": "bar",
|
||||
"locked": false,
|
||||
"created_at": "2019-11-01T01:01:01Z",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"note": "",
|
||||
"url": "https://example.com/@bar",
|
||||
"uri": "https://example.com/@bar",
|
||||
},
|
||||
"url": "https://example.com/@bar/1",
|
||||
"uri": "https://example.com/@bar/1",
|
||||
"content": "",
|
||||
"emojis": [],
|
||||
"reblogs_count": 0,
|
||||
"favourites_count": 0,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"visibility": "public",
|
||||
"media_attachments": [],
|
||||
"mentions": [],
|
||||
"tags": [],
|
||||
}
|
||||
}
|
||||
"""
|
||||
lazy var likeBData = """
|
||||
{
|
||||
"id": "3",
|
||||
"type": "favourite",
|
||||
"created_at": "2019-11-23T07:29:18Z",
|
||||
"account": {
|
||||
"id": "1",
|
||||
"username": "foo",
|
||||
"acct": "foo",
|
||||
"display_name": "foo",
|
||||
"locked": false,
|
||||
"created_at": "2019-11-01T01:01:01Z",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"note": "",
|
||||
"url": "https://example.com/@foo",
|
||||
"uri": "https://example.com/@foo",
|
||||
},
|
||||
"status": \(statusB)
|
||||
""".data(using: .utf8)!
|
||||
lazy var likeB = try! decoder.decode(Notification.self, from: likeBData)
|
||||
lazy var mentionBData = """
|
||||
{
|
||||
"id": "4",
|
||||
"type": "mention",
|
||||
"created_at": "2019-11-23T07:29:18Z",
|
||||
"account": {
|
||||
"id": "1",
|
||||
"username": "foo",
|
||||
"acct": "foo",
|
||||
"display_name": "foo",
|
||||
"locked": false,
|
||||
"created_at": "2019-11-01T01:01:01Z",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"note": "",
|
||||
"url": "https://example.com/@foo",
|
||||
"uri": "https://example.com/@foo",
|
||||
},
|
||||
"status": \(statusB)
|
||||
""".data(using: .utf8)!
|
||||
lazy var mentionB = try! decoder.decode(Notification.self, from: mentionBData)
|
||||
|
||||
|
||||
func testGroupSimple() {
|
||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeA2], only: [.favourite])
|
||||
XCTAssertEqual(groups, [NotificationGroup(notifications: [likeA1, likeA2])!])
|
||||
}
|
||||
|
||||
func testGroupWithOtherGroupableInBetween() {
|
||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeB, likeA2], only: [.favourite])
|
||||
XCTAssertEqual(groups, [
|
||||
NotificationGroup(notifications: [likeA1, likeA2])!,
|
||||
NotificationGroup(notifications: [likeB])!,
|
||||
])
|
||||
}
|
||||
|
||||
func testDontGroupWithUngroupableInBetween() {
|
||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, mentionB, likeA2], only: [.favourite])
|
||||
XCTAssertEqual(groups, [
|
||||
NotificationGroup(notifications: [likeA1])!,
|
||||
NotificationGroup(notifications: [mentionB])!,
|
||||
NotificationGroup(notifications: [likeA2])!,
|
||||
])
|
||||
}
|
||||
|
||||
func testMergeSimpleGroups() {
|
||||
let group1 = NotificationGroup(notifications: [likeA1])!
|
||||
let group2 = NotificationGroup(notifications: [likeA2])!
|
||||
let merged = NotificationGroup.mergeGroups(first: [group1], second: [group2], only: [.favourite])
|
||||
XCTAssertEqual(merged, [
|
||||
NotificationGroup(notifications: [likeA1, likeA2])!
|
||||
])
|
||||
}
|
||||
|
||||
func testMergeGroupsWithOtherGroupableInBetween() {
|
||||
let group1 = NotificationGroup(notifications: [likeA1])!
|
||||
let group2 = NotificationGroup(notifications: [likeB])!
|
||||
let group3 = NotificationGroup(notifications: [likeA2])!
|
||||
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
||||
XCTAssertEqual(merged, [
|
||||
NotificationGroup(notifications: [likeA1, likeA2])!,
|
||||
NotificationGroup(notifications: [likeB])!,
|
||||
])
|
||||
|
||||
let merged2 = NotificationGroup.mergeGroups(first: [group1], second: [group2, group3], only: [.favourite])
|
||||
XCTAssertEqual(merged2, [
|
||||
NotificationGroup(notifications: [likeA1, likeA2])!,
|
||||
NotificationGroup(notifications: [likeB])!,
|
||||
])
|
||||
}
|
||||
|
||||
func testDontMergeWithUngroupableInBetween() {
|
||||
let group1 = NotificationGroup(notifications: [likeA1])!
|
||||
let group2 = NotificationGroup(notifications: [mentionB])!
|
||||
let group3 = NotificationGroup(notifications: [likeA2])!
|
||||
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
||||
XCTAssertEqual(merged, [
|
||||
NotificationGroup(notifications: [likeA1])!,
|
||||
NotificationGroup(notifications: [mentionB])!,
|
||||
NotificationGroup(notifications: [likeA2])!,
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -28,6 +28,16 @@
|
|||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "PachydermTests"
|
||||
BuildableName = "PachydermTests"
|
||||
BlueprintName = "PachydermTests"
|
||||
ReferencedContainer = "container:Pachyderm">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
|
|
|
@ -140,11 +140,14 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
|||
self.older = older
|
||||
}
|
||||
|
||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||
let olderGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||
var snapshot = currentSnapshot()
|
||||
snapshot.appendItems(groups, toSection: .notifications)
|
||||
let existingGroups = currentSnapshot().itemIdentifiers
|
||||
let merged = NotificationGroup.mergeGroups(first: existingGroups, second: olderGroups, only: self.groupTypes)
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, NotificationGroup>()
|
||||
snapshot.appendSections([.notifications])
|
||||
snapshot.appendItems(merged, toSection: .notifications)
|
||||
completion(.success(snapshot))
|
||||
}
|
||||
}
|
||||
|
@ -173,15 +176,14 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
|||
self.newer = newer
|
||||
}
|
||||
|
||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||
let newerGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||
var snapshot = currentSnapshot()
|
||||
if let first = snapshot.itemIdentifiers(inSection: .notifications).first {
|
||||
snapshot.insertItems(groups, beforeItem: first)
|
||||
} else {
|
||||
snapshot.appendItems(groups, toSection: .notifications)
|
||||
}
|
||||
let existingGroups = currentSnapshot().itemIdentifiers
|
||||
let merged = NotificationGroup.mergeGroups(first: newerGroups, second: existingGroups, only: self.groupTypes)
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, NotificationGroup>()
|
||||
snapshot.appendSections([.notifications])
|
||||
snapshot.appendItems(merged, toSection: .notifications)
|
||||
completion(.success(snapshot))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue