Compare commits
5 Commits
10803408cd
...
bf739b9f41
Author | SHA1 | Date |
---|---|---|
Shadowfacts | bf739b9f41 | |
Shadowfacts | 4211806b5f | |
Shadowfacts | 88aada8d35 | |
Shadowfacts | 5623cedab3 | |
Shadowfacts | ccfc8331fb |
|
@ -17,6 +17,7 @@
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.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 */; };
|
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
|
||||||
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
|
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
|
||||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
|
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
|
||||||
|
@ -155,6 +156,7 @@
|
||||||
D65B4B6629773AE600DABDFB /* DeleteStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B6529773AE600DABDFB /* DeleteStatusService.swift */; };
|
D65B4B6629773AE600DABDFB /* DeleteStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B6529773AE600DABDFB /* DeleteStatusService.swift */; };
|
||||||
D65B4B682977769E00DABDFB /* StatusActionAccountListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B672977769E00DABDFB /* StatusActionAccountListViewController.swift */; };
|
D65B4B682977769E00DABDFB /* StatusActionAccountListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B672977769E00DABDFB /* StatusActionAccountListViewController.swift */; };
|
||||||
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B69297777D900DABDFB /* StatusNotFoundView.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 */; };
|
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
|
||||||
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
||||||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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; };
|
D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -966,6 +970,7 @@
|
||||||
D641C780213DD7C4004B4513 /* Screens */ = {
|
D641C780213DD7C4004B4513 /* Screens */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D65B4B89297879DE00DABDFB /* Account Follows */,
|
||||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||||
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
||||||
0411610522B457290030A9B7 /* Attachment Gallery */,
|
0411610522B457290030A9B7 /* Attachment Gallery */,
|
||||||
|
@ -1207,6 +1212,15 @@
|
||||||
path = Report;
|
path = Report;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D65B4B89297879DE00DABDFB /* Account Follows */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D65B4B8A297879E900DABDFB /* AccountFollowsViewController.swift */,
|
||||||
|
D601FA5A29787AB100A8E8B5 /* AccountFollowsListViewController.swift */,
|
||||||
|
);
|
||||||
|
path = "Account Follows";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D663626021360A9600C9CBA2 /* Preferences */ = {
|
D663626021360A9600C9CBA2 /* Preferences */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2024,6 +2038,7 @@
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||||
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
||||||
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
|
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
|
||||||
|
D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */,
|
||||||
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
||||||
D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */,
|
D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */,
|
||||||
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */,
|
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */,
|
||||||
|
@ -2060,6 +2075,7 @@
|
||||||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
|
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
|
||||||
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */,
|
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */,
|
||||||
D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */,
|
D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */,
|
||||||
|
D601FA5B29787AB100A8E8B5 /* AccountFollowsListViewController.swift in Sources */,
|
||||||
D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */,
|
D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */,
|
||||||
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
|
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
|
||||||
D61F75AB293AF11400C0B37F /* FilterKeywordMO.swift in Sources */,
|
D61F75AB293AF11400C0B37F /* FilterKeywordMO.swift in Sources */,
|
||||||
|
|
|
@ -349,6 +349,14 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
backgroundContext.perform {
|
||||||
let statuses = notifications.compactMap { $0.status }
|
let statuses = notifications.compactMap { $0.status }
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,9 +111,11 @@ class ConversationViewController: UIViewController {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
if case .unloaded = state {
|
||||||
await loadMainStatus()
|
await loadMainStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
||||||
guard let userInfo = notification.userInfo,
|
guard let userInfo = notification.userInfo,
|
||||||
|
|
|
@ -11,6 +11,8 @@ import Pachyderm
|
||||||
|
|
||||||
class StatusActionAccountListCollectionViewController: UIViewController, CollectionViewController {
|
class StatusActionAccountListCollectionViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
|
private let statusID: String
|
||||||
|
private let actionType: StatusActionAccountListViewController.ActionType
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
/// If `true`, a warning will be shown below the account list describing that the total favs/reblogs may be innacurate.
|
/// If `true`, a warning will be shown below the account list describing that the total favs/reblogs may be innacurate.
|
||||||
|
@ -21,12 +23,17 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
}
|
}
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
|
private var state: State = .unloaded
|
||||||
|
private var older: RequestRange?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Creates a new view controller showing the accounts that performed the given action on the given status.
|
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.
|
- Parameter mastodonController The `MastodonController` instance this view controller uses.
|
||||||
*/
|
*/
|
||||||
init(mastodonController: MastodonController) {
|
init(statusID: String, actionType: StatusActionAccountListViewController.ActionType, mastodonController: MastodonController) {
|
||||||
|
self.statusID = statusID
|
||||||
|
self.actionType = actionType
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
@ -38,6 +45,18 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
|
var accountsConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
|
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
|
||||||
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
switch dataSource.sectionIdentifier(for: sectionIndex)! {
|
switch dataSource.sectionIdentifier(for: sectionIndex)! {
|
||||||
case .status:
|
case .status:
|
||||||
|
@ -51,7 +70,7 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
}
|
}
|
||||||
return NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
return NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
case .accounts:
|
case .accounts:
|
||||||
return NSCollectionLayoutSection.list(using: .init(appearance: .grouped), layoutEnvironment: environment)
|
return NSCollectionLayoutSection.list(using: accountsConfig, layoutEnvironment: environment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
@ -70,12 +89,17 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.updateUI(accountID: item)
|
cell.updateUI(accountID: item)
|
||||||
}
|
}
|
||||||
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in
|
||||||
|
cell.indicator.startAnimating()
|
||||||
|
}
|
||||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||||
switch itemIdentifier {
|
switch itemIdentifier {
|
||||||
case .status(let id, let state):
|
case .status(let id, let state):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
|
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
|
||||||
case .account(let id):
|
case .account(let id):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: id)
|
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: id)
|
||||||
|
case .loadingIndicator:
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionFooter) { (headerView, collectionView, indexPath) in
|
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionFooter) { (headerView, collectionView, indexPath) in
|
||||||
|
@ -93,6 +117,12 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
clearSelectionOnAppear(animated: animated)
|
clearSelectionOnAppear(animated: animated)
|
||||||
|
|
||||||
|
if case .unloaded = state {
|
||||||
|
Task {
|
||||||
|
await loadAccounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addStatus(_ status: StatusMO, state: CollapseState) {
|
func addStatus(_ status: StatusMO, state: CollapseState) {
|
||||||
|
@ -104,12 +134,115 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAccounts(_ accountIDs: [String], animated: Bool) {
|
func setAccounts(_ accountIDs: [String], animated: Bool) {
|
||||||
|
guard case .unloaded = state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.appendItems(accountIDs.map { .account($0) }, toSection: .accounts)
|
snapshot.appendItems(accountIDs.map { .account($0) }, toSection: .accounts)
|
||||||
dataSource.apply(snapshot, animatingDifferences: animated)
|
dataSource.apply(snapshot, animatingDifferences: animated)
|
||||||
|
self.state = .loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func request(for range: RequestRange) -> Request<[Account]> {
|
||||||
|
switch actionType {
|
||||||
|
case .favorite:
|
||||||
|
return Status.getFavourites(statusID, range: range)
|
||||||
|
case .reblog:
|
||||||
|
return Status.getReblogs(statusID, range: range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
try! await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusActionAccountListCollectionViewController {
|
extension StatusActionAccountListCollectionViewController {
|
||||||
|
@ -120,6 +253,16 @@ extension StatusActionAccountListCollectionViewController {
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case status(String, CollapseState)
|
case status(String, CollapseState)
|
||||||
case account(String)
|
case account(String)
|
||||||
|
case loadingIndicator
|
||||||
|
|
||||||
|
var hideSeparators: Bool {
|
||||||
|
switch self {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
|
@ -127,6 +270,8 @@ extension StatusActionAccountListCollectionViewController {
|
||||||
return a == b
|
return a == b
|
||||||
case (.account(let a), .account(let b)):
|
case (.account(let a), .account(let b)):
|
||||||
return a == b
|
return a == b
|
||||||
|
case (.loadingIndicator, .loadingIndicator):
|
||||||
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -140,12 +285,23 @@ extension StatusActionAccountListCollectionViewController {
|
||||||
case .account(let id):
|
case .account(let id):
|
||||||
hasher.combine(1)
|
hasher.combine(1)
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
|
case .loadingIndicator:
|
||||||
|
hasher.combine(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusActionAccountListCollectionViewController: UICollectionViewDelegate {
|
extension StatusActionAccountListCollectionViewController: 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) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
switch dataSource.itemIdentifier(for: indexPath) {
|
switch dataSource.itemIdentifier(for: indexPath) {
|
||||||
case nil:
|
case nil:
|
||||||
|
@ -154,6 +310,8 @@ extension StatusActionAccountListCollectionViewController: UICollectionViewDeleg
|
||||||
selected(status: id, state: state.copy())
|
selected(status: id, state: state.copy())
|
||||||
case .account(let id):
|
case .account(let id):
|
||||||
selected(account: id)
|
selected(account: id)
|
||||||
|
case .loadingIndicator:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,6 +329,8 @@ extension StatusActionAccountListCollectionViewController: UICollectionViewDeleg
|
||||||
} actionProvider: { _ in
|
} actionProvider: { _ in
|
||||||
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
|
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
|
||||||
}
|
}
|
||||||
|
case .loadingIndicator:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,6 +363,8 @@ extension StatusActionAccountListCollectionViewController: UICollectionViewDragD
|
||||||
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
case .loadingIndicator:
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,10 +95,12 @@ class StatusActionAccountListViewController: UIViewController {
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if case .unloaded = state {
|
||||||
Task {
|
Task {
|
||||||
await loadStatus()
|
await loadStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
||||||
guard let userInfo = notification.userInfo,
|
guard let userInfo = notification.userInfo,
|
||||||
|
@ -143,45 +145,13 @@ class StatusActionAccountListViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func statusLoaded(_ status: StatusMO) async {
|
private func statusLoaded(_ status: StatusMO) async {
|
||||||
let vc = StatusActionAccountListCollectionViewController(mastodonController: mastodonController)
|
let vc = StatusActionAccountListCollectionViewController(statusID: statusID, actionType: actionType, mastodonController: mastodonController)
|
||||||
vc.addStatus(status, state: statusState)
|
vc.addStatus(status, state: statusState)
|
||||||
vc.showInacurateCountWarning = showInacurateCountWarning
|
vc.showInacurateCountWarning = showInacurateCountWarning
|
||||||
state = .displaying(vc)
|
|
||||||
|
|
||||||
if let accountIDs {
|
if let accountIDs {
|
||||||
vc.addAccounts(accountIDs, animated: false)
|
vc.setAccounts(accountIDs, animated: false)
|
||||||
} else {
|
|
||||||
await loadAccounts(list: vc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadAccounts(list: StatusActionAccountListCollectionViewController) async {
|
|
||||||
let request: Request<[Account]>
|
|
||||||
switch actionType {
|
|
||||||
case .favorite:
|
|
||||||
request = Status.getFavourites(statusID)
|
|
||||||
case .reblog:
|
|
||||||
request = Status.getReblogs(statusID)
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
// TODO: pagination
|
|
||||||
let (accounts, _) = try await mastodonController.run(request)
|
|
||||||
|
|
||||||
await withCheckedContinuation { continuation in
|
|
||||||
mastodonController.persistentContainer.addAll(accounts: accounts) {
|
|
||||||
continuation.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list.addAccounts(accounts.map(\.id), animated: true)
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { toast in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
await self.loadAccounts(list: list)
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
}
|
}
|
||||||
|
state = .displaying(vc)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showStatusNotFound() {
|
private func showStatusNotFound() {
|
||||||
|
|
|
@ -58,6 +58,7 @@ class CachedImageView: UIImageView {
|
||||||
}
|
}
|
||||||
try Task.checkCancellation()
|
try Task.checkCancellation()
|
||||||
self.image = transformedImage
|
self.image = transformedImage
|
||||||
|
self.isGrayscale = Preferences.shared.grayscaleImages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ class ProfileHeaderView: UIView {
|
||||||
@IBOutlet weak var relationshipLabel: UILabel!
|
@IBOutlet weak var relationshipLabel: UILabel!
|
||||||
@IBOutlet weak var noteTextView: StatusContentTextView!
|
@IBOutlet weak var noteTextView: StatusContentTextView!
|
||||||
@IBOutlet weak var fieldsView: ProfileFieldsView!
|
@IBOutlet weak var fieldsView: ProfileFieldsView!
|
||||||
|
@IBOutlet weak var followCountButton: UIButton!
|
||||||
private(set) var pagesSegmentedControl: ScrollingSegmentedControl<ProfileViewController.Page>!
|
private(set) var pagesSegmentedControl: ScrollingSegmentedControl<ProfileViewController.Page>!
|
||||||
|
|
||||||
var accountID: String!
|
var accountID: String!
|
||||||
|
@ -148,6 +149,26 @@ class ProfileHeaderView: UIView {
|
||||||
fieldsView.delegate = delegate
|
fieldsView.delegate = delegate
|
||||||
fieldsView.updateUI(account: account)
|
fieldsView.updateUI(account: account)
|
||||||
|
|
||||||
|
let (followingAbbr, followingSpelledOut) = formatBigNumber(account.followingCount)
|
||||||
|
let (followersAbbr, followersSpelledOut) = formatBigNumber(account.followersCount)
|
||||||
|
let followCountTitle = NSMutableAttributedString()
|
||||||
|
followCountTitle.append(NSAttributedString(string: followingAbbr, attributes: [
|
||||||
|
.font: UIFont.preferredFont(forTextStyle: .body).withTraits(.traitBold)!,
|
||||||
|
.foregroundColor: UIColor.label,
|
||||||
|
]))
|
||||||
|
followCountTitle.append(NSAttributedString(string: " Following, ", attributes: [
|
||||||
|
.foregroundColor: UIColor.secondaryLabel,
|
||||||
|
]))
|
||||||
|
followCountTitle.append(NSAttributedString(string: followersAbbr, attributes: [
|
||||||
|
.font: UIFont.preferredFont(forTextStyle: .body).withTraits(.traitBold)!,
|
||||||
|
.foregroundColor: UIColor.label,
|
||||||
|
]))
|
||||||
|
followCountTitle.append(NSAttributedString(string: " Follower\(account.followersCount == 1 ? "" : "s")", attributes: [
|
||||||
|
.foregroundColor: UIColor.secondaryLabel,
|
||||||
|
]))
|
||||||
|
followCountButton.setAttributedTitle(followCountTitle, for: .normal)
|
||||||
|
followCountButton.accessibilityLabel = "\(followingSpelledOut) following, \(followersSpelledOut) followers"
|
||||||
|
|
||||||
accessibilityElements = [
|
accessibilityElements = [
|
||||||
displayNameLabel!,
|
displayNameLabel!,
|
||||||
usernameLabel!,
|
usernameLabel!,
|
||||||
|
@ -260,6 +281,22 @@ class ProfileHeaderView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func formatBigNumber(_ value: Int) -> (String, String) {
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.maximumFractionDigits = 1
|
||||||
|
|
||||||
|
for (threshold, abbr, spelledOut) in [(1_000_000, "m", "million"), (1_000, "k", "thousand")] {
|
||||||
|
if value >= threshold {
|
||||||
|
let frac = Double(value) / Double(threshold)
|
||||||
|
let s = formatter.string(from: frac as NSNumber)!
|
||||||
|
return ("\(s)\(abbr)", "\(s) \(spelledOut)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = formatter.string(from: value as NSNumber)!
|
||||||
|
return (s, s)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Interaction
|
// MARK: Interaction
|
||||||
|
|
||||||
@objc func avatarPressed() {
|
@objc func avatarPressed() {
|
||||||
|
@ -313,6 +350,10 @@ class ProfileHeaderView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IBAction func followCountButtonPressed(_ sender: Any) {
|
||||||
|
guard let accountID else { return }
|
||||||
|
delegate?.show(AccountFollowsViewController(accountID: accountID, mastodonController: mastodonController))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileHeaderView {
|
extension ProfileHeaderView {
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
<action selector="followPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="OM3-lq-Z14"/>
|
<action selector="followPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="OM3-lq-Z14"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="u4P-3i-gEq">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="u4P-3i-gEq">
|
||||||
<rect key="frame" x="16" y="266" width="398" height="596"/>
|
<rect key="frame" x="16" y="266" width="398" height="596"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Follows you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UF8-nI-KVj">
|
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Follows you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UF8-nI-KVj">
|
||||||
|
@ -71,19 +71,37 @@
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1O8-2P-Gbf" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1O8-2P-Gbf" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="382" height="460"/>
|
<rect key="frame" x="0.0" y="0.0" width="382" height="259.5"/>
|
||||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||||
<color key="textColor" systemColor="labelColor"/>
|
<color key="textColor" systemColor="labelColor"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
</textView>
|
</textView>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vKC-m1-Sbs" customClass="ProfileFieldsView" customModule="Tusker" customModuleProvider="target">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vKC-m1-Sbs" customClass="ProfileFieldsView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="468" width="398" height="128"/>
|
<rect key="frame" x="0.0" y="263.5" width="398" height="128"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="128" placeholder="YES" id="xbR-M6-H0I"/>
|
<constraint firstAttribute="height" constant="128" placeholder="YES" id="xbR-M6-H0I"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ood-3e-sSu" userLabel="Spacer">
|
||||||
|
<rect key="frame" x="0.0" y="395.5" width="240" height="8"/>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="8" id="5ri-vD-wXe"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5w9-LA-8kc">
|
||||||
|
<rect key="frame" x="0.0" y="407.5" width="219" height="188.5"/>
|
||||||
|
<state key="normal" title="Button"/>
|
||||||
|
<buttonConfiguration key="configuration" style="plain" title="123 Following, 1.2k Followers">
|
||||||
|
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||||
|
<directionalEdgeInsets key="contentInsets" top="0.0" leading="0.0" bottom="0.0" trailing="0.0"/>
|
||||||
|
</buttonConfiguration>
|
||||||
|
<connections>
|
||||||
|
<action selector="followCountButtonPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="QOO-zK-pfu"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="vKC-m1-Sbs" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" id="0dI-ax-7eI"/>
|
<constraint firstItem="vKC-m1-Sbs" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" id="0dI-ax-7eI"/>
|
||||||
|
@ -149,6 +167,7 @@
|
||||||
<outlet property="displayNameLabel" destination="vcl-Gl-kXl" id="64n-a9-my0"/>
|
<outlet property="displayNameLabel" destination="vcl-Gl-kXl" id="64n-a9-my0"/>
|
||||||
<outlet property="fieldsView" destination="vKC-m1-Sbs" id="FeE-jh-lYH"/>
|
<outlet property="fieldsView" destination="vKC-m1-Sbs" id="FeE-jh-lYH"/>
|
||||||
<outlet property="followButton" destination="cr8-p9-xkc" id="E1n-gh-mCl"/>
|
<outlet property="followButton" destination="cr8-p9-xkc" id="E1n-gh-mCl"/>
|
||||||
|
<outlet property="followCountButton" destination="5w9-LA-8kc" id="umN-5g-q8N"/>
|
||||||
<outlet property="headerImageView" destination="dgG-dR-lSv" id="HXT-v4-2iX"/>
|
<outlet property="headerImageView" destination="dgG-dR-lSv" id="HXT-v4-2iX"/>
|
||||||
<outlet property="lockImageView" destination="KNY-GD-beC" id="9EJ-iM-Eos"/>
|
<outlet property="lockImageView" destination="KNY-GD-beC" id="9EJ-iM-Eos"/>
|
||||||
<outlet property="moreButton" destination="vFa-g3-xIP" id="dEX-1a-PHF"/>
|
<outlet property="moreButton" destination="vFa-g3-xIP" id="dEX-1a-PHF"/>
|
||||||
|
|
Loading…
Reference in New Issue