mirror of
https://github.com/metabolist/metatext.git
synced 2024-12-22 13:37:01 +00:00
Refactoring/preferences
This commit is contained in:
parent
c39f2d94d3
commit
2df47efdc9
21 changed files with 280 additions and 83 deletions
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
28
Shared/Model/MastodonPreferences.swift
Normal file
28
Shared/Model/MastodonPreferences.swift
Normal 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
13
Shared/Model/Status.swift
Normal 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
|
||||
}
|
||||
}
|
17
Shared/Model/Unknowable.swift
Normal file
17
Shared/Model/Unknowable.swift
Normal 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" }
|
||||
}
|
23
Shared/Networking/Mastodon API/PreferencesEndpoint.swift
Normal file
23
Shared/Networking/Mastodon API/PreferencesEndpoint.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
15
Shared/View Models/PreferencesViewModel.swift
Normal file
15
Shared/View Models/PreferencesViewModel.swift
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
15
Shared/Views/PreferencesView.swift
Normal file
15
Shared/Views/PreferencesView.swift
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 () },
|
||||
|
|
Loading…
Reference in a new issue