Refactoring

This commit is contained in:
Justin Mazzocchi 2020-09-07 09:33:36 -07:00
parent df5ca6ddb2
commit 367d79ed2c
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
15 changed files with 176 additions and 146 deletions

View File

@ -102,15 +102,24 @@ public extension IdentityDatabase {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func updatePreferences(_ preferences: Identity.Preferences, func updatePreferences(_ preferences: Mastodon.Preferences,
forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> { forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
databaseQueue.writePublisher { databaseQueue.writePublisher {
let data = try IdentityRecord.databaseJSONEncoder(for: "preferences").encode(preferences) guard let storedPreferences = try IdentityRecord.filter(Column("id") == identityID)
.fetchOne($0)?
try IdentityRecord .preferences else {
.filter(Column("id") == identityID) throw IdentityDatabaseError.identityNotFound
.updateAll($0, Column("preferences").set(to: data))
} }
try Self.writePreferences(storedPreferences.updated(from: preferences), id: identityID)($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func updatePreferences(_ preferences: Identity.Preferences,
forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
databaseQueue.writePublisher(updates: Self.writePreferences(preferences, id: identityID))
.ignoreOutput() .ignoreOutput()
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -201,6 +210,16 @@ private extension IdentityDatabase {
.asRequest(of: IdentityResult.self) .asRequest(of: IdentityResult.self)
} }
private static func writePreferences(_ preferences: Identity.Preferences, id: UUID) -> (Database) throws -> Void {
{
let data = try IdentityRecord.databaseJSONEncoder(for: "preferences").encode(preferences)
try IdentityRecord
.filter(Column("id") == id)
.updateAll($0, Column("preferences").set(to: data))
}
}
private static func migrate(_ writer: DatabaseWriter) throws { private static func migrate(_ writer: DatabaseWriter) throws {
var migrator = DatabaseMigrator() var migrator = DatabaseMigrator()

View File

@ -0,0 +1,43 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import DB
import Foundation
public class IdentifiedEnvironment {
@Published public private(set) var identity: Identity
public let appEnvironment: AppEnvironment
public let identityService: IdentityService
public let observationErrors: AnyPublisher<Error, Never>
init(id: UUID, database: IdentityDatabase, environment: AppEnvironment) throws {
appEnvironment = environment
// The scheduling on the observation is immediate so an initial value can be extracted
let sharedObservation = database.identityObservation(id: id).share()
var initialIdentity: Identity?
_ = sharedObservation.first().sink(
receiveCompletion: { _ in },
receiveValue: { initialIdentity = $0 })
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
self.identity = identity
identityService = try IdentityService(id: identity.id,
instanceURL: identity.url,
database: database,
environment: environment)
let observationErrorsSubject = PassthroughSubject<Error, Never>()
self.observationErrors = observationErrorsSubject.eraseToAnyPublisher()
sharedObservation.catch { error -> Empty<Identity, Never> in
observationErrorsSubject.send(error)
return Empty()
}
.assign(to: &$identity)
}
}

View File

@ -11,16 +11,16 @@ public struct AllIdentitiesService {
public let mostRecentlyUsedIdentityID: AnyPublisher<UUID?, Never> public let mostRecentlyUsedIdentityID: AnyPublisher<UUID?, Never>
public let instanceFilterService: InstanceFilterService public let instanceFilterService: InstanceFilterService
private let identityDatabase: IdentityDatabase private let database: IdentityDatabase
private let environment: AppEnvironment private let environment: AppEnvironment
public init(environment: AppEnvironment) throws { public init(environment: AppEnvironment) throws {
self.identityDatabase = try IdentityDatabase(inMemory: environment.inMemoryContent, self.database = try IdentityDatabase(inMemory: environment.inMemoryContent,
fixture: environment.identityFixture, fixture: environment.identityFixture,
keychain: environment.keychain) keychain: environment.keychain)
self.environment = environment self.environment = environment
mostRecentlyUsedIdentityID = identityDatabase.mostRecentlyUsedIdentityIDObservation() mostRecentlyUsedIdentityID = database.mostRecentlyUsedIdentityIDObservation()
.replaceError(with: nil) .replaceError(with: nil)
.eraseToAnyPublisher() .eraseToAnyPublisher()
instanceFilterService = InstanceFilterService(environment: environment) instanceFilterService = InstanceFilterService(environment: environment)
@ -28,14 +28,12 @@ public struct AllIdentitiesService {
} }
public extension AllIdentitiesService { public extension AllIdentitiesService {
func identityService(id: UUID) throws -> IdentityService { func identifiedEnvironment(id: UUID) throws -> IdentifiedEnvironment {
try IdentityService(identityID: id, try IdentifiedEnvironment(id: id, database: database, environment: environment)
identityDatabase: identityDatabase,
environment: environment)
} }
func createIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> { func createIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> {
identityDatabase.createIdentity(id: id, url: instanceURL) database.createIdentity(id: id, url: instanceURL)
} }
func authorizeIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> { func authorizeIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> {
@ -61,7 +59,7 @@ public extension AllIdentitiesService {
mastodonAPIClient.instanceURL = identity.url mastodonAPIClient.instanceURL = identity.url
return identityDatabase.deleteIdentity(id: identity.id) return database.deleteIdentity(id: identity.id)
.collect() .collect()
.tryMap { _ in .tryMap { _ in
DeletionEndpoint.oauthRevoke( DeletionEndpoint.oauthRevoke(
@ -80,10 +78,10 @@ public extension AllIdentitiesService {
} }
func updatePushSubscriptions(deviceToken: Data) -> AnyPublisher<Never, Error> { func updatePushSubscriptions(deviceToken: Data) -> AnyPublisher<Never, Error> {
identityDatabase.identitiesWithOutdatedDeviceTokens(deviceToken: deviceToken) database.identitiesWithOutdatedDeviceTokens(deviceToken: deviceToken)
.tryMap { identities -> [AnyPublisher<Never, Never>] in .tryMap { identities -> [AnyPublisher<Never, Never>] in
try identities.map { try identities.map {
try identityService(id: $0.id) try IdentityService(id: $0.id, instanceURL: $0.url, database: database, environment: environment)
.createPushSubscription(deviceToken: deviceToken, alerts: $0.pushSubscriptionAlerts) .createPushSubscription(deviceToken: deviceToken, alerts: $0.pushSubscriptionAlerts)
.catch { _ in Empty() } // don't want to disrupt pipeline .catch { _ in Empty() } // don't want to disrupt pipeline
.eraseToAnyPublisher() .eraseToAnyPublisher()

View File

@ -8,10 +8,8 @@ import Mastodon
import MastodonAPI import MastodonAPI
import Secrets import Secrets
public class IdentityService { public struct IdentityService {
@Published public private(set) var identity: Identity private let identityID: UUID
public let observationErrors: AnyPublisher<Error, Never>
private let identityDatabase: IdentityDatabase private let identityDatabase: IdentityDatabase
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
private let environment: AppEnvironment private let environment: AppEnvironment
@ -19,40 +17,20 @@ public class IdentityService {
private let secrets: Secrets private let secrets: Secrets
private let observationErrorsInput = PassthroughSubject<Error, Never>() private let observationErrorsInput = PassthroughSubject<Error, Never>()
init(identityID: UUID, init(id: UUID, instanceURL: URL, database: IdentityDatabase, environment: AppEnvironment) throws {
identityDatabase: IdentityDatabase, identityID = id
environment: AppEnvironment) throws { identityDatabase = database
self.identityDatabase = identityDatabase
self.environment = environment self.environment = environment
observationErrors = observationErrorsInput.eraseToAnyPublisher()
let observation = identityDatabase.identityObservation(id: identityID).share()
var initialIdentity: Identity?
_ = observation.first().sink(
receiveCompletion: { _ in },
receiveValue: { initialIdentity = $0 })
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
self.identity = identity
secrets = Secrets( secrets = Secrets(
identityID: identityID, identityID: id,
keychain: environment.keychain) keychain: environment.keychain)
mastodonAPIClient = MastodonAPIClient(session: environment.session) mastodonAPIClient = MastodonAPIClient(session: environment.session)
mastodonAPIClient.instanceURL = identity.url mastodonAPIClient.instanceURL = instanceURL
mastodonAPIClient.accessToken = try? secrets.getAccessToken() mastodonAPIClient.accessToken = try? secrets.getAccessToken()
contentDatabase = try ContentDatabase(identityID: identityID, contentDatabase = try ContentDatabase(identityID: id,
inMemory: environment.inMemoryContent, inMemory: environment.inMemoryContent,
keychain: environment.keychain) keychain: environment.keychain)
observation.catch { [weak self] error -> Empty<Identity, Never> in
self?.observationErrorsInput.send(error)
return Empty()
}
.assign(to: &$identity)
} }
} }
@ -60,28 +38,24 @@ public extension IdentityService {
var isAuthorized: Bool { mastodonAPIClient.accessToken != nil } var isAuthorized: Bool { mastodonAPIClient.accessToken != nil }
func updateLastUse() -> AnyPublisher<Never, Error> { func updateLastUse() -> AnyPublisher<Never, Error> {
identityDatabase.updateLastUsedAt(identityID: identity.id) identityDatabase.updateLastUsedAt(identityID: identityID)
} }
func verifyCredentials() -> AnyPublisher<Never, Error> { func verifyCredentials() -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(AccountEndpoint.verifyCredentials) mastodonAPIClient.request(AccountEndpoint.verifyCredentials)
.zip(Just(identity.id).first().setFailureType(to: Error.self)) .flatMap { identityDatabase.updateAccount($0, forIdentityID: identityID) }
.flatMap(identityDatabase.updateAccount)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func refreshServerPreferences() -> AnyPublisher<Never, Error> { func refreshServerPreferences() -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(PreferencesEndpoint.preferences) mastodonAPIClient.request(PreferencesEndpoint.preferences)
.zip(Just(self).first().setFailureType(to: Error.self)) .flatMap { identityDatabase.updatePreferences($0, forIdentityID: identityID) }
.map { ($1.identity.preferences.updated(from: $0), $1.identity.id) }
.flatMap(identityDatabase.updatePreferences)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func refreshInstance() -> AnyPublisher<Never, Error> { func refreshInstance() -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(InstanceEndpoint.instance) mastodonAPIClient.request(InstanceEndpoint.instance)
.zip(Just(identity.id).first().setFailureType(to: Error.self)) .flatMap { identityDatabase.updateInstance($0, forIdentityID: identityID) }
.flatMap(identityDatabase.updateInstance)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -90,7 +64,7 @@ public extension IdentityService {
} }
func recentIdentitiesObservation() -> AnyPublisher<[Identity], Error> { func recentIdentitiesObservation() -> AnyPublisher<[Identity], Error> {
identityDatabase.recentIdentitiesObservation(excluding: identity.id) identityDatabase.recentIdentitiesObservation(excluding: identityID)
} }
func refreshLists() -> AnyPublisher<Never, Error> { func refreshLists() -> AnyPublisher<Never, Error> {
@ -145,8 +119,7 @@ public extension IdentityService {
func deleteFilter(id: String) -> AnyPublisher<Never, Error> { func deleteFilter(id: String) -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(DeletionEndpoint.filter(id: id)) mastodonAPIClient.request(DeletionEndpoint.filter(id: id))
.map { _ in id } .flatMap { _ in contentDatabase.deleteFilter(id: id) }
.flatMap(contentDatabase.deleteFilter(id:))
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -159,12 +132,10 @@ public extension IdentityService {
} }
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Never, Error> { func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Never, Error> {
identityDatabase.updatePreferences(preferences, forIdentityID: identity.id) identityDatabase.updatePreferences(preferences, forIdentityID: identityID)
.collect() .collect()
.zip(Just(self).first().setFailureType(to: Error.self)) .filter { _ in preferences.useServerPostingReadingPreferences }
.filter { $1.identity.preferences.useServerPostingReadingPreferences } .flatMap { _ in refreshServerPreferences() }
.map { _ in () }
.flatMap(refreshServerPreferences)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -179,7 +150,6 @@ public extension IdentityService {
return Fail(error: error).eraseToAnyPublisher() return Fail(error: error).eraseToAnyPublisher()
} }
let identityID = identity.id
let endpoint = Self.pushSubscriptionEndpointURL let endpoint = Self.pushSubscriptionEndpointURL
.appendingPathComponent(deviceToken.base16EncodedString()) .appendingPathComponent(deviceToken.base16EncodedString())
.appendingPathComponent(identityID.uuidString) .appendingPathComponent(identityID.uuidString)
@ -196,9 +166,7 @@ public extension IdentityService {
} }
func updatePushSubscription(alerts: PushSubscription.Alerts) -> AnyPublisher<Never, Error> { func updatePushSubscription(alerts: PushSubscription.Alerts) -> AnyPublisher<Never, Error> {
let identityID = identity.id mastodonAPIClient.request(PushSubscriptionEndpoint.update(alerts: alerts))
return mastodonAPIClient.request(PushSubscriptionEndpoint.update(alerts: alerts))
.map { ($0.alerts, nil, identityID) } .map { ($0.alerts, nil, identityID) }
.flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:forIdentityID:)) .flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:forIdentityID:))
.eraseToAnyPublisher() .eraseToAnyPublisher()

View File

@ -15,13 +15,13 @@ public class EditFilterViewModel: ObservableObject {
didSet { filter.expiresAt = date } didSet { filter.expiresAt = date }
} }
private let identityService: IdentityService private let environment: IdentifiedEnvironment
private let saveCompletedInput = PassthroughSubject<Void, Never>() private let saveCompletedInput = PassthroughSubject<Void, Never>()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(filter: Filter, identityService: IdentityService) { init(filter: Filter, environment: IdentifiedEnvironment) {
self.filter = filter self.filter = filter
self.identityService = identityService self.environment = environment
date = filter.expiresAt ?? Date() date = filter.expiresAt ?? Date()
saveCompleted = saveCompletedInput.eraseToAnyPublisher() saveCompleted = saveCompletedInput.eraseToAnyPublisher()
} }
@ -41,7 +41,7 @@ public extension EditFilterViewModel {
} }
func save() { func save() {
(isNew ? identityService.createFilter(filter) : identityService.updateFilter(filter)) (isNew ? environment.identityService.createFilter(filter) : environment.identityService.updateFilter(filter))
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.handleEvents( .handleEvents(
receiveSubscription: { [weak self] _ in self?.saving = true }, receiveSubscription: { [weak self] _ in self?.saving = true },

View File

@ -10,19 +10,19 @@ public class FiltersViewModel: ObservableObject {
@Published public var expiredFilters = [Filter]() @Published public var expiredFilters = [Filter]()
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
private let identityService: IdentityService private let environment: IdentifiedEnvironment
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(identityService: IdentityService) { init(environment: IdentifiedEnvironment) {
self.identityService = identityService self.environment = environment
let now = Date() let now = Date()
identityService.activeFiltersObservation(date: now) environment.identityService.activeFiltersObservation(date: now)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$activeFilters) .assign(to: &$activeFilters)
identityService.expiredFiltersObservation(date: now) environment.identityService.expiredFiltersObservation(date: now)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$expiredFilters) .assign(to: &$expiredFilters)
} }
@ -30,20 +30,20 @@ public class FiltersViewModel: ObservableObject {
public extension FiltersViewModel { public extension FiltersViewModel {
func refreshFilters() { func refreshFilters() {
identityService.refreshFilters() environment.identityService.refreshFilters()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }
func delete(filter: Filter) { func delete(filter: Filter) {
identityService.deleteFilter(id: filter.id) environment.identityService.deleteFilter(id: filter.id)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }
func editFilterViewModel(filter: Filter) -> EditFilterViewModel { func editFilterViewModel(filter: Filter) -> EditFilterViewModel {
EditFilterViewModel(filter: filter, identityService: identityService) EditFilterViewModel(filter: filter, environment: environment)
} }
} }

View File

@ -5,18 +5,18 @@ import Foundation
import ServiceLayer import ServiceLayer
public class IdentitiesViewModel: ObservableObject { public class IdentitiesViewModel: ObservableObject {
@Published public private(set) var identity: Identity public let currentIdentityID: UUID
@Published public var identities = [Identity]() @Published public var identities = [Identity]()
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
private let identityService: IdentityService private let environment: IdentifiedEnvironment
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(identityService: IdentityService) { init(environment: IdentifiedEnvironment) {
self.identityService = identityService self.environment = environment
identity = identityService.identity currentIdentityID = environment.identity.id
identityService.identitiesObservation() environment.identityService.identitiesObservation()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$identities) .assign(to: &$identities)
} }

View File

@ -10,13 +10,13 @@ public class ListsViewModel: ObservableObject {
@Published public private(set) var creatingList = false @Published public private(set) var creatingList = false
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
private let identityService: IdentityService private let environment: IdentifiedEnvironment
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(identityService: IdentityService) { init(environment: IdentifiedEnvironment) {
self.identityService = identityService self.environment = environment
identityService.listsObservation() environment.identityService.listsObservation()
.map { .map {
$0.compactMap { $0.compactMap {
guard case let .list(list) = $0 else { return nil } guard case let .list(list) = $0 else { return nil }
@ -31,14 +31,14 @@ public class ListsViewModel: ObservableObject {
public extension ListsViewModel { public extension ListsViewModel {
func refreshLists() { func refreshLists() {
identityService.refreshLists() environment.identityService.refreshLists()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }
func createList(title: String) { func createList(title: String) {
identityService.createList(title: title) environment.identityService.createList(title: title)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.handleEvents( .handleEvents(
receiveSubscription: { [weak self] _ in self?.creatingList = true }, receiveSubscription: { [weak self] _ in self?.creatingList = true },
@ -48,7 +48,7 @@ public extension ListsViewModel {
} }
func delete(list: MastodonList) { func delete(list: MastodonList) {
identityService.deleteList(id: list.id) environment.identityService.deleteList(id: list.id)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)

View File

@ -9,14 +9,14 @@ public class NotificationTypesPreferencesViewModel: ObservableObject {
@Published public var pushSubscriptionAlerts: PushSubscription.Alerts @Published public var pushSubscriptionAlerts: PushSubscription.Alerts
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
private let identityService: IdentityService private let environment: IdentifiedEnvironment
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(identityService: IdentityService) { init(environment: IdentifiedEnvironment) {
self.identityService = identityService self.environment = environment
pushSubscriptionAlerts = identityService.identity.pushSubscriptionAlerts pushSubscriptionAlerts = environment.identity.pushSubscriptionAlerts
identityService.$identity environment.$identity
.map(\.pushSubscriptionAlerts) .map(\.pushSubscriptionAlerts)
.dropFirst() .dropFirst()
.removeDuplicates() .removeDuplicates()
@ -32,14 +32,14 @@ public class NotificationTypesPreferencesViewModel: ObservableObject {
private extension NotificationTypesPreferencesViewModel { private extension NotificationTypesPreferencesViewModel {
func update(alerts: PushSubscription.Alerts) { func update(alerts: PushSubscription.Alerts) {
guard alerts != identityService.identity.pushSubscriptionAlerts else { return } guard alerts != environment.identity.pushSubscriptionAlerts else { return }
identityService.updatePushSubscription(alerts: alerts) environment.identityService.updatePushSubscription(alerts: alerts)
.sink { [weak self] in .sink { [weak self] in
guard let self = self, case let .failure(error) = $0 else { return } guard let self = self, case let .failure(error) = $0 else { return }
self.alertItem = AlertItem(error: error) self.alertItem = AlertItem(error: error)
self.pushSubscriptionAlerts = self.identityService.identity.pushSubscriptionAlerts self.pushSubscriptionAlerts = self.environment.identity.pushSubscriptionAlerts
} receiveValue: { _ in } } receiveValue: { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -8,14 +8,14 @@ public class PostingReadingPreferencesViewModel: ObservableObject {
@Published public var preferences: Identity.Preferences @Published public var preferences: Identity.Preferences
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
private let identityService: IdentityService private let environment: IdentifiedEnvironment
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(identityService: IdentityService) { init(environment: IdentifiedEnvironment) {
self.identityService = identityService self.environment = environment
preferences = identityService.identity.preferences preferences = environment.identity.preferences
identityService.$identity environment.$identity
.map(\.preferences) .map(\.preferences)
.dropFirst() .dropFirst()
.removeDuplicates() .removeDuplicates()
@ -23,7 +23,7 @@ public class PostingReadingPreferencesViewModel: ObservableObject {
$preferences $preferences
.dropFirst() .dropFirst()
.flatMap(identityService.updatePreferences) .flatMap(environment.identityService.updatePreferences)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)

View File

@ -7,26 +7,26 @@ public class PreferencesViewModel: ObservableObject {
public let handle: String public let handle: String
public let shouldShowNotificationTypePreferences: Bool public let shouldShowNotificationTypePreferences: Bool
private let identityService: IdentityService private let environment: IdentifiedEnvironment
init(identityService: IdentityService) { init(environment: IdentifiedEnvironment) {
self.identityService = identityService self.environment = environment
handle = identityService.identity.handle handle = environment.identity.handle
shouldShowNotificationTypePreferences = identityService.identity.lastRegisteredDeviceToken != nil shouldShowNotificationTypePreferences = environment.identity.lastRegisteredDeviceToken != nil
} }
} }
public extension PreferencesViewModel { public extension PreferencesViewModel {
func postingReadingPreferencesViewModel() -> PostingReadingPreferencesViewModel { func postingReadingPreferencesViewModel() -> PostingReadingPreferencesViewModel {
PostingReadingPreferencesViewModel(identityService: identityService) PostingReadingPreferencesViewModel(environment: environment)
} }
func notificationTypesPreferencesViewModel() -> NotificationTypesPreferencesViewModel { func notificationTypesPreferencesViewModel() -> NotificationTypesPreferencesViewModel {
NotificationTypesPreferencesViewModel(identityService: identityService) NotificationTypesPreferencesViewModel(environment: environment)
} }
func filtersViewModel() -> FiltersViewModel { func filtersViewModel() -> FiltersViewModel {
FiltersViewModel(identityService: identityService) FiltersViewModel(environment: environment)
} }
} }

View File

@ -8,6 +8,7 @@ public final class RootViewModel: ObservableObject {
@Published public private(set) var tabNavigationViewModel: TabNavigationViewModel? @Published public private(set) var tabNavigationViewModel: TabNavigationViewModel?
@Published private var mostRecentlyUsedIdentityID: UUID? @Published private var mostRecentlyUsedIdentityID: UUID?
private let environment: AppEnvironment
private let allIdentitiesService: AllIdentitiesService private let allIdentitiesService: AllIdentitiesService
private let userNotificationService: UserNotificationService private let userNotificationService: UserNotificationService
private let registerForRemoteNotifications: () -> AnyPublisher<Data, Error> private let registerForRemoteNotifications: () -> AnyPublisher<Data, Error>
@ -15,6 +16,7 @@ public final class RootViewModel: ObservableObject {
public init(environment: AppEnvironment, public init(environment: AppEnvironment,
registerForRemoteNotifications: @escaping () -> AnyPublisher<Data, Error>) throws { registerForRemoteNotifications: @escaping () -> AnyPublisher<Data, Error>) throws {
self.environment = environment
allIdentitiesService = try AllIdentitiesService(environment: environment) allIdentitiesService = try AllIdentitiesService(environment: environment)
userNotificationService = UserNotificationService(environment: environment) userNotificationService = UserNotificationService(environment: environment)
self.registerForRemoteNotifications = registerForRemoteNotifications self.registerForRemoteNotifications = registerForRemoteNotifications
@ -41,34 +43,34 @@ public extension RootViewModel {
return return
} }
let identityService: IdentityService let identifiedEnvironment: IdentifiedEnvironment
do { do {
identityService = try allIdentitiesService.identityService(id: id) identifiedEnvironment = try allIdentitiesService.identifiedEnvironment(id: id)
} catch { } catch {
return return
} }
identityService.observationErrors identifiedEnvironment.observationErrors
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
.map { [weak self] _ in self?.mostRecentlyUsedIdentityID } .map { [weak self] _ in self?.mostRecentlyUsedIdentityID }
.sink { [weak self] in self?.newIdentitySelected(id: $0) } .sink { [weak self] in self?.newIdentitySelected(id: $0) }
.store(in: &cancellables) .store(in: &cancellables)
identityService.updateLastUse() identifiedEnvironment.identityService.updateLastUse()
.sink { _ in } receiveValue: { _ in } .sink { _ in } receiveValue: { _ in }
.store(in: &cancellables) .store(in: &cancellables)
userNotificationService.isAuthorized() userNotificationService.isAuthorized()
.filter { $0 } .filter { $0 }
.zip(registerForRemoteNotifications()) .zip(registerForRemoteNotifications())
.filter { identityService.identity.lastRegisteredDeviceToken != $1 } .filter { identifiedEnvironment.identity.lastRegisteredDeviceToken != $1 }
.map { ($1, identityService.identity.pushSubscriptionAlerts) } .map { ($1, identifiedEnvironment.identity.pushSubscriptionAlerts) }
.flatMap(identityService.createPushSubscription(deviceToken:alerts:)) .flatMap(identifiedEnvironment.identityService.createPushSubscription(deviceToken:alerts:))
.sink { _ in } receiveValue: { _ in } .sink { _ in } receiveValue: { _ in }
.store(in: &cancellables) .store(in: &cancellables)
tabNavigationViewModel = TabNavigationViewModel(identityService: identityService) tabNavigationViewModel = TabNavigationViewModel(environment: identifiedEnvironment)
} }
func deleteIdentity(_ identity: Identity) { func deleteIdentity(_ identity: Identity) {

View File

@ -6,25 +6,25 @@ import ServiceLayer
public class SecondaryNavigationViewModel: ObservableObject { public class SecondaryNavigationViewModel: ObservableObject {
@Published public private(set) var identity: Identity @Published public private(set) var identity: Identity
private let identityService: IdentityService private let environment: IdentifiedEnvironment
init(identityService: IdentityService) { init(environment: IdentifiedEnvironment) {
self.identityService = identityService self.environment = environment
identity = identityService.identity identity = environment.identity
identityService.$identity.dropFirst().assign(to: &$identity) environment.$identity.dropFirst().assign(to: &$identity)
} }
} }
public extension SecondaryNavigationViewModel { public extension SecondaryNavigationViewModel {
func identitiesViewModel() -> IdentitiesViewModel { func identitiesViewModel() -> IdentitiesViewModel {
IdentitiesViewModel(identityService: identityService) IdentitiesViewModel(environment: environment)
} }
func listsViewModel() -> ListsViewModel { func listsViewModel() -> ListsViewModel {
ListsViewModel(identityService: identityService) ListsViewModel(environment: environment)
} }
func preferencesViewModel() -> PreferencesViewModel { func preferencesViewModel() -> PreferencesViewModel {
PreferencesViewModel(identityService: identityService) PreferencesViewModel(environment: environment)
} }
} }

View File

@ -14,19 +14,19 @@ public class TabNavigationViewModel: ObservableObject {
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
public var selectedTab: Tab? = .timelines public var selectedTab: Tab? = .timelines
private let identityService: IdentityService private let environment: IdentifiedEnvironment
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(identityService: IdentityService) { init(environment: IdentifiedEnvironment) {
self.identityService = identityService self.environment = environment
identity = identityService.identity identity = environment.identity
identityService.$identity.dropFirst().assign(to: &$identity) environment.$identity.dropFirst().assign(to: &$identity)
identityService.recentIdentitiesObservation() environment.identityService.recentIdentitiesObservation()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$recentIdentities) .assign(to: &$recentIdentities)
identityService.listsObservation() environment.identityService.listsObservation()
.map { Timeline.nonLists + $0 } .map { Timeline.nonLists + $0 }
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$timelinesAndLists) .assign(to: &$timelinesAndLists)
@ -54,42 +54,42 @@ public extension TabNavigationViewModel {
} }
func refreshIdentity() { func refreshIdentity() {
if identityService.isAuthorized { if environment.identityService.isAuthorized {
identityService.verifyCredentials() environment.identityService.verifyCredentials()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
identityService.refreshLists() environment.identityService.refreshLists()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
identityService.refreshFilters() environment.identityService.refreshFilters()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
if identity.preferences.useServerPostingReadingPreferences { if identity.preferences.useServerPostingReadingPreferences {
identityService.refreshServerPreferences() environment.identityService.refreshServerPreferences()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }
} }
identityService.refreshInstance() environment.identityService.refreshInstance()
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { _ in } .sink { _ in }
.store(in: &cancellables) .store(in: &cancellables)
} }
func secondaryNavigationViewModel() -> SecondaryNavigationViewModel { func secondaryNavigationViewModel() -> SecondaryNavigationViewModel {
SecondaryNavigationViewModel(identityService: identityService) SecondaryNavigationViewModel(environment: environment)
} }
func viewModel(timeline: Timeline) -> StatusListViewModel { func viewModel(timeline: Timeline) -> StatusListViewModel {
StatusListViewModel(statusListService: identityService.service(timeline: timeline)) StatusListViewModel(statusListService: environment.identityService.service(timeline: timeline))
} }
} }

View File

@ -43,12 +43,12 @@ struct IdentitiesView: View {
Spacer() Spacer()
} }
Spacer() Spacer()
if identity.id == viewModel.identity.id { if identity.id == viewModel.currentIdentityID {
Image(systemName: "checkmark.circle") Image(systemName: "checkmark.circle")
} }
} }
} }
.disabled(identity.id == viewModel.identity.id) .disabled(identity.id == viewModel.currentIdentityID)
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
.onDelete { .onDelete {