mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
Refactoring
This commit is contained in:
parent
367d79ed2c
commit
b4549521cb
40 changed files with 282 additions and 369 deletions
|
@ -14,7 +14,7 @@ public enum IdentityDatabaseError: Error {
|
|||
public struct IdentityDatabase {
|
||||
private let databaseQueue: DatabaseQueue
|
||||
|
||||
public init(inMemory: Bool, fixture: IdentityFixture?, keychain: Keychain.Type) throws {
|
||||
public init(inMemory: Bool, keychain: Keychain.Type) throws {
|
||||
if inMemory {
|
||||
databaseQueue = DatabaseQueue()
|
||||
} else {
|
||||
|
@ -29,10 +29,6 @@ public struct IdentityDatabase {
|
|||
}
|
||||
|
||||
try Self.migrate(databaseQueue)
|
||||
|
||||
if let fixture = fixture {
|
||||
try populate(fixture: fixture)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -262,22 +258,4 @@ private extension IdentityDatabase {
|
|||
|
||||
try migrator.migrate(writer)
|
||||
}
|
||||
|
||||
func populate(fixture: IdentityFixture) throws {
|
||||
_ = createIdentity(id: fixture.id, url: fixture.instanceURL)
|
||||
.receive(on: ImmediateScheduler.shared)
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
|
||||
if let instance = fixture.instance {
|
||||
_ = updateInstance(instance, forIdentityID: fixture.id)
|
||||
.receive(on: ImmediateScheduler.shared)
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
}
|
||||
|
||||
if let account = fixture.account {
|
||||
_ = updateAccount(account, forIdentityID: fixture.id)
|
||||
.receive(on: ImmediateScheduler.shared)
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,6 @@ import Stubbing
|
|||
|
||||
extension AccountEndpoint: Stubbing {
|
||||
public func data(url: URL) -> Data? {
|
||||
switch self {
|
||||
case .verifyCredentials: return try? Data(contentsOf: Bundle.module.url(forResource: "account",
|
||||
withExtension: "json")!)
|
||||
}
|
||||
StubData.account
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,6 @@ import Stubbing
|
|||
|
||||
extension InstanceEndpoint: Stubbing {
|
||||
public func data(url: URL) -> Data? {
|
||||
switch self {
|
||||
case .instance: return try? Data(contentsOf: Bundle.module.url(forResource: "instance", withExtension: "json")!)
|
||||
}
|
||||
StubData.instance
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,18 +5,7 @@ import MastodonAPI
|
|||
import Stubbing
|
||||
|
||||
extension PreferencesEndpoint: Stubbing {
|
||||
public func dataString(url: URL) -> String? {
|
||||
switch self {
|
||||
case .preferences:
|
||||
return """
|
||||
{
|
||||
"posting:default:visibility": "public",
|
||||
"posting:default:sensitive": false,
|
||||
"posting:default:language": null,
|
||||
"reading:expand:media": "default",
|
||||
"reading:expand:spoilers": false
|
||||
}
|
||||
"""
|
||||
}
|
||||
public func data(url: URL) -> Data? {
|
||||
StubData.preferences
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"posting:default:visibility": "public",
|
||||
"posting:default:sensitive": false,
|
||||
"posting:default:language": null,
|
||||
"reading:expand:media": "default",
|
||||
"reading:expand:spoilers": false
|
||||
}
|
18
MastodonAPI/Sources/MastodonAPIStubs/StubData.swift
Normal file
18
MastodonAPI/Sources/MastodonAPIStubs/StubData.swift
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum StubData {}
|
||||
|
||||
public extension StubData {
|
||||
// swiftlint:disable force_try
|
||||
static let account = try! Data(contentsOf: Bundle.module.url(forResource: "account",
|
||||
withExtension: "json")!)
|
||||
static let instance = try! Data(contentsOf: Bundle.module.url(forResource: "instance",
|
||||
withExtension: "json")!)
|
||||
static let preferences = try! Data(contentsOf: Bundle.module.url(forResource: "preferences",
|
||||
withExtension: "json")!)
|
||||
static let timeline = try! Data(contentsOf: Bundle.module.url(forResource: "timeline",
|
||||
withExtension: "json")!)
|
||||
// swiftlint:enable force_try
|
||||
}
|
|
@ -6,6 +6,6 @@ import Stubbing
|
|||
|
||||
extension TimelinesEndpoint: Stubbing {
|
||||
public func data(url: URL) -> Data? {
|
||||
try? Data(contentsOf: Bundle.module.url(forResource: "timeline", withExtension: "json")!)
|
||||
StubData.timeline
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ public struct Secrets {
|
|||
|
||||
public extension Secrets {
|
||||
enum Item: String, CaseIterable {
|
||||
case instanceURL
|
||||
case clientID
|
||||
case clientSecret
|
||||
case accessToken
|
||||
|
@ -101,6 +102,14 @@ public extension Secrets {
|
|||
}
|
||||
}
|
||||
|
||||
func getInstanceURL() throws -> URL {
|
||||
try item(.instanceURL)
|
||||
}
|
||||
|
||||
func setInstanceURL(_ instanceURL: URL) throws {
|
||||
try set(instanceURL, forItem: .instanceURL)
|
||||
}
|
||||
|
||||
func getClientID() throws -> String {
|
||||
try item(.clientID)
|
||||
}
|
||||
|
@ -219,6 +228,18 @@ extension String: SecretsStorable {
|
|||
}
|
||||
}
|
||||
|
||||
extension URL: SecretsStorable {
|
||||
public var dataStoredInSecrets: Data { absoluteString.dataStoredInSecrets }
|
||||
|
||||
public static func fromDataStoredInSecrets(_ data: Data) throws -> URL {
|
||||
guard let url = URL(string: try String.fromDataStoredInSecrets(data)) else {
|
||||
throw SecretsStorableError.conversionFromDataStoredInSecrets(data)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
private struct PushKey {
|
||||
static let authLength = 16
|
||||
static let sizeInBits = 256
|
||||
|
|
|
@ -14,7 +14,7 @@ public struct AppEnvironment {
|
|||
let userDefaults: UserDefaults
|
||||
let userNotificationClient: UserNotificationClient
|
||||
let inMemoryContent: Bool
|
||||
let identityFixture: IdentityFixture?
|
||||
let fixtureDatabase: IdentityDatabase?
|
||||
|
||||
public init(session: Session,
|
||||
webAuthSessionType: WebAuthSession.Type,
|
||||
|
@ -22,14 +22,14 @@ public struct AppEnvironment {
|
|||
userDefaults: UserDefaults,
|
||||
userNotificationClient: UserNotificationClient,
|
||||
inMemoryContent: Bool,
|
||||
identityFixture: IdentityFixture?) {
|
||||
fixtureDatabase: IdentityDatabase?) {
|
||||
self.session = session
|
||||
self.webAuthSessionType = webAuthSessionType
|
||||
self.keychain = keychain
|
||||
self.userDefaults = userDefaults
|
||||
self.userNotificationClient = userNotificationClient
|
||||
self.inMemoryContent = inMemoryContent
|
||||
self.identityFixture = identityFixture
|
||||
self.fixtureDatabase = fixtureDatabase
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,6 @@ public extension AppEnvironment {
|
|||
userDefaults: .standard,
|
||||
userNotificationClient: .live(userNotificationCenter),
|
||||
inMemoryContent: false,
|
||||
identityFixture: nil)
|
||||
fixtureDatabase: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
|
@ -15,9 +15,9 @@ public struct AllIdentitiesService {
|
|||
private let environment: AppEnvironment
|
||||
|
||||
public init(environment: AppEnvironment) throws {
|
||||
self.database = try IdentityDatabase(inMemory: environment.inMemoryContent,
|
||||
fixture: environment.identityFixture,
|
||||
keychain: environment.keychain)
|
||||
self.database = try environment.fixtureDatabase ?? IdentityDatabase(
|
||||
inMemory: environment.inMemoryContent,
|
||||
keychain: environment.keychain)
|
||||
self.environment = environment
|
||||
|
||||
mostRecentlyUsedIdentityID = database.mostRecentlyUsedIdentityIDObservation()
|
||||
|
@ -28,8 +28,8 @@ public struct AllIdentitiesService {
|
|||
}
|
||||
|
||||
public extension AllIdentitiesService {
|
||||
func identifiedEnvironment(id: UUID) throws -> IdentifiedEnvironment {
|
||||
try IdentifiedEnvironment(id: id, database: database, environment: environment)
|
||||
func identityService(id: UUID) throws -> IdentityService {
|
||||
try IdentityService(id: id, database: database, environment: environment)
|
||||
}
|
||||
|
||||
func createIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> {
|
||||
|
@ -42,6 +42,7 @@ public extension AllIdentitiesService {
|
|||
|
||||
return authenticationService.authorizeApp(instanceURL: instanceURL)
|
||||
.tryMap { appAuthorization -> (URL, AppAuthorization) in
|
||||
try secrets.setInstanceURL(instanceURL)
|
||||
try secrets.setClientID(appAuthorization.clientId)
|
||||
try secrets.setClientSecret(appAuthorization.clientSecret)
|
||||
|
||||
|
@ -81,7 +82,7 @@ public extension AllIdentitiesService {
|
|||
database.identitiesWithOutdatedDeviceTokens(deviceToken: deviceToken)
|
||||
.tryMap { identities -> [AnyPublisher<Never, Never>] in
|
||||
try identities.map {
|
||||
try IdentityService(id: $0.id, instanceURL: $0.url, database: database, environment: environment)
|
||||
try IdentityService(id: $0.id, database: database, environment: environment)
|
||||
.createPushSubscription(deviceToken: deviceToken, alerts: $0.pushSubscriptionAlerts)
|
||||
.catch { _ in Empty() } // don't want to disrupt pipeline
|
||||
.eraseToAnyPublisher()
|
||||
|
|
|
@ -17,7 +17,7 @@ public struct IdentityService {
|
|||
private let secrets: Secrets
|
||||
private let observationErrorsInput = PassthroughSubject<Error, Never>()
|
||||
|
||||
init(id: UUID, instanceURL: URL, database: IdentityDatabase, environment: AppEnvironment) throws {
|
||||
init(id: UUID, database: IdentityDatabase, environment: AppEnvironment) throws {
|
||||
identityID = id
|
||||
identityDatabase = database
|
||||
self.environment = environment
|
||||
|
@ -25,7 +25,7 @@ public struct IdentityService {
|
|||
identityID: id,
|
||||
keychain: environment.keychain)
|
||||
mastodonAPIClient = MastodonAPIClient(session: environment.session)
|
||||
mastodonAPIClient.instanceURL = instanceURL
|
||||
mastodonAPIClient.instanceURL = try secrets.getInstanceURL()
|
||||
mastodonAPIClient.accessToken = try? secrets.getAccessToken()
|
||||
|
||||
contentDatabase = try ContentDatabase(identityID: id,
|
||||
|
@ -86,6 +86,10 @@ public extension IdentityService {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func observation() -> AnyPublisher<Identity, Error> {
|
||||
identityDatabase.identityObservation(id: identityID)
|
||||
}
|
||||
|
||||
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
||||
contentDatabase.listsObservation()
|
||||
}
|
||||
|
|
|
@ -3,12 +3,19 @@
|
|||
import DB
|
||||
import Foundation
|
||||
import HTTP
|
||||
import Keychain
|
||||
import MockKeychain
|
||||
import ServiceLayer
|
||||
import Stubbing
|
||||
|
||||
public extension AppEnvironment {
|
||||
static func mock(identityFixture: IdentityFixture? = nil) -> Self {
|
||||
static func mock(session: Session = Session(configuration: .stubbing),
|
||||
webAuthSessionType: WebAuthSession.Type = SuccessfulMockWebAuthSession.self,
|
||||
keychain: Keychain.Type = MockKeychain.self,
|
||||
userDefaults: UserDefaults = MockUserDefaults(),
|
||||
userNotificationClient: UserNotificationClient = .mock,
|
||||
inMemoryContent: Bool = true,
|
||||
fixtureDatabase: IdentityDatabase? = nil) -> Self {
|
||||
AppEnvironment(
|
||||
session: Session(configuration: .stubbing),
|
||||
webAuthSessionType: SuccessfulMockWebAuthSession.self,
|
||||
|
@ -16,6 +23,6 @@ public extension AppEnvironment {
|
|||
userDefaults: MockUserDefaults(),
|
||||
userNotificationClient: .mock,
|
||||
inMemoryContent: true,
|
||||
identityFixture: identityFixture)
|
||||
fixtureDatabase: fixtureDatabase)
|
||||
}
|
||||
}
|
||||
|
|
53
ViewModels/Sources/PreviewViewModels/PreviewViewModels.swift
Normal file
53
ViewModels/Sources/PreviewViewModels/PreviewViewModels.swift
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import DB
|
||||
import Foundation
|
||||
import Mastodon
|
||||
import MastodonAPIStubs
|
||||
import MockKeychain
|
||||
import Secrets
|
||||
import ServiceLayer
|
||||
import ServiceLayerMocks
|
||||
import ViewModels
|
||||
|
||||
// swiftlint:disable force_try
|
||||
|
||||
let db: IdentityDatabase = {
|
||||
let id = UUID()
|
||||
let url = URL(string: "https://mastodon.social")!
|
||||
let db = try! IdentityDatabase(inMemory: true, keychain: MockKeychain.self)
|
||||
let decoder = MastodonDecoder()
|
||||
let instance = try! decoder.decode(Instance.self, from: StubData.instance)
|
||||
let account = try! decoder.decode(Account.self, from: StubData.account)
|
||||
let secrets = Secrets(identityID: id, keychain: MockKeychain.self)
|
||||
|
||||
try! secrets.setInstanceURL(url)
|
||||
try! secrets.setAccessToken(UUID().uuidString)
|
||||
|
||||
_ = db.createIdentity(id: id, url: url)
|
||||
.receive(on: ImmediateScheduler.shared)
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
|
||||
_ = db.updateInstance(instance, forIdentityID: id)
|
||||
.receive(on: ImmediateScheduler.shared)
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
|
||||
_ = db.updateAccount(account, forIdentityID: id)
|
||||
.receive(on: ImmediateScheduler.shared)
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
|
||||
return db
|
||||
}()
|
||||
|
||||
let environment = AppEnvironment.mock(fixtureDatabase: db)
|
||||
|
||||
public extension RootViewModel {
|
||||
static let preview = try! RootViewModel(environment: environment) { Empty().eraseToAnyPublisher() }
|
||||
}
|
||||
|
||||
public extension Identification {
|
||||
static let preview = RootViewModel.preview.identification!
|
||||
}
|
||||
|
||||
// swiftlint:enable force_try
|
|
@ -1,104 +0,0 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import HTTP
|
||||
import Mastodon
|
||||
import MastodonAPI
|
||||
import MastodonAPIStubs
|
||||
import ServiceLayer
|
||||
import ServiceLayerMocks
|
||||
import ViewModels
|
||||
|
||||
private let decoder = MastodonDecoder()
|
||||
private let devInstanceURL = URL(string: "https://mastodon.social")!
|
||||
|
||||
// swiftlint:disable force_try
|
||||
extension AppEnvironment {
|
||||
public static let mockAuthenticated: Self = .mock(
|
||||
identityFixture: .init(
|
||||
id: UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!,
|
||||
instanceURL: devInstanceURL,
|
||||
instance: try! decoder.decode(Instance.self,
|
||||
from: InstanceEndpoint.instance.data(url: devInstanceURL)!),
|
||||
account: try! decoder.decode(Account.self,
|
||||
from: AccountEndpoint.verifyCredentials.data(url: devInstanceURL)!)))
|
||||
}
|
||||
|
||||
extension RootViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> Self {
|
||||
try! Self(environment: environment,
|
||||
registerForRemoteNotifications: { Empty().eraseToAnyPublisher() })
|
||||
}
|
||||
}
|
||||
// swiftlint:enable force_try
|
||||
|
||||
extension AddIdentityViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> AddIdentityViewModel {
|
||||
RootViewModel.mock(environment: environment).addIdentityViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
extension TabNavigationViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> TabNavigationViewModel {
|
||||
RootViewModel.mock(environment: environment).tabNavigationViewModel!
|
||||
}
|
||||
}
|
||||
|
||||
extension SecondaryNavigationViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> SecondaryNavigationViewModel {
|
||||
TabNavigationViewModel.mock(environment: environment)
|
||||
.secondaryNavigationViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
extension IdentitiesViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> IdentitiesViewModel {
|
||||
SecondaryNavigationViewModel.mock(environment: environment).identitiesViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
extension ListsViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> ListsViewModel {
|
||||
SecondaryNavigationViewModel.mock(environment: environment).listsViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
extension PreferencesViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> PreferencesViewModel {
|
||||
SecondaryNavigationViewModel.mock(environment: environment).preferencesViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
extension PostingReadingPreferencesViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> PostingReadingPreferencesViewModel {
|
||||
PreferencesViewModel.mock(environment: environment)
|
||||
.postingReadingPreferencesViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationTypesPreferencesViewModel {
|
||||
public static func mock(
|
||||
environment: AppEnvironment = .mockAuthenticated) -> NotificationTypesPreferencesViewModel {
|
||||
PreferencesViewModel.mock(environment: environment)
|
||||
.notificationTypesPreferencesViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
extension FiltersViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> FiltersViewModel {
|
||||
PreferencesViewModel.mock(environment: environment).filtersViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
extension EditFilterViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> EditFilterViewModel {
|
||||
FiltersViewModel.mock(environment: environment).editFilterViewModel(filter: .new)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusListViewModel {
|
||||
public static func mock(environment: AppEnvironment = .mockAuthenticated) -> StatusListViewModel {
|
||||
TabNavigationViewModel.mock(environment: environment).viewModel(timeline: .home)
|
||||
}
|
||||
}
|
|
@ -15,13 +15,13 @@ public class EditFilterViewModel: ObservableObject {
|
|||
didSet { filter.expiresAt = date }
|
||||
}
|
||||
|
||||
private let environment: IdentifiedEnvironment
|
||||
private let identification: Identification
|
||||
private let saveCompletedInput = PassthroughSubject<Void, Never>()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(filter: Filter, environment: IdentifiedEnvironment) {
|
||||
public init(filter: Filter, identification: Identification) {
|
||||
self.filter = filter
|
||||
self.environment = environment
|
||||
self.identification = identification
|
||||
date = filter.expiresAt ?? Date()
|
||||
saveCompleted = saveCompletedInput.eraseToAnyPublisher()
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ public extension EditFilterViewModel {
|
|||
}
|
||||
|
||||
func save() {
|
||||
(isNew ? environment.identityService.createFilter(filter) : environment.identityService.updateFilter(filter))
|
||||
(isNew ? identification.service.createFilter(filter) : identification.service.updateFilter(filter))
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.handleEvents(
|
||||
receiveSubscription: { [weak self] _ in self?.saving = true },
|
||||
|
|
41
ViewModels/Sources/ViewModels/Entities/Identification.swift
Normal file
41
ViewModels/Sources/ViewModels/Entities/Identification.swift
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import ServiceLayer
|
||||
|
||||
enum IdentificationError: Error {
|
||||
case initialIdentityValueAbsent
|
||||
}
|
||||
|
||||
public final class Identification: ObservableObject {
|
||||
@Published private(set) var identity: Identity
|
||||
let service: IdentityService
|
||||
let observationErrors: AnyPublisher<Error, Never>
|
||||
|
||||
init(service: IdentityService) throws {
|
||||
self.service = service
|
||||
// The scheduling on the observation is immediate so an initial value can be extracted
|
||||
let sharedObservation = service.observation().share()
|
||||
var initialIdentity: Identity?
|
||||
|
||||
_ = sharedObservation.first().sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { initialIdentity = $0 })
|
||||
|
||||
guard let identity = initialIdentity else { throw IdentificationError.initialIdentityValueAbsent }
|
||||
|
||||
self.identity = identity
|
||||
|
||||
let observationErrorsSubject = PassthroughSubject<Error, Never>()
|
||||
|
||||
observationErrors = observationErrorsSubject.eraseToAnyPublisher()
|
||||
|
||||
sharedObservation.catch { error -> Empty<Identity, Never> in
|
||||
observationErrorsSubject.send(error)
|
||||
|
||||
return Empty()
|
||||
}
|
||||
.assign(to: &$identity)
|
||||
}
|
||||
}
|
|
@ -10,19 +10,19 @@ public class FiltersViewModel: ObservableObject {
|
|||
@Published public var expiredFilters = [Filter]()
|
||||
@Published public var alertItem: AlertItem?
|
||||
|
||||
private let environment: IdentifiedEnvironment
|
||||
private let identification: Identification
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(environment: IdentifiedEnvironment) {
|
||||
self.environment = environment
|
||||
public init(identification: Identification) {
|
||||
self.identification = identification
|
||||
|
||||
let now = Date()
|
||||
|
||||
environment.identityService.activeFiltersObservation(date: now)
|
||||
identification.service.activeFiltersObservation(date: now)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.assign(to: &$activeFilters)
|
||||
|
||||
environment.identityService.expiredFiltersObservation(date: now)
|
||||
identification.service.expiredFiltersObservation(date: now)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.assign(to: &$expiredFilters)
|
||||
}
|
||||
|
@ -30,20 +30,16 @@ public class FiltersViewModel: ObservableObject {
|
|||
|
||||
public extension FiltersViewModel {
|
||||
func refreshFilters() {
|
||||
environment.identityService.refreshFilters()
|
||||
identification.service.refreshFilters()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func delete(filter: Filter) {
|
||||
environment.identityService.deleteFilter(id: filter.id)
|
||||
identification.service.deleteFilter(id: filter.id)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func editFilterViewModel(filter: Filter) -> EditFilterViewModel {
|
||||
EditFilterViewModel(filter: filter, environment: environment)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,14 +9,14 @@ public class IdentitiesViewModel: ObservableObject {
|
|||
@Published public var identities = [Identity]()
|
||||
@Published public var alertItem: AlertItem?
|
||||
|
||||
private let environment: IdentifiedEnvironment
|
||||
private let identification: Identification
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(environment: IdentifiedEnvironment) {
|
||||
self.environment = environment
|
||||
currentIdentityID = environment.identity.id
|
||||
public init(identification: Identification) {
|
||||
self.identification = identification
|
||||
currentIdentityID = identification.identity.id
|
||||
|
||||
environment.identityService.identitiesObservation()
|
||||
identification.service.identitiesObservation()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.assign(to: &$identities)
|
||||
}
|
||||
|
|
|
@ -10,13 +10,13 @@ public class ListsViewModel: ObservableObject {
|
|||
@Published public private(set) var creatingList = false
|
||||
@Published public var alertItem: AlertItem?
|
||||
|
||||
private let environment: IdentifiedEnvironment
|
||||
private let identification: Identification
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(environment: IdentifiedEnvironment) {
|
||||
self.environment = environment
|
||||
public init(identification: Identification) {
|
||||
self.identification = identification
|
||||
|
||||
environment.identityService.listsObservation()
|
||||
identification.service.listsObservation()
|
||||
.map {
|
||||
$0.compactMap {
|
||||
guard case let .list(list) = $0 else { return nil }
|
||||
|
@ -31,14 +31,14 @@ public class ListsViewModel: ObservableObject {
|
|||
|
||||
public extension ListsViewModel {
|
||||
func refreshLists() {
|
||||
environment.identityService.refreshLists()
|
||||
identification.service.refreshLists()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func createList(title: String) {
|
||||
environment.identityService.createList(title: title)
|
||||
identification.service.createList(title: title)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.handleEvents(
|
||||
receiveSubscription: { [weak self] _ in self?.creatingList = true },
|
||||
|
@ -48,7 +48,7 @@ public extension ListsViewModel {
|
|||
}
|
||||
|
||||
func delete(list: MastodonList) {
|
||||
environment.identityService.deleteList(id: list.id)
|
||||
identification.service.deleteList(id: list.id)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
|
|
|
@ -9,14 +9,14 @@ public class NotificationTypesPreferencesViewModel: ObservableObject {
|
|||
@Published public var pushSubscriptionAlerts: PushSubscription.Alerts
|
||||
@Published public var alertItem: AlertItem?
|
||||
|
||||
private let environment: IdentifiedEnvironment
|
||||
private let identification: Identification
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(environment: IdentifiedEnvironment) {
|
||||
self.environment = environment
|
||||
pushSubscriptionAlerts = environment.identity.pushSubscriptionAlerts
|
||||
public init(identification: Identification) {
|
||||
self.identification = identification
|
||||
pushSubscriptionAlerts = identification.identity.pushSubscriptionAlerts
|
||||
|
||||
environment.$identity
|
||||
identification.$identity
|
||||
.map(\.pushSubscriptionAlerts)
|
||||
.dropFirst()
|
||||
.removeDuplicates()
|
||||
|
@ -32,14 +32,14 @@ public class NotificationTypesPreferencesViewModel: ObservableObject {
|
|||
|
||||
private extension NotificationTypesPreferencesViewModel {
|
||||
func update(alerts: PushSubscription.Alerts) {
|
||||
guard alerts != environment.identity.pushSubscriptionAlerts else { return }
|
||||
guard alerts != identification.identity.pushSubscriptionAlerts else { return }
|
||||
|
||||
environment.identityService.updatePushSubscription(alerts: alerts)
|
||||
identification.service.updatePushSubscription(alerts: alerts)
|
||||
.sink { [weak self] in
|
||||
guard let self = self, case let .failure(error) = $0 else { return }
|
||||
|
||||
self.alertItem = AlertItem(error: error)
|
||||
self.pushSubscriptionAlerts = self.environment.identity.pushSubscriptionAlerts
|
||||
self.pushSubscriptionAlerts = self.identification.identity.pushSubscriptionAlerts
|
||||
} receiveValue: { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
|
|
@ -8,14 +8,14 @@ public class PostingReadingPreferencesViewModel: ObservableObject {
|
|||
@Published public var preferences: Identity.Preferences
|
||||
@Published public var alertItem: AlertItem?
|
||||
|
||||
private let environment: IdentifiedEnvironment
|
||||
private let identification: Identification
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(environment: IdentifiedEnvironment) {
|
||||
self.environment = environment
|
||||
preferences = environment.identity.preferences
|
||||
public init(identification: Identification) {
|
||||
self.identification = identification
|
||||
preferences = identification.identity.preferences
|
||||
|
||||
environment.$identity
|
||||
identification.$identity
|
||||
.map(\.preferences)
|
||||
.dropFirst()
|
||||
.removeDuplicates()
|
||||
|
@ -23,7 +23,7 @@ public class PostingReadingPreferencesViewModel: ObservableObject {
|
|||
|
||||
$preferences
|
||||
.dropFirst()
|
||||
.flatMap(environment.identityService.updatePreferences)
|
||||
.flatMap(identification.service.updatePreferences)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
|
|
|
@ -7,26 +7,12 @@ public class PreferencesViewModel: ObservableObject {
|
|||
public let handle: String
|
||||
public let shouldShowNotificationTypePreferences: Bool
|
||||
|
||||
private let environment: IdentifiedEnvironment
|
||||
private let identification: Identification
|
||||
|
||||
init(environment: IdentifiedEnvironment) {
|
||||
self.environment = environment
|
||||
handle = environment.identity.handle
|
||||
public init(identification: Identification) {
|
||||
self.identification = identification
|
||||
handle = identification.identity.handle
|
||||
|
||||
shouldShowNotificationTypePreferences = environment.identity.lastRegisteredDeviceToken != nil
|
||||
}
|
||||
}
|
||||
|
||||
public extension PreferencesViewModel {
|
||||
func postingReadingPreferencesViewModel() -> PostingReadingPreferencesViewModel {
|
||||
PostingReadingPreferencesViewModel(environment: environment)
|
||||
}
|
||||
|
||||
func notificationTypesPreferencesViewModel() -> NotificationTypesPreferencesViewModel {
|
||||
NotificationTypesPreferencesViewModel(environment: environment)
|
||||
}
|
||||
|
||||
func filtersViewModel() -> FiltersViewModel {
|
||||
FiltersViewModel(environment: environment)
|
||||
shouldShowNotificationTypePreferences = identification.identity.lastRegisteredDeviceToken != nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,9 @@ import Foundation
|
|||
import ServiceLayer
|
||||
|
||||
public final class RootViewModel: ObservableObject {
|
||||
@Published public private(set) var tabNavigationViewModel: TabNavigationViewModel?
|
||||
@Published public private(set) var identification: Identification?
|
||||
|
||||
@Published private var mostRecentlyUsedIdentityID: UUID?
|
||||
private let environment: AppEnvironment
|
||||
private let allIdentitiesService: AllIdentitiesService
|
||||
private let userNotificationService: UserNotificationService
|
||||
private let registerForRemoteNotifications: () -> AnyPublisher<Data, Error>
|
||||
|
@ -16,7 +15,6 @@ public final class RootViewModel: ObservableObject {
|
|||
|
||||
public init(environment: AppEnvironment,
|
||||
registerForRemoteNotifications: @escaping () -> AnyPublisher<Data, Error>) throws {
|
||||
self.environment = environment
|
||||
allIdentitiesService = try AllIdentitiesService(environment: environment)
|
||||
userNotificationService = UserNotificationService(environment: environment)
|
||||
self.registerForRemoteNotifications = registerForRemoteNotifications
|
||||
|
@ -38,39 +36,38 @@ public final class RootViewModel: ObservableObject {
|
|||
public extension RootViewModel {
|
||||
func newIdentitySelected(id: UUID?) {
|
||||
guard let id = id else {
|
||||
tabNavigationViewModel = nil
|
||||
identification = nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let identifiedEnvironment: IdentifiedEnvironment
|
||||
let identification: Identification
|
||||
|
||||
do {
|
||||
identifiedEnvironment = try allIdentitiesService.identifiedEnvironment(id: id)
|
||||
identification = try Identification(service: allIdentitiesService.identityService(id: id))
|
||||
self.identification = identification
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
identifiedEnvironment.observationErrors
|
||||
identification.observationErrors
|
||||
.receive(on: RunLoop.main)
|
||||
.map { [weak self] _ in self?.mostRecentlyUsedIdentityID }
|
||||
.sink { [weak self] in self?.newIdentitySelected(id: $0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
identifiedEnvironment.identityService.updateLastUse()
|
||||
identification.service.updateLastUse()
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
.store(in: &cancellables)
|
||||
|
||||
userNotificationService.isAuthorized()
|
||||
.filter { $0 }
|
||||
.zip(registerForRemoteNotifications())
|
||||
.filter { identifiedEnvironment.identity.lastRegisteredDeviceToken != $1 }
|
||||
.map { ($1, identifiedEnvironment.identity.pushSubscriptionAlerts) }
|
||||
.flatMap(identifiedEnvironment.identityService.createPushSubscription(deviceToken:alerts:))
|
||||
.filter { identification.identity.lastRegisteredDeviceToken != $1 }
|
||||
.map { ($1, identification.identity.pushSubscriptionAlerts) }
|
||||
.flatMap(identification.service.createPushSubscription(deviceToken:alerts:))
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
.store(in: &cancellables)
|
||||
|
||||
tabNavigationViewModel = TabNavigationViewModel(environment: identifiedEnvironment)
|
||||
}
|
||||
|
||||
func deleteIdentity(_ identity: Identity) {
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import ServiceLayer
|
||||
|
||||
public class SecondaryNavigationViewModel: ObservableObject {
|
||||
@Published public private(set) var identity: Identity
|
||||
|
||||
private let environment: IdentifiedEnvironment
|
||||
|
||||
init(environment: IdentifiedEnvironment) {
|
||||
self.environment = environment
|
||||
identity = environment.identity
|
||||
environment.$identity.dropFirst().assign(to: &$identity)
|
||||
}
|
||||
}
|
||||
|
||||
public extension SecondaryNavigationViewModel {
|
||||
func identitiesViewModel() -> IdentitiesViewModel {
|
||||
IdentitiesViewModel(environment: environment)
|
||||
}
|
||||
|
||||
func listsViewModel() -> ListsViewModel {
|
||||
ListsViewModel(environment: environment)
|
||||
}
|
||||
|
||||
func preferencesViewModel() -> PreferencesViewModel {
|
||||
PreferencesViewModel(environment: environment)
|
||||
}
|
||||
}
|
|
@ -14,19 +14,19 @@ public class TabNavigationViewModel: ObservableObject {
|
|||
@Published public var alertItem: AlertItem?
|
||||
public var selectedTab: Tab? = .timelines
|
||||
|
||||
private let environment: IdentifiedEnvironment
|
||||
private let identification: Identification
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(environment: IdentifiedEnvironment) {
|
||||
self.environment = environment
|
||||
identity = environment.identity
|
||||
environment.$identity.dropFirst().assign(to: &$identity)
|
||||
public init(identification: Identification) {
|
||||
self.identification = identification
|
||||
identity = identification.identity
|
||||
identification.$identity.dropFirst().assign(to: &$identity)
|
||||
|
||||
environment.identityService.recentIdentitiesObservation()
|
||||
identification.service.recentIdentitiesObservation()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.assign(to: &$recentIdentities)
|
||||
|
||||
environment.identityService.listsObservation()
|
||||
identification.service.listsObservation()
|
||||
.map { Timeline.nonLists + $0 }
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.assign(to: &$timelinesAndLists)
|
||||
|
@ -54,42 +54,38 @@ public extension TabNavigationViewModel {
|
|||
}
|
||||
|
||||
func refreshIdentity() {
|
||||
if environment.identityService.isAuthorized {
|
||||
environment.identityService.verifyCredentials()
|
||||
if identification.service.isAuthorized {
|
||||
identification.service.verifyCredentials()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
|
||||
environment.identityService.refreshLists()
|
||||
identification.service.refreshLists()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
|
||||
environment.identityService.refreshFilters()
|
||||
identification.service.refreshFilters()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
|
||||
if identity.preferences.useServerPostingReadingPreferences {
|
||||
environment.identityService.refreshServerPreferences()
|
||||
identification.service.refreshServerPreferences()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
environment.identityService.refreshInstance()
|
||||
identification.service.refreshInstance()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func secondaryNavigationViewModel() -> SecondaryNavigationViewModel {
|
||||
SecondaryNavigationViewModel(environment: environment)
|
||||
}
|
||||
|
||||
func viewModel(timeline: Timeline) -> StatusListViewModel {
|
||||
StatusListViewModel(statusListService: environment.identityService.service(timeline: timeline))
|
||||
StatusListViewModel(statusListService: identification.service.service(timeline: timeline))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,15 +46,8 @@ class AddIdentityViewModelTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testDoesNotAlertCanceledLogin() throws {
|
||||
let environment = AppEnvironment(
|
||||
session: Session(configuration: .stubbing),
|
||||
webAuthSessionType: CanceledLoginMockWebAuthSession.self,
|
||||
keychain: MockKeychain.self,
|
||||
userDefaults: MockUserDefaults(),
|
||||
userNotificationClient: .mock,
|
||||
inMemoryContent: true,
|
||||
identityFixture: nil)
|
||||
let allIdentitiesService = try AllIdentitiesService(environment: environment)
|
||||
let allIdentitiesService = try AllIdentitiesService(
|
||||
environment: .mock(webAuthSessionType: CanceledLoginMockWebAuthSession.self))
|
||||
let sut = AddIdentityViewModel(allIdentitiesService: allIdentitiesService)
|
||||
let recorder = sut.$alertItem.record()
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ class RootViewModelTests: XCTestCase {
|
|||
let sut = try RootViewModel(
|
||||
environment: .mock(),
|
||||
registerForRemoteNotifications: { Empty().setFailureType(to: Error.self).eraseToAnyPublisher() })
|
||||
let recorder = sut.$tabNavigationViewModel.record()
|
||||
let recorder = sut.$identification.record()
|
||||
|
||||
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ import PreviewViewModels
|
|||
|
||||
struct AddAccountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddIdentityView(viewModel: .mock())
|
||||
AddIdentityView(viewModel: RootViewModel.preview.addIdentityViewModel())
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -101,7 +101,7 @@ import PreviewViewModels
|
|||
|
||||
struct EditFilterView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EditFilterView(viewModel: .mock())
|
||||
EditFilterView(viewModel: .init(filter: .new, identification: .preview))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -6,12 +6,13 @@ import ViewModels
|
|||
|
||||
struct FiltersView: View {
|
||||
@StateObject var viewModel: FiltersViewModel
|
||||
@EnvironmentObject var identification: Identification
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
NavigationLink(destination: EditFilterView(
|
||||
viewModel: viewModel.editFilterViewModel(filter: .new))) {
|
||||
viewModel: .init(filter: .new, identification: identification))) {
|
||||
Label("add", systemImage: "plus.circle")
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +37,7 @@ private extension FiltersView {
|
|||
Section(header: Text(title)) {
|
||||
ForEach(filters) { filter in
|
||||
NavigationLink(destination: EditFilterView(
|
||||
viewModel: viewModel.editFilterViewModel(filter: filter))) {
|
||||
viewModel: .init(filter: filter, identification: identification))) {
|
||||
HStack {
|
||||
Text(filter.phrase)
|
||||
Spacer()
|
||||
|
@ -60,7 +61,7 @@ import PreviewViewModels
|
|||
|
||||
struct FiltersView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FiltersView(viewModel: .mock())
|
||||
FiltersView(viewModel: .init(identification: .preview))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -72,8 +72,8 @@ import PreviewViewModels
|
|||
|
||||
struct IdentitiesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
IdentitiesView(viewModel: .mock())
|
||||
.environmentObject(RootViewModel.mock())
|
||||
IdentitiesView(viewModel: .init(identification: .preview))
|
||||
.environmentObject(RootViewModel.preview)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -60,8 +60,8 @@ import PreviewViewModels
|
|||
|
||||
struct ListsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ListsView(viewModel: .mock())
|
||||
.environmentObject(TabNavigationViewModel.mock())
|
||||
ListsView(viewModel: .init(identification: .preview))
|
||||
.environmentObject(TabNavigationViewModel(identification: .preview))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -29,7 +29,7 @@ import PreviewViewModels
|
|||
|
||||
struct NotificationTypesPreferencesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NotificationTypesPreferencesView(viewModel: .mock())
|
||||
NotificationTypesPreferencesView(viewModel: .init(identification: .preview))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -55,7 +55,7 @@ import PreviewViewModels
|
|||
|
||||
struct PostingReadingPreferencesViewView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PostingReadingPreferencesView(viewModel: .mock())
|
||||
PostingReadingPreferencesView(viewModel: .init(identification: .preview))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -5,20 +5,21 @@ import ViewModels
|
|||
|
||||
struct PreferencesView: View {
|
||||
@StateObject var viewModel: PreferencesViewModel
|
||||
@EnvironmentObject var identification: Identification
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text(viewModel.handle)) {
|
||||
NavigationLink("preferences.posting-reading",
|
||||
destination: PostingReadingPreferencesView(
|
||||
viewModel: viewModel.postingReadingPreferencesViewModel()))
|
||||
viewModel: .init(identification: identification)))
|
||||
NavigationLink("preferences.filters",
|
||||
destination: FiltersView(
|
||||
viewModel: viewModel.filtersViewModel()))
|
||||
viewModel: .init(identification: identification)))
|
||||
if viewModel.shouldShowNotificationTypePreferences {
|
||||
NavigationLink("preferences.notification-types",
|
||||
destination: NotificationTypesPreferencesView(
|
||||
viewModel: viewModel.notificationTypesPreferencesViewModel()))
|
||||
viewModel: .init(identification: identification)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +32,7 @@ import PreviewViewModels
|
|||
|
||||
struct PreferencesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PreferencesView(viewModel: .mock())
|
||||
PreferencesView(viewModel: .init(identification: .preview))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -7,9 +7,11 @@ struct RootView: View {
|
|||
@StateObject var viewModel: RootViewModel
|
||||
|
||||
var body: some View {
|
||||
if let tabNavigationViewModel = viewModel.tabNavigationViewModel {
|
||||
TabNavigationView(viewModel: tabNavigationViewModel)
|
||||
if let identification = viewModel.identification {
|
||||
TabNavigationView()
|
||||
.id(UUID())
|
||||
.environmentObject(identification)
|
||||
.environmentObject(TabNavigationViewModel(identification: identification))
|
||||
.environmentObject(viewModel)
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
|
@ -21,11 +23,12 @@ struct RootView: View {
|
|||
}
|
||||
|
||||
#if DEBUG
|
||||
import Combine
|
||||
import PreviewViewModels
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RootView(viewModel: .mock())
|
||||
RootView(viewModel: .preview)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -5,7 +5,8 @@ import SwiftUI
|
|||
import ViewModels
|
||||
|
||||
struct SecondaryNavigationView: View {
|
||||
@StateObject var viewModel: SecondaryNavigationViewModel
|
||||
@EnvironmentObject var identification: Identification
|
||||
@EnvironmentObject var tabNavigationViewModel: TabNavigationViewModel
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@Environment(\.displayScale) var displayScale: CGFloat
|
||||
|
||||
|
@ -14,19 +15,19 @@ struct SecondaryNavigationView: View {
|
|||
Form {
|
||||
Section {
|
||||
NavigationLink(
|
||||
destination: IdentitiesView(viewModel: viewModel.identitiesViewModel()),
|
||||
destination: IdentitiesView(viewModel: .init(identification: identification)),
|
||||
label: {
|
||||
HStack {
|
||||
KFImage(viewModel.identity.image,
|
||||
KFImage(tabNavigationViewModel.identity.image,
|
||||
options: .downsampled(dimension: 50, scaleFactor: displayScale))
|
||||
VStack(alignment: .leading) {
|
||||
if let account = viewModel.identity.account {
|
||||
if let account = tabNavigationViewModel.identity.account {
|
||||
CustomEmojiText(
|
||||
text: account.displayName,
|
||||
emoji: account.emojis,
|
||||
textStyle: .headline)
|
||||
}
|
||||
Text(viewModel.identity.handle)
|
||||
Text(tabNavigationViewModel.identity.handle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
|
@ -40,7 +41,7 @@ struct SecondaryNavigationView: View {
|
|||
})
|
||||
}
|
||||
Section {
|
||||
NavigationLink(destination: ListsView(viewModel: viewModel.listsViewModel())) {
|
||||
NavigationLink(destination: ListsView(viewModel: .init(identification: identification))) {
|
||||
Label("secondary-navigation.lists", systemImage: "scroll")
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +49,7 @@ struct SecondaryNavigationView: View {
|
|||
NavigationLink(
|
||||
"secondary-navigation.preferences",
|
||||
destination: PreferencesView(
|
||||
viewModel: viewModel.preferencesViewModel()))
|
||||
viewModel: .init(identification: identification)))
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
@ -71,9 +72,9 @@ import PreviewViewModels
|
|||
|
||||
struct SecondaryNavigationView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SecondaryNavigationView(viewModel: .mock())
|
||||
.environmentObject(RootViewModel.mock())
|
||||
.environmentObject(TabNavigationViewModel.mock())
|
||||
SecondaryNavigationView()
|
||||
.environmentObject(Identification.preview)
|
||||
.environmentObject(TabNavigationViewModel(identification: .preview))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -20,7 +20,7 @@ import PreviewViewModels
|
|||
|
||||
struct StatusListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
StatusListView(viewModel: .mock())
|
||||
StatusListView(viewModel: TabNavigationViewModel(identification: .preview).viewModel(timeline: .home))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -6,7 +6,7 @@ import SwiftUI
|
|||
import ViewModels
|
||||
|
||||
struct TabNavigationView: View {
|
||||
@ObservedObject var viewModel: TabNavigationViewModel
|
||||
@EnvironmentObject var viewModel: TabNavigationViewModel
|
||||
@EnvironmentObject var rootViewModel: RootViewModel
|
||||
@Environment(\.displayScale) var displayScale: CGFloat
|
||||
|
||||
|
@ -25,7 +25,7 @@ struct TabNavigationView: View {
|
|||
}
|
||||
}
|
||||
.sheet(isPresented: $viewModel.presentingSecondaryNavigation) {
|
||||
SecondaryNavigationView(viewModel: viewModel.secondaryNavigationViewModel())
|
||||
SecondaryNavigationView()
|
||||
.environmentObject(viewModel)
|
||||
}
|
||||
.alertItem($viewModel.alertItem)
|
||||
|
@ -145,8 +145,10 @@ import PreviewViewModels
|
|||
|
||||
struct TabNavigation_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TabNavigationView(viewModel: .mock())
|
||||
.environmentObject(RootViewModel.mock())
|
||||
TabNavigationView()
|
||||
.environmentObject(Identification.preview)
|
||||
.environmentObject(TabNavigationViewModel(identification: .preview))
|
||||
.environmentObject(RootViewModel.preview)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
Loading…
Reference in a new issue