mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-29 03:21:02 +00:00
Refactoring
This commit is contained in:
parent
350bf29b49
commit
09c518d712
22 changed files with 388 additions and 250 deletions
|
@ -97,11 +97,13 @@ extension Identity {
|
||||||
|
|
||||||
extension AppEnvironment {
|
extension AppEnvironment {
|
||||||
static func fresh(
|
static func fresh(
|
||||||
|
URLSessionConfiguration: URLSessionConfiguration = .stubbing,
|
||||||
identityDatabase: IdentityDatabase = .fresh(),
|
identityDatabase: IdentityDatabase = .fresh(),
|
||||||
preferences: Preferences = .fresh(),
|
preferences: Preferences = .fresh(),
|
||||||
secrets: Secrets = .fresh(),
|
secrets: Secrets = .fresh(),
|
||||||
webAuthSessionType: WebAuthSession.Type = SuccessfulStubbingWebAuthSession.self) -> AppEnvironment {
|
webAuthSessionType: WebAuthSession.Type = SuccessfulStubbingWebAuthSession.self) -> AppEnvironment {
|
||||||
AppEnvironment(
|
AppEnvironment(
|
||||||
|
URLSessionConfiguration: URLSessionConfiguration,
|
||||||
identityDatabase: identityDatabase,
|
identityDatabase: identityDatabase,
|
||||||
preferences: preferences,
|
preferences: preferences,
|
||||||
secrets: secrets,
|
secrets: secrets,
|
||||||
|
@ -109,14 +111,23 @@ extension AppEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
static let development = AppEnvironment(
|
static let development = AppEnvironment(
|
||||||
|
URLSessionConfiguration: .stubbing,
|
||||||
identityDatabase: .development,
|
identityDatabase: .development,
|
||||||
preferences: .development,
|
preferences: .development,
|
||||||
secrets: .development,
|
secrets: .development,
|
||||||
webAuthSessionType: SuccessfulStubbingWebAuthSession.self)
|
webAuthSessionType: SuccessfulStubbingWebAuthSession.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SceneViewModel {
|
extension RootViewModel {
|
||||||
static let development = SceneViewModel(networkClient: .development, environment: .development)
|
static let development = RootViewModel(environment: .development)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MainNavigationViewModel {
|
||||||
|
static let development = RootViewModel.development.mainNavigationViewModel(identityID: devIdentityID)!
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SettingsViewModel {
|
||||||
|
static let development = MainNavigationViewModel.development.settingsViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:enable force_try
|
// swiftlint:enable force_try
|
||||||
|
|
|
@ -21,13 +21,17 @@
|
||||||
D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */; };
|
D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */; };
|
||||||
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* DevelopmentModels.swift */; };
|
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* DevelopmentModels.swift */; };
|
||||||
D04FD74324D4AA34007D572D /* DevelopmentModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* DevelopmentModels.swift */; };
|
D04FD74324D4AA34007D572D /* DevelopmentModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* DevelopmentModels.swift */; };
|
||||||
D052BBC724D749C800A80A7A /* SceneViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */; };
|
D052BBC724D749C800A80A7A /* RootViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC624D749C800A80A7A /* RootViewModelTests.swift */; };
|
||||||
D052BBCA24D74C9200A80A7A /* FakeUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */; };
|
D052BBCA24D74C9200A80A7A /* FakeUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */; };
|
||||||
D052BBCB24D74C9300A80A7A /* FakeUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */; };
|
D052BBCB24D74C9300A80A7A /* FakeUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */; };
|
||||||
D052BBCF24D750C000A80A7A /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCE24D750C000A80A7A /* Preferences.swift */; };
|
D052BBCF24D750C000A80A7A /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCE24D750C000A80A7A /* Preferences.swift */; };
|
||||||
D052BBD024D750C000A80A7A /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCE24D750C000A80A7A /* Preferences.swift */; };
|
D052BBD024D750C000A80A7A /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCE24D750C000A80A7A /* Preferences.swift */; };
|
||||||
D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; };
|
D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; };
|
||||||
D052BBD224D750CB00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; };
|
D052BBD224D750CB00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; };
|
||||||
|
D052BBE024D805E300A80A7A /* MainNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBDF24D805E300A80A7A /* MainNavigationViewModel.swift */; };
|
||||||
|
D052BBE124D805E300A80A7A /* MainNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBDF24D805E300A80A7A /* MainNavigationViewModel.swift */; };
|
||||||
|
D052BBE424D81C4700A80A7A /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBE324D81C4700A80A7A /* CurrentValuePublisher.swift */; };
|
||||||
|
D052BBE524D81C4700A80A7A /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBE324D81C4700A80A7A /* CurrentValuePublisher.swift */; };
|
||||||
D065F53924D37E5100741304 /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = D065F53824D37E5100741304 /* CombineExpectations */; };
|
D065F53924D37E5100741304 /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = D065F53824D37E5100741304 /* CombineExpectations */; };
|
||||||
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
||||||
D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
||||||
|
@ -71,10 +75,10 @@
|
||||||
D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */; };
|
D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */; };
|
||||||
D0B93B3024D55098007AF646 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B93B2F24D55098007AF646 /* Screen.swift */; };
|
D0B93B3024D55098007AF646 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B93B2F24D55098007AF646 /* Screen.swift */; };
|
||||||
D0B93B3124D55098007AF646 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B93B2F24D55098007AF646 /* Screen.swift */; };
|
D0B93B3124D55098007AF646 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B93B2F24D55098007AF646 /* Screen.swift */; };
|
||||||
D0BEC93824C9632800E864C4 /* SceneViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* SceneViewModel.swift */; };
|
D0BEC93824C9632800E864C4 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* RootViewModel.swift */; };
|
||||||
D0BEC93924C9632800E864C4 /* SceneViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* SceneViewModel.swift */; };
|
D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* RootViewModel.swift */; };
|
||||||
D0BEC93B24C96FD500E864C4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* ContentView.swift */; };
|
D0BEC93B24C96FD500E864C4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* RootView.swift */; };
|
||||||
D0BEC93C24C96FD500E864C4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* ContentView.swift */; };
|
D0BEC93C24C96FD500E864C4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* RootView.swift */; };
|
||||||
D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */; };
|
D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */; };
|
||||||
D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */; };
|
D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */; };
|
||||||
D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* TimelineView.swift */; };
|
D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* TimelineView.swift */; };
|
||||||
|
@ -157,10 +161,12 @@
|
||||||
D04FD73824D4A7B4007D572D /* AccountEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
D04FD73824D4A7B4007D572D /* AccountEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||||
D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InstanceEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InstanceEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||||
D04FD74124D4AA34007D572D /* DevelopmentModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevelopmentModels.swift; sourceTree = "<group>"; };
|
D04FD74124D4AA34007D572D /* DevelopmentModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevelopmentModels.swift; sourceTree = "<group>"; };
|
||||||
D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneViewModelTests.swift; sourceTree = "<group>"; };
|
D052BBC624D749C800A80A7A /* RootViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeUserDefaults.swift; sourceTree = "<group>"; };
|
D052BBC824D74B6400A80A7A /* FakeUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeUserDefaults.swift; sourceTree = "<group>"; };
|
||||||
D052BBCC24D750A100A80A7A /* AppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = "<group>"; };
|
D052BBCC24D750A100A80A7A /* AppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = "<group>"; };
|
||||||
D052BBCE24D750C000A80A7A /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
D052BBCE24D750C000A80A7A /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||||
|
D052BBDF24D805E300A80A7A /* MainNavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
D052BBE324D81C4700A80A7A /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = "<group>"; };
|
||||||
D065F53A24D3B33A00741304 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
D065F53A24D3B33A00741304 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D065F53D24D3D20300741304 /* InstanceEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceEndpoint.swift; sourceTree = "<group>"; };
|
D065F53D24D3D20300741304 /* InstanceEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceEndpoint.swift; sourceTree = "<group>"; };
|
||||||
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -182,8 +188,8 @@
|
||||||
D081A40424D0F1A8001B016E /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
D081A40424D0F1A8001B016E /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = "<group>"; };
|
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D0B93B2F24D55098007AF646 /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
|
D0B93B2F24D55098007AF646 /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
|
||||||
D0BEC93724C9632800E864C4 /* SceneViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneViewModel.swift; sourceTree = "<group>"; };
|
D0BEC93724C9632800E864C4 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = "<group>"; };
|
||||||
D0BEC93A24C96FD500E864C4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
D0BEC93A24C96FD500E864C4 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
||||||
D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModel.swift; sourceTree = "<group>"; };
|
D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModel.swift; sourceTree = "<group>"; };
|
||||||
D0BEC94924CA231200E864C4 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
D0BEC94924CA231200E864C4 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
||||||
D0BEC94E24CA2B5300E864C4 /* SidebarNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarNavigation.swift; sourceTree = "<group>"; };
|
D0BEC94E24CA2B5300E864C4 /* SidebarNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarNavigation.swift; sourceTree = "<group>"; };
|
||||||
|
@ -269,6 +275,7 @@
|
||||||
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */,
|
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */,
|
||||||
D0666A3A24C6B56200F3F04B /* Model */,
|
D0666A3A24C6B56200F3F04B /* Model */,
|
||||||
D0DB6EFA24C5730600D965FE /* Networking */,
|
D0DB6EFA24C5730600D965FE /* Networking */,
|
||||||
|
D052BBE224D81C2300A80A7A /* Publishers */,
|
||||||
D0DB6EFB24C658E400D965FE /* View Models */,
|
D0DB6EFB24C658E400D965FE /* View Models */,
|
||||||
D0DB6EF024C5224F00D965FE /* Views */,
|
D0DB6EF024C5224F00D965FE /* Views */,
|
||||||
);
|
);
|
||||||
|
@ -304,6 +311,14 @@
|
||||||
path = macOS;
|
path = macOS;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D052BBE224D81C2300A80A7A /* Publishers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D052BBE324D81C4700A80A7A /* CurrentValuePublisher.swift */,
|
||||||
|
);
|
||||||
|
path = Publishers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D0666A2224C677B400F3F04B /* Tests */ = {
|
D0666A2224C677B400F3F04B /* Tests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -354,7 +369,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */,
|
D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */,
|
||||||
D0BEC93A24C96FD500E864C4 /* ContentView.swift */,
|
D0BEC93A24C96FD500E864C4 /* RootView.swift */,
|
||||||
D0B93B2F24D55098007AF646 /* Screen.swift */,
|
D0B93B2F24D55098007AF646 /* Screen.swift */,
|
||||||
D04FD73224D48F37007D572D /* SettingsView.swift */,
|
D04FD73224D48F37007D572D /* SettingsView.swift */,
|
||||||
D0BEC94924CA231200E864C4 /* TimelineView.swift */,
|
D0BEC94924CA231200E864C4 /* TimelineView.swift */,
|
||||||
|
@ -379,7 +394,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */,
|
D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */,
|
||||||
D0BEC93724C9632800E864C4 /* SceneViewModel.swift */,
|
D052BBDF24D805E300A80A7A /* MainNavigationViewModel.swift */,
|
||||||
|
D0BEC93724C9632800E864C4 /* RootViewModel.swift */,
|
||||||
D04FD73524D49506007D572D /* SettingsViewModel.swift */,
|
D04FD73524D49506007D572D /* SettingsViewModel.swift */,
|
||||||
D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */,
|
D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */,
|
||||||
);
|
);
|
||||||
|
@ -413,7 +429,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */,
|
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */,
|
||||||
D052BBC624D749C800A80A7A /* SceneViewModelTests.swift */,
|
D052BBC624D749C800A80A7A /* RootViewModelTests.swift */,
|
||||||
);
|
);
|
||||||
path = "View Models";
|
path = "View Models";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -656,8 +672,9 @@
|
||||||
D0ED1BE024CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
D0ED1BE024CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
||||||
D0B93B3024D55098007AF646 /* Screen.swift in Sources */,
|
D0B93B3024D55098007AF646 /* Screen.swift in Sources */,
|
||||||
D052BBD224D750CB00A80A7A /* AppEnvironment.swift in Sources */,
|
D052BBD224D750CB00A80A7A /* AppEnvironment.swift in Sources */,
|
||||||
|
D052BBE424D81C4700A80A7A /* CurrentValuePublisher.swift in Sources */,
|
||||||
D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
||||||
D0BEC93824C9632800E864C4 /* SceneViewModel.swift in Sources */,
|
D0BEC93824C9632800E864C4 /* RootViewModel.swift in Sources */,
|
||||||
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */,
|
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */,
|
||||||
D0666A4524C6BC0A00F3F04B /* DatabaseError.swift in Sources */,
|
D0666A4524C6BC0A00F3F04B /* DatabaseError.swift in Sources */,
|
||||||
D0ED1BDD24CF982600B4899C /* AccessTokenEndpoint.swift in Sources */,
|
D0ED1BDD24CF982600B4899C /* AccessTokenEndpoint.swift in Sources */,
|
||||||
|
@ -669,7 +686,7 @@
|
||||||
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||||
D0666A4224C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */,
|
D0666A4224C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */,
|
||||||
D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */,
|
D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */,
|
||||||
D0BEC93B24C96FD500E864C4 /* ContentView.swift in Sources */,
|
D0BEC93B24C96FD500E864C4 /* RootView.swift in Sources */,
|
||||||
D04FD73624D49506007D572D /* SettingsViewModel.swift in Sources */,
|
D04FD73624D49506007D572D /* SettingsViewModel.swift in Sources */,
|
||||||
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
||||||
D0DC175824D0130800A75C65 /* HTTPStubs.swift in Sources */,
|
D0DC175824D0130800A75C65 /* HTTPStubs.swift in Sources */,
|
||||||
|
@ -694,6 +711,7 @@
|
||||||
D065F53E24D3D20300741304 /* InstanceEndpoint.swift in Sources */,
|
D065F53E24D3D20300741304 /* InstanceEndpoint.swift in Sources */,
|
||||||
D0666A6F24C6DFB300F3F04B /* AccessToken.swift in Sources */,
|
D0666A6F24C6DFB300F3F04B /* AccessToken.swift in Sources */,
|
||||||
D0ED1BCB24CF744200B4899C /* MastodonClient.swift in Sources */,
|
D0ED1BCB24CF744200B4899C /* MastodonClient.swift in Sources */,
|
||||||
|
D052BBE024D805E300A80A7A /* MainNavigationViewModel.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -719,8 +737,9 @@
|
||||||
D0ED1BE124CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
D0ED1BE124CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
||||||
D0B93B3124D55098007AF646 /* Screen.swift in Sources */,
|
D0B93B3124D55098007AF646 /* Screen.swift in Sources */,
|
||||||
D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */,
|
D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */,
|
||||||
|
D052BBE524D81C4700A80A7A /* CurrentValuePublisher.swift in Sources */,
|
||||||
D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
||||||
D0BEC93924C9632800E864C4 /* SceneViewModel.swift in Sources */,
|
D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */,
|
||||||
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
|
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
|
||||||
D0666A4624C6BC0A00F3F04B /* DatabaseError.swift in Sources */,
|
D0666A4624C6BC0A00F3F04B /* DatabaseError.swift in Sources */,
|
||||||
D0ED1BDE24CF982600B4899C /* AccessTokenEndpoint.swift in Sources */,
|
D0ED1BDE24CF982600B4899C /* AccessTokenEndpoint.swift in Sources */,
|
||||||
|
@ -732,7 +751,7 @@
|
||||||
D0DC175324D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
D0DC175324D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||||
D0666A4324C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */,
|
D0666A4324C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */,
|
||||||
D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */,
|
D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */,
|
||||||
D0BEC93C24C96FD500E864C4 /* ContentView.swift in Sources */,
|
D0BEC93C24C96FD500E864C4 /* RootView.swift in Sources */,
|
||||||
D04FD73724D49506007D572D /* SettingsViewModel.swift in Sources */,
|
D04FD73724D49506007D572D /* SettingsViewModel.swift in Sources */,
|
||||||
D04FD74324D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
D04FD74324D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
||||||
D0DC175924D0130800A75C65 /* HTTPStubs.swift in Sources */,
|
D0DC175924D0130800A75C65 /* HTTPStubs.swift in Sources */,
|
||||||
|
@ -757,6 +776,7 @@
|
||||||
D065F53F24D3D20300741304 /* InstanceEndpoint.swift in Sources */,
|
D065F53F24D3D20300741304 /* InstanceEndpoint.swift in Sources */,
|
||||||
D0666A7024C6DFB300F3F04B /* AccessToken.swift in Sources */,
|
D0666A7024C6DFB300F3F04B /* AccessToken.swift in Sources */,
|
||||||
D0ED1BCC24CF744200B4899C /* MastodonClient.swift in Sources */,
|
D0ED1BCC24CF744200B4899C /* MastodonClient.swift in Sources */,
|
||||||
|
D052BBE124D805E300A80A7A /* MainNavigationViewModel.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -765,7 +785,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */,
|
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */,
|
||||||
D052BBC724D749C800A80A7A /* SceneViewModelTests.swift in Sources */,
|
D052BBC724D749C800A80A7A /* RootViewModelTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ struct MetatextApp: App {
|
||||||
}
|
}
|
||||||
|
|
||||||
environment = AppEnvironment(
|
environment = AppEnvironment(
|
||||||
|
URLSessionConfiguration: .default,
|
||||||
identityDatabase: identityDatabase,
|
identityDatabase: identityDatabase,
|
||||||
preferences: Preferences(userDefaults: .standard),
|
preferences: Preferences(userDefaults: .standard),
|
||||||
secrets: Secrets(keychain: Keychain(service: "com.metabolist.metatext")),
|
secrets: Secrets(keychain: Keychain(service: "com.metabolist.metatext")),
|
||||||
|
@ -24,11 +25,7 @@ struct MetatextApp: App {
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
RootView(viewModel: RootViewModel(environment: environment))
|
||||||
.environmentObject(
|
|
||||||
SceneViewModel(
|
|
||||||
networkClient: MastodonClient(),
|
|
||||||
environment: environment))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct AppEnvironment {
|
struct AppEnvironment {
|
||||||
|
let URLSessionConfiguration: URLSessionConfiguration
|
||||||
let identityDatabase: IdentityDatabase
|
let identityDatabase: IdentityDatabase
|
||||||
let preferences: Preferences
|
let preferences: Preferences
|
||||||
let secrets: Secrets
|
let secrets: Secrets
|
||||||
|
|
|
@ -37,4 +37,6 @@ extension Identity {
|
||||||
|
|
||||||
return instance?.title ?? url.host ?? url.absoluteString
|
return instance?.title ?? url.host ?? url.absoluteString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var image: URL? { account?.avatar ?? instance?.thumbnail }
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,10 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import GRDB
|
import GRDB
|
||||||
|
|
||||||
|
enum IdentityDatabaseError: Error {
|
||||||
|
case identityNotFound
|
||||||
|
}
|
||||||
|
|
||||||
struct IdentityDatabase {
|
struct IdentityDatabase {
|
||||||
private let databaseQueue: DatabaseQueue
|
private let databaseQueue: DatabaseQueue
|
||||||
|
|
||||||
|
@ -61,7 +65,7 @@ extension IdentityDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func identityObservation(id: String) -> AnyPublisher<Identity?, Error> {
|
func identityObservation(id: String) -> AnyPublisher<Identity, Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
StoredIdentity
|
StoredIdentity
|
||||||
.filter(Column("id") == id)
|
.filter(Column("id") == id)
|
||||||
|
@ -71,8 +75,8 @@ extension IdentityDatabase {
|
||||||
.fetchOne)
|
.fetchOne)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseQueue, scheduling: .immediate)
|
.publisher(in: databaseQueue, scheduling: .immediate)
|
||||||
.map {
|
.tryMap {
|
||||||
guard let result = $0 else { return nil }
|
guard let result = $0 else { throw IdentityDatabaseError.identityNotFound }
|
||||||
|
|
||||||
return Identity(result: result)
|
return Identity(result: result)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,7 @@ class HTTPClient {
|
||||||
private let session: Session
|
private let session: Session
|
||||||
private let decoder: DataDecoder
|
private let decoder: DataDecoder
|
||||||
|
|
||||||
init(
|
init(configuration: URLSessionConfiguration, decoder: DataDecoder = JSONDecoder()) {
|
||||||
configuration: URLSessionConfiguration = URLSessionConfiguration.af.default,
|
|
||||||
decoder: DataDecoder = JSONDecoder()) {
|
|
||||||
self.session = Session(configuration: configuration)
|
self.session = Session(configuration: configuration)
|
||||||
self.decoder = decoder
|
self.decoder = decoder
|
||||||
}
|
}
|
||||||
|
|
24
Shared/Publishers/CurrentValuePublisher.swift
Normal file
24
Shared/Publishers/CurrentValuePublisher.swift
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// This publisher acts as a `@Published private var` inside ObservableObjects that doesn't trigger `objectWillChange`
|
||||||
|
|
||||||
|
class CurrentValuePublisher<Output> {
|
||||||
|
@Published private(set) var value: Output
|
||||||
|
private let internalPublisher: AnyPublisher<Output, Never>
|
||||||
|
|
||||||
|
init<P>(initial: Output, then: P) where P: Publisher, P.Output == Output, P.Failure == Never {
|
||||||
|
value = initial
|
||||||
|
internalPublisher = then.eraseToAnyPublisher()
|
||||||
|
then.assign(to: &$value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CurrentValuePublisher: Publisher {
|
||||||
|
typealias Failure = Never
|
||||||
|
|
||||||
|
func receive<S>(subscriber: S) where S: Subscriber, Output == S.Input, S.Failure == Never {
|
||||||
|
internalPublisher.receive(subscriber: subscriber)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,15 +7,18 @@ class AddIdentityViewModel: ObservableObject {
|
||||||
@Published var urlFieldText = ""
|
@Published var urlFieldText = ""
|
||||||
@Published var alertItem: AlertItem?
|
@Published var alertItem: AlertItem?
|
||||||
@Published private(set) var loading = false
|
@Published private(set) var loading = false
|
||||||
@Published private(set) var addedIdentityID: String?
|
let addedIdentityID: AnyPublisher<String, Never>
|
||||||
|
|
||||||
private let networkClient: HTTPClient
|
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
|
private let networkClient: MastodonClient
|
||||||
private let webAuthSessionContextProvider = WebAuthSessionContextProvider()
|
private let webAuthSessionContextProvider = WebAuthSessionContextProvider()
|
||||||
|
private let addedIdentityIDInput = PassthroughSubject<String, Never>()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(networkClient: HTTPClient, environment: AppEnvironment) {
|
init(environment: AppEnvironment) {
|
||||||
self.networkClient = networkClient
|
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
self.networkClient = MastodonClient(configuration: environment.URLSessionConfiguration)
|
||||||
|
addedIdentityID = addedIdentityIDInput.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func goTapped() {
|
func goTapped() {
|
||||||
|
@ -54,8 +57,8 @@ class AddIdentityViewModel: ObservableObject {
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
receiveSubscription: { [weak self] _ in self?.loading = true },
|
receiveSubscription: { [weak self] _ in self?.loading = true },
|
||||||
receiveCompletion: { [weak self] _ in self?.loading = false })
|
receiveCompletion: { [weak self] _ in self?.loading = false })
|
||||||
.map { $0 as String? }
|
.sink(receiveValue: addedIdentityIDInput.send)
|
||||||
.assign(to: &$addedIdentityID)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
96
Shared/View Models/MainNavigationViewModel.swift
Normal file
96
Shared/View Models/MainNavigationViewModel.swift
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class MainNavigationViewModel: ObservableObject {
|
||||||
|
var selectedTab: Tab? = .timelines
|
||||||
|
@Published var presentingSettings = false
|
||||||
|
@Published private(set) var alertItem: AlertItem?
|
||||||
|
@Published private(set) var handle: String
|
||||||
|
@Published private(set) var image: URL?
|
||||||
|
|
||||||
|
private let environment: AppEnvironment
|
||||||
|
private let identity: CurrentValuePublisher<Identity>
|
||||||
|
private let networkClient: MastodonClient
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(identity: CurrentValuePublisher<Identity>, environment: AppEnvironment) {
|
||||||
|
self.identity = identity
|
||||||
|
self.environment = environment
|
||||||
|
networkClient = MastodonClient(configuration: environment.URLSessionConfiguration)
|
||||||
|
|
||||||
|
networkClient.instanceURL = identity.value.url
|
||||||
|
|
||||||
|
do {
|
||||||
|
networkClient.accessToken = try environment.secrets.item(.accessToken, forIdentityID: identity.value.id)
|
||||||
|
} catch {
|
||||||
|
alertItem = AlertItem(error: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = identity.value.handle
|
||||||
|
identity.map(\.handle).assign(to: &$handle)
|
||||||
|
|
||||||
|
image = identity.value.image
|
||||||
|
identity.map(\.image).assign(to: &$image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MainNavigationViewModel {
|
||||||
|
func refreshIdentity() {
|
||||||
|
let id = identity.value.id
|
||||||
|
|
||||||
|
if networkClient.accessToken != nil {
|
||||||
|
networkClient.request(AccountEndpoint.verifyCredentials)
|
||||||
|
.map { ($0, id) }
|
||||||
|
.flatMap(environment.identityDatabase.updateAccount)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.sink(receiveValue: {})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
networkClient.request(InstanceEndpoint.instance)
|
||||||
|
.map { ($0, id) }
|
||||||
|
.flatMap(environment.identityDatabase.updateInstance)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.sink(receiveValue: {})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func settingsViewModel() -> SettingsViewModel {
|
||||||
|
SettingsViewModel(identity: identity, environment: environment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MainNavigationViewModel {
|
||||||
|
enum Tab: CaseIterable {
|
||||||
|
case timelines
|
||||||
|
case search
|
||||||
|
case notifications
|
||||||
|
case messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MainNavigationViewModel.Tab {
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .timelines: return "Timelines"
|
||||||
|
case .search: return "Search"
|
||||||
|
case .notifications: return "Notifications"
|
||||||
|
case .messages: return "Messages"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .timelines: return "house"
|
||||||
|
case .search: return "magnifyingglass"
|
||||||
|
case .notifications: return "bell"
|
||||||
|
case .messages: return "envelope"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MainNavigationViewModel.Tab: Identifiable {
|
||||||
|
var id: Self { self }
|
||||||
|
}
|
52
Shared/View Models/RootViewModel.swift
Normal file
52
Shared/View Models/RootViewModel.swift
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class RootViewModel: ObservableObject {
|
||||||
|
@Published private(set) var identityID: String?
|
||||||
|
@Published var alertItem: AlertItem?
|
||||||
|
|
||||||
|
private let environment: AppEnvironment
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(environment: AppEnvironment) {
|
||||||
|
self.environment = environment
|
||||||
|
identityID = environment.preferences[.recentIdentityID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RootViewModel {
|
||||||
|
func addIdentityViewModel() -> AddIdentityViewModel {
|
||||||
|
let addAccountViewModel = AddIdentityViewModel(environment: environment)
|
||||||
|
|
||||||
|
addAccountViewModel.addedIdentityID.map { $0 as String? }.assign(to: &$identityID)
|
||||||
|
|
||||||
|
return addAccountViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
func mainNavigationViewModel(identityID: String) -> MainNavigationViewModel? {
|
||||||
|
environment.preferences[.recentIdentityID] = identityID
|
||||||
|
|
||||||
|
let identityObservation = environment.identityDatabase.identityObservation(id: identityID)
|
||||||
|
.share()
|
||||||
|
var initialIdentity: Identity?
|
||||||
|
|
||||||
|
// setting `initialIdentity` works because of immediate scheduling
|
||||||
|
identityObservation.sink(receiveCompletion: { _ in }, receiveValue: { initialIdentity = $0 })
|
||||||
|
.store(in: &cancellables)
|
||||||
|
identityObservation.map { $0.id }
|
||||||
|
.catch { [weak self] _ -> AnyPublisher<String?, Never> in
|
||||||
|
Just(self?.environment.preferences[.recentIdentityID]).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.assign(to: &$identityID)
|
||||||
|
|
||||||
|
guard let presentIdentity = initialIdentity else { return nil }
|
||||||
|
|
||||||
|
return MainNavigationViewModel(
|
||||||
|
identity: CurrentValuePublisher(
|
||||||
|
initial: presentIdentity,
|
||||||
|
then: identityObservation.assignErrorsToAlertItem(to: \.alertItem, on: self)),
|
||||||
|
environment: environment)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,112 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
class SceneViewModel: ObservableObject {
|
|
||||||
@Published private(set) var identity: Identity?
|
|
||||||
@Published var alertItem: AlertItem?
|
|
||||||
@Published var presentingSettings = false
|
|
||||||
var selectedTopLevelNavigation: TopLevelNavigation? = .timelines
|
|
||||||
|
|
||||||
private let networkClient: MastodonClient
|
|
||||||
private let environment: AppEnvironment
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
init(networkClient: MastodonClient, environment: AppEnvironment) {
|
|
||||||
self.networkClient = networkClient
|
|
||||||
self.environment = environment
|
|
||||||
|
|
||||||
if let recentIdentityID = environment.preferences[.recentIdentityID] as String? {
|
|
||||||
changeIdentity(id: recentIdentityID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SceneViewModel {
|
|
||||||
func refreshIdentity() {
|
|
||||||
guard let identity = identity else { return }
|
|
||||||
|
|
||||||
if networkClient.accessToken != nil {
|
|
||||||
networkClient.request(AccountEndpoint.verifyCredentials)
|
|
||||||
.map { ($0, identity.id) }
|
|
||||||
.flatMap(environment.identityDatabase.updateAccount)
|
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
|
||||||
.sink(receiveValue: {})
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
networkClient.request(InstanceEndpoint.instance)
|
|
||||||
.map { ($0, identity.id) }
|
|
||||||
.flatMap(environment.identityDatabase.updateInstance)
|
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
|
||||||
.sink(receiveValue: {})
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addIdentityViewModel() -> AddIdentityViewModel {
|
|
||||||
let addAccountViewModel = AddIdentityViewModel(networkClient: networkClient, environment: environment)
|
|
||||||
|
|
||||||
addAccountViewModel.$addedIdentityID
|
|
||||||
.compactMap { $0 }
|
|
||||||
.sink(receiveValue: changeIdentity(id:))
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
return addAccountViewModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension SceneViewModel {
|
|
||||||
private func changeIdentity(id: String) {
|
|
||||||
environment.identityDatabase.identityObservation(id: id)
|
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
|
||||||
.handleEvents(receiveOutput: { [weak self] in
|
|
||||||
guard let self = self, let identity = $0 else { return }
|
|
||||||
|
|
||||||
self.networkClient.instanceURL = identity.url
|
|
||||||
|
|
||||||
do {
|
|
||||||
self.networkClient.accessToken =
|
|
||||||
try self.environment.secrets.item(.accessToken, forIdentityID: identity.id)
|
|
||||||
} catch {
|
|
||||||
self.alertItem = AlertItem(error: error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.assign(to: &$identity)
|
|
||||||
|
|
||||||
refreshIdentity()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SceneViewModel {
|
|
||||||
enum TopLevelNavigation: CaseIterable {
|
|
||||||
case timelines
|
|
||||||
case search
|
|
||||||
case notifications
|
|
||||||
case messages
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SceneViewModel.TopLevelNavigation {
|
|
||||||
var title: String {
|
|
||||||
switch self {
|
|
||||||
case .timelines: return "Timelines"
|
|
||||||
case .search: return "Search"
|
|
||||||
case .notifications: return "Notifications"
|
|
||||||
case .messages: return "Messages"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemImageName: String {
|
|
||||||
switch self {
|
|
||||||
case .timelines: return "house"
|
|
||||||
case .search: return "magnifyingglass"
|
|
||||||
case .notifications: return "bell"
|
|
||||||
case .messages: return "envelope"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SceneViewModel.TopLevelNavigation: Identifiable {
|
|
||||||
var id: Self { self }
|
|
||||||
}
|
|
|
@ -3,9 +3,11 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class SettingsViewModel: ObservableObject {
|
class SettingsViewModel: ObservableObject {
|
||||||
let identity: Identity
|
private let identity: CurrentValuePublisher<Identity>
|
||||||
|
private let environment: AppEnvironment
|
||||||
|
|
||||||
init(identity: Identity) {
|
init(identity: CurrentValuePublisher<Identity>, environment: AppEnvironment) {
|
||||||
self.identity = identity
|
self.identity = identity
|
||||||
|
self.environment = environment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,14 @@ struct AddIdentityView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
#if os(macOS)
|
||||||
Spacer()
|
Spacer()
|
||||||
#if os(iOS)
|
urlTextField
|
||||||
|
#else
|
||||||
urlTextField
|
urlTextField
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.keyboardType(.URL)
|
.keyboardType(.URL)
|
||||||
#else
|
|
||||||
urlTextField
|
|
||||||
#endif
|
#endif
|
||||||
Group {
|
Group {
|
||||||
if viewModel.loading {
|
if viewModel.loading {
|
||||||
|
@ -26,9 +26,11 @@ struct AddIdentityView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
#if os(macOS)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.padding()
|
.paddingIfMac()
|
||||||
.alertItem($viewModel.alertItem)
|
.alertItem($viewModel.alertItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,12 +41,20 @@ extension AddIdentityView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension View {
|
||||||
|
func paddingIfMac() -> some View {
|
||||||
|
#if os(macOS)
|
||||||
|
return padding()
|
||||||
|
#else
|
||||||
|
return self
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
struct AddAccountView_Previews: PreviewProvider {
|
struct AddAccountView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
AddIdentityView(viewModel: AddIdentityViewModel(
|
AddIdentityView(viewModel: AddIdentityViewModel(environment: .development))
|
||||||
networkClient: MastodonClient.development,
|
|
||||||
environment: .development))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct RootView: View {
|
||||||
@EnvironmentObject var sceneViewModel: SceneViewModel
|
@EnvironmentObject var sceneViewModel: SceneViewModel
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ContentView {
|
private extension RootView {
|
||||||
private func mainNavigation(identity: Identity) -> some View {
|
private func mainNavigation(identity: Identity) -> some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
return SidebarNavigation(identity: identity)
|
return SidebarNavigation(identity: identity)
|
||||||
|
@ -39,7 +39,7 @@ private extension ContentView {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView()
|
RootView()
|
||||||
.environmentObject(SceneViewModel.development)
|
.environmentObject(SceneViewModel.development)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
40
Shared/Views/RootView.swift
Normal file
40
Shared/Views/RootView.swift
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct RootView: View {
|
||||||
|
@StateObject var viewModel: RootViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if
|
||||||
|
let identityID = viewModel.identityID,
|
||||||
|
let mainNavigationViewModel = viewModel.mainNavigationViewModel(identityID: identityID) {
|
||||||
|
Self.mainNavigation(viewModel: mainNavigationViewModel)
|
||||||
|
} else {
|
||||||
|
addIdentity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension RootView {
|
||||||
|
private static func mainNavigation(viewModel: MainNavigationViewModel) -> some View {
|
||||||
|
#if os(macOS)
|
||||||
|
return SidebarNavigation().environmentObject(viewModel)
|
||||||
|
.frame(minWidth: 900, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity)
|
||||||
|
#else
|
||||||
|
return TabNavigation().environmentObject(viewModel)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var addIdentity: some View {
|
||||||
|
AddIdentityView(viewModel: viewModel.addIdentityViewModel())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
struct ContentView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
RootView(viewModel: .development)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -3,38 +3,37 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import KingfisherSwiftUI
|
import KingfisherSwiftUI
|
||||||
import struct Kingfisher.DownsamplingImageProcessor
|
import struct Kingfisher.DownsamplingImageProcessor
|
||||||
import struct Kingfisher.RoundCornerImageProcessor
|
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@StateObject var viewModel: SettingsViewModel
|
@StateObject var viewModel: SettingsViewModel
|
||||||
@EnvironmentObject var sceneViewModel: SceneViewModel
|
@EnvironmentObject var mainNavigationViewModel: MainNavigationViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
Form {
|
Form {
|
||||||
HStack {
|
HStack {
|
||||||
KFImage(viewModel.identity.account?.avatar,
|
KFImage(mainNavigationViewModel.image,
|
||||||
options: [
|
options: [
|
||||||
.processor(
|
.processor(
|
||||||
DownsamplingImageProcessor(size: CGSize(width: 50, height: 50))
|
DownsamplingImageProcessor(size: CGSize(width: 50, height: 50))
|
||||||
.append(another: RoundCornerImageProcessor(radius: .widthFraction(0.5)))
|
|
||||||
),
|
),
|
||||||
.scaleFactor(Screen.scale),
|
.scaleFactor(Screen.scale),
|
||||||
.cacheOriginalImage
|
.cacheOriginalImage
|
||||||
])
|
])
|
||||||
Text(viewModel.identity.handle)
|
.clipShape(Circle())
|
||||||
|
Text(mainNavigationViewModel.handle)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitleAndItems(sceneViewModel: sceneViewModel)
|
.navigationBarTitleAndItems(mainNavigationViewModel: mainNavigationViewModel)
|
||||||
}
|
}
|
||||||
.navigationViewStyle
|
.navigationViewStyle
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Divider()
|
Divider()
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: { sceneViewModel.presentingSettings.toggle() }) {
|
Button(action: { mainNavigationViewModel.presentingSettings.toggle() }) {
|
||||||
Text("Done")
|
Text("Done")
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
|
@ -48,12 +47,12 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension View {
|
private extension View {
|
||||||
func navigationBarTitleAndItems(sceneViewModel: SceneViewModel) -> some View {
|
func navigationBarTitleAndItems(mainNavigationViewModel: MainNavigationViewModel) -> some View {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
return navigationBarTitle(Text("settings"), displayMode: .inline)
|
return navigationBarTitle(Text("settings"), displayMode: .inline)
|
||||||
.navigationBarItems(
|
.navigationBarItems(
|
||||||
leading: Button {
|
leading: Button {
|
||||||
sceneViewModel.presentingSettings.toggle()
|
mainNavigationViewModel.presentingSettings.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "xmark.circle.fill").imageScale(.large)
|
Image(systemName: "xmark.circle.fill").imageScale(.large)
|
||||||
})
|
})
|
||||||
|
@ -82,8 +81,8 @@ private extension View {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
struct SettingsView_Previews: PreviewProvider {
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SettingsView(viewModel: SettingsViewModel(identity: .development))
|
SettingsView(viewModel: .development)
|
||||||
.environmentObject(SceneViewModel.development)
|
.environmentObject(MainNavigationViewModel.development)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -8,16 +8,15 @@ import CombineExpectations
|
||||||
class AddIdentityViewModelTests: XCTestCase {
|
class AddIdentityViewModelTests: XCTestCase {
|
||||||
func testAddIdentity() throws {
|
func testAddIdentity() throws {
|
||||||
let environment = AppEnvironment.fresh()
|
let environment = AppEnvironment.fresh()
|
||||||
let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: environment)
|
let sut = AddIdentityViewModel(environment: environment)
|
||||||
let addedIDRecorder = sut.$addedIdentityID.record()
|
let addedIDRecorder = sut.addedIdentityID.record()
|
||||||
XCTAssertNil(try wait(for: addedIDRecorder.next(), timeout: 1))
|
|
||||||
|
|
||||||
sut.urlFieldText = "https://mastodon.social"
|
sut.urlFieldText = "https://mastodon.social"
|
||||||
sut.goTapped()
|
sut.goTapped()
|
||||||
|
|
||||||
let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)!
|
let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)
|
||||||
let identityRecorder = environment.identityDatabase.identityObservation(id: addedIdentityID).record()
|
let identityRecorder = environment.identityDatabase.identityObservation(id: addedIdentityID).record()
|
||||||
let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)!
|
let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)
|
||||||
|
|
||||||
XCTAssertEqual(addedIdentity.id, addedIdentityID)
|
XCTAssertEqual(addedIdentity.id, addedIdentityID)
|
||||||
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
||||||
|
@ -35,22 +34,21 @@ class AddIdentityViewModelTests: XCTestCase {
|
||||||
|
|
||||||
func testAddIdentityWithoutScheme() throws {
|
func testAddIdentityWithoutScheme() throws {
|
||||||
let environment = AppEnvironment.fresh()
|
let environment = AppEnvironment.fresh()
|
||||||
let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: environment)
|
let sut = AddIdentityViewModel(environment: environment)
|
||||||
let addedIDRecorder = sut.$addedIdentityID.record()
|
let addedIDRecorder = sut.addedIdentityID.record()
|
||||||
XCTAssertNil(try wait(for: addedIDRecorder.next(), timeout: 1))
|
|
||||||
|
|
||||||
sut.urlFieldText = "mastodon.social"
|
sut.urlFieldText = "mastodon.social"
|
||||||
sut.goTapped()
|
sut.goTapped()
|
||||||
|
|
||||||
let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)!
|
let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)
|
||||||
let identityRecorder = environment.identityDatabase.identityObservation(id: addedIdentityID).record()
|
let identityRecorder = environment.identityDatabase.identityObservation(id: addedIdentityID).record()
|
||||||
let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)!
|
let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)
|
||||||
|
|
||||||
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInvalidURL() throws {
|
func testInvalidURL() throws {
|
||||||
let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: .fresh())
|
let sut = AddIdentityViewModel(environment: .fresh())
|
||||||
let recorder = sut.$alertItem.record()
|
let recorder = sut.$alertItem.record()
|
||||||
|
|
||||||
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||||
|
@ -65,7 +63,7 @@ class AddIdentityViewModelTests: XCTestCase {
|
||||||
|
|
||||||
func testDoesNotAlertCanceledLogin() throws {
|
func testDoesNotAlertCanceledLogin() throws {
|
||||||
let environment = AppEnvironment.fresh(webAuthSessionType: CanceledLoginStubbingWebAuthSession.self)
|
let environment = AppEnvironment.fresh(webAuthSessionType: CanceledLoginStubbingWebAuthSession.self)
|
||||||
let sut = AddIdentityViewModel(networkClient: MastodonClient.fresh(), environment: environment)
|
let sut = AddIdentityViewModel(environment: environment)
|
||||||
let recorder = sut.$alertItem.record()
|
let recorder = sut.$alertItem.record()
|
||||||
|
|
||||||
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||||
|
|
24
Tests/View Models/RootViewModelTests.swift
Normal file
24
Tests/View Models/RootViewModelTests.swift
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
import Combine
|
||||||
|
import CombineExpectations
|
||||||
|
@testable import Metatext
|
||||||
|
|
||||||
|
class RootViewModelTests: XCTestCase {
|
||||||
|
func testAddIdentity() throws {
|
||||||
|
let sut = RootViewModel(environment: .fresh())
|
||||||
|
let identityIDRecorder = sut.$identityID.record()
|
||||||
|
|
||||||
|
XCTAssertNil(try wait(for: identityIDRecorder.next(), timeout: 1))
|
||||||
|
|
||||||
|
let addIdentityViewModel = sut.addIdentityViewModel()
|
||||||
|
|
||||||
|
addIdentityViewModel.urlFieldText = "https://mastodon.social"
|
||||||
|
addIdentityViewModel.goTapped()
|
||||||
|
|
||||||
|
let identityID = try wait(for: identityIDRecorder.next(), timeout: 1)!
|
||||||
|
|
||||||
|
XCTAssertNotNil(identityID)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
import Combine
|
|
||||||
import CombineExpectations
|
|
||||||
@testable import Metatext
|
|
||||||
|
|
||||||
class SceneViewModelTests: XCTestCase {
|
|
||||||
func testAddIdentity() throws {
|
|
||||||
let sut = SceneViewModel(networkClient: .fresh(), environment: .fresh())
|
|
||||||
let identityRecorder = sut.$identity.record()
|
|
||||||
|
|
||||||
XCTAssertNil(try wait(for: identityRecorder.next(), timeout: 1))
|
|
||||||
|
|
||||||
let addIdentityViewModel = sut.addIdentityViewModel()
|
|
||||||
|
|
||||||
addIdentityViewModel.urlFieldText = "https://mastodon.social"
|
|
||||||
addIdentityViewModel.goTapped()
|
|
||||||
|
|
||||||
let identity = try wait(for: identityRecorder.next(), timeout: 1)!
|
|
||||||
|
|
||||||
XCTAssertEqual(identity.id, addIdentityViewModel.addedIdentityID)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,70 +3,66 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import KingfisherSwiftUI
|
import KingfisherSwiftUI
|
||||||
import struct Kingfisher.DownsamplingImageProcessor
|
import struct Kingfisher.DownsamplingImageProcessor
|
||||||
import struct Kingfisher.RoundCornerImageProcessor
|
|
||||||
|
|
||||||
struct TabNavigation: View {
|
struct TabNavigation: View {
|
||||||
let identity: Identity
|
@EnvironmentObject var viewModel: MainNavigationViewModel
|
||||||
@EnvironmentObject var sceneViewModel: SceneViewModel
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $sceneViewModel.selectedTopLevelNavigation) {
|
TabView(selection: $viewModel.selectedTab) {
|
||||||
ForEach(SceneViewModel.TopLevelNavigation.allCases) { topLevelNavigation in
|
ForEach(MainNavigationViewModel.Tab.allCases) { tab in
|
||||||
NavigationView {
|
NavigationView {
|
||||||
view(topLevelNavigation: topLevelNavigation)
|
view(tab: tab)
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label(topLevelNavigation.title, systemImage: topLevelNavigation.systemImageName)
|
Label(tab.title, systemImage: tab.systemImageName)
|
||||||
.accessibility(label: Text(topLevelNavigation.title))
|
.accessibility(label: Text(tab.title))
|
||||||
}
|
}
|
||||||
.tag(topLevelNavigation)
|
.tag(tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $sceneViewModel.presentingSettings) {
|
.sheet(isPresented: $viewModel.presentingSettings) {
|
||||||
SettingsView(viewModel: SettingsViewModel(identity: identity))
|
SettingsView(viewModel: viewModel.settingsViewModel())
|
||||||
.environmentObject(sceneViewModel)
|
.environmentObject(viewModel)
|
||||||
}
|
}
|
||||||
|
.onAppear { viewModel.refreshIdentity() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension TabNavigation {
|
private extension TabNavigation {
|
||||||
func view(topLevelNavigation: SceneViewModel.TopLevelNavigation) -> some View {
|
func view(tab: MainNavigationViewModel.Tab) -> some View {
|
||||||
Group {
|
Group {
|
||||||
switch topLevelNavigation {
|
switch tab {
|
||||||
case .timelines:
|
case .timelines:
|
||||||
TimelineView()
|
TimelineView()
|
||||||
.navigationBarTitle(identity.handle, displayMode: .inline)
|
.navigationBarTitle(viewModel.handle, displayMode: .inline)
|
||||||
.navigationBarItems(
|
.navigationBarItems(
|
||||||
leading: Button {
|
leading: Button {
|
||||||
sceneViewModel.presentingSettings.toggle()
|
viewModel.presentingSettings.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
KFImage(identity.account?.avatar
|
KFImage(viewModel.image,
|
||||||
?? identity.instance?.thumbnail,
|
|
||||||
options: [
|
options: [
|
||||||
.processor(
|
.processor(
|
||||||
DownsamplingImageProcessor(size: CGSize(width: 28, height: 28))
|
DownsamplingImageProcessor(size: CGSize(width: 28, height: 28))
|
||||||
.append(another: RoundCornerImageProcessor(radius: .widthFraction(0.5)))
|
|
||||||
),
|
),
|
||||||
.scaleFactor(Screen.scale),
|
.scaleFactor(Screen.scale),
|
||||||
.cacheOriginalImage
|
.cacheOriginalImage
|
||||||
])
|
])
|
||||||
.placeholder { Image(systemName: "gear") }
|
.placeholder { Image(systemName: "gear") }
|
||||||
.renderingMode(.original)
|
.renderingMode(.original)
|
||||||
|
.clipShape(Circle())
|
||||||
})
|
})
|
||||||
default: Text(topLevelNavigation.title)
|
default: Text(tab.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Preview
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
struct TabNavigation_Previews: PreviewProvider {
|
struct TabNavigation_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
TabNavigation(identity: .development)
|
TabNavigation()
|
||||||
.environmentObject(SceneViewModel.development)
|
.environmentObject(MainNavigationViewModel.development)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -6,20 +6,19 @@ import struct Kingfisher.DownsamplingImageProcessor
|
||||||
import struct Kingfisher.RoundCornerImageProcessor
|
import struct Kingfisher.RoundCornerImageProcessor
|
||||||
|
|
||||||
struct SidebarNavigation: View {
|
struct SidebarNavigation: View {
|
||||||
let identity: Identity
|
@EnvironmentObject var viewModel: MainNavigationViewModel
|
||||||
@EnvironmentObject var sceneViewModel: SceneViewModel
|
|
||||||
|
|
||||||
var sidebar: some View {
|
var sidebar: some View {
|
||||||
List(selection: $sceneViewModel.selectedTopLevelNavigation) {
|
List(selection: $viewModel.selectedTab) {
|
||||||
ForEach(SceneViewModel.TopLevelNavigation.allCases) { topLevelNavigation in
|
ForEach(MainNavigationViewModel.Tab.allCases) { tab in
|
||||||
NavigationLink(destination: view(topLevelNavigation: topLevelNavigation)) {
|
NavigationLink(destination: view(topLevelNavigation: tab)) {
|
||||||
Label(topLevelNavigation.title, systemImage: topLevelNavigation.systemImageName)
|
Label(tab.title, systemImage: tab.systemImageName)
|
||||||
}
|
}
|
||||||
.accessibility(label: Text(topLevelNavigation.title))
|
.accessibility(label: Text(tab.title))
|
||||||
.tag(topLevelNavigation)
|
.tag(tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay(Pocket(identity: identity), alignment: .bottom)
|
.overlay(Pocket(), alignment: .bottom)
|
||||||
.listStyle(SidebarListStyle())
|
.listStyle(SidebarListStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +33,7 @@ struct SidebarNavigation: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension SidebarNavigation {
|
private extension SidebarNavigation {
|
||||||
func view(topLevelNavigation: SceneViewModel.TopLevelNavigation) -> some View {
|
func view(topLevelNavigation: MainNavigationViewModel.Tab) -> some View {
|
||||||
Group {
|
Group {
|
||||||
switch topLevelNavigation {
|
switch topLevelNavigation {
|
||||||
case .timelines:
|
case .timelines:
|
||||||
|
@ -45,15 +44,13 @@ private extension SidebarNavigation {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Pocket: View {
|
struct Pocket: View {
|
||||||
let identity: Identity
|
@EnvironmentObject var viewModel: MainNavigationViewModel
|
||||||
@EnvironmentObject var sceneViewModel: SceneViewModel
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Divider()
|
Divider()
|
||||||
Button(action: { sceneViewModel.presentingSettings.toggle() }) {
|
Button(action: { viewModel.presentingSettings.toggle() }) {
|
||||||
KFImage(identity.account?.avatar
|
KFImage(viewModel.image,
|
||||||
?? identity.instance?.thumbnail,
|
|
||||||
options: [
|
options: [
|
||||||
.processor(
|
.processor(
|
||||||
DownsamplingImageProcessor(size: CGSize(width: 50, height: 50))
|
DownsamplingImageProcessor(size: CGSize(width: 50, height: 50))
|
||||||
|
@ -74,9 +71,9 @@ private extension SidebarNavigation {
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $sceneViewModel.presentingSettings) {
|
.sheet(isPresented: $viewModel.presentingSettings) {
|
||||||
SettingsView(viewModel: SettingsViewModel(identity: identity))
|
SettingsView(viewModel: viewModel.settingsViewModel())
|
||||||
.environmentObject(sceneViewModel)
|
.environmentObject(viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,8 +82,8 @@ private extension SidebarNavigation {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
struct SidebarNavigation_Previews: PreviewProvider {
|
struct SidebarNavigation_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SidebarNavigation(identity: .development)
|
SidebarNavigation()
|
||||||
.environmentObject(SceneViewModel.development)
|
.environmentObject(MainNavigationViewModel.development)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in a new issue