Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions IceCubesApp/App/Tabs/NotificationTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ struct NotificationsTab: View {
case .follow, .follow_request:
routerPath.navigate(
to: .accountDetailWithAccount(account: newValue.notification.account))
case .added_to_collection, .collection_update:
if let collection = newValue.notification.collection {
routerPath.navigate(to: .collectionDetail(collection: collection))
}
default:
if let status = newValue.notification.status {
routerPath.navigate(to: .statusDetailWithStatus(status: status))
Expand Down
66 changes: 66 additions & 0 deletions IceCubesApp/Resources/Localization/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"sourceLanguage" : "en",
"strings" : {
Expand Down Expand Up @@ -40214,6 +40214,50 @@
}
}
},
"notifications.label.added-to-collection" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "added you to a collection"
}
}
}
},
"notifications.label.added-to-collection.push" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "👥 Added you to a collection: "
}
}
}
},
"notifications.label.collection-update" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "updated a collection you're in"
}
}
}
},
"notifications.label.collection-update.push" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "👥 Updated a collection: "
}
}
}
},
"notifications.label.favorite %lld" : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -42335,6 +42379,28 @@
}
}
},
"notifications.menu-title.added-to-collection" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Added to collection"
}
}
}
},
"notifications.menu-title.collection-update" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Collection updated"
}
}
}
},
"notifications.menu-title.favorite" : {
"extractionState" : "manual",
"localizations" : {
Expand Down
5 changes: 5 additions & 0 deletions Packages/Env/Sources/Env/CurrentInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ import Observation
version >= 4.3
}

/// Collections shipped in Mastodon 4.6, which bumped the Mastodon API version to 10.
public var isCollectionsSupported: Bool {
instance?.apiVersions?.mastodon ?? 0 >= 10
}

public var isQuoteSupported: Bool {
instance?.apiVersions?.mastodon ?? 0 >= 7
}
Expand Down
4 changes: 4 additions & 0 deletions Packages/Models/Sources/Models/ConsolidatedNotification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public struct ConsolidatedNotification: Identifiable {
public let createdAt: ServerDate
public let accounts: [Account]
public let status: Status?
/// Set for `added_to_collection` and `collection_update` notifications (Mastodon 4.6+).
public let collection: AccountCollection?
public let mostRecentNotificationId: String

public var id: String { groupKey ?? mostRecentNotificationId }
Expand All @@ -27,13 +29,15 @@ public struct ConsolidatedNotification: Identifiable {
createdAt: ServerDate,
accounts: [Account],
status: Status?,
collection: AccountCollection? = nil,
groupKey: String? = nil
) {
self.notifications = notifications
self.type = type
self.createdAt = createdAt
self.accounts = accounts
self.status = status ?? nil
self.collection = collection
self.groupKey = groupKey
self.mostRecentNotificationId = mostRecentNotificationId
}
Expand Down
7 changes: 5 additions & 2 deletions Packages/Models/Sources/Models/Notification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
public struct Notification: Decodable, Identifiable, Equatable {
public enum NotificationType: String, CaseIterable {
case follow, follow_request, mention, reblog, status, favourite, poll, update, quote,
quoted_update
quoted_update, added_to_collection, collection_update
}

public let id: String
Expand All @@ -12,6 +12,8 @@ public struct Notification: Decodable, Identifiable, Equatable {
public let account: Account
public let status: Status?
public let groupKey: String?
/// Attached when `type` is `added_to_collection` or `collection_update` (Mastodon 4.6+).
public let collection: AccountCollection?

public var supportedType: NotificationType? {
.init(rawValue: type)
Expand All @@ -24,7 +26,8 @@ public struct Notification: Decodable, Identifiable, Equatable {
createdAt: ServerDate(),
account: .placeholder(),
status: .placeholder(),
groupKey: nil)
groupKey: nil,
collection: nil)
}
}

Expand Down
2 changes: 2 additions & 0 deletions Packages/Models/Sources/Models/NotificationGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public struct NotificationGroup: Codable, Identifiable, Sendable {
public let latestPageNotificationAt: ServerDate
public let sampleAccountIds: [String]
public let statusId: String?
/// Attached when `type` is `added_to_collection` or `collection_update` (Mastodon 4.6+).
public let collection: AccountCollection?

public var id: String { groupKey }
}
143 changes: 143 additions & 0 deletions Packages/Models/Tests/ModelsTests/CollectionNotificationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import Foundation
import Testing

@testable import Models

private let collectionJSON = """
{
"id": "116131056935959117",
"account_id": "113668893442515793",
"uri": "https://example.com/ap/113668893442515793/collections/116131056935959117",
"url": "https://example.com/collections/116131056935959117",
"name": "Excellent people",
"description": "Well worth following",
"language": "en",
"local": true,
"sensitive": false,
"discoverable": true,
"tag": null,
"item_count": 1,
"items": [
{
"id": "116141056635954112",
"account_id": "112658193342215767",
"state": "accepted",
"created_at": "2026-02-25T11:35:01.394Z"
}
],
"created_at": "2026-02-25T11:35:01.394Z",
"updated_at": "2026-02-25T11:37:38.182Z"
}
"""

private let accountJSON = """
{
"id": "113668893442515793",
"username": "curator",
"acct": "curator@example.com",
"display_name": "Curator",
"note": "",
"avatar": "https://example.com/avatar.png",
"header": "https://example.com/header.png",
"locked": false,
"emojis": [],
"fields": [],
"created_at": "2026-01-01T00:00:00.000Z",
"followers_count": 0,
"following_count": 0,
"statuses_count": 0,
"bot": false,
"discoverable": true
}
"""

private func makeDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}

@Test
func testAddedToCollectionNotificationDecoding() throws {
let json = """
{
"id": "34975861",
"type": "added_to_collection",
"created_at": "2026-06-10T09:42:00.000Z",
"group_key": "ungrouped-34975861",
"account": \(accountJSON),
"collection": \(collectionJSON)
}
"""

let notification = try makeDecoder().decode(
Models.Notification.self, from: Data(json.utf8))

#expect(notification.supportedType == .added_to_collection)
#expect(notification.status == nil)
#expect(notification.collection?.id == "116131056935959117")
#expect(notification.collection?.name == "Excellent people")
#expect(notification.collection?.acceptedAccountIds == ["112658193342215767"])
}

@Test
func testCollectionUpdateNotificationDecoding() throws {
let json = """
{
"id": "34975862",
"type": "collection_update",
"created_at": "2026-06-10T09:43:00.000Z",
"group_key": "ungrouped-34975862",
"account": \(accountJSON),
"collection": \(collectionJSON)
}
"""

let notification = try makeDecoder().decode(
Models.Notification.self, from: Data(json.utf8))

#expect(notification.supportedType == .collection_update)
#expect(notification.collection?.itemCount == 1)
}

@Test
func testNotificationWithoutCollectionStillDecodes() throws {
let json = """
{
"id": "34975863",
"type": "follow",
"created_at": "2026-06-10T09:44:00.000Z",
"group_key": "ungrouped-34975863",
"account": \(accountJSON)
}
"""

let notification = try makeDecoder().decode(
Models.Notification.self, from: Data(json.utf8))

#expect(notification.supportedType == .follow)
#expect(notification.collection == nil)
}

@Test
func testNotificationGroupDecodesCollection() throws {
let json = """
{
"group_key": "added_to_collection-116131056935959117",
"notifications_count": 1,
"type": "added_to_collection",
"most_recent_notification_id": 34975861,
"page_min_id": "34975861",
"page_max_id": "34975861",
"latest_page_notification_at": "2026-06-10T09:42:00.000Z",
"sample_account_ids": ["113668893442515793"],
"collection": \(collectionJSON)
}
"""

let group = try makeDecoder().decode(NotificationGroup.self, from: Data(json.utf8))

#expect(group.type == "added_to_collection")
#expect(group.statusId == nil)
#expect(group.collection?.name == "Excellent people")
}
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ public final class NotificationsListDataSource {
createdAt: event.notification.createdAt,
accounts: [event.notification.account],
status: event.notification.status,
collection: event.notification.collection,
groupKey: groupKey
)

Expand Down Expand Up @@ -419,6 +420,7 @@ public final class NotificationsListDataSource {
createdAt: newGroup.createdAt,
accounts: updatedAccounts,
status: existingGroup.status,
collection: newGroup.collection ?? existingGroup.collection,
groupKey: groupKey
)

Expand Down Expand Up @@ -483,6 +485,7 @@ public final class NotificationsListDataSource {
createdAt: group.latestPageNotificationAt,
accounts: accounts,
status: status,
collection: group.collection,
groupKey: group.groupKey
))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public struct NotificationsListView: View {
.tint(theme.labelColor)
}
Divider()
ForEach(Notification.NotificationType.allCases, id: \.self) { type in
ForEach(filterableTypes, id: \.self) { type in
Button {
applyFilter(type: type)
} label: {
Expand Down Expand Up @@ -167,6 +167,17 @@ public struct NotificationsListView: View {
}
}

private var filterableTypes: [Models.Notification.NotificationType] {
Models.Notification.NotificationType.allCases.filter { type in
switch type {
case .added_to_collection, .collection_update:
currentInstance.isCollectionsSupported
default:
true
}
}
}

@ViewBuilder
private var notificationsView: some View {
switch viewState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ extension [Models.Notification] {
type: supportedType,
createdAt: notification.createdAt,
accounts: notifications.map(\.account),
status: notification.status)
status: notification.status,
collection: notification.collection)
}
.sorted {
$0.createdAt.asDate > $1.createdAt.asDate
Expand Down
Loading
Loading