forked from shadowfacts/Tusker
parent
88aada8d35
commit
4211806b5f
|
@ -17,6 +17,7 @@
|
|||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
||||
D601FA5B29787AB100A8E8B5 /* AccountFollowsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5A29787AB100A8E8B5 /* AccountFollowsListViewController.swift */; };
|
||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
|
||||
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
|
||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
|
||||
|
@ -155,6 +156,7 @@
|
|||
D65B4B6629773AE600DABDFB /* DeleteStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B6529773AE600DABDFB /* DeleteStatusService.swift */; };
|
||||
D65B4B682977769E00DABDFB /* StatusActionAccountListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B672977769E00DABDFB /* StatusActionAccountListViewController.swift */; };
|
||||
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B69297777D900DABDFB /* StatusNotFoundView.swift */; };
|
||||
D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B8A297879E900DABDFB /* AccountFollowsViewController.swift */; };
|
||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
|
||||
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
||||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
|
||||
|
@ -413,6 +415,7 @@
|
|||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
||||
D601FA5A29787AB100A8E8B5 /* AccountFollowsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFollowsListViewController.swift; sourceTree = "<group>"; };
|
||||
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; };
|
||||
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = "<group>"; };
|
||||
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -547,6 +550,7 @@
|
|||
D65B4B6529773AE600DABDFB /* DeleteStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteStatusService.swift; sourceTree = "<group>"; };
|
||||
D65B4B672977769E00DABDFB /* StatusActionAccountListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListViewController.swift; sourceTree = "<group>"; };
|
||||
D65B4B69297777D900DABDFB /* StatusNotFoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusNotFoundView.swift; sourceTree = "<group>"; };
|
||||
D65B4B8A297879E900DABDFB /* AccountFollowsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFollowsViewController.swift; sourceTree = "<group>"; };
|
||||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = "<group>"; };
|
||||
D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -966,6 +970,7 @@
|
|||
D641C780213DD7C4004B4513 /* Screens */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D65B4B89297879DE00DABDFB /* Account Follows */,
|
||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
||||
0411610522B457290030A9B7 /* Attachment Gallery */,
|
||||
|
@ -1207,6 +1212,15 @@
|
|||
path = Report;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D65B4B89297879DE00DABDFB /* Account Follows */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D65B4B8A297879E900DABDFB /* AccountFollowsViewController.swift */,
|
||||
D601FA5A29787AB100A8E8B5 /* AccountFollowsListViewController.swift */,
|
||||
);
|
||||
path = "Account Follows";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D663626021360A9600C9CBA2 /* Preferences */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2024,6 +2038,7 @@
|
|||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
||||
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
|
||||
D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */,
|
||||
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
||||
D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */,
|
||||
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */,
|
||||
|
@ -2060,6 +2075,7 @@
|
|||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
|
||||
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */,
|
||||
D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */,
|
||||
D601FA5B29787AB100A8E8B5 /* AccountFollowsListViewController.swift in Sources */,
|
||||
D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */,
|
||||
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
|
||||
D61F75AB293AF11400C0B37F /* FilterKeywordMO.swift in Sources */,
|
||||
|
|
|
@ -348,6 +348,14 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
|||
accounts.forEach { self.accountSubject.send($0.id) }
|
||||
}
|
||||
}
|
||||
|
||||
func addAll(accounts: [Account], in context: NSManagedObjectContext? = nil) async {
|
||||
await withCheckedContinuation { continuation in
|
||||
addAll(accounts: accounts, in: context) {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
|
||||
backgroundContext.perform {
|
||||
|
|
|
@ -0,0 +1,277 @@
|
|||
//
|
||||
// AccountFollowsListViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/18/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class AccountFollowsListViewController: UIViewController, CollectionViewController {
|
||||
|
||||
let accountID: String
|
||||
let mastodonController: MastodonController
|
||||
let mode: AccountFollowsViewController.Mode
|
||||
|
||||
var collectionView: UICollectionView! {
|
||||
view as? UICollectionView
|
||||
}
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
private var state: State = .unloaded
|
||||
private var newer: RequestRange?
|
||||
private var older: RequestRange?
|
||||
|
||||
init(accountID: String, mastodonController: MastodonController, mode: AccountFollowsViewController.Mode) {
|
||||
self.accountID = accountID
|
||||
self.mastodonController = mastodonController
|
||||
self.mode = mode
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
title = mode.title
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||
config.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
|
||||
}
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.delegate = self
|
||||
collectionView.dragDelegate = self
|
||||
collectionView.allowsFocus = true
|
||||
dataSource = createDataSource()
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in
|
||||
cell.delegate = self
|
||||
cell.updateUI(accountID: item)
|
||||
}
|
||||
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in
|
||||
cell.indicator.startAnimating()
|
||||
}
|
||||
return UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
|
||||
switch itemIdentifier {
|
||||
case .account(let id):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: id)
|
||||
case .loadingIndicator:
|
||||
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
clearSelectionOnAppear(animated: animated)
|
||||
|
||||
if case .unloaded = state {
|
||||
Task {
|
||||
await loadInitial()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func request(for range: RequestRange) -> Request<[Account]> {
|
||||
switch mode {
|
||||
case .following:
|
||||
return Account.getFollowing(accountID, range: range)
|
||||
case .followers:
|
||||
return Account.getFollowers(accountID, range: range)
|
||||
}
|
||||
}
|
||||
|
||||
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>) async {
|
||||
await Task { @MainActor in
|
||||
self.dataSource.apply(snapshot)
|
||||
}.value
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadInitial() async {
|
||||
guard case .unloaded = state else {
|
||||
return
|
||||
}
|
||||
|
||||
self.state = .loadingInitial
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems([.loadingIndicator])
|
||||
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.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems(accounts.map { .account($0.id) })
|
||||
await apply(snapshot: snapshot)
|
||||
|
||||
} catch {
|
||||
self.state = .unloaded
|
||||
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { [unowned self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self.loadInitial()
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadOlder() async {
|
||||
guard case .loaded = state,
|
||||
let older else {
|
||||
return
|
||||
}
|
||||
|
||||
self.state = .loadingOlder
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.appendItems([.loadingIndicator])
|
||||
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) })
|
||||
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 AccountFollowsListViewController {
|
||||
enum State {
|
||||
case unloaded
|
||||
case loadingInitial
|
||||
case loaded
|
||||
case loadingOlder
|
||||
}
|
||||
}
|
||||
|
||||
extension AccountFollowsListViewController {
|
||||
enum Section {
|
||||
case accounts
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case account(String)
|
||||
case loadingIndicator
|
||||
|
||||
var hideSeparators: Bool {
|
||||
switch self {
|
||||
case .account(_):
|
||||
return false
|
||||
case .loadingIndicator:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AccountFollowsListViewController: UICollectionViewDelegate {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard case .account(let id) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
selected(account: id)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard case .account(let id) = dataSource.itemIdentifier(for: indexPath),
|
||||
let cell = collectionView.cellForItem(at: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
return UIContextMenuConfiguration {
|
||||
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
|
||||
} actionProvider: { _ in
|
||||
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension AccountFollowsListViewController: UICollectionViewDragDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
guard case .account(let id) = dataSource.itemIdentifier(for: indexPath),
|
||||
let currentAccountID = mastodonController.accountInfo?.id,
|
||||
let account = mastodonController.persistentContainer.account(for: id) else {
|
||||
return []
|
||||
}
|
||||
let provider = NSItemProvider(object: account.url as NSURL)
|
||||
let activity = UserActivityManager.showProfileActivity(id: id, accountID: currentAccountID)
|
||||
activity.displaysAuxiliaryScene = true
|
||||
provider.registerObject(activity, visibility: .all)
|
||||
return [UIDragItem(itemProvider: provider)]
|
||||
}
|
||||
}
|
||||
|
||||
extension AccountFollowsListViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController! { mastodonController }
|
||||
}
|
||||
|
||||
extension AccountFollowsListViewController: MenuActionProvider {
|
||||
}
|
||||
|
||||
extension AccountFollowsListViewController: ToastableViewController {
|
||||
}
|
||||
|
||||
extension AccountFollowsListViewController: StatusBarTappableViewController {
|
||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||
collectionView.scrollToTop()
|
||||
return .stop
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// AccountFollowsViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/18/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class AccountFollowsViewController: SegmentedPageViewController<AccountFollowsViewController.Mode> {
|
||||
|
||||
let accountID: String
|
||||
let mastodonController: MastodonController
|
||||
|
||||
init(accountID: String, mastodonController: MastodonController) {
|
||||
self.accountID = accountID
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(pages: [.following, .followers]) { mode in
|
||||
AccountFollowsListViewController(accountID: accountID, mastodonController: mastodonController, mode: mode)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AccountFollowsViewController {
|
||||
enum Mode: SegmentedPageViewControllerPage {
|
||||
case following, followers
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .following:
|
||||
return "Following"
|
||||
case .followers:
|
||||
return "Followers"
|
||||
}
|
||||
}
|
||||
|
||||
var segmentedControlTitle: String { title }
|
||||
}
|
||||
}
|
|
@ -351,6 +351,8 @@ class ProfileHeaderView: UIView {
|
|||
}
|
||||
|
||||
@IBAction func followCountButtonPressed(_ sender: Any) {
|
||||
guard let accountID else { return }
|
||||
delegate?.show(AccountFollowsViewController(accountID: accountID, mastodonController: mastodonController))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue