From b80fd9146a47875ebff3a231f93cc9aacbebbdbb Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Fri, 28 Aug 2020 20:50:58 -0700 Subject: [PATCH] Lists --- Databases/ContentDatabase.swift | 33 ++++++++++ Development Assets/DevelopmentModels.swift | 4 ++ Localizations/Localizable.strings | 4 +- Metatext.xcodeproj/project.pbxproj | 16 +++++ Model/Timeline.swift | 30 +++++---- .../Endpoints/DeletionEndpoint.swift | 13 +++- .../Mastodon API/Endpoints/ListEndpoint.swift | 36 +++++++++++ .../Endpoints/ListsEndpoint.swift | 19 ++++++ Services/IdentityService.swift | 23 +++++++ View Models/ListsViewModel.swift | 54 ++++++++++++++++ .../SecondaryNavigationViewModel.swift | 4 ++ View Models/TabNavigationViewModel.swift | 22 ++++--- Views/IdentitiesView.swift | 2 +- Views/ListsView.swift | 64 +++++++++++++++++++ Views/SecondaryNavigationView.swift | 10 ++- Views/TabNavigationView.swift | 4 +- 16 files changed, 306 insertions(+), 32 deletions(-) create mode 100644 Networking/Mastodon API/Endpoints/ListEndpoint.swift create mode 100644 Networking/Mastodon API/Endpoints/ListsEndpoint.swift create mode 100644 View Models/ListsViewModel.swift create mode 100644 Views/ListsView.swift diff --git a/Databases/ContentDatabase.swift b/Databases/ContentDatabase.swift index 18c2f9f..043f0bf 100644 --- a/Databases/ContentDatabase.swift +++ b/Databases/ContentDatabase.swift @@ -45,6 +45,30 @@ extension ContentDatabase { .eraseToAnyPublisher() } + func updateLists(_ lists: [MastodonList]) -> AnyPublisher { + databaseQueue.writePublisher { + for list in lists { + try Timeline.list(list).save($0) + } + + try Timeline.filter(!(Timeline.nonLists.map(\.id) + lists.map(\.id)).contains(Column("id"))).deleteAll($0) + } + .ignoreOutput() + .eraseToAnyPublisher() + } + + func createList(_ list: MastodonList) -> AnyPublisher { + databaseQueue.writePublisher(updates: Timeline.list(list).save) + .ignoreOutput() + .eraseToAnyPublisher() + } + + func deleteList(id: String) -> AnyPublisher { + databaseQueue.writePublisher(updates: Timeline.filter(Column("id") == id).deleteAll) + .ignoreOutput() + .eraseToAnyPublisher() + } + func statusesObservation(timeline: Timeline) -> AnyPublisher<[Status], Error> { ValueObservation .tracking(timeline.statuses @@ -78,6 +102,15 @@ extension ContentDatabase { .map { $0.map(Status.init(statusResult:)) } .eraseToAnyPublisher() } + + func listsObservation() -> AnyPublisher<[Timeline], Error> { + ValueObservation.tracking(Timeline.filter(!Timeline.nonLists.map(\.id).contains(Column("id"))) + .order(Column("listTitle").collating(.localizedCaseInsensitiveCompare).asc) + .fetchAll) + .removeDuplicates() + .publisher(in: databaseQueue) + .eraseToAnyPublisher() + } } private extension ContentDatabase { diff --git a/Development Assets/DevelopmentModels.swift b/Development Assets/DevelopmentModels.swift index b4bec4e..9034ac0 100644 --- a/Development Assets/DevelopmentModels.swift +++ b/Development Assets/DevelopmentModels.swift @@ -98,6 +98,10 @@ extension IdentitiesViewModel { static let development = IdentitiesViewModel(identityService: .development) } +extension ListsViewModel { + static let development = ListsViewModel(identityService: .development) +} + extension PreferencesViewModel { static let development = PreferencesViewModel(identityService: .development) } diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index 8e3f470..4e5aefd 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -1,13 +1,15 @@ // Copyright © 2020 Metabolist. All rights reserved. +"add" = "Add"; "apns-default-message" = "New notification"; "add-identity.instance-url" = "Instance URL"; "add-identity.log-in" = "Log in"; "add-identity.browse-anonymously" = "Browse anonymously"; "oauth.error.code-not-found" = "OAuth error: code not found"; "secondary-navigation.manage-accounts" = "Manage Accounts"; +"secondary-navigation.lists" = "Lists"; "secondary-navigation.preferences" = "Preferences"; -"identities.add" = "Add"; +"lists.new-list-title" = "New List Title"; "preferences" = "Preferences"; "preferences.posting-reading" = "Posting and Reading"; "preferences.posting" = "Posting"; diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index fcb18a7..68864e7 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -29,6 +29,10 @@ D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; }; D0BEB1F524F9A216001B0F04 /* Paged.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F424F9A216001B0F04 /* Paged.swift */; }; D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */; }; + D0BEB1F924F9D627001B0F04 /* ListsEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F824F9D627001B0F04 /* ListsEndpoint.swift */; }; + D0BEB1FD24F9E4E5001B0F04 /* ListsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */; }; + D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; }; + D0BEB20124FA0220001B0F04 /* ListEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20024FA0220001B0F04 /* ListEndpoint.swift */; }; D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; }; D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; }; D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.swift */; }; @@ -198,6 +202,10 @@ D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = ""; }; D0BEB1F424F9A216001B0F04 /* Paged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paged.swift; sourceTree = ""; }; D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = ""; }; + D0BEB1F824F9D627001B0F04 /* ListsEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsEndpoint.swift; sourceTree = ""; }; + D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsViewModel.swift; sourceTree = ""; }; + D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = ""; }; + D0BEB20024FA0220001B0F04 /* ListEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListEndpoint.swift; sourceTree = ""; }; D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = ""; }; D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D0C7D42224F76169001EBDBB /* IdentitiesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesView.swift; sourceTree = ""; }; @@ -424,6 +432,7 @@ D01F41E024F8885900D55A2D /* Attachments */, D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */, D0C7D42224F76169001EBDBB /* IdentitiesView.swift */, + D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */, D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */, D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */, D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */, @@ -509,6 +518,7 @@ D0C7D46024F76169001EBDBB /* AddIdentityViewModel.swift */, D01F41DE24F8868800D55A2D /* AttachmentViewModel.swift */, D0C7D45F24F76169001EBDBB /* IdentitiesViewModel.swift */, + D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */, D0C7D45D24F76169001EBDBB /* NotificationTypesPreferencesViewModel.swift */, D0C7D45A24F76169001EBDBB /* PostingReadingPreferencesViewModel.swift */, D0C7D46124F76169001EBDBB /* PreferencesViewModel.swift */, @@ -582,6 +592,8 @@ D0C7D48324F76169001EBDBB /* ContextEndpoint.swift */, D0C7D48224F76169001EBDBB /* DeletionEndpoint.swift */, D0C7D47D24F76169001EBDBB /* InstanceEndpoint.swift */, + D0BEB20024FA0220001B0F04 /* ListEndpoint.swift */, + D0BEB1F824F9D627001B0F04 /* ListsEndpoint.swift */, D0BEB1F424F9A216001B0F04 /* Paged.swift */, D0C7D47C24F76169001EBDBB /* PreferencesEndpoint.swift */, D0C7D47B24F76169001EBDBB /* PushSubscriptionEndpoint.swift */, @@ -908,6 +920,7 @@ D0C7D4B324F7616A001EBDBB /* MastodonError.swift in Sources */, D0C7D4E924F7616A001EBDBB /* AccessTokenEndpoint.swift in Sources */, D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */, + D0BEB20124FA0220001B0F04 /* ListEndpoint.swift in Sources */, D0C7D4BA24F7616A001EBDBB /* AppAuthorization.swift in Sources */, D0C7D4AB24F7616A001EBDBB /* Identity.swift in Sources */, D0C7D4C024F7616A001EBDBB /* AlertItem.swift in Sources */, @@ -925,6 +938,7 @@ D0C7D4DC24F7616A001EBDBB /* Data+Extensions.swift in Sources */, D0DC177724D0CF2600A75C65 /* MockKeychainService.swift in Sources */, D0DC174624CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */, + D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */, D0C7D4F824F7616A001EBDBB /* SecretsService.swift in Sources */, D0C7D4DE24F7616A001EBDBB /* HTTPTarget.swift in Sources */, D0C7D4F624F7616A001EBDBB /* KeychainService.swift in Sources */, @@ -946,6 +960,7 @@ D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */, D0C7D4E424F7616A001EBDBB /* PreferencesEndpoint.swift in Sources */, D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */, + D0BEB1FD24F9E4E5001B0F04 /* ListsViewModel.swift in Sources */, D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */, D0C7D4B124F7616A001EBDBB /* Card.swift in Sources */, D0C7D4F324F7616A001EBDBB /* ContextService.swift in Sources */, @@ -959,6 +974,7 @@ D0C7D4EC24F7616A001EBDBB /* StatusEndpoint.swift in Sources */, D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */, D0C7D4BB24F7616A001EBDBB /* Emoji.swift in Sources */, + D0BEB1F924F9D627001B0F04 /* ListsEndpoint.swift in Sources */, D0C7D4AD24F7616A001EBDBB /* AccessToken.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Model/Timeline.swift b/Model/Timeline.swift index 015d946..9584c4f 100644 --- a/Model/Timeline.swift +++ b/Model/Timeline.swift @@ -2,7 +2,7 @@ import Foundation -enum Timeline: Identifiable { +enum Timeline: Hashable { case home case local case federated @@ -10,18 +10,7 @@ enum Timeline: Identifiable { } extension Timeline { - var id: String { - switch self { - case .home: - return "home" - case .local: - return "local" - case .federated: - return "federated" - case let .list(list): - return list.id - } - } + static let nonLists: [Timeline] = [.home, .local, .federated] var endpoint: TimelinesEndpoint { switch self { @@ -36,3 +25,18 @@ extension Timeline { } } } + +extension Timeline: Identifiable { + var id: String { + switch self { + case .home: + return "home" + case .local: + return "local" + case .federated: + return "federated" + case let .list(list): + return list.id + } + } +} diff --git a/Networking/Mastodon API/Endpoints/DeletionEndpoint.swift b/Networking/Mastodon API/Endpoints/DeletionEndpoint.swift index b19f8a4..c9146b6 100644 --- a/Networking/Mastodon API/Endpoints/DeletionEndpoint.swift +++ b/Networking/Mastodon API/Endpoints/DeletionEndpoint.swift @@ -4,6 +4,7 @@ import Foundation enum DeletionEndpoint { case oauthRevoke(token: String, clientID: String, clientSecret: String) + case list(id: String) } extension DeletionEndpoint: MastodonEndpoint { @@ -12,14 +13,18 @@ extension DeletionEndpoint: MastodonEndpoint { var context: [String] { switch self { case .oauthRevoke: - return [] + return ["oauth"] + case .list: + return defaultContext + ["lists"] } } var pathComponentsInContext: [String] { switch self { case .oauthRevoke: - return ["oauth", "revoke"] + return ["revoke"] + case let .list(id): + return [id] } } @@ -27,6 +32,8 @@ extension DeletionEndpoint: MastodonEndpoint { switch self { case .oauthRevoke: return .post + case .list: + return .delete } } @@ -34,6 +41,8 @@ extension DeletionEndpoint: MastodonEndpoint { switch self { case let .oauthRevoke(token, clientID, clientSecret): return ["token": token, "client_id": clientID, "client_secret": clientSecret] + case .list: + return nil } } } diff --git a/Networking/Mastodon API/Endpoints/ListEndpoint.swift b/Networking/Mastodon API/Endpoints/ListEndpoint.swift new file mode 100644 index 0000000..835f74a --- /dev/null +++ b/Networking/Mastodon API/Endpoints/ListEndpoint.swift @@ -0,0 +1,36 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +enum ListEndpoint { + case create(title: String) +} + +extension ListEndpoint: MastodonEndpoint { + typealias ResultType = MastodonList + + var context: [String] { + defaultContext + ["lists"] + } + + var pathComponentsInContext: [String] { + switch self { + case .create: + return [] + } + } + + var parameters: [String : Any]? { + switch self { + case let .create(title): + return ["title": title] + } + } + + var method: HTTPMethod { + switch self { + case .create: + return .post + } + } +} diff --git a/Networking/Mastodon API/Endpoints/ListsEndpoint.swift b/Networking/Mastodon API/Endpoints/ListsEndpoint.swift new file mode 100644 index 0000000..ee3afaa --- /dev/null +++ b/Networking/Mastodon API/Endpoints/ListsEndpoint.swift @@ -0,0 +1,19 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +enum ListsEndpoint { + case lists +} + +extension ListsEndpoint: MastodonEndpoint { + typealias ResultType = [MastodonList] + + var pathComponentsInContext: [String] { + ["lists"] + } + + var method: HTTPMethod { + .get + } +} diff --git a/Services/IdentityService.swift b/Services/IdentityService.swift index c2c543d..ee92e6a 100644 --- a/Services/IdentityService.swift +++ b/Services/IdentityService.swift @@ -86,6 +86,29 @@ extension IdentityService { identityDatabase.recentIdentitiesObservation(excluding: identity.id) } + func refreshLists() -> AnyPublisher { + networkClient.request(ListsEndpoint.lists) + .flatMap(contentDatabase.updateLists(_:)) + .eraseToAnyPublisher() + } + + func listsObservation() -> AnyPublisher<[Timeline], Error> { + contentDatabase.listsObservation() + } + + func createList(title: String) -> AnyPublisher { + networkClient.request(ListEndpoint.create(title: title)) + .flatMap(contentDatabase.createList(_:)) + .eraseToAnyPublisher() + } + + func deleteList(id: String) -> AnyPublisher { + networkClient.request(DeletionEndpoint.list(id: id)) + .map { _ in id } + .flatMap(contentDatabase.deleteList(id:)) + .eraseToAnyPublisher() + } + func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher { identityDatabase.updatePreferences(preferences, forIdentityID: identity.id) .zip(Just(self).first().setFailureType(to: Error.self)) diff --git a/View Models/ListsViewModel.swift b/View Models/ListsViewModel.swift new file mode 100644 index 0000000..5f89482 --- /dev/null +++ b/View Models/ListsViewModel.swift @@ -0,0 +1,54 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import Combine + +class ListsViewModel: ObservableObject { + @Published private(set) var lists = [MastodonList]() + @Published private(set) var creatingList = false + @Published var alertItem: AlertItem? + + private let identityService: IdentityService + private var cancellables = Set() + + init(identityService: IdentityService) { + self.identityService = identityService + + identityService.listsObservation() + .map { + $0.compactMap { + guard case let .list(list) = $0 else { return nil } + + return list + } + } + .assignErrorsToAlertItem(to: \.alertItem, on: self) + .assign(to: &$lists) + } +} + +extension ListsViewModel { + func refreshLists() { + identityService.refreshLists() + .assignErrorsToAlertItem(to: \.alertItem, on: self) + .sink { _ in } + .store(in: &cancellables) + } + + func createList(title: String) { + identityService.createList(title: title) + .assignErrorsToAlertItem(to: \.alertItem, on: self) + .handleEvents( + receiveSubscription: { [weak self] _ in self?.creatingList = true }, + receiveCompletion: { [weak self] _ in self?.creatingList = false }) + .sink { _ in } + .store(in: &cancellables) + } + + func delete(list: MastodonList) { + identityService.deleteList(id: list.id) + .assignErrorsToAlertItem(to: \.alertItem, on: self) + .sink { _ in } + .store(in: &cancellables) + } +} diff --git a/View Models/SecondaryNavigationViewModel.swift b/View Models/SecondaryNavigationViewModel.swift index 4b73ef6..7f787c3 100644 --- a/View Models/SecondaryNavigationViewModel.swift +++ b/View Models/SecondaryNavigationViewModel.swift @@ -18,6 +18,10 @@ extension SecondaryNavigationViewModel { IdentitiesViewModel(identityService: identityService) } + func listsViewModel() -> ListsViewModel { + ListsViewModel(identityService: identityService) + } + func preferencesViewModel() -> PreferencesViewModel { PreferencesViewModel(identityService: identityService) } diff --git a/View Models/TabNavigationViewModel.swift b/View Models/TabNavigationViewModel.swift index 8ceb0b5..9db22fe 100644 --- a/View Models/TabNavigationViewModel.swift +++ b/View Models/TabNavigationViewModel.swift @@ -6,8 +6,8 @@ import Combine class TabNavigationViewModel: ObservableObject { @Published private(set) var identity: Identity @Published private(set) var recentIdentities = [Identity]() - @Published private(set) var timeline = Timeline.home - @Published private(set) var timelinesAndLists = TabNavigationViewModel.timelines + @Published var timeline = Timeline.home + @Published private(set) var timelinesAndLists = Timeline.nonLists @Published var presentingSecondaryNavigation = false @Published var alertItem: AlertItem? var selectedTab: Tab? = .timelines @@ -23,6 +23,11 @@ class TabNavigationViewModel: ObservableObject { identityService.recentIdentitiesObservation() .assignErrorsToAlertItem(to: \.alertItem, on: self) .assign(to: &$recentIdentities) + + identityService.listsObservation() + .map { Timeline.nonLists + $0 } + .assignErrorsToAlertItem(to: \.alertItem, on: self) + .assign(to: &$timelinesAndLists) } } @@ -65,6 +70,11 @@ extension TabNavigationViewModel { .sink { _ in } .store(in: &cancellables) + identityService.refreshLists() + .assignErrorsToAlertItem(to: \.alertItem, on: self) + .sink { _ in } + .store(in: &cancellables) + if identity.preferences.useServerPostingReadingPreferences { identityService.refreshServerPreferences() .assignErrorsToAlertItem(to: \.alertItem, on: self) @@ -86,14 +96,6 @@ extension TabNavigationViewModel { func viewModel(timeline: Timeline) -> StatusListViewModel { StatusListViewModel(statusListService: identityService.service(timeline: timeline)) } - - func select(timeline: Timeline) { - self.timeline = timeline - } -} - -private extension TabNavigationViewModel { - static let timelines: [Timeline] = [.home, .local, .federated] } extension TabNavigationViewModel { diff --git a/Views/IdentitiesView.swift b/Views/IdentitiesView.swift index e5da94e..2ef228f 100644 --- a/Views/IdentitiesView.swift +++ b/Views/IdentitiesView.swift @@ -14,7 +14,7 @@ struct IdentitiesView: View { NavigationLink( destination: AddIdentityView(viewModel: rootViewModel.addIdentityViewModel()), label: { - Label("identities.add", systemImage: "plus.circle") + Label("add", systemImage: "plus.circle") }) } Section { diff --git a/Views/ListsView.swift b/Views/ListsView.swift new file mode 100644 index 0000000..9c220e4 --- /dev/null +++ b/Views/ListsView.swift @@ -0,0 +1,64 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import SwiftUI + +struct ListsView: View { + @StateObject var viewModel: ListsViewModel + @EnvironmentObject var tabNavigationViewModel: TabNavigationViewModel + @State private var newListTitle = "" + + var body: some View { + Form { + Section { + TextField("lists.new-list-title", text: $newListTitle) + .disabled(viewModel.creatingList) + if viewModel.creatingList { + ProgressView() + .frame(maxWidth: .infinity, alignment: .center) + } else { + Button { + viewModel.createList(title: newListTitle) + } label: { + Label("add", systemImage: "plus.circle") + } + .disabled(newListTitle == "") + } + } + Section { + ForEach(viewModel.lists) { list in + Button(list.title) { + tabNavigationViewModel.timeline = .list(list) + tabNavigationViewModel.presentingSecondaryNavigation = false + } + } + .onDelete { + guard let index = $0.first else { return } + + viewModel.delete(list: viewModel.lists[index]) + } + } + } + .navigationTitle(Text("secondary-navigation.lists")) + .toolbar { + ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) { + EditButton() + } + } + .alertItem($viewModel.alertItem) + .onAppear(perform: viewModel.refreshLists) + .onReceive(viewModel.$creatingList) { + if !$0 { + newListTitle = "" + } + } + } +} + +#if DEBUG +struct ListsView_Previews: PreviewProvider { + static var previews: some View { + ListsView(viewModel: .development) + .environmentObject(TabNavigationViewModel.development) + } +} +#endif diff --git a/Views/SecondaryNavigationView.swift b/Views/SecondaryNavigationView.swift index b8b7ade..c5ac196 100644 --- a/Views/SecondaryNavigationView.swift +++ b/Views/SecondaryNavigationView.swift @@ -5,7 +5,6 @@ import KingfisherSwiftUI struct SecondaryNavigationView: View { @StateObject var viewModel: SecondaryNavigationViewModel - @EnvironmentObject var rootViewModel: RootViewModel @Environment(\.presentationMode) var presentationMode @Environment(\.displayScale) var displayScale: CGFloat @@ -14,8 +13,7 @@ struct SecondaryNavigationView: View { Form { Section { NavigationLink( - destination: IdentitiesView(viewModel: viewModel.identitiesViewModel()) - .environmentObject(rootViewModel), + destination: IdentitiesView(viewModel: viewModel.identitiesViewModel()), label: { HStack { KFImage(viewModel.identity.image, @@ -40,6 +38,11 @@ struct SecondaryNavigationView: View { } }) } + Section { + NavigationLink(destination: ListsView(viewModel: viewModel.listsViewModel())) { + Label("secondary-navigation.lists", systemImage: "scroll") + } + } Section { NavigationLink( "secondary-navigation.preferences", @@ -67,6 +70,7 @@ struct SecondaryNavigationView_Previews: PreviewProvider { static var previews: some View { SecondaryNavigationView(viewModel: .development) .environmentObject(RootViewModel.development) + .environmentObject(TabNavigationViewModel.development) } } #endif diff --git a/Views/TabNavigationView.swift b/Views/TabNavigationView.swift index 3a376ae..133a60d 100644 --- a/Views/TabNavigationView.swift +++ b/Views/TabNavigationView.swift @@ -24,7 +24,7 @@ struct TabNavigationView: View { } .sheet(isPresented: $viewModel.presentingSecondaryNavigation) { SecondaryNavigationView(viewModel: viewModel.secondaryNavigationViewModel()) - .environmentObject(rootViewModel) + .environmentObject(viewModel) } .alertItem($viewModel.alertItem) .onAppear(perform: viewModel.refreshIdentity) @@ -60,7 +60,7 @@ private extension TabNavigationView { trailing: Menu { ForEach(viewModel.timelinesAndLists) { timeline in Button { - viewModel.select(timeline: timeline) + viewModel.timeline = timeline } label: { Label(viewModel.title(timeline: timeline), systemImage: viewModel.systemImageName(timeline: timeline))