IceCubesApp/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift

209 lines
7.3 KiB
Swift
Raw Normal View History

import Env
2022-12-19 11:28:55 +00:00
import Foundation
import Models
2023-01-17 10:36:01 +00:00
import Network
import Observation
2023-01-17 10:36:01 +00:00
import SwiftUI
2022-12-19 11:28:55 +00:00
@MainActor
@Observable class NotificationsViewModel {
2022-12-19 11:28:55 +00:00
public enum State {
public enum PagingState {
case none, hasNextPage
2022-12-19 11:28:55 +00:00
}
2023-01-17 10:36:01 +00:00
2022-12-19 11:28:55 +00:00
case loading
case display(notifications: [ConsolidatedNotification], nextPageState: State.PagingState)
2022-12-19 11:28:55 +00:00
case error(error: Error)
}
2023-02-12 15:29:41 +00:00
2023-02-06 19:08:29 +00:00
enum Constants {
static let notificationLimit: Int = 30
}
2023-01-17 10:36:01 +00:00
public enum Tab: LocalizedStringKey, CaseIterable {
case all = "notifications.tab.all"
case mentions = "notifications.tab.mentions"
2022-12-22 06:00:44 +00:00
}
2023-01-17 10:36:01 +00:00
2022-12-30 07:36:22 +00:00
var client: Client? {
didSet {
if oldValue != client {
consolidatedNotifications = []
2022-12-30 07:36:22 +00:00
}
}
}
2023-02-12 15:29:41 +00:00
var currentAccount: CurrentAccount?
2023-01-17 10:36:01 +00:00
private let filterKey = "notification-filter"
var state: State = .loading
2023-12-28 11:03:01 +00:00
var isLockedType: Bool = false
var selectedType: Models.Notification.NotificationType? {
2022-12-22 06:00:44 +00:00
didSet {
guard oldValue != selectedType,
2024-01-06 10:24:41 +00:00
client?.id != nil
else { return }
2023-12-28 11:03:01 +00:00
if !isLockedType {
UserDefaults.standard.set(selectedType?.rawValue ?? "", forKey: filterKey)
}
consolidatedNotifications = []
2022-12-22 06:00:44 +00:00
}
}
func loadSelectedType() {
guard let value = UserDefaults.standard.string(forKey: filterKey)
else {
selectedType = nil
return
}
selectedType = .init(rawValue: value)
}
2023-01-17 10:36:01 +00:00
var scrollToTopVisible: Bool = false
2023-01-04 07:14:37 +00:00
private var queryTypes: [String]? {
if let selectedType {
var excludedTypes = Models.Notification.NotificationType.allCases
2023-01-22 05:38:30 +00:00
excludedTypes.removeAll(where: { $0 == selectedType })
2023-09-16 12:15:03 +00:00
return excludedTypes.map(\.rawValue)
}
return nil
2023-01-17 10:36:01 +00:00
}
private var consolidatedNotifications: [ConsolidatedNotification] = []
2023-01-17 10:36:01 +00:00
2022-12-19 11:28:55 +00:00
func fetchNotifications() async {
guard let client, let currentAccount else { return }
2022-12-19 11:28:55 +00:00
do {
2022-12-29 09:39:34 +00:00
var nextPageState: State.PagingState = .hasNextPage
if consolidatedNotifications.isEmpty {
2022-12-21 16:39:48 +00:00
state = .loading
let notifications: [Models.Notification] =
2023-02-06 19:08:29 +00:00
try await client.get(endpoint: Notifications.notifications(minId: nil,
maxId: nil,
2023-02-06 19:08:29 +00:00
types: queryTypes,
limit: Constants.notificationLimit))
consolidatedNotifications = await notifications.consolidated(selectedType: selectedType)
markAsRead()
2023-02-06 19:08:29 +00:00
nextPageState = notifications.count < Constants.notificationLimit ? .none : .hasNextPage
} else if let firstId = consolidatedNotifications.first?.id {
var newNotifications: [Models.Notification] = await fetchNewPages(minId: firstId, maxPages: 10)
nextPageState = consolidatedNotifications.notificationCount < Constants.notificationLimit ? .none : .hasNextPage
2023-01-17 10:36:01 +00:00
newNotifications = newNotifications.filter { notification in
!consolidatedNotifications.contains(where: { $0.id == notification.id })
2023-01-17 10:36:01 +00:00
}
2024-02-14 11:48:14 +00:00
await consolidatedNotifications.insert(
contentsOf: newNotifications.consolidated(selectedType: selectedType),
at: 0
)
2022-12-21 16:39:48 +00:00
}
if consolidatedNotifications.contains(where: { $0.type == .follow_request }) {
await currentAccount.fetchFollowerRequests()
}
markAsRead()
2024-02-14 11:48:14 +00:00
2023-01-12 18:12:23 +00:00
withAnimation {
state = .display(notifications: consolidatedNotifications,
nextPageState: consolidatedNotifications.isEmpty ? .none : nextPageState)
2023-01-12 18:12:23 +00:00
}
2022-12-19 11:28:55 +00:00
} catch {
state = .error(error: error)
}
}
2023-02-12 15:29:41 +00:00
2023-02-06 19:08:29 +00:00
private func fetchNewPages(minId: String, maxPages: Int) async -> [Models.Notification] {
guard let client else { return [] }
var pagesLoaded = 0
var allNotifications: [Models.Notification] = []
var latestMinId = minId
do {
while let newNotifications: [Models.Notification] =
2023-02-12 15:29:41 +00:00
try await client.get(endpoint: Notifications.notifications(minId: latestMinId,
maxId: nil,
types: queryTypes,
limit: Constants.notificationLimit)),
2023-02-06 19:08:29 +00:00
!newNotifications.isEmpty,
pagesLoaded < maxPages
{
pagesLoaded += 1
allNotifications.insert(contentsOf: newNotifications, at: 0)
latestMinId = newNotifications.first?.id ?? ""
}
} catch {
return allNotifications
}
return allNotifications
}
2023-01-17 10:36:01 +00:00
func fetchNextPage() async throws {
2022-12-19 11:28:55 +00:00
guard let client else { return }
guard let lastId = consolidatedNotifications.last?.notificationIds.last else { return }
let newNotifications: [Models.Notification] =
try await client.get(endpoint: Notifications.notifications(minId: nil,
maxId: lastId,
types: queryTypes,
limit: Constants.notificationLimit))
await consolidatedNotifications.append(contentsOf: newNotifications.consolidated(selectedType: selectedType))
if consolidatedNotifications.contains(where: { $0.type == .follow_request }) {
await currentAccount?.fetchFollowerRequests()
2022-12-19 11:28:55 +00:00
}
state = .display(notifications: consolidatedNotifications,
nextPageState: newNotifications.count < Constants.notificationLimit ? .none : .hasNextPage)
2022-12-19 11:28:55 +00:00
}
2024-02-14 11:48:14 +00:00
func markAsRead() {
guard let client, let id = consolidatedNotifications.first?.notifications.first?.id else { return }
Task {
do {
let _: Marker = try await client.post(endpoint: Markers.markNotifications(lastReadId: id))
2024-02-14 11:48:14 +00:00
} catch {}
}
}
2023-01-17 10:36:01 +00:00
2022-12-25 12:09:43 +00:00
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.flatMap(\.notificationIds).contains(event.notification.id),
selectedType == nil || selectedType?.rawValue == event.notification.type
{
2023-02-04 08:02:16 +00:00
if event.notification.isConsolidable(selectedType: selectedType),
2023-02-04 16:17:38 +00:00
!consolidatedNotifications.isEmpty
{
// If the notification type can be consolidated, try to consolidate with the latest row
let latestConsolidatedNotification = consolidatedNotifications.removeFirst()
await consolidatedNotifications.insert(
contentsOf: ([event.notification] + latestConsolidatedNotification.notifications)
.consolidated(selectedType: selectedType),
at: 0
)
} else {
// Otherwise, just insert the new notification
await consolidatedNotifications.insert(
contentsOf: [event.notification].consolidated(selectedType: selectedType),
at: 0
)
}
if event.notification.supportedType == .follow_request, let currentAccount {
await currentAccount.fetchFollowerRequests()
}
withAnimation {
state = .display(notifications: consolidatedNotifications, nextPageState: .hasNextPage)
}
2023-01-19 06:45:42 +00:00
}
2022-12-25 12:09:43 +00:00
}
}
2022-12-19 11:28:55 +00:00
}