Refactoring
This commit is contained in:
parent
367d79ed2c
commit
b4549521cb
|
@ -14,7 +14,7 @@ public enum IdentityDatabaseError: Error {
|
||||||
public struct IdentityDatabase {
|
public struct IdentityDatabase {
|
||||||
private let databaseQueue: DatabaseQueue
|
private let databaseQueue: DatabaseQueue
|
||||||
|
|
||||||
public init(inMemory: Bool, fixture: IdentityFixture?, keychain: Keychain.Type) throws {
|
public init(inMemory: Bool, keychain: Keychain.Type) throws {
|
||||||
if inMemory {
|
if inMemory {
|
||||||
databaseQueue = DatabaseQueue()
|
databaseQueue = DatabaseQueue()
|
||||||
} else {
|
} else {
|
||||||
|
@ -29,10 +29,6 @@ public struct IdentityDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
try Self.migrate(databaseQueue)
|
try Self.migrate(databaseQueue)
|
||||||
|
|
||||||
if let fixture = fixture {
|
|
||||||
try populate(fixture: fixture)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,22 +258,4 @@ private extension IdentityDatabase {
|
||||||
|
|
||||||
try migrator.migrate(writer)
|
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 {
|
extension AccountEndpoint: Stubbing {
|
||||||
public func data(url: URL) -> Data? {
|
public func data(url: URL) -> Data? {
|
||||||
switch self {
|
StubData.account
|
||||||
case .verifyCredentials: return try? Data(contentsOf: Bundle.module.url(forResource: "account",
|
|
||||||
withExtension: "json")!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,6 @@ import Stubbing
|
||||||
|
|
||||||
extension InstanceEndpoint: Stubbing {
|
extension InstanceEndpoint: Stubbing {
|
||||||
public func data(url: URL) -> Data? {
|
public func data(url: URL) -> Data? {
|
||||||
switch self {
|
StubData.instance
|
||||||
case .instance: return try? Data(contentsOf: Bundle.module.url(forResource: "instance", withExtension: "json")!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,7 @@ import MastodonAPI
|
||||||
import Stubbing
|
import Stubbing
|
||||||
|
|
||||||
extension PreferencesEndpoint: Stubbing {
|
extension PreferencesEndpoint: Stubbing {
|
||||||
public func dataString(url: URL) -> String? {
|
public func data(url: URL) -> Data? {
|
||||||
switch self {
|
StubData.preferences
|
||||||
case .preferences:
|
|
||||||
return """
|
|
||||||
{
|
|
||||||
"posting:default:visibility": "public",
|
|
||||||
"posting:default:sensitive": false,
|
|
||||||
"posting:default:language": null,
|
|
||||||
"reading:expand:media": "default",
|
|
||||||
"reading:expand:spoilers": false
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"posting:default:visibility": "public",
|
||||||
|
"posting:default:sensitive": false,
|
||||||
|
"posting:default:language": null,
|
||||||
|
"reading:expand:media": "default",
|
||||||
|
"reading:expand:spoilers": false
|
||||||
|
}
|
|
@ -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 {
|
extension TimelinesEndpoint: Stubbing {
|
||||||
public func data(url: URL) -> Data? {
|
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 {
|
public extension Secrets {
|
||||||
enum Item: String, CaseIterable {
|
enum Item: String, CaseIterable {
|
||||||
|
case instanceURL
|
||||||
case clientID
|
case clientID
|
||||||
case clientSecret
|
case clientSecret
|
||||||
case accessToken
|
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 {
|
func getClientID() throws -> String {
|
||||||
try item(.clientID)
|
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 {
|
private struct PushKey {
|
||||||
static let authLength = 16
|
static let authLength = 16
|
||||||
static let sizeInBits = 256
|
static let sizeInBits = 256
|
||||||
|
|
|
@ -14,7 +14,7 @@ public struct AppEnvironment {
|
||||||
let userDefaults: UserDefaults
|
let userDefaults: UserDefaults
|
||||||
let userNotificationClient: UserNotificationClient
|
let userNotificationClient: UserNotificationClient
|
||||||
let inMemoryContent: Bool
|
let inMemoryContent: Bool
|
||||||
let identityFixture: IdentityFixture?
|
let fixtureDatabase: IdentityDatabase?
|
||||||
|
|
||||||
public init(session: Session,
|
public init(session: Session,
|
||||||
webAuthSessionType: WebAuthSession.Type,
|
webAuthSessionType: WebAuthSession.Type,
|
||||||
|
@ -22,14 +22,14 @@ public struct AppEnvironment {
|
||||||
userDefaults: UserDefaults,
|
userDefaults: UserDefaults,
|
||||||
userNotificationClient: UserNotificationClient,
|
userNotificationClient: UserNotificationClient,
|
||||||
inMemoryContent: Bool,
|
inMemoryContent: Bool,
|
||||||
identityFixture: IdentityFixture?) {
|
fixtureDatabase: IdentityDatabase?) {
|
||||||
self.session = session
|
self.session = session
|
||||||
self.webAuthSessionType = webAuthSessionType
|
self.webAuthSessionType = webAuthSessionType
|
||||||
self.keychain = keychain
|
self.keychain = keychain
|
||||||
self.userDefaults = userDefaults
|
self.userDefaults = userDefaults
|
||||||
self.userNotificationClient = userNotificationClient
|
self.userNotificationClient = userNotificationClient
|
||||||
self.inMemoryContent = inMemoryContent
|
self.inMemoryContent = inMemoryContent
|
||||||
self.identityFixture = identityFixture
|
self.fixtureDatabase = fixtureDatabase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,6 @@ public extension AppEnvironment {
|
||||||
userDefaults: .standard,
|
userDefaults: .standard,
|
||||||
userNotificationClient: .live(userNotificationCenter),
|
userNotificationClient: .live(userNotificationCenter),
|
||||||
inMemoryContent: false,
|
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,8 +15,8 @@ public struct AllIdentitiesService {
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
|
|
||||||
public init(environment: AppEnvironment) throws {
|
public init(environment: AppEnvironment) throws {
|
||||||
self.database = try IdentityDatabase(inMemory: environment.inMemoryContent,
|
self.database = try environment.fixtureDatabase ?? IdentityDatabase(
|
||||||
fixture: environment.identityFixture,
|
inMemory: environment.inMemoryContent,
|
||||||
keychain: environment.keychain)
|
keychain: environment.keychain)
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ public struct AllIdentitiesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension AllIdentitiesService {
|
public extension AllIdentitiesService {
|
||||||
func identifiedEnvironment(id: UUID) throws -> IdentifiedEnvironment {
|
func identityService(id: UUID) throws -> IdentityService {
|
||||||
try IdentifiedEnvironment(id: id, database: database, environment: environment)
|
try IdentityService(id: id, database: database, environment: environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> {
|
func createIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> {
|
||||||
|
@ -42,6 +42,7 @@ public extension AllIdentitiesService {
|
||||||
|
|
||||||
return authenticationService.authorizeApp(instanceURL: instanceURL)
|
return authenticationService.authorizeApp(instanceURL: instanceURL)
|
||||||
.tryMap { appAuthorization -> (URL, AppAuthorization) in
|
.tryMap { appAuthorization -> (URL, AppAuthorization) in
|
||||||
|
try secrets.setInstanceURL(instanceURL)
|
||||||
try secrets.setClientID(appAuthorization.clientId)
|
try secrets.setClientID(appAuthorization.clientId)
|
||||||
try secrets.setClientSecret(appAuthorization.clientSecret)
|
try secrets.setClientSecret(appAuthorization.clientSecret)
|
||||||
|
|
||||||
|
@ -81,7 +82,7 @@ public extension AllIdentitiesService {
|
||||||
database.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, instanceURL: $0.url, database: database, environment: environment)
|
try IdentityService(id: $0.id, 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()
|
||||||
|
|
|
@ -17,7 +17,7 @@ public struct IdentityService {
|
||||||
private let secrets: Secrets
|
private let secrets: Secrets
|
||||||
private let observationErrorsInput = PassthroughSubject<Error, Never>()
|
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
|
identityID = id
|
||||||
identityDatabase = database
|
identityDatabase = database
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
@ -25,7 +25,7 @@ public struct IdentityService {
|
||||||
identityID: id,
|
identityID: id,
|
||||||
keychain: environment.keychain)
|
keychain: environment.keychain)
|
||||||
mastodonAPIClient = MastodonAPIClient(session: environment.session)
|
mastodonAPIClient = MastodonAPIClient(session: environment.session)
|
||||||
mastodonAPIClient.instanceURL = instanceURL
|
mastodonAPIClient.instanceURL = try secrets.getInstanceURL()
|
||||||
mastodonAPIClient.accessToken = try? secrets.getAccessToken()
|
mastodonAPIClient.accessToken = try? secrets.getAccessToken()
|
||||||
|
|
||||||
contentDatabase = try ContentDatabase(identityID: id,
|
contentDatabase = try ContentDatabase(identityID: id,
|
||||||
|
@ -86,6 +86,10 @@ public extension IdentityService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func observation() -> AnyPublisher<Identity, Error> {
|
||||||
|
identityDatabase.identityObservation(id: identityID)
|
||||||
|
}
|
||||||
|
|
||||||
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
||||||
contentDatabase.listsObservation()
|
contentDatabase.listsObservation()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,19 @@
|
||||||
import DB
|
import DB
|
||||||
import Foundation
|
import Foundation
|
||||||
import HTTP
|
import HTTP
|
||||||
|
import Keychain
|
||||||
import MockKeychain
|
import MockKeychain
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
import Stubbing
|
import Stubbing
|
||||||
|
|
||||||
public extension AppEnvironment {
|
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(
|
AppEnvironment(
|
||||||
session: Session(configuration: .stubbing),
|
session: Session(configuration: .stubbing),
|
||||||
webAuthSessionType: SuccessfulMockWebAuthSession.self,
|
webAuthSessionType: SuccessfulMockWebAuthSession.self,
|
||||||
|
@ -16,6 +23,6 @@ public extension AppEnvironment {
|
||||||
userDefaults: MockUserDefaults(),
|
userDefaults: MockUserDefaults(),
|
||||||
userNotificationClient: .mock,
|
userNotificationClient: .mock,
|
||||||
inMemoryContent: true,
|
inMemoryContent: true,
|
||||||
identityFixture: identityFixture)
|
fixtureDatabase: fixtureDatabase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
didSet { filter.expiresAt = date }
|
||||||
}
|
}
|
||||||
|
|
||||||
private let environment: IdentifiedEnvironment
|
private let identification: Identification
|
||||||
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, environment: IdentifiedEnvironment) {
|
public init(filter: Filter, identification: Identification) {
|
||||||
self.filter = filter
|
self.filter = filter
|
||||||
self.environment = environment
|
self.identification = identification
|
||||||
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 ? environment.identityService.createFilter(filter) : environment.identityService.updateFilter(filter))
|
(isNew ? identification.service.createFilter(filter) : identification.service.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 },
|
||||||
|
|
|
@ -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 expiredFilters = [Filter]()
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
|
|
||||||
private let environment: IdentifiedEnvironment
|
private let identification: Identification
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(environment: IdentifiedEnvironment) {
|
public init(identification: Identification) {
|
||||||
self.environment = environment
|
self.identification = identification
|
||||||
|
|
||||||
let now = Date()
|
let now = Date()
|
||||||
|
|
||||||
environment.identityService.activeFiltersObservation(date: now)
|
identification.service.activeFiltersObservation(date: now)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$activeFilters)
|
.assign(to: &$activeFilters)
|
||||||
|
|
||||||
environment.identityService.expiredFiltersObservation(date: now)
|
identification.service.expiredFiltersObservation(date: now)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$expiredFilters)
|
.assign(to: &$expiredFilters)
|
||||||
}
|
}
|
||||||
|
@ -30,20 +30,16 @@ public class FiltersViewModel: ObservableObject {
|
||||||
|
|
||||||
public extension FiltersViewModel {
|
public extension FiltersViewModel {
|
||||||
func refreshFilters() {
|
func refreshFilters() {
|
||||||
environment.identityService.refreshFilters()
|
identification.service.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) {
|
||||||
environment.identityService.deleteFilter(id: filter.id)
|
identification.service.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 {
|
|
||||||
EditFilterViewModel(filter: filter, environment: environment)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,14 @@ public class IdentitiesViewModel: ObservableObject {
|
||||||
@Published public var identities = [Identity]()
|
@Published public var identities = [Identity]()
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
|
|
||||||
private let environment: IdentifiedEnvironment
|
private let identification: Identification
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(environment: IdentifiedEnvironment) {
|
public init(identification: Identification) {
|
||||||
self.environment = environment
|
self.identification = identification
|
||||||
currentIdentityID = environment.identity.id
|
currentIdentityID = identification.identity.id
|
||||||
|
|
||||||
environment.identityService.identitiesObservation()
|
identification.service.identitiesObservation()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$identities)
|
.assign(to: &$identities)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 environment: IdentifiedEnvironment
|
private let identification: Identification
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(environment: IdentifiedEnvironment) {
|
public init(identification: Identification) {
|
||||||
self.environment = environment
|
self.identification = identification
|
||||||
|
|
||||||
environment.identityService.listsObservation()
|
identification.service.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() {
|
||||||
environment.identityService.refreshLists()
|
identification.service.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) {
|
||||||
environment.identityService.createList(title: title)
|
identification.service.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) {
|
||||||
environment.identityService.deleteList(id: list.id)
|
identification.service.deleteList(id: list.id)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
|
@ -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 environment: IdentifiedEnvironment
|
private let identification: Identification
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(environment: IdentifiedEnvironment) {
|
public init(identification: Identification) {
|
||||||
self.environment = environment
|
self.identification = identification
|
||||||
pushSubscriptionAlerts = environment.identity.pushSubscriptionAlerts
|
pushSubscriptionAlerts = identification.identity.pushSubscriptionAlerts
|
||||||
|
|
||||||
environment.$identity
|
identification.$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 != 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
|
.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.environment.identity.pushSubscriptionAlerts
|
self.pushSubscriptionAlerts = self.identification.identity.pushSubscriptionAlerts
|
||||||
} receiveValue: { _ in }
|
} receiveValue: { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 environment: IdentifiedEnvironment
|
private let identification: Identification
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(environment: IdentifiedEnvironment) {
|
public init(identification: Identification) {
|
||||||
self.environment = environment
|
self.identification = identification
|
||||||
preferences = environment.identity.preferences
|
preferences = identification.identity.preferences
|
||||||
|
|
||||||
environment.$identity
|
identification.$identity
|
||||||
.map(\.preferences)
|
.map(\.preferences)
|
||||||
.dropFirst()
|
.dropFirst()
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
|
@ -23,7 +23,7 @@ public class PostingReadingPreferencesViewModel: ObservableObject {
|
||||||
|
|
||||||
$preferences
|
$preferences
|
||||||
.dropFirst()
|
.dropFirst()
|
||||||
.flatMap(environment.identityService.updatePreferences)
|
.flatMap(identification.service.updatePreferences)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
|
@ -7,26 +7,12 @@ public class PreferencesViewModel: ObservableObject {
|
||||||
public let handle: String
|
public let handle: String
|
||||||
public let shouldShowNotificationTypePreferences: Bool
|
public let shouldShowNotificationTypePreferences: Bool
|
||||||
|
|
||||||
private let environment: IdentifiedEnvironment
|
private let identification: Identification
|
||||||
|
|
||||||
init(environment: IdentifiedEnvironment) {
|
public init(identification: Identification) {
|
||||||
self.environment = environment
|
self.identification = identification
|
||||||
handle = environment.identity.handle
|
handle = identification.identity.handle
|
||||||
|
|
||||||
shouldShowNotificationTypePreferences = environment.identity.lastRegisteredDeviceToken != nil
|
shouldShowNotificationTypePreferences = identification.identity.lastRegisteredDeviceToken != nil
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension PreferencesViewModel {
|
|
||||||
func postingReadingPreferencesViewModel() -> PostingReadingPreferencesViewModel {
|
|
||||||
PostingReadingPreferencesViewModel(environment: environment)
|
|
||||||
}
|
|
||||||
|
|
||||||
func notificationTypesPreferencesViewModel() -> NotificationTypesPreferencesViewModel {
|
|
||||||
NotificationTypesPreferencesViewModel(environment: environment)
|
|
||||||
}
|
|
||||||
|
|
||||||
func filtersViewModel() -> FiltersViewModel {
|
|
||||||
FiltersViewModel(environment: environment)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,9 @@ import Foundation
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public final class RootViewModel: ObservableObject {
|
public final class RootViewModel: ObservableObject {
|
||||||
@Published public private(set) var tabNavigationViewModel: TabNavigationViewModel?
|
@Published public private(set) var identification: Identification?
|
||||||
|
|
||||||
@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>
|
||||||
|
@ -16,7 +15,6 @@ 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
|
||||||
|
@ -38,39 +36,38 @@ public final class RootViewModel: ObservableObject {
|
||||||
public extension RootViewModel {
|
public extension RootViewModel {
|
||||||
func newIdentitySelected(id: UUID?) {
|
func newIdentitySelected(id: UUID?) {
|
||||||
guard let id = id else {
|
guard let id = id else {
|
||||||
tabNavigationViewModel = nil
|
identification = nil
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let identifiedEnvironment: IdentifiedEnvironment
|
let identification: Identification
|
||||||
|
|
||||||
do {
|
do {
|
||||||
identifiedEnvironment = try allIdentitiesService.identifiedEnvironment(id: id)
|
identification = try Identification(service: allIdentitiesService.identityService(id: id))
|
||||||
|
self.identification = identification
|
||||||
} catch {
|
} catch {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
identifiedEnvironment.observationErrors
|
identification.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)
|
||||||
|
|
||||||
identifiedEnvironment.identityService.updateLastUse()
|
identification.service.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 { identifiedEnvironment.identity.lastRegisteredDeviceToken != $1 }
|
.filter { identification.identity.lastRegisteredDeviceToken != $1 }
|
||||||
.map { ($1, identifiedEnvironment.identity.pushSubscriptionAlerts) }
|
.map { ($1, identification.identity.pushSubscriptionAlerts) }
|
||||||
.flatMap(identifiedEnvironment.identityService.createPushSubscription(deviceToken:alerts:))
|
.flatMap(identification.service.createPushSubscription(deviceToken:alerts:))
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
tabNavigationViewModel = TabNavigationViewModel(environment: identifiedEnvironment)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentity(_ identity: Identity) {
|
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?
|
@Published public var alertItem: AlertItem?
|
||||||
public var selectedTab: Tab? = .timelines
|
public var selectedTab: Tab? = .timelines
|
||||||
|
|
||||||
private let environment: IdentifiedEnvironment
|
private let identification: Identification
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(environment: IdentifiedEnvironment) {
|
public init(identification: Identification) {
|
||||||
self.environment = environment
|
self.identification = identification
|
||||||
identity = environment.identity
|
identity = identification.identity
|
||||||
environment.$identity.dropFirst().assign(to: &$identity)
|
identification.$identity.dropFirst().assign(to: &$identity)
|
||||||
|
|
||||||
environment.identityService.recentIdentitiesObservation()
|
identification.service.recentIdentitiesObservation()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$recentIdentities)
|
.assign(to: &$recentIdentities)
|
||||||
|
|
||||||
environment.identityService.listsObservation()
|
identification.service.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,38 @@ public extension TabNavigationViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshIdentity() {
|
func refreshIdentity() {
|
||||||
if environment.identityService.isAuthorized {
|
if identification.service.isAuthorized {
|
||||||
environment.identityService.verifyCredentials()
|
identification.service.verifyCredentials()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
environment.identityService.refreshLists()
|
identification.service.refreshLists()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
environment.identityService.refreshFilters()
|
identification.service.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 {
|
||||||
environment.identityService.refreshServerPreferences()
|
identification.service.refreshServerPreferences()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
environment.identityService.refreshInstance()
|
identification.service.refreshInstance()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func secondaryNavigationViewModel() -> SecondaryNavigationViewModel {
|
|
||||||
SecondaryNavigationViewModel(environment: environment)
|
|
||||||
}
|
|
||||||
|
|
||||||
func viewModel(timeline: Timeline) -> StatusListViewModel {
|
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 {
|
func testDoesNotAlertCanceledLogin() throws {
|
||||||
let environment = AppEnvironment(
|
let allIdentitiesService = try AllIdentitiesService(
|
||||||
session: Session(configuration: .stubbing),
|
environment: .mock(webAuthSessionType: CanceledLoginMockWebAuthSession.self))
|
||||||
webAuthSessionType: CanceledLoginMockWebAuthSession.self,
|
|
||||||
keychain: MockKeychain.self,
|
|
||||||
userDefaults: MockUserDefaults(),
|
|
||||||
userNotificationClient: .mock,
|
|
||||||
inMemoryContent: true,
|
|
||||||
identityFixture: nil)
|
|
||||||
let allIdentitiesService = try AllIdentitiesService(environment: environment)
|
|
||||||
let sut = AddIdentityViewModel(allIdentitiesService: allIdentitiesService)
|
let sut = AddIdentityViewModel(allIdentitiesService: allIdentitiesService)
|
||||||
let recorder = sut.$alertItem.record()
|
let recorder = sut.$alertItem.record()
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ class RootViewModelTests: XCTestCase {
|
||||||
let sut = try RootViewModel(
|
let sut = try RootViewModel(
|
||||||
environment: .mock(),
|
environment: .mock(),
|
||||||
registerForRemoteNotifications: { Empty().setFailureType(to: Error.self).eraseToAnyPublisher() })
|
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))
|
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct AddAccountView_Previews: PreviewProvider {
|
struct AddAccountView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
AddIdentityView(viewModel: .mock())
|
AddIdentityView(viewModel: RootViewModel.preview.addIdentityViewModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -101,7 +101,7 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct EditFilterView_Previews: PreviewProvider {
|
struct EditFilterView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
EditFilterView(viewModel: .mock())
|
EditFilterView(viewModel: .init(filter: .new, identification: .preview))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -6,12 +6,13 @@ import ViewModels
|
||||||
|
|
||||||
struct FiltersView: View {
|
struct FiltersView: View {
|
||||||
@StateObject var viewModel: FiltersViewModel
|
@StateObject var viewModel: FiltersViewModel
|
||||||
|
@EnvironmentObject var identification: Identification
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink(destination: EditFilterView(
|
NavigationLink(destination: EditFilterView(
|
||||||
viewModel: viewModel.editFilterViewModel(filter: .new))) {
|
viewModel: .init(filter: .new, identification: identification))) {
|
||||||
Label("add", systemImage: "plus.circle")
|
Label("add", systemImage: "plus.circle")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +37,7 @@ private extension FiltersView {
|
||||||
Section(header: Text(title)) {
|
Section(header: Text(title)) {
|
||||||
ForEach(filters) { filter in
|
ForEach(filters) { filter in
|
||||||
NavigationLink(destination: EditFilterView(
|
NavigationLink(destination: EditFilterView(
|
||||||
viewModel: viewModel.editFilterViewModel(filter: filter))) {
|
viewModel: .init(filter: filter, identification: identification))) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(filter.phrase)
|
Text(filter.phrase)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -60,7 +61,7 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct FiltersView_Previews: PreviewProvider {
|
struct FiltersView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
FiltersView(viewModel: .mock())
|
FiltersView(viewModel: .init(identification: .preview))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -72,8 +72,8 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct IdentitiesView_Previews: PreviewProvider {
|
struct IdentitiesView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
IdentitiesView(viewModel: .mock())
|
IdentitiesView(viewModel: .init(identification: .preview))
|
||||||
.environmentObject(RootViewModel.mock())
|
.environmentObject(RootViewModel.preview)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -60,8 +60,8 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct ListsView_Previews: PreviewProvider {
|
struct ListsView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ListsView(viewModel: .mock())
|
ListsView(viewModel: .init(identification: .preview))
|
||||||
.environmentObject(TabNavigationViewModel.mock())
|
.environmentObject(TabNavigationViewModel(identification: .preview))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -29,7 +29,7 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct NotificationTypesPreferencesView_Previews: PreviewProvider {
|
struct NotificationTypesPreferencesView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NotificationTypesPreferencesView(viewModel: .mock())
|
NotificationTypesPreferencesView(viewModel: .init(identification: .preview))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -55,7 +55,7 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct PostingReadingPreferencesViewView_Previews: PreviewProvider {
|
struct PostingReadingPreferencesViewView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
PostingReadingPreferencesView(viewModel: .mock())
|
PostingReadingPreferencesView(viewModel: .init(identification: .preview))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -5,20 +5,21 @@ import ViewModels
|
||||||
|
|
||||||
struct PreferencesView: View {
|
struct PreferencesView: View {
|
||||||
@StateObject var viewModel: PreferencesViewModel
|
@StateObject var viewModel: PreferencesViewModel
|
||||||
|
@EnvironmentObject var identification: Identification
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section(header: Text(viewModel.handle)) {
|
Section(header: Text(viewModel.handle)) {
|
||||||
NavigationLink("preferences.posting-reading",
|
NavigationLink("preferences.posting-reading",
|
||||||
destination: PostingReadingPreferencesView(
|
destination: PostingReadingPreferencesView(
|
||||||
viewModel: viewModel.postingReadingPreferencesViewModel()))
|
viewModel: .init(identification: identification)))
|
||||||
NavigationLink("preferences.filters",
|
NavigationLink("preferences.filters",
|
||||||
destination: FiltersView(
|
destination: FiltersView(
|
||||||
viewModel: viewModel.filtersViewModel()))
|
viewModel: .init(identification: identification)))
|
||||||
if viewModel.shouldShowNotificationTypePreferences {
|
if viewModel.shouldShowNotificationTypePreferences {
|
||||||
NavigationLink("preferences.notification-types",
|
NavigationLink("preferences.notification-types",
|
||||||
destination: NotificationTypesPreferencesView(
|
destination: NotificationTypesPreferencesView(
|
||||||
viewModel: viewModel.notificationTypesPreferencesViewModel()))
|
viewModel: .init(identification: identification)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +32,7 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct PreferencesView_Previews: PreviewProvider {
|
struct PreferencesView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
PreferencesView(viewModel: .mock())
|
PreferencesView(viewModel: .init(identification: .preview))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -7,9 +7,11 @@ struct RootView: View {
|
||||||
@StateObject var viewModel: RootViewModel
|
@StateObject var viewModel: RootViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let tabNavigationViewModel = viewModel.tabNavigationViewModel {
|
if let identification = viewModel.identification {
|
||||||
TabNavigationView(viewModel: tabNavigationViewModel)
|
TabNavigationView()
|
||||||
.id(UUID())
|
.id(UUID())
|
||||||
|
.environmentObject(identification)
|
||||||
|
.environmentObject(TabNavigationViewModel(identification: identification))
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
} else {
|
} else {
|
||||||
|
@ -21,11 +23,12 @@ struct RootView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
import Combine
|
||||||
import PreviewViewModels
|
import PreviewViewModels
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
RootView(viewModel: .mock())
|
RootView(viewModel: .preview)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -5,7 +5,8 @@ import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
struct SecondaryNavigationView: View {
|
struct SecondaryNavigationView: View {
|
||||||
@StateObject var viewModel: SecondaryNavigationViewModel
|
@EnvironmentObject var identification: Identification
|
||||||
|
@EnvironmentObject var tabNavigationViewModel: TabNavigationViewModel
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
@Environment(\.displayScale) var displayScale: CGFloat
|
@Environment(\.displayScale) var displayScale: CGFloat
|
||||||
|
|
||||||
|
@ -14,19 +15,19 @@ struct SecondaryNavigationView: View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink(
|
NavigationLink(
|
||||||
destination: IdentitiesView(viewModel: viewModel.identitiesViewModel()),
|
destination: IdentitiesView(viewModel: .init(identification: identification)),
|
||||||
label: {
|
label: {
|
||||||
HStack {
|
HStack {
|
||||||
KFImage(viewModel.identity.image,
|
KFImage(tabNavigationViewModel.identity.image,
|
||||||
options: .downsampled(dimension: 50, scaleFactor: displayScale))
|
options: .downsampled(dimension: 50, scaleFactor: displayScale))
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if let account = viewModel.identity.account {
|
if let account = tabNavigationViewModel.identity.account {
|
||||||
CustomEmojiText(
|
CustomEmojiText(
|
||||||
text: account.displayName,
|
text: account.displayName,
|
||||||
emoji: account.emojis,
|
emoji: account.emojis,
|
||||||
textStyle: .headline)
|
textStyle: .headline)
|
||||||
}
|
}
|
||||||
Text(viewModel.identity.handle)
|
Text(tabNavigationViewModel.identity.handle)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
@ -40,7 +41,7 @@ struct SecondaryNavigationView: View {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Section {
|
Section {
|
||||||
NavigationLink(destination: ListsView(viewModel: viewModel.listsViewModel())) {
|
NavigationLink(destination: ListsView(viewModel: .init(identification: identification))) {
|
||||||
Label("secondary-navigation.lists", systemImage: "scroll")
|
Label("secondary-navigation.lists", systemImage: "scroll")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +49,7 @@ struct SecondaryNavigationView: View {
|
||||||
NavigationLink(
|
NavigationLink(
|
||||||
"secondary-navigation.preferences",
|
"secondary-navigation.preferences",
|
||||||
destination: PreferencesView(
|
destination: PreferencesView(
|
||||||
viewModel: viewModel.preferencesViewModel()))
|
viewModel: .init(identification: identification)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
@ -71,9 +72,9 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct SecondaryNavigationView_Previews: PreviewProvider {
|
struct SecondaryNavigationView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SecondaryNavigationView(viewModel: .mock())
|
SecondaryNavigationView()
|
||||||
.environmentObject(RootViewModel.mock())
|
.environmentObject(Identification.preview)
|
||||||
.environmentObject(TabNavigationViewModel.mock())
|
.environmentObject(TabNavigationViewModel(identification: .preview))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -20,7 +20,7 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct StatusListView_Previews: PreviewProvider {
|
struct StatusListView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
StatusListView(viewModel: .mock())
|
StatusListView(viewModel: TabNavigationViewModel(identification: .preview).viewModel(timeline: .home))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -6,7 +6,7 @@ import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
struct TabNavigationView: View {
|
struct TabNavigationView: View {
|
||||||
@ObservedObject var viewModel: TabNavigationViewModel
|
@EnvironmentObject var viewModel: TabNavigationViewModel
|
||||||
@EnvironmentObject var rootViewModel: RootViewModel
|
@EnvironmentObject var rootViewModel: RootViewModel
|
||||||
@Environment(\.displayScale) var displayScale: CGFloat
|
@Environment(\.displayScale) var displayScale: CGFloat
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ struct TabNavigationView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $viewModel.presentingSecondaryNavigation) {
|
.sheet(isPresented: $viewModel.presentingSecondaryNavigation) {
|
||||||
SecondaryNavigationView(viewModel: viewModel.secondaryNavigationViewModel())
|
SecondaryNavigationView()
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
}
|
}
|
||||||
.alertItem($viewModel.alertItem)
|
.alertItem($viewModel.alertItem)
|
||||||
|
@ -145,8 +145,10 @@ import PreviewViewModels
|
||||||
|
|
||||||
struct TabNavigation_Previews: PreviewProvider {
|
struct TabNavigation_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
TabNavigationView(viewModel: .mock())
|
TabNavigationView()
|
||||||
.environmentObject(RootViewModel.mock())
|
.environmentObject(Identification.preview)
|
||||||
|
.environmentObject(TabNavigationViewModel(identification: .preview))
|
||||||
|
.environmentObject(RootViewModel.preview)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in New Issue