завел уведы вроде как ура

This commit is contained in:
Lain Iwakura 2025-08-07 21:06:50 +03:00
parent d053ffccc6
commit e78c66b0c8
No known key found for this signature in database
GPG Key ID: C7C18257F2ADC6F8
5 changed files with 246 additions and 23 deletions

View File

@ -550,9 +550,31 @@ class APIClient: ObservableObject {
}
do {
let threads = try JSONDecoder().decode([Thread].self, from: data)
let newThreads = threads.filter { $0.id > lastKnownThreadId }
completion(.success(newThreads))
let currentThreads = try JSONDecoder().decode([Thread].self, from: data)
let savedThreadsKey = "savedThreads_\(boardCode)"
let savedThreadsData = UserDefaults.standard.data(forKey: savedThreadsKey)
if let savedThreadsData = savedThreadsData,
let savedThreads = try? JSONDecoder().decode([Thread].self, from: savedThreadsData) {
let savedThreadIds = Set(savedThreads.map { $0.id })
let newThreads = currentThreads.filter { !savedThreadIds.contains($0.id) }
if !newThreads.isEmpty {
print("Найдено \(newThreads.count) новых тредов в /\(boardCode)/")
}
completion(.success(newThreads))
} else {
print("Первая синхронизация для /\(boardCode)/ - сохраняем \(currentThreads.count) тредов")
completion(.success([]))
}
if let encodedData = try? JSONEncoder().encode(currentThreads) {
UserDefaults.standard.set(encodedData, forKey: savedThreadsKey)
}
} catch {
completion(.failure(error))
}

View File

@ -24,11 +24,12 @@ class BackgroundTaskManager {
func scheduleBackgroundTask() {
let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier)
let settings = Settings()
let interval = TimeInterval(settings.notificationInterval)
let interval = TimeInterval(settings.notificationInterval * 60)
request.earliestBeginDate = Date(timeIntervalSinceNow: interval)
do {
try BGTaskScheduler.shared.submit(request)
print("Фоновая задача запланирована на \(interval) секунд")
} catch {
print("Не удалось запланировать фоновую задачу: \(error)")
}
@ -51,20 +52,18 @@ class BackgroundTaskManager {
for boardCode in notificationManager.subscribedBoards {
group.enter()
let lastKnownId = UserDefaults.standard.integer(forKey: "lastThreadId_\(boardCode)")
APIClient().checkNewThreads(forBoard: boardCode, lastKnownThreadId: lastKnownId) { result in
APIClient().checkNewThreads(forBoard: boardCode, lastKnownThreadId: 0) { result in
switch result {
case .success(let newThreads):
if !newThreads.isEmpty {
hasNewThreads = true
print("Найдено \(newThreads.count) новых тредов в /\(boardCode)/")
for thread in newThreads {
self.notificationManager.scheduleNotification(for: thread, boardCode: boardCode)
}
UserDefaults.standard.set(newThreads.first?.id ?? lastKnownId, forKey: "lastThreadId_\(boardCode)")
}
case .failure:
break
case .failure(let error):
print("Ошибка проверки новых тредов для /\(boardCode)/: \(error)")
}
group.leave()
}

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import UserNotifications
@main
struct MobileMkchApp: App {
@ -22,6 +23,25 @@ struct MobileMkchApp: App {
}
}
private func setupNotifications() {
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
notificationManager.requestPermission { granted in
if granted {
print("Разрешения на уведомления получены")
} else {
print("Разрешения на уведомления отклонены")
}
}
}
private func handleNotificationLaunch() {
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let userInfo = scene.session.userInfo {
print("Приложение запущено из уведомления")
}
notificationManager.clearBadge()
}
var body: some Scene {
WindowGroup {
Group {
@ -38,6 +58,8 @@ struct MobileMkchApp: App {
.onAppear {
BackgroundTaskManager.shared.registerBackgroundTasks()
setupBackgroundTasks()
setupNotifications()
handleNotificationLaunch()
}
}
}

View File

@ -2,6 +2,18 @@ import Foundation
import UserNotifications
import UIKit
class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
static let shared = NotificationDelegate()
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .sound, .badge])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
completionHandler()
}
}
class NotificationManager: ObservableObject {
static let shared = NotificationManager()
@ -17,11 +29,20 @@ class NotificationManager: ObservableObject {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
DispatchQueue.main.async {
self.isNotificationsEnabled = granted
if granted {
self.registerForRemoteNotifications()
}
completion(granted)
}
}
}
private func registerForRemoteNotifications() {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
func checkNotificationStatus() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
@ -33,36 +54,90 @@ class NotificationManager: ObservableObject {
func subscribeToBoard(_ boardCode: String) {
subscribedBoards.insert(boardCode)
saveSubscribedBoards()
BackgroundTaskManager.shared.scheduleBackgroundTask()
}
private func syncThreadsForBoard(_ boardCode: String) {
let url = URL(string: "https://mkch.pooziqo.xyz/api/board/\(boardCode)")!
var request = URLRequest(url: url)
request.setValue("MobileMkch/2.0.0-ios-alpha", forHTTPHeaderField: "User-Agent")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let threads = try? JSONDecoder().decode([Thread].self, from: data) {
let savedThreadsKey = "savedThreads_\(boardCode)"
if let encodedData = try? JSONEncoder().encode(threads) {
UserDefaults.standard.set(encodedData, forKey: savedThreadsKey)
print("Синхронизировано \(threads.count) тредов для /\(boardCode)/")
}
}
}.resume()
}
func unsubscribeFromBoard(_ boardCode: String) {
subscribedBoards.remove(boardCode)
saveSubscribedBoards()
let savedThreadsKey = "savedThreads_\(boardCode)"
UserDefaults.standard.removeObject(forKey: savedThreadsKey)
}
func scheduleNotification(for thread: Thread, boardCode: String) {
guard isNotificationsEnabled else { return }
let content = UNMutableNotificationContent()
content.title = "Новый тред"
content.body = "\(thread.title) в /\(boardCode)/"
content.sound = .default
content.badge = 1
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
let request = UNNotificationRequest(identifier: "thread_\(thread.id)", content: content, trigger: trigger)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: "thread_\(thread.id)_\(boardCode)", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Ошибка планирования уведомления: \(error)")
}
}
}
func scheduleTestNotification() {
guard isNotificationsEnabled else { return }
let content = UNMutableNotificationContent()
content.title = "Тестовое уведомление"
content.body = "Новый тред: Тестовый тред в /test/"
content.sound = .default
content.badge = 1
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let request = UNNotificationRequest(identifier: "test_notification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Ошибка планирования тестового уведомления: \(error)")
}
}
}
func clearAllNotifications() {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
}
func clearBadge() {
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = 0
}
}
func clearAllSavedThreads() {
for boardCode in subscribedBoards {
let savedThreadsKey = "savedThreads_\(boardCode)"
UserDefaults.standard.removeObject(forKey: savedThreadsKey)
}
print("Очищены все сохраненные треды")
}
private func loadSubscribedBoards() {

View File

@ -7,6 +7,8 @@ struct NotificationSettingsView: View {
@State private var showingPermissionAlert = false
@State private var boards: [Board] = []
@State private var isLoadingBoards = false
@State private var isCheckingThreads = false
@State private var showingTestNotification = false
var body: some View {
VStack {
@ -40,7 +42,7 @@ struct NotificationSettingsView: View {
.cornerRadius(8)
.padding(.horizontal)
Text("Уведомления находятся в бета-версии и могут работать нестабильно. Функция может работать нестабильно или не работать ВОВСЕ.")
Text("Уведомления находятся в бета-версии и могут работать нестабильно.")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
@ -71,12 +73,59 @@ struct NotificationSettingsView: View {
}
.onChange(of: settings.notificationInterval) { _ in
settings.saveSettings()
BackgroundTaskManager.shared.scheduleBackgroundTask()
}
Button("Проверить новые треды сейчас") {
checkNewThreadsNow()
Button(action: {
showingTestNotification = true
notificationManager.scheduleTestNotification()
}) {
HStack {
Image(systemName: "bell.badge")
Text("Отправить тестовое уведомление")
}
}
.foregroundColor(.blue)
.disabled(!notificationManager.isNotificationsEnabled)
Button(action: {
isCheckingThreads = true
checkNewThreadsNow()
}) {
HStack {
if isCheckingThreads {
ProgressView()
.scaleEffect(0.8)
} else {
Image(systemName: "arrow.clockwise")
}
Text("Проверить новые треды сейчас")
}
}
.foregroundColor(.blue)
.disabled(isCheckingThreads || notificationManager.subscribedBoards.isEmpty)
Button(action: {
syncAllBoards()
}) {
HStack {
Image(systemName: "arrow.triangle.2.circlepath")
Text("Синхронизировать все доски")
}
}
.foregroundColor(.orange)
.disabled(notificationManager.subscribedBoards.isEmpty)
Button(action: {
notificationManager.clearAllSavedThreads()
}) {
HStack {
Image(systemName: "trash")
Text("Очистить все сохраненные треды")
}
}
.foregroundColor(.red)
.disabled(notificationManager.subscribedBoards.isEmpty)
}
}
@ -89,6 +138,9 @@ struct NotificationSettingsView: View {
Text("Загрузка досок...")
.foregroundColor(.secondary)
}
} else if boards.isEmpty {
Text("Не удалось загрузить доски")
.foregroundColor(.secondary)
} else {
ForEach(boards) { board in
HStack {
@ -117,6 +169,22 @@ struct NotificationSettingsView: View {
}
}
}
Section(header: Text("Статус")) {
HStack {
Text("Разрешения")
Spacer()
Text(notificationManager.isNotificationsEnabled ? "Включены" : "Отключены")
.foregroundColor(notificationManager.isNotificationsEnabled ? .green : .red)
}
HStack {
Text("Подписки")
Spacer()
Text("\(notificationManager.subscribedBoards.count) досок")
.foregroundColor(.secondary)
}
}
}
}
}
@ -139,6 +207,11 @@ struct NotificationSettingsView: View {
} message: {
Text("Для получения уведомлений о новых тредах необходимо разрешить уведомления в настройках")
}
.alert("Тестовое уведомление", isPresented: $showingTestNotification) {
Button("OK") { }
} message: {
Text("Тестовое уведомление отправлено. Проверьте, получили ли вы его.")
}
}
}
@ -154,25 +227,57 @@ extension NotificationSettingsView {
private func checkNewThreadsNow() {
guard !notificationManager.subscribedBoards.isEmpty else { return }
for boardCode in notificationManager.subscribedBoards {
let lastKnownId = UserDefaults.standard.integer(forKey: "lastThreadId_\(boardCode)")
let group = DispatchGroup()
var foundNewThreads = false
apiClient.checkNewThreads(forBoard: boardCode, lastKnownThreadId: lastKnownId) { result in
for boardCode in notificationManager.subscribedBoards {
group.enter()
apiClient.checkNewThreads(forBoard: boardCode, lastKnownThreadId: 0) { result in
DispatchQueue.main.async {
switch result {
case .success(let newThreads):
if !newThreads.isEmpty {
foundNewThreads = true
for thread in newThreads {
notificationManager.scheduleNotification(for: thread, boardCode: boardCode)
}
UserDefaults.standard.set(newThreads.first?.id ?? lastKnownId, forKey: "lastThreadId_\(boardCode)")
}
case .failure:
break
case .failure(let error):
print("Ошибка проверки тредов для /\(boardCode)/: \(error)")
}
group.leave()
}
}
}
group.notify(queue: .main) {
isCheckingThreads = false
if !foundNewThreads {
print("Новых тредов не найдено")
}
}
}
private func syncAllBoards() {
for boardCode in notificationManager.subscribedBoards {
let savedThreadsKey = "savedThreads_\(boardCode)"
UserDefaults.standard.removeObject(forKey: savedThreadsKey)
let url = URL(string: "https://mkch.pooziqo.xyz/api/board/\(boardCode)")!
var request = URLRequest(url: url)
request.setValue("MobileMkch/2.0.0-ios-alpha", forHTTPHeaderField: "User-Agent")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let threads = try? JSONDecoder().decode([Thread].self, from: data) {
if let encodedData = try? JSONEncoder().encode(threads) {
UserDefaults.standard.set(encodedData, forKey: savedThreadsKey)
print("Синхронизировано \(threads.count) тредов для /\(boardCode)/")
}
}
}.resume()
}
}
private func loadBoards() {