Multiple identities
This commit is contained in:
parent
9dd67a6c69
commit
6f0cf59fd0
|
@ -123,11 +123,15 @@ extension RootViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainNavigationViewModel {
|
extension MainNavigationViewModel {
|
||||||
static let development = RootViewModel.development.mainNavigationViewModel(identityID: devIdentityID)!
|
static let development = try! MainNavigationViewModel(identityID: devIdentityID, environment: .development)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SettingsViewModel {
|
extension SettingsViewModel {
|
||||||
static let development = MainNavigationViewModel.development.settingsViewModel()
|
static let development = MainNavigationViewModel.development.settingsViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension IdentitiesViewModel {
|
||||||
|
static let development = SettingsViewModel.development.identitiesViewModel()
|
||||||
|
}
|
||||||
|
|
||||||
// swiftlint:enable force_try
|
// swiftlint:enable force_try
|
||||||
|
|
|
@ -30,8 +30,6 @@
|
||||||
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 */; };
|
D052BBE024D805E300A80A7A /* MainNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBDF24D805E300A80A7A /* MainNavigationViewModel.swift */; };
|
||||||
D052BBE124D805E300A80A7A /* 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 */; };
|
||||||
|
@ -65,6 +63,10 @@
|
||||||
D06B492024D3FB8000642749 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D06B491E24D3F7FE00642749 /* Localizable.strings */; };
|
D06B492024D3FB8000642749 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D06B491E24D3F7FE00642749 /* Localizable.strings */; };
|
||||||
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
||||||
D06B492524D4612400642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492424D4612400642749 /* KingfisherSwiftUI */; };
|
D06B492524D4612400642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492424D4612400642749 /* KingfisherSwiftUI */; };
|
||||||
|
D06BAB4E24D942BD0081B8FD /* IdentitiesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BAB4D24D942BC0081B8FD /* IdentitiesViewModel.swift */; };
|
||||||
|
D06BAB4F24D942BD0081B8FD /* IdentitiesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BAB4D24D942BC0081B8FD /* IdentitiesViewModel.swift */; };
|
||||||
|
D06BAB5124D942CF0081B8FD /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BAB5024D942CF0081B8FD /* IdentitiesView.swift */; };
|
||||||
|
D06BAB5224D942CF0081B8FD /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BAB5024D942CF0081B8FD /* IdentitiesView.swift */; };
|
||||||
D074577724D29006004758DB /* StubbingWebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthSession.swift */; };
|
D074577724D29006004758DB /* StubbingWebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthSession.swift */; };
|
||||||
D074577824D29006004758DB /* StubbingWebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthSession.swift */; };
|
D074577824D29006004758DB /* StubbingWebAuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthSession.swift */; };
|
||||||
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; };
|
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; };
|
||||||
|
@ -73,8 +75,6 @@
|
||||||
D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; };
|
D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; };
|
||||||
D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */; };
|
D0B23F0D24D210E90066F411 /* 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 */; };
|
D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */; };
|
||||||
D0B93B3024D55098007AF646 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B93B2F24D55098007AF646 /* Screen.swift */; };
|
|
||||||
D0B93B3124D55098007AF646 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B93B2F24D55098007AF646 /* Screen.swift */; };
|
|
||||||
D0BEC93824C9632800E864C4 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* RootViewModel.swift */; };
|
D0BEC93824C9632800E864C4 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* RootViewModel.swift */; };
|
||||||
D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* RootViewModel.swift */; };
|
D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* RootViewModel.swift */; };
|
||||||
D0BEC93B24C96FD500E864C4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* RootView.swift */; };
|
D0BEC93B24C96FD500E864C4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* RootView.swift */; };
|
||||||
|
@ -166,7 +166,6 @@
|
||||||
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>"; };
|
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; };
|
||||||
|
@ -183,11 +182,12 @@
|
||||||
D0666A6E24C6DFB300F3F04B /* AccessToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = "<group>"; };
|
D0666A6E24C6DFB300F3F04B /* AccessToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = "<group>"; };
|
||||||
D0666A7124C6E0D300F3F04B /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
|
D0666A7124C6E0D300F3F04B /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
|
||||||
D06B491E24D3F7FE00642749 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
|
D06B491E24D3F7FE00642749 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
D06BAB4D24D942BC0081B8FD /* IdentitiesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitiesViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
D06BAB5024D942CF0081B8FD /* IdentitiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitiesView.swift; sourceTree = "<group>"; };
|
||||||
D074577624D29006004758DB /* StubbingWebAuthSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubbingWebAuthSession.swift; sourceTree = "<group>"; };
|
D074577624D29006004758DB /* StubbingWebAuthSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubbingWebAuthSession.swift; sourceTree = "<group>"; };
|
||||||
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = "<group>"; };
|
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
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>"; };
|
|
||||||
D0BEC93724C9632800E864C4 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = "<group>"; };
|
D0BEC93724C9632800E864C4 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = "<group>"; };
|
||||||
D0BEC93A24C96FD500E864C4 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.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>"; };
|
||||||
|
@ -275,7 +275,6 @@
|
||||||
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 */,
|
||||||
);
|
);
|
||||||
|
@ -311,14 +310,6 @@
|
||||||
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 = (
|
||||||
|
@ -369,8 +360,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */,
|
D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */,
|
||||||
|
D06BAB5024D942CF0081B8FD /* IdentitiesView.swift */,
|
||||||
D0BEC93A24C96FD500E864C4 /* RootView.swift */,
|
D0BEC93A24C96FD500E864C4 /* RootView.swift */,
|
||||||
D0B93B2F24D55098007AF646 /* Screen.swift */,
|
|
||||||
D04FD73224D48F37007D572D /* SettingsView.swift */,
|
D04FD73224D48F37007D572D /* SettingsView.swift */,
|
||||||
D0BEC94924CA231200E864C4 /* TimelineView.swift */,
|
D0BEC94924CA231200E864C4 /* TimelineView.swift */,
|
||||||
);
|
);
|
||||||
|
@ -394,6 +385,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */,
|
D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */,
|
||||||
|
D06BAB4D24D942BC0081B8FD /* IdentitiesViewModel.swift */,
|
||||||
D052BBDF24D805E300A80A7A /* MainNavigationViewModel.swift */,
|
D052BBDF24D805E300A80A7A /* MainNavigationViewModel.swift */,
|
||||||
D0BEC93724C9632800E864C4 /* RootViewModel.swift */,
|
D0BEC93724C9632800E864C4 /* RootViewModel.swift */,
|
||||||
D04FD73524D49506007D572D /* SettingsViewModel.swift */,
|
D04FD73524D49506007D572D /* SettingsViewModel.swift */,
|
||||||
|
@ -670,9 +662,7 @@
|
||||||
D0666A5A24C6C64100F3F04B /* MastodonEncoder.swift in Sources */,
|
D0666A5A24C6C64100F3F04B /* MastodonEncoder.swift in Sources */,
|
||||||
D0666A5124C6C3BC00F3F04B /* Account.swift in Sources */,
|
D0666A5124C6C3BC00F3F04B /* Account.swift in Sources */,
|
||||||
D0ED1BE024CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
D0ED1BE024CF98FB00B4899C /* AccountEndpoint.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 /* RootViewModel.swift in Sources */,
|
D0BEC93824C9632800E864C4 /* RootViewModel.swift in Sources */,
|
||||||
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */,
|
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */,
|
||||||
|
@ -683,6 +673,7 @@
|
||||||
D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
||||||
D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
||||||
D052BBCA24D74C9200A80A7A /* FakeUserDefaults.swift in Sources */,
|
D052BBCA24D74C9200A80A7A /* FakeUserDefaults.swift in Sources */,
|
||||||
|
D06BAB4E24D942BD0081B8FD /* IdentitiesViewModel.swift in Sources */,
|
||||||
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 */,
|
||||||
|
@ -697,6 +688,7 @@
|
||||||
D0666A5724C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
|
D0666A5724C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
|
||||||
D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */,
|
D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */,
|
||||||
D0DC177424D0B58800A75C65 /* Keychain.swift in Sources */,
|
D0DC177424D0B58800A75C65 /* Keychain.swift in Sources */,
|
||||||
|
D06BAB5124D942CF0081B8FD /* IdentitiesView.swift in Sources */,
|
||||||
D074577724D29006004758DB /* StubbingWebAuthSession.swift in Sources */,
|
D074577724D29006004758DB /* StubbingWebAuthSession.swift in Sources */,
|
||||||
D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
||||||
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
||||||
|
@ -735,9 +727,7 @@
|
||||||
D0666A5B24C6C64100F3F04B /* MastodonEncoder.swift in Sources */,
|
D0666A5B24C6C64100F3F04B /* MastodonEncoder.swift in Sources */,
|
||||||
D0666A5224C6C3BC00F3F04B /* Account.swift in Sources */,
|
D0666A5224C6C3BC00F3F04B /* Account.swift in Sources */,
|
||||||
D0ED1BE124CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
D0ED1BE124CF98FB00B4899C /* AccountEndpoint.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 /* RootViewModel.swift in Sources */,
|
D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */,
|
||||||
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
|
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
|
||||||
|
@ -748,6 +738,7 @@
|
||||||
D0DC175624D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
D0DC175624D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
||||||
D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
||||||
D052BBCB24D74C9300A80A7A /* FakeUserDefaults.swift in Sources */,
|
D052BBCB24D74C9300A80A7A /* FakeUserDefaults.swift in Sources */,
|
||||||
|
D06BAB4F24D942BD0081B8FD /* IdentitiesViewModel.swift in Sources */,
|
||||||
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 */,
|
||||||
|
@ -762,6 +753,7 @@
|
||||||
D0666A5824C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
|
D0666A5824C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
|
||||||
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
|
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
|
||||||
D0DC177524D0B58800A75C65 /* Keychain.swift in Sources */,
|
D0DC177524D0B58800A75C65 /* Keychain.swift in Sources */,
|
||||||
|
D06BAB5224D942CF0081B8FD /* IdentitiesView.swift in Sources */,
|
||||||
D074577824D29006004758DB /* StubbingWebAuthSession.swift in Sources */,
|
D074577824D29006004758DB /* StubbingWebAuthSession.swift in Sources */,
|
||||||
D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
||||||
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Identity: Codable, Hashable {
|
struct Identity: Codable, Hashable, Identifiable {
|
||||||
let id: String
|
let id: String
|
||||||
let url: URL
|
let url: URL
|
||||||
|
let lastUsedAt: Date
|
||||||
let instance: Identity.Instance?
|
let instance: Identity.Instance?
|
||||||
let account: Identity.Account?
|
let account: Identity.Account?
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,24 @@ struct IdentityDatabase {
|
||||||
|
|
||||||
extension IdentityDatabase {
|
extension IdentityDatabase {
|
||||||
func createIdentity(id: String, url: URL) -> AnyPublisher<Void, Error> {
|
func createIdentity(id: String, url: URL) -> AnyPublisher<Void, Error> {
|
||||||
databaseQueue.writePublisher(updates: StoredIdentity(id: id, url: url, instanceURI: nil).save)
|
databaseQueue.writePublisher(
|
||||||
|
updates: StoredIdentity(
|
||||||
|
id: id,
|
||||||
|
url: url,
|
||||||
|
lastUsedAt: Date(),
|
||||||
|
instanceURI: nil).save)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateLastUsedAt(identityID: String) -> AnyPublisher<Void, Error> {
|
||||||
|
databaseQueue.writePublisher {
|
||||||
|
try StoredIdentity
|
||||||
|
.filter(Column("id") == identityID)
|
||||||
|
.updateAll($0, Column("lastUsedAt").set(to: Date()))
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func updateInstance(_ instance: Instance, forIdentityID identityID: String) -> AnyPublisher<Void, Error> {
|
func updateInstance(_ instance: Instance, forIdentityID identityID: String) -> AnyPublisher<Void, Error> {
|
||||||
databaseQueue.writePublisher {
|
databaseQueue.writePublisher {
|
||||||
try Identity.Instance(
|
try Identity.Instance(
|
||||||
|
@ -82,6 +96,29 @@ extension IdentityDatabase {
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func identitiesObservation() -> AnyPublisher<[Identity], Error> {
|
||||||
|
ValueObservation.tracking(
|
||||||
|
StoredIdentity
|
||||||
|
.including(optional: StoredIdentity.instance)
|
||||||
|
.including(optional: StoredIdentity.account)
|
||||||
|
.asRequest(of: IdentityResult.self).fetchAll)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseQueue, scheduling: .immediate)
|
||||||
|
.map { $0.map(Identity.init(result:)) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func identityCountObservation() -> AnyPublisher<Int, Error> {
|
||||||
|
ValueObservation.tracking(StoredIdentity.fetchCount)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseQueue, scheduling: .immediate)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
var mostRecentlyUsedIdentityID: String? {
|
||||||
|
try? databaseQueue.read(StoredIdentity.select(Column("id")).order(Column("lastUsedAt").desc).fetchOne)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension IdentityDatabase {
|
private extension IdentityDatabase {
|
||||||
|
@ -99,6 +136,7 @@ private extension IdentityDatabase {
|
||||||
try db.create(table: "storedIdentity", ifNotExists: true) { t in
|
try db.create(table: "storedIdentity", ifNotExists: true) { t in
|
||||||
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||||
t.column("url", .text).notNull()
|
t.column("url", .text).notNull()
|
||||||
|
t.column("lastUsedAt", .datetime).notNull()
|
||||||
t.column("instanceURI", .text)
|
t.column("instanceURI", .text)
|
||||||
.indexed()
|
.indexed()
|
||||||
.references("instance", column: "uri")
|
.references("instance", column: "uri")
|
||||||
|
@ -126,6 +164,7 @@ private extension IdentityDatabase {
|
||||||
private struct StoredIdentity: Codable, Hashable, TableRecord, FetchableRecord, PersistableRecord {
|
private struct StoredIdentity: Codable, Hashable, TableRecord, FetchableRecord, PersistableRecord {
|
||||||
let id: String
|
let id: String
|
||||||
let url: URL
|
let url: URL
|
||||||
|
let lastUsedAt: Date
|
||||||
let instanceURI: String?
|
let instanceURI: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +189,12 @@ private struct IdentityResult: Codable, Hashable, FetchableRecord {
|
||||||
|
|
||||||
private extension Identity {
|
private extension Identity {
|
||||||
init(result: IdentityResult) {
|
init(result: IdentityResult) {
|
||||||
self.init(id: result.identity.id, url: result.identity.url, instance: result.instance, account: result.account)
|
self.init(
|
||||||
|
id: result.identity.id,
|
||||||
|
url: result.identity.url,
|
||||||
|
lastUsedAt: result.identity.lastUsedAt,
|
||||||
|
instance: result.instance,
|
||||||
|
account: result.account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
class CurrentValuePublisher<Output> {
|
|
||||||
@Published private var wrappedValue: Output
|
|
||||||
|
|
||||||
init<P>(initial: Output, then: P) where P: Publisher, P.Output == Output, P.Failure == Never {
|
|
||||||
wrappedValue = initial
|
|
||||||
then.assign(to: &$wrappedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CurrentValuePublisher {
|
|
||||||
var value: Output { wrappedValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CurrentValuePublisher: Publisher {
|
|
||||||
typealias Failure = Never
|
|
||||||
|
|
||||||
func receive<S>(subscriber: S) where S: Subscriber, S.Input == Output, S.Failure == Never {
|
|
||||||
$wrappedValue.receive(subscriber: subscriber)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -179,11 +179,7 @@ private extension Publisher where Output == AccessToken {
|
||||||
return (id, instanceURL)
|
return (id, instanceURL)
|
||||||
}
|
}
|
||||||
.flatMap(environment.identityDatabase.createIdentity)
|
.flatMap(environment.identityDatabase.createIdentity)
|
||||||
.map {
|
.map { id }
|
||||||
environment.preferences[.recentIdentityID] = id
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class IdentitiesViewModel: ObservableObject {
|
||||||
|
@Published private(set) var identity: Identity
|
||||||
|
@Published var identities = [Identity]()
|
||||||
|
@Published var alertItem: AlertItem?
|
||||||
|
|
||||||
|
private let environment: AppEnvironment
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(identity: Published<Identity>, environment: AppEnvironment) {
|
||||||
|
_identity = identity
|
||||||
|
self.environment = environment
|
||||||
|
|
||||||
|
environment.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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,41 +4,50 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class MainNavigationViewModel: ObservableObject {
|
class MainNavigationViewModel: ObservableObject {
|
||||||
var selectedTab: Tab? = .timelines
|
@Published private(set) var identity: Identity
|
||||||
@Published var presentingSettings = false
|
@Published var presentingSettings = false
|
||||||
@Published private(set) var alertItem: AlertItem?
|
@Published var alertItem: AlertItem?
|
||||||
@Published private(set) var handle: String
|
var selectedTab: Tab? = .timelines
|
||||||
@Published private(set) var image: URL?
|
|
||||||
|
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
private let identity: CurrentValuePublisher<Identity>
|
|
||||||
private let networkClient: MastodonClient
|
private let networkClient: MastodonClient
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(identity: CurrentValuePublisher<Identity>, environment: AppEnvironment) {
|
init(identityID: String, environment: AppEnvironment) throws {
|
||||||
self.identity = identity
|
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
networkClient = MastodonClient(configuration: environment.URLSessionConfiguration)
|
networkClient = MastodonClient(configuration: environment.URLSessionConfiguration)
|
||||||
|
|
||||||
networkClient.instanceURL = identity.value.url
|
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 {
|
do {
|
||||||
networkClient.accessToken = try environment.secrets.item(.accessToken, forIdentityID: identity.value.id)
|
networkClient.accessToken = try environment.secrets.item(.accessToken, forIdentityID: identity.id)
|
||||||
} catch {
|
} catch {
|
||||||
alertItem = AlertItem(error: error)
|
alertItem = AlertItem(error: error)
|
||||||
}
|
}
|
||||||
|
|
||||||
handle = identity.value.handle
|
observation.assignErrorsToAlertItem(to: \.alertItem, on: self).assign(to: &$identity)
|
||||||
identity.map(\.handle).assign(to: &$handle)
|
|
||||||
|
|
||||||
image = identity.value.image
|
environment.identityDatabase.updateLastUsedAt(identityID: identityID)
|
||||||
identity.map(\.image).assign(to: &$image)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.sink(receiveValue: {})
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainNavigationViewModel {
|
extension MainNavigationViewModel {
|
||||||
func refreshIdentity() {
|
func refreshIdentity() {
|
||||||
let id = identity.value.id
|
let id = identity.id
|
||||||
|
|
||||||
if networkClient.accessToken != nil {
|
if networkClient.accessToken != nil {
|
||||||
networkClient.request(AccountEndpoint.verifyCredentials)
|
networkClient.request(AccountEndpoint.verifyCredentials)
|
||||||
|
@ -58,7 +67,7 @@ extension MainNavigationViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func settingsViewModel() -> SettingsViewModel {
|
func settingsViewModel() -> SettingsViewModel {
|
||||||
SettingsViewModel(identity: identity, environment: environment)
|
SettingsViewModel(identity: _identity, environment: environment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,49 +4,39 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class RootViewModel: ObservableObject {
|
class RootViewModel: ObservableObject {
|
||||||
@Published private(set) var identityID: String?
|
@Published private(set) var mainNavigationViewModel: MainNavigationViewModel?
|
||||||
@Published var alertItem: AlertItem?
|
|
||||||
|
|
||||||
|
@Published private var identityID: String?
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(environment: AppEnvironment) {
|
init(environment: AppEnvironment) {
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
identityID = environment.preferences[.recentIdentityID]
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RootViewModel {
|
extension RootViewModel {
|
||||||
|
func newIdentitySelected(id: String) {
|
||||||
|
identityID = id
|
||||||
|
}
|
||||||
|
|
||||||
func addIdentityViewModel() -> AddIdentityViewModel {
|
func addIdentityViewModel() -> AddIdentityViewModel {
|
||||||
let addAccountViewModel = AddIdentityViewModel(environment: environment)
|
let addAccountViewModel = AddIdentityViewModel(environment: environment)
|
||||||
|
|
||||||
addAccountViewModel.addedIdentityID.map { $0 as String? }.assign(to: &$identityID)
|
addAccountViewModel.addedIdentityID
|
||||||
|
.sink(receiveValue: newIdentitySelected(id:))
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
return addAccountViewModel
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,17 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class SettingsViewModel: ObservableObject {
|
class SettingsViewModel: ObservableObject {
|
||||||
private let identity: CurrentValuePublisher<Identity>
|
@Published private(set) var identity: Identity
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
|
|
||||||
init(identity: CurrentValuePublisher<Identity>, environment: AppEnvironment) {
|
init(identity: Published<Identity>, environment: AppEnvironment) {
|
||||||
self.identity = identity
|
_identity = identity
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SettingsViewModel {
|
||||||
|
func identitiesViewModel() -> IdentitiesViewModel {
|
||||||
|
IdentitiesViewModel(identity: _identity, environment: environment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct IdentitiesView: View {
|
||||||
|
@StateObject var viewModel: IdentitiesViewModel
|
||||||
|
@EnvironmentObject var rootViewModel: RootViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
NavigationLink(
|
||||||
|
destination: AddIdentityView(viewModel: rootViewModel.addIdentityViewModel()),
|
||||||
|
label: {
|
||||||
|
Label("add new account", systemImage: "plus")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
List(viewModel.identities) { identity in
|
||||||
|
Button(identity.handle) {
|
||||||
|
rootViewModel.newIdentitySelected(id: identity.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IdentitiesView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
IdentitiesView(viewModel: .development)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,29 +6,27 @@ struct RootView: View {
|
||||||
@StateObject var viewModel: RootViewModel
|
@StateObject var viewModel: RootViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if
|
ZStack {
|
||||||
let identityID = viewModel.identityID,
|
if let mainNavigationViewModel = viewModel.mainNavigationViewModel {
|
||||||
let mainNavigationViewModel = viewModel.mainNavigationViewModel(identityID: identityID) {
|
Self.mainNavigation(mainNavigationViewModel: mainNavigationViewModel)
|
||||||
Self.mainNavigation(viewModel: mainNavigationViewModel)
|
.environmentObject(viewModel)
|
||||||
} else {
|
} else {
|
||||||
addIdentity
|
AddIdentityView(viewModel: viewModel.addIdentityViewModel())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension RootView {
|
private extension RootView {
|
||||||
private static func mainNavigation(viewModel: MainNavigationViewModel) -> some View {
|
@ViewBuilder
|
||||||
|
private static func mainNavigation(mainNavigationViewModel: MainNavigationViewModel) -> some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
return SidebarNavigation(viewModel: viewModel)
|
SidebarNavigation(viewModel: mainNavigationViewModel)
|
||||||
.frame(minWidth: 900, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity)
|
.frame(minWidth: 900, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity)
|
||||||
#else
|
#else
|
||||||
return TabNavigation(viewModel: viewModel)
|
TabNavigation(viewModel: mainNavigationViewModel)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var addIdentity: some View {
|
|
||||||
AddIdentityView(viewModel: viewModel.addIdentityViewModel())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct Screen {
|
|
||||||
static var scale: CGFloat {
|
|
||||||
#if os(macOS)
|
|
||||||
return NSScreen.main?.backingScaleFactor ?? 1
|
|
||||||
#else
|
|
||||||
return UIScreen.main.scale
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,34 +6,41 @@ import struct Kingfisher.DownsamplingImageProcessor
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@StateObject var viewModel: SettingsViewModel
|
@StateObject var viewModel: SettingsViewModel
|
||||||
@EnvironmentObject var mainNavigationViewModel: MainNavigationViewModel
|
@EnvironmentObject var rootViewModel: RootViewModel
|
||||||
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
@Environment(\.displayScale) var displayScale: CGFloat
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
Form {
|
Form {
|
||||||
HStack {
|
HStack {
|
||||||
KFImage(mainNavigationViewModel.image,
|
KFImage(viewModel.identity.image,
|
||||||
options: [
|
options: [
|
||||||
.processor(
|
.processor(
|
||||||
DownsamplingImageProcessor(size: CGSize(width: 50, height: 50))
|
DownsamplingImageProcessor(size: CGSize(width: 50, height: 50))
|
||||||
),
|
),
|
||||||
.scaleFactor(Screen.scale),
|
.scaleFactor(displayScale),
|
||||||
.cacheOriginalImage
|
.cacheOriginalImage
|
||||||
])
|
])
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
Text(mainNavigationViewModel.handle)
|
Text(viewModel.identity.handle)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
}
|
}
|
||||||
|
NavigationLink(
|
||||||
|
"accounts",
|
||||||
|
destination: IdentitiesView(
|
||||||
|
viewModel: viewModel.identitiesViewModel())
|
||||||
|
.environmentObject(rootViewModel))
|
||||||
}
|
}
|
||||||
.navigationBarTitleAndItems(mainNavigationViewModel: mainNavigationViewModel)
|
.navigationBarTitleAndItems(presentationMode: presentationMode)
|
||||||
}
|
}
|
||||||
.navigationViewStyle
|
.navigationViewStyle
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Divider()
|
Divider()
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: { mainNavigationViewModel.presentingSettings.toggle() }) {
|
Button(action: { presentationMode.wrappedValue.dismiss() }) {
|
||||||
Text("Done")
|
Text("Done")
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
|
@ -47,12 +54,12 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension View {
|
private extension View {
|
||||||
func navigationBarTitleAndItems(mainNavigationViewModel: MainNavigationViewModel) -> some View {
|
func navigationBarTitleAndItems(presentationMode: Binding<PresentationMode>) -> 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 {
|
||||||
mainNavigationViewModel.presentingSettings.toggle()
|
presentationMode.wrappedValue.dismiss()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "xmark.circle.fill").imageScale(.large)
|
Image(systemName: "xmark.circle.fill").imageScale(.large)
|
||||||
})
|
})
|
||||||
|
@ -83,6 +90,7 @@ struct SettingsView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SettingsView(viewModel: .development)
|
SettingsView(viewModel: .development)
|
||||||
.environmentObject(MainNavigationViewModel.development)
|
.environmentObject(MainNavigationViewModel.development)
|
||||||
|
.environmentObject(RootViewModel.development)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -20,7 +20,6 @@ class AddIdentityViewModelTests: XCTestCase {
|
||||||
|
|
||||||
XCTAssertEqual(addedIdentity.id, addedIdentityID)
|
XCTAssertEqual(addedIdentity.id, addedIdentityID)
|
||||||
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
||||||
XCTAssertEqual(environment.preferences[.recentIdentityID], addedIdentity.id)
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
try environment.secrets.item(.clientID, forIdentityID: addedIdentityID) as String?,
|
try environment.secrets.item(.clientID, forIdentityID: addedIdentityID) as String?,
|
||||||
"AUTHORIZATION_CLIENT_ID_STUB_VALUE")
|
"AUTHORIZATION_CLIENT_ID_STUB_VALUE")
|
||||||
|
|
|
@ -8,17 +8,17 @@ import CombineExpectations
|
||||||
class RootViewModelTests: XCTestCase {
|
class RootViewModelTests: XCTestCase {
|
||||||
func testAddIdentity() throws {
|
func testAddIdentity() throws {
|
||||||
let sut = RootViewModel(environment: .fresh())
|
let sut = RootViewModel(environment: .fresh())
|
||||||
let identityIDRecorder = sut.$identityID.record()
|
let recorder = sut.$mainNavigationViewModel.record()
|
||||||
|
|
||||||
XCTAssertNil(try wait(for: identityIDRecorder.next(), timeout: 1))
|
XCTAssertNil(try wait(for: recorder.next(), timeout: 1))
|
||||||
|
|
||||||
let addIdentityViewModel = sut.addIdentityViewModel()
|
let addIdentityViewModel = sut.addIdentityViewModel()
|
||||||
|
|
||||||
addIdentityViewModel.urlFieldText = "https://mastodon.social"
|
addIdentityViewModel.urlFieldText = "https://mastodon.social"
|
||||||
addIdentityViewModel.goTapped()
|
addIdentityViewModel.goTapped()
|
||||||
|
|
||||||
let identityID = try wait(for: identityIDRecorder.next(), timeout: 1)!
|
let mainNavigationViewModel = try wait(for: recorder.next(), timeout: 1)!
|
||||||
|
|
||||||
XCTAssertNotNil(identityID)
|
XCTAssertNotNil(mainNavigationViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,9 @@ import KingfisherSwiftUI
|
||||||
import struct Kingfisher.DownsamplingImageProcessor
|
import struct Kingfisher.DownsamplingImageProcessor
|
||||||
|
|
||||||
struct TabNavigation: View {
|
struct TabNavigation: View {
|
||||||
@StateObject var viewModel: MainNavigationViewModel
|
@ObservedObject var viewModel: MainNavigationViewModel
|
||||||
|
@EnvironmentObject var rootViewModel: RootViewModel
|
||||||
|
@Environment(\.displayScale) var displayScale: CGFloat
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $viewModel.selectedTab) {
|
TabView(selection: $viewModel.selectedTab) {
|
||||||
|
@ -23,9 +25,10 @@ struct TabNavigation: View {
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $viewModel.presentingSettings) {
|
.sheet(isPresented: $viewModel.presentingSettings) {
|
||||||
SettingsView(viewModel: viewModel.settingsViewModel())
|
SettingsView(viewModel: viewModel.settingsViewModel())
|
||||||
.environmentObject(viewModel)
|
.environmentObject(rootViewModel)
|
||||||
}
|
}
|
||||||
.onAppear(perform: viewModel.refreshIdentity)
|
.onReceive(rootViewModel.$mainNavigationViewModel.map { _ in ()},
|
||||||
|
perform: viewModel.refreshIdentity)
|
||||||
.onReceive(NotificationCenter.default
|
.onReceive(NotificationCenter.default
|
||||||
.publisher(for: UIScene.willEnterForegroundNotification)
|
.publisher(for: UIScene.willEnterForegroundNotification)
|
||||||
.map { _ in () },
|
.map { _ in () },
|
||||||
|
@ -39,17 +42,17 @@ private extension TabNavigation {
|
||||||
switch tab {
|
switch tab {
|
||||||
case .timelines:
|
case .timelines:
|
||||||
TimelineView()
|
TimelineView()
|
||||||
.navigationBarTitle(viewModel.handle, displayMode: .inline)
|
.navigationBarTitle(viewModel.identity.handle, displayMode: .inline)
|
||||||
.navigationBarItems(
|
.navigationBarItems(
|
||||||
leading: Button {
|
leading: Button {
|
||||||
viewModel.presentingSettings.toggle()
|
viewModel.presentingSettings.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
KFImage(viewModel.image,
|
KFImage(viewModel.identity.image,
|
||||||
options: [
|
options: [
|
||||||
.processor(
|
.processor(
|
||||||
DownsamplingImageProcessor(size: CGSize(width: 28, height: 28))
|
DownsamplingImageProcessor(size: CGSize(width: 28, height: 28))
|
||||||
),
|
),
|
||||||
.scaleFactor(Screen.scale),
|
.scaleFactor(displayScale),
|
||||||
.cacheOriginalImage
|
.cacheOriginalImage
|
||||||
])
|
])
|
||||||
.placeholder { Image(systemName: "gear") }
|
.placeholder { Image(systemName: "gear") }
|
||||||
|
|
|
@ -7,6 +7,8 @@ import struct Kingfisher.RoundCornerImageProcessor
|
||||||
|
|
||||||
struct SidebarNavigation: View {
|
struct SidebarNavigation: View {
|
||||||
@StateObject var viewModel: MainNavigationViewModel
|
@StateObject var viewModel: MainNavigationViewModel
|
||||||
|
@EnvironmentObject var rootViewModel: RootViewModel
|
||||||
|
@Environment(\.displayScale) var displayScale: CGFloat
|
||||||
|
|
||||||
var sidebar: some View {
|
var sidebar: some View {
|
||||||
List(selection: $viewModel.selectedTab) {
|
List(selection: $viewModel.selectedTab) {
|
||||||
|
@ -18,7 +20,10 @@ struct SidebarNavigation: View {
|
||||||
.tag(tab)
|
.tag(tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay(Pocket().environmentObject(viewModel), alignment: .bottom)
|
.overlay(Pocket()
|
||||||
|
.environmentObject(viewModel)
|
||||||
|
.environmentObject(rootViewModel),
|
||||||
|
alignment: .bottom)
|
||||||
.listStyle(SidebarListStyle())
|
.listStyle(SidebarListStyle())
|
||||||
.onAppear(perform: viewModel.refreshIdentity)
|
.onAppear(perform: viewModel.refreshIdentity)
|
||||||
.onReceive(NotificationCenter.default
|
.onReceive(NotificationCenter.default
|
||||||
|
@ -51,18 +56,19 @@ private extension SidebarNavigation {
|
||||||
|
|
||||||
struct Pocket: View {
|
struct Pocket: View {
|
||||||
@EnvironmentObject var viewModel: MainNavigationViewModel
|
@EnvironmentObject var viewModel: MainNavigationViewModel
|
||||||
|
@EnvironmentObject var rootViewModel: RootViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Divider()
|
Divider()
|
||||||
Button(action: { viewModel.presentingSettings.toggle() }) {
|
Button(action: { viewModel.presentingSettings.toggle() }) {
|
||||||
KFImage(viewModel.image,
|
KFImage(viewModel.identity.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)))
|
.append(another: RoundCornerImageProcessor(radius: .widthFraction(0.5)))
|
||||||
),
|
),
|
||||||
.scaleFactor(Screen.scale),
|
.scaleFactor(displayScale),
|
||||||
.cacheOriginalImage
|
.cacheOriginalImage
|
||||||
])
|
])
|
||||||
.placeholder { Image(systemName: "gear") }
|
.placeholder { Image(systemName: "gear") }
|
||||||
|
@ -80,6 +86,7 @@ private extension SidebarNavigation {
|
||||||
.sheet(isPresented: $viewModel.presentingSettings) {
|
.sheet(isPresented: $viewModel.presentingSettings) {
|
||||||
SettingsView(viewModel: viewModel.settingsViewModel())
|
SettingsView(viewModel: viewModel.settingsViewModel())
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
|
.environmentObject(rootViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,6 +96,7 @@ private extension SidebarNavigation {
|
||||||
struct SidebarNavigation_Previews: PreviewProvider {
|
struct SidebarNavigation_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SidebarNavigation(viewModel: .development)
|
SidebarNavigation(viewModel: .development)
|
||||||
|
.environmentObject(RootViewModel.development)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in New Issue