2023-01-18 01:02:03 +00:00
//
// S t a t u s A c t i o n A c c o u n t L i s t C o l l e c t i o n 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 9 / 5 / 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
class StatusActionAccountListCollectionViewController : UIViewController , CollectionViewController {
2023-01-29 04:07:38 +00:00
private static let pageSize = 40
2023-01-18 19:56:13 +00:00
private let statusID : String
private let actionType : StatusActionAccountListViewController . ActionType
2023-01-18 01:02:03 +00:00
private let mastodonController : MastodonController
2023-02-23 03:00:12 +00:00
private var needsInaccurateCountWarning = false
2023-01-18 01:02:03 +00:00
var collectionView : UICollectionView ! {
view as ? UICollectionView
}
private var dataSource : UICollectionViewDiffableDataSource < Section , Item > !
2023-01-18 19:56:13 +00:00
private var state : State = . unloaded
private var older : RequestRange ?
2023-01-18 01:02:03 +00:00
/* *
Creates a new view controller showing the accounts that performed the given action on the given status .
- Parameter mastodonController The ` MastodonController ` instance this view controller uses .
*/
2023-01-18 19:56:13 +00:00
init ( statusID : String , actionType : StatusActionAccountListViewController . ActionType , mastodonController : MastodonController ) {
self . statusID = statusID
self . actionType = actionType
2023-01-18 01:02:03 +00:00
self . mastodonController = mastodonController
super . init ( nibName : nil , bundle : nil )
}
required init ? ( coder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
override func loadView ( ) {
2023-01-18 19:56:13 +00:00
var accountsConfig = UICollectionLayoutListConfiguration ( appearance : . grouped )
2023-02-15 03:47:56 +00:00
accountsConfig . backgroundColor = . appGroupedBackground
2023-01-18 19:56:13 +00:00
accountsConfig . itemSeparatorHandler = { [ unowned self ] indexPath , sectionConfig in
guard let item = self . dataSource . itemIdentifier ( for : indexPath ) else {
return sectionConfig
}
var config = sectionConfig
if item . hideSeparators {
config . topSeparatorVisibility = . hidden
config . bottomSeparatorVisibility = . hidden
}
return config
}
2023-01-18 01:02:03 +00:00
let layout = UICollectionViewCompositionalLayout { [ unowned self ] sectionIndex , environment in
switch dataSource . sectionIdentifier ( for : sectionIndex ) ! {
case . status :
var config = UICollectionLayoutListConfiguration ( appearance : . grouped )
2023-02-23 03:00:12 +00:00
config . footerMode = self . needsInaccurateCountWarning ? . supplementary : . none
2023-02-15 03:47:56 +00:00
config . backgroundColor = . appGroupedBackground
2023-01-18 01:02:03 +00:00
config . leadingSwipeActionsConfigurationProvider = { [ unowned self ] in
( collectionView . cellForItem ( at : $0 ) as ? TimelineStatusCollectionViewCell ) ? . leadingSwipeActions ( )
}
config . trailingSwipeActionsConfigurationProvider = { [ unowned self ] in
( collectionView . cellForItem ( at : $0 ) as ? TimelineStatusCollectionViewCell ) ? . trailingSwipeActions ( )
}
return NSCollectionLayoutSection . list ( using : config , layoutEnvironment : environment )
case . accounts :
2023-01-18 19:56:13 +00:00
return NSCollectionLayoutSection . list ( using : accountsConfig , layoutEnvironment : environment )
2023-01-18 01:02:03 +00:00
}
}
view = UICollectionView ( frame : . zero , collectionViewLayout : layout )
collectionView . delegate = self
collectionView . dragDelegate = self
collectionView . allowsFocus = true
dataSource = createDataSource ( )
}
private func createDataSource ( ) -> UICollectionViewDiffableDataSource < Section , Item > {
let statusCell = UICollectionView . CellRegistration < TimelineStatusCollectionViewCell , ( String , CollapseState ) > { [ unowned self ] cell , indexPath , item in
cell . delegate = self
cell . updateUI ( statusID : item . 0 , state : item . 1 , filterResult : . allow , precomputedContent : nil )
2023-02-03 04:02:11 +00:00
cell . configurationUpdateHandler = { cell , state in
var config = UIBackgroundConfiguration . listGroupedCell ( ) . updated ( for : state )
if state . isHighlighted || state . isSelected {
config . backgroundColor = . appSelectedCellBackground
} else {
config . backgroundColor = . appGroupedCellBackground
}
cell . backgroundConfiguration = config
}
2023-01-18 01:02:03 +00:00
}
let accountCell = UICollectionView . CellRegistration < AccountCollectionViewCell , String > { [ unowned self ] cell , indexPath , item in
cell . delegate = self
cell . updateUI ( accountID : item )
2023-02-03 04:02:11 +00:00
cell . configurationUpdateHandler = { cell , state in
var config = UIBackgroundConfiguration . listGroupedCell ( ) . updated ( for : state )
if state . isHighlighted || state . isSelected {
config . backgroundColor = . appSelectedCellBackground
} else {
config . backgroundColor = . appGroupedCellBackground
}
cell . backgroundConfiguration = config
}
2023-01-18 01:02:03 +00:00
}
2023-01-18 19:56:13 +00:00
let loadingCell = UICollectionView . CellRegistration < LoadingCollectionViewCell , Void > { cell , indexPath , item in
cell . indicator . startAnimating ( )
}
2023-01-18 01:02:03 +00:00
let dataSource = UICollectionViewDiffableDataSource < Section , Item > ( collectionView : collectionView ) { collectionView , indexPath , itemIdentifier in
switch itemIdentifier {
case . status ( let id , let state ) :
return collectionView . dequeueConfiguredReusableCell ( using : statusCell , for : indexPath , item : ( id , state ) )
case . account ( let id ) :
return collectionView . dequeueConfiguredReusableCell ( using : accountCell , for : indexPath , item : id )
2023-01-18 19:56:13 +00:00
case . loadingIndicator :
return collectionView . dequeueConfiguredReusableCell ( using : loadingCell , for : indexPath , item : ( ) )
2023-01-18 01:02:03 +00:00
}
}
let sectionHeaderCell = UICollectionView . SupplementaryRegistration < UICollectionViewListCell > ( elementKind : UICollectionView . elementKindSectionFooter ) { ( headerView , collectionView , indexPath ) in
var config = headerView . defaultContentConfiguration ( )
config . text = NSLocalizedString ( " Favorite and reblog counts for posts originating from instances other than your own may not be accurate. " , comment : " shown on lists of status total actions " )
headerView . contentConfiguration = config
}
dataSource . supplementaryViewProvider = { collectionView , elementKind , indexPath in
return collectionView . dequeueConfiguredReusableSupplementary ( using : sectionHeaderCell , for : indexPath )
}
return dataSource
}
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
clearSelectionOnAppear ( animated : animated )
2023-01-18 19:56:13 +00:00
if case . unloaded = state {
Task {
await loadAccounts ( )
}
}
2023-01-18 01:02:03 +00:00
}
2023-02-23 03:00:12 +00:00
func addStatus ( _ status : StatusMO , state : CollapseState , showInaccurateCountWarning : Bool ) {
2023-01-18 01:02:03 +00:00
loadViewIfNeeded ( )
2023-02-23 03:00:12 +00:00
needsInaccurateCountWarning = showInaccurateCountWarning && status . url ? . host != mastodonController . instanceURL . host
2023-01-18 01:02:03 +00:00
var snapshot = NSDiffableDataSourceSnapshot < Section , Item > ( )
snapshot . appendSections ( [ . status , . accounts ] )
snapshot . appendItems ( [ . status ( status . id , state ) ] , toSection : . status )
dataSource . apply ( snapshot , animatingDifferences : false )
}
2023-01-18 19:56:13 +00:00
func setAccounts ( _ accountIDs : [ String ] , animated : Bool ) {
guard case . unloaded = state else {
return
}
2023-01-18 01:02:03 +00:00
var snapshot = dataSource . snapshot ( )
snapshot . appendItems ( accountIDs . map { . account ( $0 ) } , toSection : . accounts )
dataSource . apply ( snapshot , animatingDifferences : animated )
2023-01-18 19:56:13 +00:00
self . state = . loaded
}
private func request ( for range : RequestRange ) -> Request < [ Account ] > {
switch actionType {
case . favorite :
2023-01-29 04:07:38 +00:00
return Status . getFavourites ( statusID , range : range . withCount ( Self . pageSize ) )
2023-01-18 19:56:13 +00:00
case . reblog :
2023-01-29 04:07:38 +00:00
return Status . getReblogs ( statusID , range : range . withCount ( Self . pageSize ) )
2023-01-18 19:56:13 +00:00
}
}
func apply ( snapshot : NSDiffableDataSourceSnapshot < Section , Item > ) async {
await Task { @ MainActor in
self . dataSource . apply ( snapshot )
} . value
}
@ MainActor
private func loadAccounts ( ) async {
guard case . unloaded = state else {
return
}
self . state = . loadingInitial
var snapshot = dataSource . snapshot ( )
snapshot . appendItems ( [ . loadingIndicator ] , toSection : . accounts )
await apply ( snapshot : snapshot )
do {
let ( accounts , pagination ) = try await mastodonController . run ( request ( for : . default ) )
await mastodonController . persistentContainer . addAll ( accounts : accounts )
guard case . loadingInitial = self . state else {
return
}
self . state = . loaded
self . older = pagination ? . older
var snapshot = dataSource . snapshot ( )
snapshot . deleteItems ( [ . loadingIndicator ] )
snapshot . appendItems ( accounts . map { . account ( $0 . id ) } , toSection : . accounts )
await apply ( snapshot : snapshot )
} catch {
self . state = . unloaded
let config = ToastConfiguration ( from : error , with : " Error Loading Accounts " , in : self ) { toast in
toast . dismissToast ( animated : true )
await self . loadAccounts ( )
}
self . showToast ( configuration : config , animated : true )
}
2023-01-18 01:02:03 +00:00
}
2023-01-18 19:56:13 +00:00
@ MainActor
private func loadOlder ( ) async {
guard case . loaded = state ,
let older else {
return
}
self . state = . loadingOlder
var snapshot = self . dataSource . snapshot ( )
snapshot . appendItems ( [ . loadingIndicator ] , toSection : . accounts )
await apply ( snapshot : snapshot )
do {
let ( accounts , pagination ) = try await mastodonController . run ( request ( for : older ) )
await mastodonController . persistentContainer . addAll ( accounts : accounts )
guard case . loadingOlder = self . state else {
return
}
self . state = . loaded
self . older = pagination ? . older
var snapshot = dataSource . snapshot ( )
snapshot . deleteItems ( [ . loadingIndicator ] )
snapshot . appendItems ( accounts . map { . account ( $0 . id ) } , toSection : . accounts )
await apply ( snapshot : snapshot )
} catch {
self . state = . loaded
let config = ToastConfiguration ( from : error , with : " Error Loading More " , in : self ) { [ unowned self ] toast in
toast . dismissToast ( animated : true )
await self . loadOlder ( )
}
self . showToast ( configuration : config , animated : true )
}
}
}
extension StatusActionAccountListCollectionViewController {
enum State {
case unloaded
case loadingInitial
case loaded
case loadingOlder
}
2023-01-18 01:02:03 +00:00
}
extension StatusActionAccountListCollectionViewController {
enum Section {
case status
case accounts
}
enum Item : Hashable {
case status ( String , CollapseState )
case account ( String )
2023-01-18 19:56:13 +00:00
case loadingIndicator
var hideSeparators : Bool {
switch self {
case . loadingIndicator :
return true
default :
return false
}
}
2023-01-18 01:02:03 +00:00
static func = = ( lhs : Item , rhs : Item ) -> Bool {
switch ( lhs , rhs ) {
case ( . status ( let a , _ ) , . status ( let b , _ ) ) :
return a = = b
case ( . account ( let a ) , . account ( let b ) ) :
return a = = b
2023-01-18 19:56:13 +00:00
case ( . loadingIndicator , . loadingIndicator ) :
return true
2023-01-18 01:02:03 +00:00
default :
return false
}
}
func hash ( into hasher : inout Hasher ) {
switch self {
case . status ( let id , _ ) :
hasher . combine ( 0 )
hasher . combine ( id )
case . account ( let id ) :
hasher . combine ( 1 )
hasher . combine ( id )
2023-01-18 19:56:13 +00:00
case . loadingIndicator :
hasher . combine ( 2 )
2023-01-18 01:02:03 +00:00
}
}
}
}
extension StatusActionAccountListCollectionViewController : UICollectionViewDelegate {
2023-01-18 19:56:13 +00:00
func collectionView ( _ collectionView : UICollectionView , willDisplay cell : UICollectionViewCell , forItemAt indexPath : IndexPath ) {
if indexPath . section = = collectionView . numberOfSections - 1 ,
indexPath . row = = collectionView . numberOfItems ( inSection : indexPath . section ) - 1 {
Task {
await self . loadOlder ( )
}
}
}
2023-01-18 01:02:03 +00:00
func collectionView ( _ collectionView : UICollectionView , didSelectItemAt indexPath : IndexPath ) {
switch dataSource . itemIdentifier ( for : indexPath ) {
case nil :
return
case . status ( let id , let state ) :
selected ( status : id , state : state . copy ( ) )
case . account ( let id ) :
selected ( account : id )
2023-01-18 19:56:13 +00:00
case . loadingIndicator :
return
2023-01-18 01:02:03 +00:00
}
}
func collectionView ( _ collectionView : UICollectionView , contextMenuConfigurationForItemAt indexPath : IndexPath , point : CGPoint ) -> UIContextMenuConfiguration ? {
guard let item = dataSource . itemIdentifier ( for : indexPath ) ,
let cell = collectionView . cellForItem ( at : indexPath ) else {
return nil
}
switch item {
case . status :
return ( cell as ? TimelineStatusCollectionViewCell ) ? . contextMenuConfiguration ( )
case . account ( let id ) :
return UIContextMenuConfiguration {
ProfileViewController ( accountID : id , mastodonController : self . mastodonController )
} actionProvider : { _ in
UIMenu ( children : self . actionsForProfile ( accountID : id , source : . view ( cell ) ) )
}
2023-01-18 19:56:13 +00:00
case . loadingIndicator :
return nil
2023-01-18 01:02:03 +00:00
}
}
func collectionView ( _ collectionView : UICollectionView , willPerformPreviewActionForMenuWith configuration : UIContextMenuConfiguration , animator : UIContextMenuInteractionCommitAnimating ) {
MenuPreviewHelper . willPerformPreviewAction ( animator : animator , presenter : self )
}
}
extension StatusActionAccountListCollectionViewController : UICollectionViewDragDelegate {
func collectionView ( _ collectionView : UICollectionView , itemsForBeginning session : UIDragSession , at indexPath : IndexPath ) -> [ UIDragItem ] {
guard let currentAccountID = mastodonController . accountInfo ? . id ,
let item = dataSource . itemIdentifier ( for : indexPath ) else {
return [ ]
}
let provider : NSItemProvider
switch item {
case . status ( let id , _ ) :
guard let status = mastodonController . persistentContainer . status ( for : id ) else {
return [ ]
}
provider = NSItemProvider ( object : status . url ! as NSURL )
let activity = UserActivityManager . showConversationActivity ( mainStatusID : id , accountID : currentAccountID )
activity . displaysAuxiliaryScene = true
provider . registerObject ( activity , visibility : . all )
case . account ( let id ) :
guard let account = mastodonController . persistentContainer . account ( for : id ) else {
return [ ]
}
provider = NSItemProvider ( object : account . url as NSURL )
let activity = UserActivityManager . showProfileActivity ( id : account . id , accountID : currentAccountID )
activity . displaysAuxiliaryScene = true
provider . registerObject ( activity , visibility : . all )
2023-01-18 19:56:13 +00:00
case . loadingIndicator :
return [ ]
2023-01-18 01:02:03 +00:00
}
return [ UIDragItem ( itemProvider : provider ) ]
}
}
extension StatusActionAccountListCollectionViewController : TuskerNavigationDelegate {
var apiController : MastodonController ! { mastodonController }
}
extension StatusActionAccountListCollectionViewController : MenuActionProvider {
}
extension StatusActionAccountListCollectionViewController : StatusCollectionViewCellDelegate {
func statusCellNeedsReconfigure ( _ cell : StatusCollectionViewCell , animated : Bool , completion : ( ( ) -> Void ) ? ) {
if let indexPath = collectionView . indexPath ( for : cell ) {
var snapshot = dataSource . snapshot ( )
snapshot . reconfigureItems ( [ dataSource . itemIdentifier ( for : indexPath ) ! ] )
dataSource . apply ( snapshot , animatingDifferences : animated , completion : completion )
}
}
func statusCellShowFiltered ( _ cell : StatusCollectionViewCell ) {
fatalError ( )
}
}
extension StatusActionAccountListCollectionViewController : StatusBarTappableViewController {
func handleStatusBarTapped ( xPosition : CGFloat ) -> StatusBarTapActionResult {
collectionView . scrollToTop ( )
return . stop
}
}