Kurdtvs-Live-Kurdish-TV-Kur.../Mastodon/Scene/Settings/SettingsViewModel.swift

356 lines
14 KiB
Swift

//
// SettingsViewModel.swift
// Mastodon
//
// Created by ihugo on 2021/4/7.
//
import Combine
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
import os.log
class SettingsViewModel: NSObject, NeedsDependency {
// confirm set only once
weak var context: AppContext! { willSet { precondition(context == nil) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(coordinator == nil) } }
var dataSource: UITableViewDiffableDataSource<SettingsSection, SettingsItem>!
var disposeBag = Set<AnyCancellable>()
var updateDisposeBag = Set<AnyCancellable>()
var createDisposeBag = Set<AnyCancellable>()
let viewDidLoad = PassthroughSubject<Void, Never>()
lazy var fetchResultsController: NSFetchedResultsController<Setting> = {
let fetchRequest = Setting.sortedFetchRequest
if let box =
self.context.authenticationService.activeMastodonAuthenticationBox.value {
let domain = box.domain
fetchRequest.predicate = Setting.predicate(domain: domain)
}
fetchRequest.fetchLimit = 1
fetchRequest.returnsObjectsAsFaults = false
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: context.managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil
)
controller.delegate = self
return controller
}()
let setting = CurrentValueSubject<Setting?, Never>(nil)
/// create a subscription when:
/// - does not has one
/// - does not find subscription for selected trigger when change trigger
let createSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
/// update a subscription when:
/// - change switch for specified alerts
let updateSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
lazy var notificationDefaultValue: [String: [Bool?]] = {
let followerSwitchItems: [Bool?] = [true, nil, true, true]
let anyoneSwitchItems: [Bool?] = [true, true, true, true]
let noOneSwitchItems: [Bool?] = [nil, nil, nil, nil]
let followSwitchItems: [Bool?] = [true, true, true, true]
let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
let follower = L10n.Scene.Settings.Section.Notifications.Trigger.follower
let follow = L10n.Scene.Settings.Section.Notifications.Trigger.follow
let noOne = L10n.Scene.Settings.Section.Notifications.Trigger.noone
return [anyone: anyoneSwitchItems,
follower: followerSwitchItems,
follow: followSwitchItems,
noOne: noOneSwitchItems]
}()
struct Input {
}
struct Output {
}
init(context: AppContext, coordinator: SceneCoordinator) {
self.context = context
self.coordinator = coordinator
super.init()
}
func transform(input: Input?) -> Output? {
typealias SubscriptionResponse = Mastodon.Response.Content<Mastodon.Entity.Subscription>
createSubscriptionSubject
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] (arg) in
let (triggerBy, values) = arg
guard let self = self else {
return
}
guard let activeMastodonAuthenticationBox =
self.context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
guard values.count >= 4 else {
return
}
self.createDisposeBag.removeAll()
typealias Query = Mastodon.API.Subscriptions.CreateSubscriptionQuery
let domain = activeMastodonAuthenticationBox.domain
let query = Query(
endpoint: "http://www.google.com",
p256dh: "BLQELIDm-6b9Bl07YrEuXJ4BL_YBVQ0dvt9NQGGJxIQidJWHPNa9YrouvcQ9d7_MqzvGS9Alz60SZNCG3qfpk=",
auth: "4vQK-SvRAN5eo-8ASlrwA==",
favourite: values[0],
follow: values[1],
reblog: values[2],
mention: values[3],
poll: nil)
self.context.apiService.changeSubscription(
domain: domain,
mastodonAuthenticationBox: activeMastodonAuthenticationBox,
query: query,
triggerBy: triggerBy
)
.sink { (_) in
} receiveValue: { (_) in
}
.store(in: &self.createDisposeBag)
}
.store(in: &disposeBag)
updateSubscriptionSubject
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] (arg) in
let (triggerBy, values) = arg
guard let self = self else {
return
}
guard let activeMastodonAuthenticationBox =
self.context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
guard values.count >= 4 else {
return
}
self.updateDisposeBag.removeAll()
typealias Query = Mastodon.API.Subscriptions.UpdateSubscriptionQuery
let domain = activeMastodonAuthenticationBox.domain
let query = Query(
favourite: values[0],
follow: values[1],
reblog: values[2],
mention: values[3],
poll: nil)
self.context.apiService.updateSubscription(
domain: domain,
mastodonAuthenticationBox: activeMastodonAuthenticationBox,
query: query,
triggerBy: triggerBy
)
.sink { (_) in
} receiveValue: { (_) in
}
.store(in: &self.updateDisposeBag)
}
.store(in: &disposeBag)
// build data for table view
buildDataSource()
// request subsription data for updating or initialization
requestSubscription()
do {
try fetchResultsController.performFetch()
setting.value = fetchResultsController.fetchedObjects?.first
} catch {
assertionFailure(error.localizedDescription)
}
return nil
}
// MARK: - Private methods
fileprivate func processDataSource(_ settings: Setting?) {
var snapshot = NSDiffableDataSourceSnapshot<SettingsSection, SettingsItem>()
// appearance
let appearnceMode = SettingsItem.AppearanceMode(rawValue: settings?.appearance ?? "") ?? .automatic
let appearanceItem = SettingsItem.apperance(item: appearnceMode)
let appearance = SettingsSection.apperance(title: L10n.Scene.Settings.Section.Appearance.title, selectedMode:appearanceItem)
snapshot.appendSections([appearance])
snapshot.appendItems([appearanceItem])
// notifications
var switches: [Bool?]?
if let alerts = settings?.subscription?.first(where: { (s) -> Bool in
return s.type == settings?.triggerBy
})?.alert {
var items = [Bool?]()
items.append(alerts.favourite?.boolValue)
items.append(alerts.follow?.boolValue)
items.append(alerts.reblog?.boolValue)
items.append(alerts.mention?.boolValue)
switches = items
} else if let triggerBy = settings?.triggerBy,
let values = self.notificationDefaultValue[triggerBy] {
switches = values
self.createSubscriptionSubject.send((triggerBy: triggerBy, values: values))
} else {
// fallback a default value
let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
switches = self.notificationDefaultValue[anyone]
}
let notifications = [L10n.Scene.Settings.Section.Notifications.favorites,
L10n.Scene.Settings.Section.Notifications.follows,
L10n.Scene.Settings.Section.Notifications.boosts,
L10n.Scene.Settings.Section.Notifications.mentions,]
var notificationItems = [SettingsItem]()
for (i, noti) in notifications.enumerated() {
var value: Bool? = nil
if let switches = switches, i < switches.count {
value = switches[i]
}
let item = SettingsItem.notification(item: SettingsItem.NotificationSwitch(title: noti, isOn: value == true, enable: value != nil))
notificationItems.append(item)
}
let notificationSection = SettingsSection.notifications(title: L10n.Scene.Settings.Section.Notifications.title, items: notificationItems)
snapshot.appendSections([notificationSection])
snapshot.appendItems(notificationItems)
// boring zone
let boringLinks = [L10n.Scene.Settings.Section.Boringzone.terms,
L10n.Scene.Settings.Section.Boringzone.privacy]
var boringLinkItems = [SettingsItem]()
for l in boringLinks {
let item = SettingsItem.boringZone(item: SettingsItem.Link(title: l, color: .systemBlue))
boringLinkItems.append(item)
}
let boringSection = SettingsSection.boringZone(title: L10n.Scene.Settings.Section.Boringzone.title, items: boringLinkItems)
snapshot.appendSections([boringSection])
snapshot.appendItems(boringLinkItems)
// spicy zone
let spicyLinks = [L10n.Scene.Settings.Section.Spicyzone.clear,
L10n.Scene.Settings.Section.Spicyzone.signout]
var spicyLinkItems = [SettingsItem]()
for l in spicyLinks {
let item = SettingsItem.spicyZone(item: SettingsItem.Link(title: l, color: .systemRed))
spicyLinkItems.append(item)
}
let spicySection = SettingsSection.spicyZone(title: L10n.Scene.Settings.Section.Spicyzone.title, items: spicyLinkItems)
snapshot.appendSections([spicySection])
snapshot.appendItems(spicyLinkItems)
self.dataSource.apply(snapshot, animatingDifferences: false)
}
private func buildDataSource() {
setting.sink { [weak self] (settings) in
guard let self = self else { return }
self.processDataSource(settings)
}
.store(in: &disposeBag)
}
private func requestSubscription() {
// request subscription of notifications
typealias SubscriptionResponse = Mastodon.Response.Content<Mastodon.Entity.Subscription>
viewDidLoad.flatMap { [weak self] (_) -> AnyPublisher<SubscriptionResponse, Error> in
guard let self = self,
let activeMastodonAuthenticationBox =
self.context.authenticationService.activeMastodonAuthenticationBox.value else {
return Empty<SubscriptionResponse, Error>().eraseToAnyPublisher()
}
let domain = activeMastodonAuthenticationBox.domain
return self.context.apiService.subscription(
domain: domain,
mastodonAuthenticationBox: activeMastodonAuthenticationBox)
}
.sink { [weak self] competion in
if case .failure(_) = competion {
// create a subscription when doesn't has one
let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
if let values = self?.notificationDefaultValue[anyone] {
self?.createSubscriptionSubject.send((triggerBy: anyone, values: values))
}
}
} receiveValue: { (subscription) in
}
.store(in: &disposeBag)
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension SettingsViewModel: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard controller === fetchResultsController else {
return
}
setting.value = fetchResultsController.fetchedObjects?.first
}
}
enum SettingsSection: Hashable {
case apperance(title: String, selectedMode: SettingsItem)
case notifications(title: String, items: [SettingsItem])
case boringZone(title: String, items: [SettingsItem])
case spicyZone(title: String, items: [SettingsItem])
var title: String {
switch self {
case .apperance(let title, _),
.notifications(let title, _),
.boringZone(let title, _),
.spicyZone(let title, _):
return title
}
}
}
enum SettingsItem: Hashable {
enum AppearanceMode: String {
case automatic
case light
case dark
}
struct NotificationSwitch: Hashable {
let title: String
let isOn: Bool
let enable: Bool
}
struct Link: Hashable {
let title: String
let color: UIColor
}
case apperance(item: AppearanceMode)
case notification(item: NotificationSwitch)
case boringZone(item: Link)
case spicyZone(item: Link)
}