Refactoring/preferences

This commit is contained in:
Justin Mazzocchi 2020-08-06 18:41:59 -07:00
parent c39f2d94d3
commit 2df47efdc9
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
21 changed files with 280 additions and 83 deletions

View file

@ -118,12 +118,16 @@ extension AppEnvironment {
webAuthSessionType: SuccessfulStubbingWebAuthSession.self)
}
extension IdentifiedEnvironment {
static let development = try! IdentifiedEnvironment(identityID: devIdentityID, appEnvironment: .development)
}
extension RootViewModel {
static let development = RootViewModel(environment: .development)
}
extension MainNavigationViewModel {
static let development = try! MainNavigationViewModel(identityID: devIdentityID, environment: .development)
static let development = MainNavigationViewModel(environment: .development)
}
extension SettingsViewModel {

View file

@ -7,6 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
D0091B6824DC10B30040E8D2 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6724DC10B30040E8D2 /* PreferencesView.swift */; };
D0091B6924DC10B30040E8D2 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6724DC10B30040E8D2 /* PreferencesView.swift */; };
D0091B6B24DC10CE0040E8D2 /* PreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6A24DC10CE0040E8D2 /* PreferencesViewModel.swift */; };
D0091B6C24DC10CE0040E8D2 /* PreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6A24DC10CE0040E8D2 /* PreferencesViewModel.swift */; };
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D047FA8724C3E21200AF17C5 /* Assets.xcassets */; };
@ -91,6 +95,14 @@
D0C963FC24CC359D003BD330 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FA24CC359D003BD330 /* AlertItem.swift */; };
D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */; };
D0C963FF24CC3812003BD330 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */; };
D0CD847324DBDEC700CF380C /* MastodonPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */; };
D0CD847424DBDEC700CF380C /* MastodonPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */; };
D0CD847624DBDF3C00CF380C /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD847524DBDF3C00CF380C /* Status.swift */; };
D0CD847724DBDF3C00CF380C /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD847524DBDF3C00CF380C /* Status.swift */; };
D0CD847C24DBEA9F00CF380C /* Unknowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD847B24DBEA9F00CF380C /* Unknowable.swift */; };
D0CD847D24DBEA9F00CF380C /* Unknowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD847B24DBEA9F00CF380C /* Unknowable.swift */; };
D0CD847F24DBF1BB00CF380C /* PreferencesEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD847E24DBF1BB00CF380C /* PreferencesEndpoint.swift */; };
D0CD848024DBF1BB00CF380C /* PreferencesEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD847E24DBF1BB00CF380C /* PreferencesEndpoint.swift */; };
D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */; };
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */; };
D0DB6F0924C65AC000D965FE /* AddIdentityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */; };
@ -151,6 +163,8 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
D0091B6724DC10B30040E8D2 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
D0091B6A24DC10CE0040E8D2 /* PreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewModel.swift; sourceTree = "<group>"; };
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; };
D047FA8724C3E21200AF17C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -199,6 +213,10 @@
D0BEC95024CA2B7E00E864C4 /* TabNavigation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabNavigation.swift; sourceTree = "<group>"; };
D0C963FA24CC359D003BD330 /* AlertItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertItem.swift; sourceTree = "<group>"; };
D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = "<group>"; };
D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPreferences.swift; sourceTree = "<group>"; };
D0CD847524DBDF3C00CF380C /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
D0CD847B24DBEA9F00CF380C /* Unknowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Unknowable.swift; sourceTree = "<group>"; };
D0CD847E24DBF1BB00CF380C /* PreferencesEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesEndpoint.swift; sourceTree = "<group>"; };
D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityView.swift; sourceTree = "<group>"; };
D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModel.swift; sourceTree = "<group>"; };
D0DC174524CFEC2000A75C65 /* StubbingURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubbingURLProtocol.swift; sourceTree = "<group>"; };
@ -332,14 +350,17 @@
D052BBCC24D750A100A80A7A /* AppEnvironment.swift */,
D0ED1BD624CF94B200B4899C /* Application.swift */,
D0666A4424C6BC0A00F3F04B /* DatabaseError.swift */,
D052BBCE24D750C000A80A7A /* Defaults.swift */,
D0666A5324C6C3E500F3F04B /* Emoji.swift */,
D0666A4A24C6C37700F3F04B /* Identity.swift */,
D0666A4124C6BB7B00F3F04B /* IdentityDatabase.swift */,
D0666A4D24C6C39600F3F04B /* Instance.swift */,
D0DC177324D0B58800A75C65 /* Keychain.swift */,
D0ED1BE224CFA84400B4899C /* MastodonError.swift */,
D052BBCE24D750C000A80A7A /* Defaults.swift */,
D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */,
D0666A7124C6E0D300F3F04B /* Secrets.swift */,
D0CD847524DBDF3C00CF380C /* Status.swift */,
D0CD847B24DBEA9F00CF380C /* Unknowable.swift */,
);
path = Model;
sourceTree = "<group>";
@ -364,6 +385,7 @@
children = (
D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */,
D06BAB5024D942CF0081B8FD /* IdentitiesView.swift */,
D0091B6724DC10B30040E8D2 /* PreferencesView.swift */,
D0BEC93A24C96FD500E864C4 /* RootView.swift */,
D04FD73224D48F37007D572D /* SettingsView.swift */,
D0BEC94924CA231200E864C4 /* TimelineView.swift */,
@ -390,6 +412,7 @@
D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */,
D06BAB4D24D942BC0081B8FD /* IdentitiesViewModel.swift */,
D052BBDF24D805E300A80A7A /* MainNavigationViewModel.swift */,
D0091B6A24DC10CE0040E8D2 /* PreferencesViewModel.swift */,
D0BEC93724C9632800E864C4 /* RootViewModel.swift */,
D04FD73524D49506007D572D /* SettingsViewModel.swift */,
D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */,
@ -457,6 +480,7 @@
D0ED1BCA24CF744200B4899C /* MastodonClient.swift */,
D0ED1BCD24CF768200B4899C /* MastodonEndpoint.swift */,
D0ED1BD024CF779B00B4899C /* MastodonTarget.swift */,
D0CD847E24DBF1BB00CF380C /* PreferencesEndpoint.swift */,
);
path = "Mastodon API";
sourceTree = "<group>";
@ -652,6 +676,7 @@
files = (
D04FD73924D4A7B4007D572D /* AccountEndpoint+Stubbing.swift in Sources */,
D0DB6F0924C65AC000D965FE /* AddIdentityViewModel.swift in Sources */,
D0CD847324DBDEC700CF380C /* MastodonPreferences.swift in Sources */,
D0ED1BD724CF94B200B4899C /* Application.swift in Sources */,
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */,
@ -672,6 +697,7 @@
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */,
D0666A4524C6BC0A00F3F04B /* DatabaseError.swift in Sources */,
D0ED1BDD24CF982600B4899C /* AccessTokenEndpoint.swift in Sources */,
D0CD847F24DBF1BB00CF380C /* PreferencesEndpoint.swift in Sources */,
D0666A4B24C6C37700F3F04B /* Identity.swift in Sources */,
D0666A5424C6C3E500F3F04B /* Emoji.swift in Sources */,
D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
@ -689,6 +715,7 @@
D0C963FB24CC359D003BD330 /* AlertItem.swift in Sources */,
D0DC174624CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */,
D0DC174D24CFF1F100A75C65 /* Stubbing.swift in Sources */,
D0091B6B24DC10CE0040E8D2 /* PreferencesViewModel.swift in Sources */,
D0666A5724C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */,
D0DC177424D0B58800A75C65 /* Keychain.swift in Sources */,
@ -706,8 +733,11 @@
D0DC175B24D0154F00A75C65 /* MastodonAPI.swift in Sources */,
D0ED1BD124CF779B00B4899C /* MastodonTarget.swift in Sources */,
D065F53E24D3D20300741304 /* InstanceEndpoint.swift in Sources */,
D0CD847C24DBEA9F00CF380C /* Unknowable.swift in Sources */,
D0666A6F24C6DFB300F3F04B /* AccessToken.swift in Sources */,
D0ED1BCB24CF744200B4899C /* MastodonClient.swift in Sources */,
D0091B6824DC10B30040E8D2 /* PreferencesView.swift in Sources */,
D0CD847624DBDF3C00CF380C /* Status.swift in Sources */,
D052BBE024D805E300A80A7A /* MainNavigationViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -718,6 +748,7 @@
files = (
D04FD73A24D4A7B4007D572D /* AccountEndpoint+Stubbing.swift in Sources */,
D0DB6F0A24C65AC000D965FE /* AddIdentityViewModel.swift in Sources */,
D0CD847424DBDEC700CF380C /* MastodonPreferences.swift in Sources */,
D0ED1BD824CF94B200B4899C /* Application.swift in Sources */,
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */,
@ -738,6 +769,7 @@
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
D0666A4624C6BC0A00F3F04B /* DatabaseError.swift in Sources */,
D0ED1BDE24CF982600B4899C /* AccessTokenEndpoint.swift in Sources */,
D0CD848024DBF1BB00CF380C /* PreferencesEndpoint.swift in Sources */,
D0666A4C24C6C37700F3F04B /* Identity.swift in Sources */,
D0666A5524C6C3E500F3F04B /* Emoji.swift in Sources */,
D0DC175624D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
@ -755,6 +787,7 @@
D0C963FC24CC359D003BD330 /* AlertItem.swift in Sources */,
D0DC174724CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */,
D0DC174E24CFF1F100A75C65 /* Stubbing.swift in Sources */,
D0091B6C24DC10CE0040E8D2 /* PreferencesViewModel.swift in Sources */,
D0666A5824C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
D0DC177524D0B58800A75C65 /* Keychain.swift in Sources */,
@ -772,8 +805,11 @@
D0DC175C24D0154F00A75C65 /* MastodonAPI.swift in Sources */,
D0ED1BD224CF779B00B4899C /* MastodonTarget.swift in Sources */,
D065F53F24D3D20300741304 /* InstanceEndpoint.swift in Sources */,
D0CD847D24DBEA9F00CF380C /* Unknowable.swift in Sources */,
D0666A7024C6DFB300F3F04B /* AccessToken.swift in Sources */,
D0ED1BCC24CF744200B4899C /* MastodonClient.swift in Sources */,
D0091B6924DC10B30040E8D2 /* PreferencesView.swift in Sources */,
D0CD847724DBDF3C00CF380C /* Status.swift in Sources */,
D052BBE124D805E300A80A7A /* MainNavigationViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View file

@ -7,12 +7,12 @@ extension Publisher {
func assignErrorsToAlertItem<Root: AnyObject>(
to keyPath: ReferenceWritableKeyPath<Root, AlertItem?>,
on object: Root) -> AnyPublisher<Output, Never> {
self.catch { [weak object] error -> AnyPublisher<Output, Never> in
self.catch { [weak object] error -> Empty<Output, Never> in
DispatchQueue.main.async {
object?[keyPath: keyPath] = AlertItem(error: error)
}
return Empty().eraseToAnyPublisher()
return Empty()
}
.eraseToAnyPublisher()
}

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import Combine
struct AppEnvironment {
let URLSessionConfiguration: URLSessionConfiguration
@ -9,3 +10,41 @@ struct AppEnvironment {
let secrets: Secrets
let webAuthSessionType: WebAuthSession.Type
}
class IdentifiedEnvironment {
@Published var identity: Identity
let observationErrors: AnyPublisher<Error, Never>
let networkClient: MastodonClient
let appEnvironment: AppEnvironment
private var cancellables = Set<AnyCancellable>()
private let observationErrorsInput = PassthroughSubject<Error, Never>()
init(identityID: String, appEnvironment: AppEnvironment) throws {
self.appEnvironment = appEnvironment
observationErrors = observationErrorsInput.eraseToAnyPublisher()
networkClient = MastodonClient(configuration: appEnvironment.URLSessionConfiguration)
networkClient.accessToken = try appEnvironment.secrets.item(.accessToken, forIdentityID: identityID)
let observation = appEnvironment.identityDatabase.identityObservation(id: identityID).share()
var initialIdentity: Identity?
observation.first().sink(
receiveCompletion: { _ in },
receiveValue: { initialIdentity = $0 })
.store(in: &cancellables)
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
self.identity = identity
networkClient.instanceURL = identity.url
observation.catch { [weak self] error -> Empty<Identity, Never> in
self?.observationErrorsInput.send(error)
return Empty()
}
.assign(to: &$identity)
}
}

View file

@ -6,6 +6,7 @@ struct Identity: Codable, Hashable, Identifiable {
let id: String
let url: URL
let lastUsedAt: Date
let preferences: Identity.Preferences
let instance: Identity.Instance?
let account: Identity.Account?
}
@ -28,6 +29,16 @@ extension Identity {
let header: URL
let headerStatic: URL
}
struct Preferences: Codable, Hashable {
var useServerPostingPreferences = true
var postingDefaultVisibility = Status.Visibility.public
var postingDefaultSensitive = false
var postingDefaultLanguage: String?
var useServerReadingPreferences = true
var readingExpandMedia = MastodonPreferences.ExpandMedia.default
var readingExpandSpoilers = false
}
}
extension Identity {

View file

@ -36,6 +36,7 @@ extension IdentityDatabase {
id: id,
url: url,
lastUsedAt: Date(),
preferences: Identity.Preferences(),
instanceURI: nil).save)
.eraseToAnyPublisher()
}
@ -150,6 +151,7 @@ private extension IdentityDatabase {
t.column("instanceURI", .text)
.indexed()
.references("instance", column: "uri")
t.column("preferences", .blob).notNull()
}
try db.create(table: "account", ifNotExists: true) { t in
@ -175,6 +177,7 @@ private struct StoredIdentity: Codable, Hashable, TableRecord, FetchableRecord,
let id: String
let url: URL
let lastUsedAt: Date
let preferences: Identity.Preferences
let instanceURI: String?
}
@ -203,6 +206,7 @@ private extension Identity {
id: result.identity.id,
url: result.identity.url,
lastUsedAt: result.identity.lastUsedAt,
preferences: result.identity.preferences,
instance: result.instance,
account: result.account)
}

View file

@ -0,0 +1,28 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
struct MastodonPreferences: Codable {
enum CodingKeys: String, CodingKey {
case postingDefaultVisibility = "posting:default:visibility"
case postingDefaultSensitive = "posting:default:sensitive"
case postingDefaultLanguage = "posting:default:language"
case readingExpandMedia = "reading:expand:media"
case readingExpandSpoilers = "reading:expand:spoilers"
}
let postingDefaultVisibility: Status.Visibility
let postingDefaultSensitive: Bool
let postingDefaultLanguage: String?
let readingExpandMedia: ExpandMedia
let readingExpandSpoilers: Bool
}
extension MastodonPreferences {
enum ExpandMedia: String, Codable, Unknowable {
case `default`
case showAll
case hideAll
case unknown
}
}

13
Shared/Model/Status.swift Normal file
View file

@ -0,0 +1,13 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
struct Status {
enum Visibility: String, Codable, Unknowable {
case `public`
case unlisted
case `private`
case direct
case unknown
}
}

View file

@ -0,0 +1,17 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
protocol Unknowable: RawRepresentable, CaseIterable where RawValue: Equatable {
static var unknown: RawValue { get }
}
extension Unknowable {
init(rawValue: RawValue) {
self = Self.allCases.first { $0.rawValue == rawValue } ?? Self(rawValue: Self.unknown)
}
}
extension Unknowable where RawValue == String {
static var unknown: String { "unknown" }
}

View file

@ -0,0 +1,23 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
enum PreferencesEndpoint {
case preferences
}
extension PreferencesEndpoint: MastodonEndpoint {
typealias ResultType = MastodonPreferences
var pathComponentsInContext: [String] {
switch self {
case .preferences: return ["instance"]
}
}
var method: HTTPMethod {
switch self {
case .preferences: return .get
}
}
}

View file

@ -8,24 +8,15 @@ class IdentitiesViewModel: ObservableObject {
@Published var identities = [Identity]()
@Published var alertItem: AlertItem?
private let environment: AppEnvironment
private let environment: IdentifiedEnvironment
private var cancellables = Set<AnyCancellable>()
init(identity: Published<Identity>, environment: AppEnvironment) {
_identity = identity
init(environment: IdentifiedEnvironment) {
self.environment = environment
identity = environment.identity
environment.identityDatabase.identitiesObservation()
environment.appEnvironment.identityDatabase.identitiesObservation()
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$identities)
}
}
extension IdentitiesViewModel {
func identitySelected(id: String) {
environment.identityDatabase.updateLastUsedAt(identityID: id)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink(receiveValue: {})
.store(in: &cancellables)
}
}

View file

@ -10,39 +10,21 @@ class MainNavigationViewModel: ObservableObject {
@Published var alertItem: AlertItem?
var selectedTab: Tab? = .timelines
private let environment: AppEnvironment
private let networkClient: MastodonClient
private let environment: IdentifiedEnvironment
private var cancellables = Set<AnyCancellable>()
init(identityID: String, environment: AppEnvironment) throws {
init(environment: IdentifiedEnvironment) {
self.environment = environment
networkClient = MastodonClient(configuration: environment.URLSessionConfiguration)
identity = environment.identity
environment.$identity.dropFirst().assign(to: &$identity)
let observation = environment.identityDatabase.identityObservation(id: identityID).share()
var initialIdentity: Identity?
observation.first().sink(
receiveCompletion: { _ in },
receiveValue: { initialIdentity = $0 })
.store(in: &cancellables)
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
self.identity = identity
networkClient.instanceURL = identity.url
do {
networkClient.accessToken = try environment.secrets.item(.accessToken, forIdentityID: identity.id)
} catch {
alertItem = AlertItem(error: error)
}
observation.assignErrorsToAlertItem(to: \.alertItem, on: self).assign(to: &$identity)
environment.identityDatabase.recentIdentitiesObservation(excluding: identityID)
environment.appEnvironment.identityDatabase
.recentIdentitiesObservation(excluding: environment.identity.id)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$recentIdentities)
environment.identityDatabase.updateLastUsedAt(identityID: identityID)
environment.appEnvironment.identityDatabase
.updateLastUsedAt(identityID: environment.identity.id)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink(receiveValue: {})
.store(in: &cancellables)
@ -53,25 +35,25 @@ extension MainNavigationViewModel {
func refreshIdentity() {
let id = identity.id
if networkClient.accessToken != nil {
networkClient.request(AccountEndpoint.verifyCredentials)
if environment.networkClient.accessToken != nil {
environment.networkClient.request(AccountEndpoint.verifyCredentials)
.map { ($0, id) }
.flatMap(environment.identityDatabase.updateAccount)
.flatMap(environment.appEnvironment.identityDatabase.updateAccount)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink(receiveValue: {})
.store(in: &cancellables)
}
networkClient.request(InstanceEndpoint.instance)
environment.networkClient.request(InstanceEndpoint.instance)
.map { ($0, id) }
.flatMap(environment.identityDatabase.updateInstance)
.flatMap(environment.appEnvironment.identityDatabase.updateInstance)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink(receiveValue: {})
.store(in: &cancellables)
}
func settingsViewModel() -> SettingsViewModel {
SettingsViewModel(identity: _identity, environment: environment)
SettingsViewModel(environment: environment)
}
}

View file

@ -0,0 +1,15 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
class PreferencesViewModel: ObservableObject {
@Published var preferences: Identity.Preferences
private let environment: IdentifiedEnvironment
init(environment: IdentifiedEnvironment) {
self.environment = environment
preferences = environment.identity.preferences
environment.$identity.map(\.preferences).assign(to: &$preferences)
}
}

View file

@ -4,24 +4,13 @@ import Foundation
import Combine
class RootViewModel: ObservableObject {
@Published private(set) var mainNavigationViewModel: MainNavigationViewModel?
@Published private var identityID: String?
@Published private(set) var identityID: String?
private let environment: AppEnvironment
private var cancellables = Set<AnyCancellable>()
init(environment: AppEnvironment) {
self.environment = environment
identityID = environment.identityDatabase.mostRecentlyUsedIdentityID
$identityID
.tryMap {
guard let id = $0 else { return nil }
return try MainNavigationViewModel(identityID: id, environment: environment)
}
.replaceError(with: nil)
.assign(to: &$mainNavigationViewModel)
}
}
@ -31,12 +20,23 @@ extension RootViewModel {
}
func addIdentityViewModel() -> AddIdentityViewModel {
let addAccountViewModel = AddIdentityViewModel(environment: environment)
AddIdentityViewModel(environment: environment)
}
addAccountViewModel.addedIdentityID
.sink(receiveValue: newIdentitySelected(id:))
.store(in: &cancellables)
func mainNavigationViewModel(identityID: String) -> MainNavigationViewModel? {
let identifiedEnvironment: IdentifiedEnvironment
return addAccountViewModel
do {
identifiedEnvironment = try IdentifiedEnvironment(identityID: identityID, appEnvironment: environment)
} catch {
return nil
}
identifiedEnvironment.observationErrors
.receive(on: RunLoop.main)
.map { [weak self] _ in self?.environment.identityDatabase.mostRecentlyUsedIdentityID }
.assign(to: &$identityID)
return MainNavigationViewModel(environment: identifiedEnvironment)
}
}

View file

@ -4,16 +4,17 @@ import Foundation
class SettingsViewModel: ObservableObject {
@Published private(set) var identity: Identity
private let environment: AppEnvironment
private let environment: IdentifiedEnvironment
init(identity: Published<Identity>, environment: AppEnvironment) {
_identity = identity
init(environment: IdentifiedEnvironment) {
self.environment = environment
identity = environment.identity
environment.$identity.dropFirst().assign(to: &$identity)
}
}
extension SettingsViewModel {
func identitiesViewModel() -> IdentitiesViewModel {
IdentitiesViewModel(identity: _identity, environment: environment)
IdentitiesViewModel(environment: environment)
}
}

View file

@ -4,6 +4,7 @@ import SwiftUI
struct AddIdentityView: View {
@StateObject var viewModel: AddIdentityViewModel
@EnvironmentObject var rootViewModel: RootViewModel
var body: some View {
Form {
@ -32,6 +33,11 @@ struct AddIdentityView: View {
}
.paddingIfMac()
.alertItem($viewModel.alertItem)
.onReceive(viewModel.addedIdentityID) { id in
withAnimation {
rootViewModel.newIdentitySelected(id: id)
}
}
}
}

View file

@ -18,7 +18,9 @@ struct IdentitiesView: View {
Section {
List(viewModel.identities) { identity in
Button(identity.handle) {
rootViewModel.newIdentitySelected(id: identity.id)
withAnimation {
rootViewModel.newIdentitySelected(id: identity.id)
}
}
}
}

View file

@ -0,0 +1,15 @@
// Copyright © 2020 Metabolist. All rights reserved.
import SwiftUI
struct PreferencesView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct PreferencesView_Previews: PreviewProvider {
static var previews: some View {
PreferencesView()
}
}

View file

@ -6,13 +6,16 @@ struct RootView: View {
@StateObject var viewModel: RootViewModel
var body: some View {
ZStack {
if let mainNavigationViewModel = viewModel.mainNavigationViewModel {
Self.mainNavigation(mainNavigationViewModel: mainNavigationViewModel)
.environmentObject(viewModel)
} else {
AddIdentityView(viewModel: viewModel.addIdentityViewModel())
}
if let id = viewModel.identityID,
let mainNavigationViewModel = viewModel.mainNavigationViewModel(identityID: id) {
Self.mainNavigation(mainNavigationViewModel: mainNavigationViewModel)
.id(id)
.environmentObject(viewModel)
.transition(.opacity)
} else {
AddIdentityView(viewModel: viewModel.addIdentityViewModel())
.environmentObject(viewModel)
.transition(.opacity)
}
}
}

View file

@ -6,19 +6,26 @@ import CombineExpectations
@testable import Metatext
class RootViewModelTests: XCTestCase {
var cancellables = Set<AnyCancellable>()
func testAddIdentity() throws {
let sut = RootViewModel(environment: .fresh())
let recorder = sut.$mainNavigationViewModel.record()
let recorder = sut.$identityID.record()
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
let addIdentityViewModel = sut.addIdentityViewModel()
addIdentityViewModel.addedIdentityID
.sink(receiveValue: sut.newIdentitySelected(id:))
.store(in: &cancellables)
addIdentityViewModel.urlFieldText = "https://mastodon.social"
addIdentityViewModel.goTapped()
let mainNavigationViewModel = try wait(for: recorder.next(), timeout: 1)!
let identityID = try wait(for: recorder.next(), timeout: 1)!
XCTAssertNotNil(mainNavigationViewModel)
XCTAssertNotNil(identityID)
XCTAssertNotNil(sut.mainNavigationViewModel(identityID: identityID))
}
}

View file

@ -26,8 +26,8 @@ struct TabNavigation: View {
SettingsView(viewModel: viewModel.settingsViewModel())
.environmentObject(rootViewModel)
}
.onReceive(rootViewModel.$mainNavigationViewModel.map { _ in ()},
perform: viewModel.refreshIdentity)
.alertItem($viewModel.alertItem)
.onAppear(perform: viewModel.refreshIdentity)
.onReceive(NotificationCenter.default
.publisher(for: UIScene.willEnterForegroundNotification)
.map { _ in () },