diff --git a/Pachyderm/Sources/Pachyderm/Model/List.swift b/Pachyderm/Sources/Pachyderm/Model/List.swift index 69c1da52..6b51f63d 100644 --- a/Pachyderm/Sources/Pachyderm/Model/List.swift +++ b/Pachyderm/Sources/Pachyderm/Model/List.swift @@ -17,11 +17,12 @@ public class List: Decodable, Equatable, Hashable { } public static func ==(lhs: List, rhs: List) -> Bool { - return lhs.id == rhs.id + return lhs.id == rhs.id && lhs.title == rhs.title } public func hash(into hasher: inout Hasher) { hasher.combine(id) + hasher.combine(title) } public static func getAccounts(_ list: List, range: RequestRange = .default) -> Request<[Account]> { diff --git a/Tusker/API/CreateListService.swift b/Tusker/API/CreateListService.swift index 8dbb7d8b..90381632 100644 --- a/Tusker/API/CreateListService.swift +++ b/Tusker/API/CreateListService.swift @@ -49,7 +49,7 @@ class CreateListService { do { let request = Client.createList(title: title) let (list, _) = try await mastodonController.run(request) - NotificationCenter.default.post(name: .listsChanged, object: nil) + mastodonController.addedList(list) self.didCreateList?(list) } catch { let alert = UIAlertController(title: "Error Creating List", message: error.localizedDescription, preferredStyle: .alert) @@ -64,7 +64,3 @@ class CreateListService { } } - -extension Foundation.Notification.Name { - static let listsChanged = Notification.Name("listsChanged") -} diff --git a/Tusker/API/DeleteListService.swift b/Tusker/API/DeleteListService.swift index 95e22c37..5b4ef56a 100644 --- a/Tusker/API/DeleteListService.swift +++ b/Tusker/API/DeleteListService.swift @@ -50,7 +50,7 @@ class DeleteListService { do { let request = List.delete(list) _ = try await mastodonController.run(request) - NotificationCenter.default.post(name: .listsChanged, object: nil) + mastodonController.deletedList(list) } catch { let alert = UIAlertController(title: "Error Deleting List", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) diff --git a/Tusker/API/MastodonController.swift b/Tusker/API/MastodonController.swift index 3b1c7584..0fcf8a85 100644 --- a/Tusker/API/MastodonController.swift +++ b/Tusker/API/MastodonController.swift @@ -46,6 +46,7 @@ class MastodonController: ObservableObject { @Published private(set) var instance: Instance! @Published private(set) var nodeInfo: NodeInfo! @Published private(set) var instanceFeatures = InstanceFeatures() + @Published private(set) var lists: [List] = [] private(set) var customEmojis: [Emoji]? private var pendingOwnInstanceRequestCallbacks = [(Result) -> Void]() @@ -119,6 +120,15 @@ class MastodonController: ObservableObject { }) } + func initialize() async throws { + async let ownAccount = try getOwnAccount() + async let ownInstance = try getOwnInstance() + + _ = try await (ownAccount, ownInstance) + + loadLists() + } + func getOwnAccount(completion: ((Result) -> Void)? = nil) { if account != nil { completion?(.success(account)) @@ -264,4 +274,53 @@ class MastodonController: ObservableObject { } } + private func loadLists() { + let req = Client.getLists() + run(req) { response in + if case .success(let lists, _) = response { + DispatchQueue.main.async { + self.lists = lists.sorted(using: ListComparator()) + } + } + } + } + + @MainActor + func addedList(_ list: List) { + var new = self.lists + new.append(list) + new.sort { $0.title < $1.title } + self.lists = new + } + + @MainActor + func deletedList(_ list: List) { + self.lists.removeAll(where: { $0.id == list.id }) + } + + @MainActor + func renamedList(_ list: List) { + var new = self.lists + if let index = new.firstIndex(where: { $0.id == list.id }) { + new[index] = list + } + new.sort(using: ListComparator()) + self.lists = new + } + +} + +private struct ListComparator: SortComparator { + typealias Compared = List + + var underlying = String.Comparator(options: .caseInsensitive) + + var order: SortOrder { + get { underlying.order } + set { underlying.order = newValue } + } + + func compare(_ lhs: List, _ rhs: List) -> ComparisonResult { + return underlying.compare(lhs.title, rhs.title) + } } diff --git a/Tusker/API/RenameListService.swift b/Tusker/API/RenameListService.swift index 835a6245..ea435b0f 100644 --- a/Tusker/API/RenameListService.swift +++ b/Tusker/API/RenameListService.swift @@ -49,7 +49,7 @@ class RenameListService { do { let req = List.update(list, title: title) let (list, _) = try await mastodonController.run(req) - NotificationCenter.default.post(name: .listRenamed, object: list.id, userInfo: ["list": list]) + mastodonController.renamedList(list) } catch { let alert = UIAlertController(title: "Error Updating List", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) @@ -63,7 +63,3 @@ class RenameListService { } } - -extension Foundation.Notification.Name { - static let listRenamed = Notification.Name("listRenamed") -} diff --git a/Tusker/Scenes/AuxiliarySceneDelegate.swift b/Tusker/Scenes/AuxiliarySceneDelegate.swift index 8bd042f5..e49e6be1 100644 --- a/Tusker/Scenes/AuxiliarySceneDelegate.swift +++ b/Tusker/Scenes/AuxiliarySceneDelegate.swift @@ -42,9 +42,9 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate { let controller = MastodonController.getForAccount(account) session.mastodonController = controller - - controller.getOwnAccount() - controller.getOwnInstance() + Task { + try? await controller.initialize() + } guard let rootVC = viewController(for: activity, mastodonController: controller) else { UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil) diff --git a/Tusker/Scenes/ComposeSceneDelegate.swift b/Tusker/Scenes/ComposeSceneDelegate.swift index 1c3b339d..a23dbe87 100644 --- a/Tusker/Scenes/ComposeSceneDelegate.swift +++ b/Tusker/Scenes/ComposeSceneDelegate.swift @@ -50,8 +50,9 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate { } session.mastodonController = controller - controller.getOwnAccount() - controller.getOwnInstance() + Task { + try? await controller.initialize() + } let composeVC = ComposeHostingController(draft: draft, mastodonController: controller) composeVC.delegate = self diff --git a/Tusker/Scenes/MainSceneDelegate.swift b/Tusker/Scenes/MainSceneDelegate.swift index b14504c9..4d1dc493 100644 --- a/Tusker/Scenes/MainSceneDelegate.swift +++ b/Tusker/Scenes/MainSceneDelegate.swift @@ -203,8 +203,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate func createAppUI() -> TuskerRootViewController { let mastodonController = window!.windowScene!.session.mastodonController! - mastodonController.getOwnAccount() - mastodonController.getOwnInstance() + Task { + try? await mastodonController.initialize() + } let split = MainSplitViewController(mastodonController: mastodonController) if UIDevice.current.userInterfaceIdiom == .phone, diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index e41584e5..160f3c8f 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -24,6 +24,8 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate { var searchControllerStatusOnAppearance: Bool? = nil + private var listsCancellable: AnyCancellable? + init(mastodonController: MastodonController) { self.mastodonController = mastodonController @@ -70,9 +72,10 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate { NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(reloadLists), name: .listsChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(listRenamed(_:)), name: .listRenamed, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) + + listsCancellable = mastodonController.$lists + .sink { [unowned self] in self.reloadLists($0) } } override func viewWillAppear(_ animated: Bool) { @@ -158,7 +161,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate { snapshot.appendItems([.findInstance], toSection: .savedInstances) dataSource.apply(snapshot, animatingDifferences: false) - reloadLists() + reloadLists(mastodonController.lists) } private func addDiscoverSection(to snapshot: inout NSDiffableDataSourceSnapshot) { @@ -180,39 +183,13 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate { self.dataSource.apply(snapshot) } - @objc private func reloadLists() { - let request = Client.getLists() - mastodonController.run(request) { (response) in - guard case let .success(lists, _) = response else { - return - } - - var snapshot = self.dataSource.snapshot() - snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .lists)) - snapshot.appendItems(lists.map { .list($0) }, toSection: .lists) - snapshot.appendItems([.addList], toSection: .lists) - - DispatchQueue.main.async { - self.dataSource.apply(snapshot) - } - } - } - - @objc private func listRenamed(_ notification: Foundation.Notification) { - let list = notification.userInfo!["list"] as! List - var snapshot = dataSource.snapshot() - let existing = snapshot.itemIdentifiers(inSection: .lists).first(where: { - if case .list(let existingList) = $0, existingList.id == list.id { - return true - } else { - return false - } - }) - if let existing { - snapshot.insertItems([.list(list)], afterItem: existing) - snapshot.deleteItems([existing]) - dataSource.apply(snapshot) - } + private func reloadLists(_ lists: [List]) { + var snapshot = self.dataSource.snapshot() + snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .lists)) + snapshot.appendItems(lists.map { .list($0) }, toSection: .lists) + snapshot.appendItems([.addList], toSection: .lists) + + self.dataSource.apply(snapshot) } @MainActor diff --git a/Tusker/Screens/Lists/EditListAccountsViewController.swift b/Tusker/Screens/Lists/EditListAccountsViewController.swift index a6f20a77..ff1055c9 100644 --- a/Tusker/Screens/Lists/EditListAccountsViewController.swift +++ b/Tusker/Screens/Lists/EditListAccountsViewController.swift @@ -8,6 +8,7 @@ import UIKit import Pachyderm +import Combine class EditListAccountsViewController: EnhancedTableViewController { @@ -22,6 +23,8 @@ class EditListAccountsViewController: EnhancedTableViewController { var searchResultsController: EditListSearchResultsContainerViewController! var searchController: UISearchController! + private var listRenamedCancellable: AnyCancellable? + init(list: List, mastodonController: MastodonController) { self.list = list self.mastodonController = mastodonController @@ -30,7 +33,13 @@ class EditListAccountsViewController: EnhancedTableViewController { listChanged() - NotificationCenter.default.addObserver(self, selector: #selector(listRenamed(_:)), name: .listRenamed, object: list.id) + listRenamedCancellable = mastodonController.$lists + .compactMap { $0.first { $0.id == list.id } } + .removeDuplicates(by: { $0.title == $1.title }) + .sink { [unowned self] in + self.list = $0 + self.listChanged() + } } required init?(coder: NSCoder) { @@ -88,12 +97,6 @@ class EditListAccountsViewController: EnhancedTableViewController { title = String(format: NSLocalizedString("Edit %@", comment: "edit list screen title"), list.title) } - @objc private func listRenamed(_ notification: Foundation.Notification) { - let list = notification.userInfo!["list"] as! List - self.list = list - self.listChanged() - } - func loadAccounts() async { do { let request = List.getAccounts(list) diff --git a/Tusker/Screens/Lists/ListTimelineViewController.swift b/Tusker/Screens/Lists/ListTimelineViewController.swift index 69bc51cc..53b4a633 100644 --- a/Tusker/Screens/Lists/ListTimelineViewController.swift +++ b/Tusker/Screens/Lists/ListTimelineViewController.swift @@ -8,6 +8,7 @@ import UIKit import Pachyderm +import Combine class ListTimelineViewController: TimelineViewController { @@ -15,6 +16,8 @@ class ListTimelineViewController: TimelineViewController { var presentEditOnAppear = false + private var listRenamedCancellable: AnyCancellable? + init(for list: List, mastodonController: MastodonController) { self.list = list @@ -22,7 +25,13 @@ class ListTimelineViewController: TimelineViewController { listChanged() - NotificationCenter.default.addObserver(self, selector: #selector(listRenamed(_:)), name: .listRenamed, object: list.id) + listRenamedCancellable = mastodonController.$lists + .compactMap { $0.first { $0.id == list.id } } + .removeDuplicates(by: { $0.title == $1.title }) + .sink { [unowned self] in + self.list = $0 + self.listChanged() + } } required init?(coder aDecoder: NSCoder) { @@ -47,12 +56,6 @@ class ListTimelineViewController: TimelineViewController { title = list.title } - @objc private func listRenamed(_ notification: Foundation.Notification) { - let list = notification.userInfo!["list"] as! List - self.list = list - self.listChanged() - } - func presentEdit(animated: Bool) { let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController) editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(editListDoneButtonPressed)) diff --git a/Tusker/Screens/Main/MainSidebarViewController.swift b/Tusker/Screens/Main/MainSidebarViewController.swift index 2bfd1f8b..2878050a 100644 --- a/Tusker/Screens/Main/MainSidebarViewController.swift +++ b/Tusker/Screens/Main/MainSidebarViewController.swift @@ -8,6 +8,7 @@ import UIKit import Pachyderm +import Combine protocol MainSidebarViewControllerDelegate: AnyObject { func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) @@ -28,6 +29,8 @@ class MainSidebarViewController: UIViewController { private var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! + private var listsCancellable: AnyCancellable? + var allItems: [Item] { [ .tab(.timelines), @@ -99,10 +102,11 @@ class MainSidebarViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(reloadLists), name: .listsChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(listRenamed(_:)), name: .listRenamed, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) + listsCancellable = mastodonController.$lists + .sink { [unowned self] in self.reloadLists($0) } + onViewDidLoad?() } @@ -170,7 +174,7 @@ class MainSidebarViewController: UIViewController { dataSource.apply(snapshot, animatingDifferences: false) applyDiscoverSectionSnapshot() - reloadLists() + reloadLists(mastodonController.lists) reloadSavedHashtags() reloadSavedInstances() } @@ -203,42 +207,28 @@ class MainSidebarViewController: UIViewController { } } - @objc private func reloadLists() { - let request = Client.getLists() - mastodonController.run(request) { [weak self] (response) in - guard let self = self, case let .success(lists, _) = response else { return } - - var exploreSnapshot = NSDiffableDataSourceSectionSnapshot() - exploreSnapshot.append([.listsHeader]) - exploreSnapshot.expand([.listsHeader]) - exploreSnapshot.append(lists.map { .list($0) }, to: .listsHeader) - exploreSnapshot.append([.addList], to: .listsHeader) - DispatchQueue.main.async { - let selected = self.collectionView.indexPathsForSelectedItems?.first - - self.dataSource.apply(exploreSnapshot, to: .lists) { - if let selected = selected { - self.collectionView.selectItem(at: selected, animated: false, scrollPosition: .centeredVertically) - } - } + private func reloadLists(_ lists: [List]) { + var exploreSnapshot = NSDiffableDataSourceSectionSnapshot() + exploreSnapshot.append([.listsHeader]) + exploreSnapshot.expand([.listsHeader]) + exploreSnapshot.append(lists.map { .list($0) }, to: .listsHeader) + exploreSnapshot.append([.addList], to: .listsHeader) + var selectedItem: Item? + if let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first, + let item = dataSource.itemIdentifier(for: selectedIndexPath) { + if case .list(let list) = item, + let newList = lists.first(where: { $0.id == list.id }) { + selectedItem = .list(newList) + } else { + selectedItem = item } } - } - - @objc private func listRenamed(_ notification: Foundation.Notification) { - let list = notification.userInfo!["list"] as! List - var snapshot = dataSource.snapshot() - let existing = snapshot.itemIdentifiers(inSection: .lists).first(where: { - if case .list(let existingList) = $0, existingList.id == list.id { - return true - } else { - return false + + self.dataSource.apply(exploreSnapshot, to: .lists) { + if let selectedItem, + let indexPath = self.dataSource.indexPath(for: selectedItem) { + self.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically) } - }) - if let existing { - snapshot.insertItems([.list(list)], afterItem: existing) - snapshot.deleteItems([existing]) - dataSource.apply(snapshot) } }