2019-12-18 03:56:53 +00:00
//
// E d i t L i s t A c c o u n t s V i e w C o n t r o l l e r . s w i f t
// T u s k e r
//
// C r e a t e d b y S h a d o w f a c t s o n 1 2 / 1 7 / 1 9 .
// C o p y r i g h t © 2 0 1 9 S h a d o w f a c t s . A l l r i g h t s r e s e r v e d .
//
import UIKit
import Pachyderm
2022-11-19 19:08:39 +00:00
import Combine
2019-12-18 03:56:53 +00:00
2023-10-27 22:13:49 +00:00
class EditListAccountsViewController : UIViewController , CollectionViewController {
2019-12-18 03:56:53 +00:00
2022-12-15 02:00:36 +00:00
private var list : List
2023-10-27 22:13:49 +00:00
private let mastodonController : MastodonController
2020-01-05 20:25:07 +00:00
2023-10-27 22:13:49 +00:00
private var state = State . unloaded
2019-12-18 03:56:53 +00:00
2023-10-27 22:13:49 +00:00
private ( set ) var changedAccounts = false
2019-12-18 03:56:53 +00:00
2023-10-27 22:13:49 +00:00
private var dataSource : UICollectionViewDiffableDataSource < Section , Item > !
var collectionView : UICollectionView ! { view as ? UICollectionView }
private var nextRange : RequestRange ?
private var searchResultsController : SearchResultsViewController !
private var searchController : UISearchController !
2019-12-18 03:56:53 +00:00
2022-11-19 19:08:39 +00:00
private var listRenamedCancellable : AnyCancellable ?
2020-01-05 20:25:07 +00:00
init ( list : List , mastodonController : MastodonController ) {
2019-12-18 03:56:53 +00:00
self . list = list
2020-01-05 20:25:07 +00:00
self . mastodonController = mastodonController
2019-12-18 03:56:53 +00:00
2023-10-27 22:13:49 +00:00
super . init ( nibName : nil , bundle : nil )
2019-12-18 03:56:53 +00:00
2022-11-11 23:08:44 +00:00
listChanged ( )
2022-11-19 19:08:39 +00:00
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 ( )
}
2019-12-18 03:56:53 +00:00
}
required init ? ( coder : NSCoder ) {
fatalError ( " init(coder:) has not been implemeneted " )
}
2023-10-27 22:13:49 +00:00
override func loadView ( ) {
var config = UICollectionLayoutListConfiguration ( appearance : . grouped )
config . itemSeparatorHandler = { [ unowned self ] indexPath , sectionConfig in
var config = sectionConfig
switch dataSource . itemIdentifier ( for : indexPath ) ! {
case . loadingIndicator :
config . topSeparatorVisibility = . hidden
config . bottomSeparatorVisibility = . hidden
case . account ( id : _ ) :
config . topSeparatorInsets = TimelineStatusCollectionViewCell . separatorInsets
config . bottomSeparatorInsets = TimelineStatusCollectionViewCell . separatorInsets
}
return config
}
config . trailingSwipeActionsConfigurationProvider = { [ unowned self ] indexPath in
switch dataSource . itemIdentifier ( for : indexPath ) {
case . account ( id : let id ) :
let remove = UIContextualAction ( style : . destructive , title : " Remove " ) { [ unowned self ] _ , _ , completion in
Task {
await self . removeAccount ( id : id )
completion ( true )
}
}
return UISwipeActionsConfiguration ( actions : [ remove ] )
default :
return nil
}
}
let layout = UICollectionViewCompositionalLayout . list ( using : config )
view = UICollectionView ( frame : . zero , collectionViewLayout : layout )
collectionView . delegate = self
collectionView . allowsSelection = false
collectionView . backgroundColor = . appGroupedBackground
dataSource = createDataSource ( )
}
2019-12-18 03:56:53 +00:00
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2023-01-22 16:41:38 +00:00
searchResultsController = SearchResultsViewController ( mastodonController : mastodonController , scope : . people )
2022-11-28 02:44:17 +00:00
searchResultsController . following = true
searchResultsController . delegate = self
2019-12-18 03:56:53 +00:00
searchController = UISearchController ( searchResultsController : searchResultsController )
searchController . hidesNavigationBarDuringPresentation = false
searchController . searchResultsUpdater = searchResultsController
searchController . searchBar . autocapitalizationType = . none
2022-11-28 02:44:17 +00:00
searchController . searchBar . placeholder = NSLocalizedString ( " Search accounts you follow " , comment : " edit list search field placeholder " )
2019-12-18 03:56:53 +00:00
searchController . searchBar . delegate = searchResultsController
definesPresentationContext = true
navigationItem . searchController = searchController
navigationItem . hidesSearchBarWhenScrolling = false
2022-11-30 21:53:11 +00:00
if #available ( iOS 16.0 , * ) {
navigationItem . preferredSearchBarPlacement = . stacked
}
2019-12-18 03:56:53 +00:00
navigationItem . rightBarButtonItem = UIBarButtonItem ( title : NSLocalizedString ( " Rename " , comment : " rename list button title " ) , style : . plain , target : self , action : #selector ( renameButtonPressed ) )
2023-10-27 22:13:49 +00:00
}
private func createDataSource ( ) -> UICollectionViewDiffableDataSource < Section , Item > {
let loadingCell = UICollectionView . CellRegistration < LoadingCollectionViewCell , Void > { cell , indexPath , itemIdentifier in
cell . indicator . startAnimating ( )
}
let accountCell = UICollectionView . CellRegistration < AccountCollectionViewCell , String > { [ unowned self ] cell , indexPath , itemIdentifier in
cell . delegate = self
cell . updateUI ( accountID : itemIdentifier )
cell . configurationUpdateHandler = { cell , state in
cell . backgroundConfiguration = . appListGroupedCell ( for : state )
}
}
return UICollectionViewDiffableDataSource ( collectionView : collectionView ) { collectionView , indexPath , itemIdentifier in
switch itemIdentifier {
case . loadingIndicator :
return collectionView . dequeueConfiguredReusableCell ( using : loadingCell , for : indexPath , item : ( ) )
case . account ( id : let id ) :
return collectionView . dequeueConfiguredReusableCell ( using : accountCell , for : indexPath , item : id )
}
}
}
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
2019-12-18 03:56:53 +00:00
2023-10-27 22:13:49 +00:00
clearSelectionOnAppear ( animated : animated )
2022-11-11 22:28:19 +00:00
Task {
await loadAccounts ( )
}
2019-12-18 03:56:53 +00:00
}
2022-11-11 23:08:44 +00:00
private func listChanged ( ) {
title = String ( format : NSLocalizedString ( " Edit %@ " , comment : " edit list screen title " ) , list . title )
}
2023-10-27 22:13:49 +00:00
@ MainActor
private func loadAccounts ( ) async {
guard state = = . unloaded else { return }
state = . loading
async let results = try await mastodonController . run ( List . getAccounts ( list . id ) )
var snapshot = NSDiffableDataSourceSnapshot < Section , Item > ( )
snapshot . appendSections ( [ . accounts ] )
snapshot . appendItems ( [ . loadingIndicator ] )
await dataSource . apply ( snapshot )
2022-11-11 22:28:19 +00:00
do {
2023-10-27 22:13:49 +00:00
let ( accounts , pagination ) = try await results
2019-12-18 03:56:53 +00:00
self . nextRange = pagination ? . older
2022-11-11 22:28:19 +00:00
await withCheckedContinuation { continuation in
mastodonController . persistentContainer . addAll ( accounts : accounts ) {
continuation . resume ( )
2021-06-26 22:52:19 +00:00
}
2019-12-18 03:56:53 +00:00
}
2022-11-11 22:28:19 +00:00
2023-10-27 22:13:49 +00:00
var snapshot = NSDiffableDataSourceSnapshot < Section , Item > ( )
snapshot . appendSections ( [ . accounts ] )
2022-11-11 22:28:19 +00:00
snapshot . appendItems ( accounts . map { . account ( id : $0 . id ) } )
await dataSource . apply ( snapshot )
2023-10-27 22:13:49 +00:00
state = . loaded
2022-11-11 22:28:19 +00:00
} catch {
let config = ToastConfiguration ( from : error , with : " Error Loading Accounts " , in : self ) { [ unowned self ] toast in
toast . dismissToast ( animated : true )
await self . loadAccounts ( )
}
self . showToast ( configuration : config , animated : true )
2023-10-27 22:13:49 +00:00
state = . unloaded
await dataSource . apply ( . init ( ) )
}
}
private func loadNextPage ( ) async {
guard state = = . loaded ,
let nextRange else { return }
state = . loading
async let results = try await mastodonController . run ( List . getAccounts ( list . id , range : nextRange ) )
let origSnapshot = dataSource . snapshot ( )
var snapshot = origSnapshot
snapshot . appendItems ( [ . loadingIndicator ] )
await dataSource . apply ( snapshot )
do {
let ( accounts , pagination ) = try await results
self . nextRange = pagination ? . older
await withCheckedContinuation { continuation in
mastodonController . persistentContainer . addAll ( accounts : accounts ) {
continuation . resume ( )
}
}
var snapshot = origSnapshot
snapshot . appendItems ( accounts . map { . account ( id : $0 . id ) } )
await dataSource . apply ( snapshot )
state = . loaded
} catch {
let config = ToastConfiguration ( from : error , with : " Error Loading Accounts " , in : self ) { [ unowned self ] toast in
toast . dismissToast ( animated : true )
await self . loadNextPage ( )
}
self . showToast ( configuration : config , animated : true )
state = . loaded
await dataSource . apply ( origSnapshot )
2022-11-11 22:28:19 +00:00
}
}
private func addAccount ( id : String ) async {
2022-12-15 02:00:36 +00:00
changedAccounts = true
2022-11-11 22:28:19 +00:00
do {
2023-02-25 18:55:46 +00:00
let req = List . add ( list . id , accounts : [ id ] )
2022-11-11 22:28:19 +00:00
_ = try await mastodonController . run ( req )
self . searchController . isActive = false
await self . loadAccounts ( )
} catch {
let config = ToastConfiguration ( from : error , with : " Error Adding Account " , in : self ) { [ unowned self ] toast in
toast . dismissToast ( animated : true )
await self . addAccount ( id : id )
}
self . showToast ( configuration : config , animated : true )
}
}
private func removeAccount ( id : String ) async {
2022-12-15 02:00:36 +00:00
changedAccounts = true
2022-11-11 22:28:19 +00:00
do {
2023-02-25 18:55:46 +00:00
let request = List . remove ( list . id , accounts : [ id ] )
2022-11-11 22:28:19 +00:00
_ = try await mastodonController . run ( request )
await self . loadAccounts ( )
} catch {
let config = ToastConfiguration ( from : error , with : " Error Removing Account " , in : self ) { [ unowned self ] toast in
toast . dismissToast ( animated : true )
await self . removeAccount ( id : id )
}
self . showToast ( configuration : config , animated : true )
2019-12-18 03:56:53 +00:00
}
}
// MARK: - I n t e r a c t i o n
@objc func renameButtonPressed ( ) {
2022-11-11 23:08:44 +00:00
RenameListService ( list : list , mastodonController : mastodonController , present : { self . present ( $0 , animated : true ) } ) . run ( )
2019-12-18 03:56:53 +00:00
}
}
2023-10-27 22:13:49 +00:00
extension EditListAccountsViewController {
enum State {
case unloaded
case loading
case loaded
case loadingOlder
}
}
2019-12-18 03:56:53 +00:00
extension EditListAccountsViewController {
enum Section : Hashable {
case accounts
}
enum Item : Hashable {
2023-10-27 22:13:49 +00:00
case loadingIndicator
2019-12-18 03:56:53 +00:00
case account ( id : String )
}
2023-10-27 22:13:49 +00:00
}
extension EditListAccountsViewController : UICollectionViewDelegate {
func collectionView ( _ collectionView : UICollectionView , willDisplay cell : UICollectionViewCell , forItemAt indexPath : IndexPath ) {
if state = = . loaded ,
indexPath . item = = collectionView . numberOfItems ( inSection : indexPath . section ) - 1 {
2022-11-11 22:28:19 +00:00
Task {
2023-10-27 22:13:49 +00:00
await loadNextPage ( )
2019-12-18 03:56:53 +00:00
}
}
}
}
2021-02-06 19:35:34 +00:00
extension EditListAccountsViewController : TuskerNavigationDelegate {
2022-10-31 20:27:13 +00:00
var apiController : MastodonController ! { mastodonController }
2021-02-06 19:35:34 +00:00
}
2022-05-02 03:04:56 +00:00
extension EditListAccountsViewController : ToastableViewController {
}
extension EditListAccountsViewController : MenuActionProvider {
}
2022-11-28 02:44:17 +00:00
extension EditListAccountsViewController : SearchResultsViewControllerDelegate {
func selectedSearchResult ( account accountID : String ) {
Task {
await addAccount ( id : accountID )
}
}
}