597787a6c9
- HTTPClient: replace isRefreshing bool with shared Task to safely coalesce concurrent 401 refresh attempts; surface JSON serialization error instead of silently dropping request body - AuthService.logout: always clear Keychain tokens via defer, even when refresh token is absent, preventing stale access token - NotificationsAPIService: remove updateAppBadge (UIKit call moved to @MainActor NotificationsViewModel); drop unused UIKit import - NotificationsViewModel: guard startPolling() against duplicate tasks; update badge directly on @MainActor instead of hopping to actor - VerifyEmailView: replace Timer (never invalidated) with async Task cancelled in .onDisappear - NotificationsView: use Text(date, style: .relative) — auto-updates without custom formatter; remove duplicate Date extension - SettingsView: handle logoutAll errors explicitly with alert instead of silently proceeding with local logout - MaydayLiveActivity/Info.plist: add NSExtensionPrincipalClass so the widget extension is discoverable by the system - Live Activity widget: replace frozen duration(from:) with Text(date, style: .timer); replace frozen relativeFormatted with Text(date, style: .relative); localize status badge to Russian Co-authored-by: robonen <26167508+robonen@users.noreply.github.com>
95 lines
3.0 KiB
Swift
95 lines
3.0 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
import UIKit
|
|
|
|
@MainActor
|
|
class NotificationsViewModel: ObservableObject {
|
|
@Published var notifications: [AppNotification] = []
|
|
@Published var isLoading = false
|
|
@Published var isLoadingMore = false
|
|
@Published var error: String?
|
|
@Published var hasMore = true
|
|
|
|
private let service = NotificationsAPIService.shared
|
|
private var currentPage = 1
|
|
private let perPage = 20
|
|
private var pollingTask: Task<Void, Never>?
|
|
|
|
func load() async {
|
|
isLoading = true
|
|
error = nil
|
|
currentPage = 1
|
|
defer { isLoading = false }
|
|
do {
|
|
let page = try await service.getNotifications(page: 1, perPage: perPage)
|
|
notifications = page.items
|
|
hasMore = page.items.count == perPage
|
|
updateBadge()
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
func loadMore() async {
|
|
guard !isLoadingMore && hasMore else { return }
|
|
isLoadingMore = true
|
|
defer { isLoadingMore = false }
|
|
do {
|
|
let nextPage = currentPage + 1
|
|
let page = try await service.getNotifications(page: nextPage, perPage: perPage)
|
|
notifications.append(contentsOf: page.items)
|
|
currentPage = nextPage
|
|
hasMore = page.items.count == perPage
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
func markAsRead(_ notification: AppNotification) async {
|
|
guard !notification.isRead else { return }
|
|
do {
|
|
try await service.markAsRead(id: notification.id)
|
|
if let index = notifications.firstIndex(where: { $0.id == notification.id }) {
|
|
let updated = AppNotification(
|
|
id: notification.id,
|
|
topic: notification.topic,
|
|
subject: notification.subject,
|
|
body: notification.body,
|
|
metadata: notification.metadata,
|
|
status: .read,
|
|
channel: notification.channel,
|
|
readAt: Date(),
|
|
createdAt: notification.createdAt,
|
|
updatedAt: Date()
|
|
)
|
|
notifications[index] = updated
|
|
}
|
|
updateBadge()
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
func startPolling() {
|
|
// Guard against starting a second polling loop if already running.
|
|
guard pollingTask == nil else { return }
|
|
pollingTask = Task {
|
|
while !Task.isCancelled {
|
|
try? await Task.sleep(for: .seconds(30))
|
|
guard !Task.isCancelled else { break }
|
|
await load()
|
|
}
|
|
}
|
|
}
|
|
|
|
func stopPolling() {
|
|
pollingTask?.cancel()
|
|
pollingTask = nil
|
|
}
|
|
|
|
private func updateBadge() {
|
|
let unreadCount = notifications.filter { !$0.isRead }.count
|
|
UIApplication.shared.applicationIconBadgeNumber = unreadCount
|
|
}
|
|
}
|