mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
Lists
This commit is contained in:
parent
e5d7b0a12b
commit
b80fd9146a
16 changed files with 306 additions and 32 deletions
|
@ -45,6 +45,30 @@ extension ContentDatabase {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func updateLists(_ lists: [MastodonList]) -> AnyPublisher<Never, Error> {
|
||||
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<Never, Error> {
|
||||
databaseQueue.writePublisher(updates: Timeline.list(list).save)
|
||||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func deleteList(id: String) -> AnyPublisher<Never, Error> {
|
||||
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
D0BEB1F424F9A216001B0F04 /* Paged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paged.swift; sourceTree = "<group>"; };
|
||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = "<group>"; };
|
||||
D0BEB1F824F9D627001B0F04 /* ListsEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsEndpoint.swift; sourceTree = "<group>"; };
|
||||
D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsViewModel.swift; sourceTree = "<group>"; };
|
||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = "<group>"; };
|
||||
D0BEB20024FA0220001B0F04 /* ListEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListEndpoint.swift; sourceTree = "<group>"; };
|
||||
D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
|
||||
D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesView.swift; sourceTree = "<group>"; };
|
||||
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
36
Networking/Mastodon API/Endpoints/ListEndpoint.swift
Normal file
36
Networking/Mastodon API/Endpoints/ListEndpoint.swift
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
19
Networking/Mastodon API/Endpoints/ListsEndpoint.swift
Normal file
19
Networking/Mastodon API/Endpoints/ListsEndpoint.swift
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -86,6 +86,29 @@ extension IdentityService {
|
|||
identityDatabase.recentIdentitiesObservation(excluding: identity.id)
|
||||
}
|
||||
|
||||
func refreshLists() -> AnyPublisher<Never, Error> {
|
||||
networkClient.request(ListsEndpoint.lists)
|
||||
.flatMap(contentDatabase.updateLists(_:))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
||||
contentDatabase.listsObservation()
|
||||
}
|
||||
|
||||
func createList(title: String) -> AnyPublisher<Never, Error> {
|
||||
networkClient.request(ListEndpoint.create(title: title))
|
||||
.flatMap(contentDatabase.createList(_:))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func deleteList(id: String) -> AnyPublisher<Never, Error> {
|
||||
networkClient.request(DeletionEndpoint.list(id: id))
|
||||
.map { _ in id }
|
||||
.flatMap(contentDatabase.deleteList(id:))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Never, Error> {
|
||||
identityDatabase.updatePreferences(preferences, forIdentityID: identity.id)
|
||||
.zip(Just(self).first().setFailureType(to: Error.self))
|
||||
|
|
54
View Models/ListsViewModel.swift
Normal file
54
View Models/ListsViewModel.swift
Normal file
|
@ -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<AnyCancellable>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -18,6 +18,10 @@ extension SecondaryNavigationViewModel {
|
|||
IdentitiesViewModel(identityService: identityService)
|
||||
}
|
||||
|
||||
func listsViewModel() -> ListsViewModel {
|
||||
ListsViewModel(identityService: identityService)
|
||||
}
|
||||
|
||||
func preferencesViewModel() -> PreferencesViewModel {
|
||||
PreferencesViewModel(identityService: identityService)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
64
Views/ListsView.swift
Normal file
64
Views/ListsView.swift
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in a new issue