aa14fa990b
Tracks which notification IDs the user has seen (persisted in UserDefaults). RootView loads notifications in the background on launch so the badge is ready before the user opens the tab. Opening the notifications screen marks all as read and clears the badge. First launch: badge stays at zero to avoid overwhelming new users with historical notifications.
108 lines
4.0 KiB
Swift
108 lines
4.0 KiB
Swift
import Foundation
|
|
import Observation
|
|
import OrgSocialKit
|
|
|
|
@Observable @MainActor
|
|
final class NotificationsViewModel {
|
|
|
|
var notifications: [OrgSocialNotification] = []
|
|
var authorNicks: [URL: String] = [:]
|
|
var isLoading = false
|
|
var errorMessage: String?
|
|
private(set) var unreadCount: Int = 0
|
|
|
|
private let client = RelayClient()
|
|
|
|
private static let seenIDsKey = "notif.seenIDs"
|
|
private static let hasViewedKey = "notif.hasViewed"
|
|
|
|
var feedURL: URL? {
|
|
guard let raw = UserDefaults.standard.string(forKey: "publicFeedURL"),
|
|
let url = URL(string: raw),
|
|
url.scheme?.hasPrefix("http") == true else { return nil }
|
|
return url
|
|
}
|
|
|
|
var relayURL: URL {
|
|
let raw = UserDefaults.standard.string(forKey: "relayURL") ?? "https://relay.org-social.org"
|
|
return URL(string: raw) ?? URL(string: "https://relay.org-social.org")!
|
|
}
|
|
|
|
var useRelay: Bool {
|
|
UserDefaults.standard.object(forKey: "useRelay") as? Bool ?? true
|
|
}
|
|
|
|
func load() async {
|
|
guard !isLoading else { return }
|
|
guard useRelay else {
|
|
errorMessage = "Notifications require a relay. Enable Use Relay in Settings."
|
|
notifications = []
|
|
return
|
|
}
|
|
guard let feedURL else {
|
|
errorMessage = "Configure your Public Feed URL in Settings."
|
|
return
|
|
}
|
|
isLoading = true
|
|
errorMessage = nil
|
|
defer { isLoading = false }
|
|
do {
|
|
let fetched = try await client.fetchNotifications(for: feedURL, from: relayURL)
|
|
// Deduplicate by ID to prevent ForEach from receiving duplicate keys,
|
|
// which causes UICollectionView item-count mismatches and crashes.
|
|
var seen = Set<String>()
|
|
let blocks = BlockList.shared
|
|
notifications = fetched
|
|
.filter { seen.insert($0.id).inserted }
|
|
.filter { notif in
|
|
guard let url = notif.authorFeedURL else { return true }
|
|
return !blocks.isBlocked(url)
|
|
}
|
|
recomputeUnread()
|
|
Task { await enrichNicks() }
|
|
} catch RelayError.httpError(let code) where code == 404 {
|
|
try? await client.registerFeed(feedURL, with: relayURL)
|
|
errorMessage = "Your feed isn't indexed by the relay yet. It will be available in a few minutes."
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
// MARK: - Unread badge
|
|
|
|
/// Marks all currently loaded notifications as seen and resets the badge to zero.
|
|
func markAllRead() {
|
|
guard !notifications.isEmpty else { return }
|
|
let ids = notifications.map(\.id)
|
|
UserDefaults.standard.set(ids, forKey: Self.seenIDsKey)
|
|
UserDefaults.standard.set(true, forKey: Self.hasViewedKey)
|
|
unreadCount = 0
|
|
}
|
|
|
|
/// Recomputes `unreadCount` from the current notifications vs. the persisted seen-IDs set.
|
|
/// On the very first launch (hasViewed == false) the badge stays at zero to avoid
|
|
/// overwhelming a new user with historical notifications.
|
|
private func recomputeUnread() {
|
|
guard UserDefaults.standard.bool(forKey: Self.hasViewedKey) else { return }
|
|
let seen = Set(UserDefaults.standard.stringArray(forKey: Self.seenIDsKey) ?? [])
|
|
unreadCount = notifications.filter { !seen.contains($0.id) }.count
|
|
}
|
|
|
|
private func enrichNicks() async {
|
|
let unknownURLs = Set(notifications.compactMap(\.authorFeedURL))
|
|
.filter { authorNicks[$0] == nil }
|
|
await withTaskGroup(of: (URL, String?).self) { group in
|
|
for url in unknownURLs {
|
|
group.addTask {
|
|
let profile = await PartialFeedFetcher().fetchProfileHeader(from: url)
|
|
let nick: String? = profile?.nick.flatMap { $0.isEmpty ? nil : $0 }
|
|
return (url, nick)
|
|
}
|
|
}
|
|
for await (url, nick) in group {
|
|
if let nick { authorNicks[url] = nick }
|
|
}
|
|
}
|
|
}
|
|
}
|