diff --git a/CHANGELOG.md b/CHANGELOG.md index fdac0dd84..371425f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Decreased the opacity on disabled buttons. ### Internal Changes +- Moved the database cleanup routine into a background execution task. [#1426](https://github.com/planetary-social/nos/issues/1426) - Fix the Crowdin GitHub integration by using the official GitHub action. [#1520](https://github.com/planetary-social/nos/issues/1520) - Update Xcode to version 15.4, adding compatibility for Xcode 16. - Reduced spammy "Failed to parse Follow" log messages. diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index 8a18135be..2a00fc501 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -2863,8 +2863,9 @@ GCC_PREPROCESSOR_DEFINITIONS = "STAGING=1"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Nos/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Nos Staging"; + INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev"; INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -3038,6 +3039,7 @@ INFOPLIST_FILE = Nos/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev"; INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -3307,7 +3309,9 @@ GCC_OPTIMIZATION_LEVEL = 0; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Nos/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev"; INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -3362,7 +3366,9 @@ ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Nos/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev"; INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; diff --git a/Nos/Controller/PersistenceController.swift b/Nos/Controller/PersistenceController.swift index 4a2db75a5..782efe8fc 100644 --- a/Nos/Controller/PersistenceController.swift +++ b/Nos/Controller/PersistenceController.swift @@ -1,6 +1,7 @@ import CoreData import Logger import Dependencies +import BackgroundTasks final class PersistenceController { @@ -121,6 +122,14 @@ final class PersistenceController { private static func saveVersionToDisk(_ newVersion: Int) { UserDefaults.standard.set(newVersion, forKey: Self.versionKey) } +} + +// MARK: - Maintenance & Cleanup + +extension PersistenceController { + + /// The ID of the background task that runs the `DatabaseCleaner`. Needs to match the value in Info.plist. + static let cleanupBackgroundTaskID = "com.verse.nos.database-cleaner" /// Cleans up unneeded entities from the database. Our local database is really just a cache, and we need to /// invalidate old items to keep it from growing indefinitely. @@ -140,6 +149,36 @@ final class PersistenceController { } } + /// Tells iOS to wake our app up in the background to run `cleanupEntities()` periodically. This tends to run + /// once a day when the device is connected to power. + func scheduleBackgroundCleanupTask() { + @Dependency(\.analytics) var analytics + @Dependency(\.crashReporting) var crashReporting + let scheduler = BGTaskScheduler.shared + + scheduler.register(forTaskWithIdentifier: Self.cleanupBackgroundTaskID, using: nil) { task in + + task.expirationHandler = { + analytics.databaseCleanupTaskExpired() + } + + Task { + await self.cleanupEntities() + task.setTaskCompleted(success: true) + } + } + + let request = BGProcessingTaskRequest(identifier: Self.cleanupBackgroundTaskID) + request.requiresNetworkConnectivity = false + request.requiresExternalPower = false + + do { + try scheduler.submit(request) + } catch { + crashReporting.report("Unable to schedule background task: \(error)") + } + } + static func databaseStatistics(from context: NSManagedObjectContext) async throws -> [(String, Int)] { try await context.perform { var statistics = [String: Int]() @@ -160,6 +199,8 @@ final class PersistenceController { } } +// MARK: - Testing & Debug + #if DEBUG extension PersistenceController { func loadSampleData(context: NSManagedObjectContext) async throws { diff --git a/Nos/Info.plist b/Nos/Info.plist index bfdbc42d9..7815a6e91 100644 --- a/Nos/Info.plist +++ b/Nos/Info.plist @@ -46,6 +46,7 @@ UIBackgroundModes remote-notification + processing UNS_API_KEY $(UNS_API_KEY) @@ -63,5 +64,9 @@ Nos uses the microphone when you are record a video from within the app. WALLET_CONNECT_PROJECT_ID $(WALLET_CONNECT_PROJECT_ID) + BGTaskSchedulerPermittedIdentifiers + + com.verse.nos.database-cleaner + diff --git a/Nos/NosApp.swift b/Nos/NosApp.swift index fbc27ff61..188a98189 100644 --- a/Nos/NosApp.swift +++ b/Nos/NosApp.swift @@ -21,6 +21,7 @@ struct NosApp: App { // hack to fix confirmationDialog color issue // https://github.com/planetary-social/nos/issues/1064 UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = .systemBlue + persistenceController.scheduleBackgroundCleanupTask() } var body: some Scene { @@ -33,9 +34,6 @@ struct NosApp: App { .environment(currentUser) .environmentObject(pushNotificationService) .onOpenURL { DeepLinkService.handle($0, router: router) } - .task { - await persistenceController.cleanupEntities() - } .onChange(of: scenePhase) { _, newPhase in switch newPhase { case .inactive: diff --git a/Nos/Service/Analytics.swift b/Nos/Service/Analytics.swift index 22a520b85..282643c59 100644 --- a/Nos/Service/Analytics.swift +++ b/Nos/Service/Analytics.swift @@ -129,6 +129,18 @@ class Analytics { track("Database Statistics", properties: properties) } + func databaseCleanupStarted() { + track("Database Cleanup Started") + } + + func databaseCleanupTaskExpired() { + track("Database Cleanup Task Expired") + } + + func databaseCleanupCompleted(duration: TimeInterval) { + track("Database Cleanup Completed", properties: ["duration": duration]) + } + func logout() { track("Logged out") postHog?.reset() diff --git a/Nos/Service/DatabaseCleaner.swift b/Nos/Service/DatabaseCleaner.swift index a695cb683..bfe3f84ff 100644 --- a/Nos/Service/DatabaseCleaner.swift +++ b/Nos/Service/DatabaseCleaner.swift @@ -23,6 +23,7 @@ enum DatabaseCleaner { @Dependency(\.analytics) var analytics let startTime = Date.now + analytics.databaseCleanupStarted() Log.info("Starting Core Data cleanup...") Log.info("Database statistics: \(try await PersistenceController.databaseStatistics(from: context))") @@ -78,6 +79,7 @@ enum DatabaseCleaner { let elapsedTime = Date.now.timeIntervalSince1970 - startTime.timeIntervalSince1970 Log.info("Finished Core Data cleanup in \(elapsedTime) seconds.") + analytics.databaseCleanupCompleted(duration: elapsedTime) } /// This converts old hydrated events back to stubs. We do this because EventReferences can form long chains