From afc2bfcf6b36f736dc9b3aa223d2c22310081d81 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 17 Dec 2019 22:56:53 -0500 Subject: [PATCH] Add list editing --- Pachyderm/Model/List.swift | 8 +- Tusker.xcodeproj/project.pbxproj | 4 + .../Explore/ExploreViewController.swift | 2 - .../EditListAccountsViewController.swift | 171 ++++++++++++++++++ .../Lists/ListTimelineViewController.swift | 23 +++ .../Search/SearchResultsViewController.swift | 40 +++- .../TimelineTableViewController.swift | 4 + .../Account Cell/AccountTableViewCell.xib | 10 +- 8 files changed, 248 insertions(+), 14 deletions(-) create mode 100644 Tusker/Screens/Lists/EditListAccountsViewController.swift diff --git a/Pachyderm/Model/List.swift b/Pachyderm/Model/List.swift index b53f78dc..0d141088 100644 --- a/Pachyderm/Model/List.swift +++ b/Pachyderm/Model/List.swift @@ -30,15 +30,15 @@ public class List: Decodable { return Request(method: .delete, path: "/api/v1/lists/\(list.id)") } - public static func add(_ list: List, accounts: [Account]) -> Request { + public static func add(_ list: List, accounts accountIDs: [String]) -> Request { return Request(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters( - "account_ids" => accounts.map { $0.id } + "account_ids" => accountIDs )) } - public static func remove(_ list: List, accounts: [Account]) -> Request { + public static func remove(_ list: List, accounts accountIDs: [String]) -> Request { return Request(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters( - "account_ids" => accounts.map { $0.id } + "account_ids" => accountIDs )) } diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 09937243..0994adbe 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */; }; D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */; }; D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; }; + D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */; }; D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF75217E923E00CC0648 /* DraftsManager.swift */; }; D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627FF78217E950100CC0648 /* DraftsTableViewController.xib */; }; D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */; }; @@ -357,6 +358,7 @@ D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BasicTableViewCell.xib; sourceTree = ""; }; D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksTableViewController.swift; sourceTree = ""; }; D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = ""; }; + D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = ""; }; D627FF75217E923E00CC0648 /* DraftsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsManager.swift; sourceTree = ""; }; D627FF78217E950100CC0648 /* DraftsTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DraftsTableViewController.xib; sourceTree = ""; }; D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsTableViewController.swift; sourceTree = ""; }; @@ -745,6 +747,7 @@ isa = PBXGroup; children = ( D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */, + D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */, ); path = Lists; sourceTree = ""; @@ -1637,6 +1640,7 @@ D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, 0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */, D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */, + D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */, D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */, diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index 6a267551..1e850196 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -17,8 +17,6 @@ class ExploreViewController: EnhancedTableViewController { var resultsController: SearchResultsViewController! var searchController: UISearchController! - let searchSubject = PassthroughSubject() - init() { super.init(style: .insetGrouped) diff --git a/Tusker/Screens/Lists/EditListAccountsViewController.swift b/Tusker/Screens/Lists/EditListAccountsViewController.swift new file mode 100644 index 00000000..ff7e2c1d --- /dev/null +++ b/Tusker/Screens/Lists/EditListAccountsViewController.swift @@ -0,0 +1,171 @@ +// +// EditListAccountsViewController.swift +// Tusker +// +// Created by Shadowfacts on 12/17/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +class EditListAccountsViewController: EnhancedTableViewController { + + let list: List + + var dataSource: DataSource! + + var nextRange: RequestRange? + + var searchResultsController: SearchResultsViewController! + var searchController: UISearchController! + + init(list: List) { + self.list = list + + super.init(style: .plain) + + title = String(format: NSLocalizedString("Edit %@", comment: "edit list screen title"), list.title) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemeneted") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: "accountCell") + + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 66 + + dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in + guard case let .account(id) = item else { fatalError() } + + let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell + cell.updateUI(accountID: id) + return cell + }) + dataSource.editListAccountsController = self + + searchResultsController = SearchResultsViewController() + searchResultsController.delegate = self + searchResultsController.onlySections = [.accounts] + searchController = UISearchController(searchResultsController: searchResultsController) + searchController.hidesNavigationBarDuringPresentation = false + searchController.searchResultsUpdater = searchResultsController + searchController.searchBar.autocapitalizationType = .none + searchController.searchBar.placeholder = NSLocalizedString("Search for accounts to add", comment: "edit list search field placeholder") + searchController.searchBar.delegate = searchResultsController + definesPresentationContext = true + + navigationItem.searchController = searchController + navigationItem.hidesSearchBarWhenScrolling = false + + navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Rename", comment: "rename list button title"), style: .plain, target: self, action: #selector(renameButtonPressed)) + + loadAccounts() + } + + func loadAccounts() { + let request = List.getAccounts(list) + MastodonController.client.run(request) { (response) in + guard case let .success(accounts, pagination) = response else { + fatalError() + } + + self.nextRange = pagination?.older + + MastodonCache.addAll(accounts: accounts) + + var snapshot = self.dataSource.snapshot() + snapshot.deleteSections([.accounts]) + snapshot.appendSections([.accounts]) + snapshot.appendItems(accounts.map { .account(id: $0.id) }) + + DispatchQueue.main.async { + self.dataSource.apply(snapshot) + } + } + } + + // MARK: - Table view delegate + + override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + return .delete + } + + // MARK: - Interaction + + @objc func renameButtonPressed() { + let alert = UIAlertController(title: NSLocalizedString("Rename List", comment: "rename list alert title"), message: nil, preferredStyle: .alert) + alert.addTextField { (textField) in + textField.text = self.list.title + } + alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "rename list alert cancel button"), style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("Rename", comment: "renaem list alert rename button"), style: .default, handler: { (_) in + guard let text = alert.textFields?.first?.text else { + fatalError() + } + let request = List.update(self.list, title: text) + MastodonController.client.run(request) { (response) in + guard case .success(_, _) = response else { + fatalError() + } + // todo: show success message somehow + } + })) + present(alert, animated: true) + } + +} + +extension EditListAccountsViewController { + enum Section: Hashable { + case accounts + } + enum Item: Hashable { + case account(id: String) + } + + class DataSource: UITableViewDiffableDataSource { + weak var editListAccountsController: EditListAccountsViewController? + + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return true + } + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + guard editingStyle == .delete, + case let .account(id) = itemIdentifier(for: indexPath) else { + return + } + + let request = List.remove(editListAccountsController!.list, accounts: [id]) + MastodonController.client.run(request) { (response) in + guard case .success(_, _) = response else { + fatalError() + } + + self.editListAccountsController?.loadAccounts() + } + } + } +} + +extension EditListAccountsViewController: SearchResultsViewControllerDelegate { + func selectedSearchResult(account accountID: String) { + let request = List.add(list, accounts: [accountID]) + MastodonController.client.run(request) { (response) in + guard case .success(_, _) = response else { + fatalError() + } + + self.loadAccounts() + DispatchQueue.main.async { + self.searchController.isActive = false + } + } + } +} diff --git a/Tusker/Screens/Lists/ListTimelineViewController.swift b/Tusker/Screens/Lists/ListTimelineViewController.swift index 4103ec2c..e1e5a9f6 100644 --- a/Tusker/Screens/Lists/ListTimelineViewController.swift +++ b/Tusker/Screens/Lists/ListTimelineViewController.swift @@ -24,5 +24,28 @@ class ListTimelineViewController: TimelineTableViewController { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editButtonPressed)) + } + + // MARK: - Interaction + + @objc func editButtonPressed() { + let editListAccountsController = EditListAccountsViewController(list: list) + editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed)) + let navController = UINavigationController(rootViewController: editListAccountsController) + present(navController, animated: true) + } + + @objc func doneButtonPressed() { + dismiss(animated: true) + + // todo: show loading indicator + timelineSegments = [] + loadInitialStatuses() + } } diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index 30752c0d..e1388f42 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -14,14 +14,29 @@ fileprivate let accountCell = "accountCell" fileprivate let statusCell = "statusCell" fileprivate let hashtagCell = "hashtagCell" +protocol SearchResultsViewControllerDelegate: class { + func selectedSearchResult(account accountID: String) + func selectedSearchResult(hashtag: Hashtag) + func selectedSearchResult(status statusID: String) +} + +extension SearchResultsViewControllerDelegate { + func selectedSearchResult(account accountID: String) {} + func selectedSearchResult(hashtag: Hashtag) {} + func selectedSearchResult(status statusID: String) {} +} + class SearchResultsViewController: EnhancedTableViewController { weak var exploreNavigationController: UINavigationController? + weak var delegate: SearchResultsViewControllerDelegate? var dataSource: UITableViewDiffableDataSource! var activityIndicator: UIActivityIndicatorView! + var onlySections: [Section] = Section.allCases + let searchSubject = PassthroughSubject() var currentQuery: String? @@ -114,16 +129,16 @@ class SearchResultsViewController: EnhancedTableViewController { guard self.currentQuery == query else { return } var snapshot = NSDiffableDataSourceSnapshot() - if !results.accounts.isEmpty { + if self.onlySections.contains(.accounts) && !results.accounts.isEmpty { snapshot.appendSections([.accounts]) snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts) MastodonCache.addAll(accounts: results.accounts) } - if !results.hashtags.isEmpty { + if self.onlySections.contains(.hashtags) && !results.hashtags.isEmpty { snapshot.appendSections([.hashtags]) snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .hashtags) } - if !results.statuses.isEmpty { + if self.onlySections.contains(.statuses) && !results.statuses.isEmpty { snapshot.appendSections([.statuses]) snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses) MastodonCache.addAll(statuses: results.statuses) @@ -132,6 +147,25 @@ class SearchResultsViewController: EnhancedTableViewController { self.dataSource.apply(snapshot) } } + + // MARK: - Table view delegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let delegate = delegate { + switch dataSource.itemIdentifier(for: indexPath) { + case nil: + return + case let .account(id): + delegate.selectedSearchResult(account: id) + case let .hashtag(hashtag): + delegate.selectedSearchResult(hashtag: hashtag) + case let .status(id, _): + delegate.selectedSearchResult(status: id) + } + } else { + super.tableView(tableView, didSelectRowAt: indexPath) + } + } } diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 81df1d64..a7ad73cd 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -70,6 +70,10 @@ class TimelineTableViewController: EnhancedTableViewController { tableView.prefetchDataSource = self guard MastodonController.client?.accessToken != nil else { return } + loadInitialStatuses() + } + + func loadInitialStatuses() { let request = MastodonController.client.getStatuses(timeline: timeline) MastodonController.client.run(request) { response in guard case let .success(statuses, pagination) = response else { fatalError() } diff --git a/Tusker/Views/Account Cell/AccountTableViewCell.xib b/Tusker/Views/Account Cell/AccountTableViewCell.xib index 71f6e121..b1f6d9a9 100644 --- a/Tusker/Views/Account Cell/AccountTableViewCell.xib +++ b/Tusker/Views/Account Cell/AccountTableViewCell.xib @@ -1,15 +1,15 @@ - + - + - + @@ -19,8 +19,8 @@ - - + +