Vernissage/Vernissage/VernissageApp.swift

268 lines
11 KiB
Swift
Raw Normal View History

2022-12-28 09:37:16 +01:00
//
// https://mczachurski.dev
2023-04-09 20:51:33 +02:00
// Copyright © 2023 Marcin Czachurski and the repository contributors.
2023-03-28 10:35:38 +02:00
// Licensed under the Apache License 2.0.
2022-12-28 09:37:16 +01:00
//
import SwiftUI
2023-01-24 20:38:21 +01:00
import Nuke
import NukeUI
2023-04-07 14:20:12 +02:00
import ClientKit
2023-04-07 16:59:18 +02:00
import EnvironmentKit
2023-04-10 12:35:49 +02:00
import WidgetKit
2023-10-20 07:45:18 +02:00
import SwiftData
2023-10-21 09:44:40 +02:00
import TipKit
import OSLog
import BackgroundTasks
2022-12-28 09:37:16 +01:00
@main
2023-01-03 14:09:22 +01:00
struct VernissageApp: App {
2022-12-31 16:31:05 +01:00
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@Environment(\.scenePhase) private var phase
2022-12-31 16:31:05 +01:00
2023-10-19 13:24:02 +02:00
@State var applicationState = ApplicationState.shared
@State var client = Client.shared
@State var routerPath = RouterPath()
@State var tipsStore = TipsStore()
2022-12-31 16:31:05 +01:00
@State var applicationViewMode: ApplicationViewMode = .loading
2023-01-12 18:34:48 +01:00
@State var tintColor = ApplicationState.shared.tintColor.color()
2023-01-13 13:37:01 +01:00
@State var theme = ApplicationState.shared.theme.colorScheme()
2023-10-20 07:45:18 +02:00
let modelContainer = SwiftDataHandler.shared.sharedModelContainer
let timer = Timer.publish(every: 120, on: .main, in: .common).autoconnect()
2023-02-03 15:16:30 +01:00
2022-12-28 09:37:16 +01:00
var body: some Scene {
WindowGroup {
2023-10-19 13:24:02 +02:00
NavigationStack {
2022-12-31 16:31:05 +01:00
switch applicationViewMode {
case .loading:
2023-01-06 13:44:02 +01:00
LoadingView()
2023-01-23 18:01:27 +01:00
.withAppRouteur()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
2022-12-31 16:31:05 +01:00
case .signIn:
2023-03-29 17:06:41 +02:00
SignInView { accountModel in
self.setApplicationState(accountModel: accountModel)
2022-12-31 16:31:05 +01:00
}
2023-01-23 18:01:27 +01:00
.withAppRouteur()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
2022-12-31 16:31:05 +01:00
case .mainView:
2023-01-23 18:01:27 +01:00
MainView()
.withAppRouteur()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
2023-02-13 21:10:07 +01:00
.withOverlayDestinations(overlayDestinations: $routerPath.presentedOverlay)
2023-04-16 17:44:35 +02:00
.withAlertDestinations(alertDestinations: $routerPath.presentedAlert)
2022-12-31 16:31:05 +01:00
}
}
2023-10-20 07:45:18 +02:00
.modelContainer(modelContainer)
2023-10-19 13:24:02 +02:00
.environment(applicationState)
.environment(client)
.environment(routerPath)
.environment(tipsStore)
2023-01-12 18:34:48 +01:00
.tint(self.tintColor)
2023-01-13 13:37:01 +01:00
.preferredColorScheme(self.theme)
2023-01-12 18:34:48 +01:00
.task {
await self.onApplicationStart()
2022-12-29 17:27:15 +01:00
}
.onReceive(timer) { _ in
Task {
2023-10-22 10:09:02 +02:00
// Refresh indicator of new photos and new notifications each two minutes (when application is in the foreground)..
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
}
}
2023-10-19 09:29:49 +02:00
.onChange(of: applicationState.theme) { oldValue, newValue in
2023-01-23 18:01:27 +01:00
self.theme = newValue.colorScheme()
}
2023-10-19 09:29:49 +02:00
.onChange(of: applicationState.tintColor) { oldValue, newValue in
2023-01-23 18:01:27 +01:00
self.tintColor = newValue.color()
}
2023-10-19 09:29:49 +02:00
.onChange(of: applicationState.account) { oldValue, newValue in
2023-02-23 08:09:02 +01:00
if newValue == nil {
self.applicationViewMode = .signIn
}
}
2023-10-19 09:29:49 +02:00
.onChange(of: applicationState.showStatusId) { oldValue, newValue in
2023-03-11 18:30:33 +01:00
if let statusId = newValue {
self.routerPath.navigate(to: .status(id: statusId))
self.applicationState.showStatusId = nil
}
}
2023-10-19 09:29:49 +02:00
.onChange(of: applicationState.showAccountId) { oldValue, newValue in
2023-05-01 07:50:24 +02:00
if let accountId = newValue {
self.routerPath.navigate(to: .userProfile(accountId: accountId, accountDisplayName: nil, accountUserName: ""))
self.applicationState.showAccountId = nil
}
}
2022-12-28 09:37:16 +01:00
}
.onChange(of: phase) { oldValue, newValue in
switch newValue {
case .background:
scheduleAppRefresh()
case .active:
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
Task {
// Refresh indicator of new photos and new statuses when application is become active.
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
}
}
// Reload widget content when application become active.
WidgetCenter.shared.reloadAllTimelines()
default: break
}
}
.backgroundTask(.appRefresh(AppConstants.backgroundFetcherName)) {
await self.setBadgeCount()
}
2022-12-28 09:37:16 +01:00
}
2023-03-29 17:06:41 +02:00
@MainActor
private func onApplicationStart() async {
UIPageControl.appearance().currentPageIndicatorTintColor = UIColor.white.withAlphaComponent(0.7)
UIPageControl.appearance().pageIndicatorTintColor = UIColor.white.withAlphaComponent(0.4)
2023-10-21 09:44:40 +02:00
// Configure TipKit.
try? Tips.configure([.displayFrequency(.daily), .datastoreLocation(.applicationDefault)])
// Set custom configurations for Nuke image/data loaders.
self.setImagePipelines()
// Load user preferences from database.
self.loadUserPreferences()
// Refresh other access tokens.
await self.refreshAccessTokens()
2023-03-29 17:06:41 +02:00
// When user doesn't exists then we have to open sign in view.
2023-10-20 07:45:18 +02:00
let modelContext = self.modelContainer.mainContext
guard let currentAccount = AccountDataHandler.shared.getCurrentAccountData(modelContext: modelContext) else {
2023-03-29 17:06:41 +02:00
self.applicationViewMode = .signIn
return
}
2023-03-29 17:06:41 +02:00
// Create model based on core data entity.
2023-04-07 14:20:12 +02:00
let accountModel = currentAccount.toAccountModel()
2023-03-29 17:06:41 +02:00
// Verify access token correctness.
let authorizationSession = AuthorizationSession()
2023-10-20 07:45:18 +02:00
await AuthorizationService.shared.verifyAccount(session: authorizationSession,
accountModel: accountModel,
modelContext: modelContext) { signedInAccountModel in
2023-03-29 17:06:41 +02:00
guard let signedInAccountModel else {
self.applicationViewMode = .signIn
return
}
self.setApplicationState(accountModel: signedInAccountModel, checkNewPhotos: true)
}
}
2023-02-20 16:41:18 +01:00
2023-03-29 17:06:41 +02:00
private func setApplicationState(accountModel: AccountModel, checkNewPhotos: Bool = false) {
2023-02-20 16:41:18 +01:00
Task { @MainActor in
let instance = try? await self.client.instances.instance(url: accountModel.serverUrl)
// Refresh client state.
2023-02-20 16:41:18 +01:00
self.client.setAccount(account: accountModel)
// Refresh application state.
self.applicationState.changeApplicationState(accountModel: accountModel,
instance: instance,
2023-10-22 10:09:02 +02:00
lastSeenStatusId: accountModel.lastSeenStatusId,
lastSeenNotificationId: accountModel.lastSeenNotificationId)
// Change view displayed by application.
2023-02-20 16:41:18 +01:00
self.applicationViewMode = .mainView
2023-02-20 16:41:18 +01:00
// Check amount of newly added photos.
if checkNewPhotos {
2023-10-22 10:09:02 +02:00
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
2023-02-20 16:41:18 +01:00
}
}
}
2023-01-24 20:38:21 +01:00
private func loadUserPreferences() {
2023-10-20 07:45:18 +02:00
let modelContext = self.modelContainer.mainContext
ApplicationSettingsHandler.shared.update(applicationState: self.applicationState, modelContext: modelContext)
2023-04-07 18:58:15 +02:00
self.tintColor = self.applicationState.tintColor.color()
self.theme = self.applicationState.theme.colorScheme()
2023-01-24 20:38:21 +01:00
}
2023-01-24 20:38:21 +01:00
private func setImagePipelines() {
let pipeline = ImagePipeline {
$0.dataLoader = DataLoader(configuration: {
// Disable disk caching built into URLSession
let conf = DataLoader.defaultConfiguration
conf.urlCache = nil
return conf
}())
2023-01-24 20:38:21 +01:00
$0.imageCache = ImageCache.shared
if let dataCache = try? DataCache(name: AppConstants.imagePipelineCacheName) {
$0.dataCache = dataCache
}
2023-01-24 20:38:21 +01:00
}
2023-01-24 20:38:21 +01:00
ImagePipeline.shared = pipeline
}
2023-02-06 14:50:28 +01:00
private func refreshAccessTokens() async {
2023-10-20 07:45:18 +02:00
let modelContext = self.modelContainer.mainContext
let defaultSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
2023-02-06 14:50:28 +01:00
// Run refreshing access tokens once per day.
guard let refreshTokenDate = Calendar.current.date(byAdding: .day, value: 1, to: defaultSettings.lastRefreshTokens), refreshTokenDate < Date.now else {
return
}
2023-02-06 14:50:28 +01:00
// Refresh access tokens.
2023-10-20 07:45:18 +02:00
await AuthorizationService.shared.refreshAccessTokens(modelContext: modelContext)
2023-02-06 14:50:28 +01:00
// Update time when refresh tokens has been updated.
defaultSettings.lastRefreshTokens = Date.now
2023-10-20 07:45:18 +02:00
try? modelContext.save()
2023-02-06 14:50:28 +01:00
}
2023-02-22 14:33:03 +01:00
private func calculateNewPhotosInBackground() async {
2023-10-22 10:09:02 +02:00
let modelContext = self.modelContainer.mainContext
2023-10-20 07:45:18 +02:00
2023-10-25 08:36:04 +02:00
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext
)
}
2023-10-22 10:09:02 +02:00
private func calculateNewNotificationsInBackground() async {
Logger.main.info("Calculating new notifications started.")
2023-10-22 10:09:02 +02:00
let modelContext = self.modelContainer.mainContext
2023-10-25 08:36:04 +02:00
let amountOfNewNotifications = await NotificationsService.shared.amountOfNewNotifications(modelContext: modelContext)
self.applicationState.amountOfNewNotifications = amountOfNewNotifications
2023-10-22 10:09:02 +02:00
do {
try await NotificationsService.shared.setBadgeCount(amountOfNewNotifications, modelContext: modelContext)
Logger.main.info("New notifications (\(amountOfNewNotifications)) calculated successfully.")
} catch {
Logger.main.error("Error ['Set badge count failed']: \(error.localizedDescription)")
2023-10-22 10:09:02 +02:00
}
}
private func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: AppConstants.backgroundFetcherName)
request.earliestBeginDate = .now.addingTimeInterval(20 * 60)
do {
try BGTaskScheduler.shared.submit(request)
Logger.main.info("Background task scheduled successfully.")
} catch {
Logger.main.error("Error ['Registering background task failed']: \(error.localizedDescription)")
}
}
private func setBadgeCount() async {
scheduleAppRefresh()
await self.calculateNewNotificationsInBackground()
}
2022-12-31 16:31:05 +01:00
}