завел уведы вроде как ура
This commit is contained in:
parent
d053ffccc6
commit
e78c66b0c8
@ -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))
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user