Compare commits
No commits in common. "2464e2530f8fe917855189a066fb1d57a32bc7f1" and "eb496243c78379412072f95a2fea89ec792f1f52" have entirely different histories.
2464e2530f
...
eb496243c7
|
@ -11,18 +11,14 @@ import Foundation
|
|||
public struct List: ListProtocol, Decodable, Equatable, Hashable, Sendable {
|
||||
public let id: String
|
||||
public let title: String
|
||||
public let replyPolicy: ReplyPolicy?
|
||||
public let exclusive: Bool?
|
||||
|
||||
public var timeline: Timeline {
|
||||
return .list(id: id)
|
||||
}
|
||||
|
||||
public init(id: String, title: String, replyPolicy: ReplyPolicy?, exclusive: Bool?) {
|
||||
public init(id: String, title: String) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.replyPolicy = replyPolicy
|
||||
self.exclusive = exclusive
|
||||
}
|
||||
|
||||
public static func ==(lhs: List, rhs: List) -> Bool {
|
||||
|
@ -40,15 +36,8 @@ public struct List: ListProtocol, Decodable, Equatable, Hashable, Sendable {
|
|||
return request
|
||||
}
|
||||
|
||||
public static func update(_ listID: String, title: String, replyPolicy: ReplyPolicy?, exclusive: Bool?) -> Request<List> {
|
||||
var params = ["title" => title]
|
||||
if let replyPolicy {
|
||||
params.append("replies_policy" => replyPolicy.rawValue)
|
||||
}
|
||||
if let exclusive {
|
||||
params.append("exclusive" => exclusive)
|
||||
}
|
||||
return Request<List>(method: .put, path: "/api/v1/lists/\(listID)", body: ParametersBody(params))
|
||||
public static func update(_ listID: String, title: String) -> Request<List> {
|
||||
return Request<List>(method: .put, path: "/api/v1/lists/\(listID)", body: ParametersBody(["title" => title]))
|
||||
}
|
||||
|
||||
public static func delete(_ listID: String) -> Request<Empty> {
|
||||
|
@ -70,13 +59,5 @@ public struct List: ListProtocol, Decodable, Equatable, Hashable, Sendable {
|
|||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case title
|
||||
case replyPolicy = "replies_policy"
|
||||
case exclusive
|
||||
}
|
||||
}
|
||||
|
||||
extension List {
|
||||
public enum ReplyPolicy: String, Codable, Hashable, CaseIterable, Sendable {
|
||||
case followed, list, none
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,4 @@ import Foundation
|
|||
public protocol ListProtocol {
|
||||
var id: String { get }
|
||||
var title: String { get }
|
||||
var replyPolicy: List.ReplyPolicy? { get }
|
||||
var exclusive: Bool? { get }
|
||||
}
|
||||
|
|
|
@ -214,6 +214,8 @@
|
|||
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
|
||||
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */; };
|
||||
D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A3812956123A0036B6EF /* TimelinePosition.swift */; };
|
||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */; };
|
||||
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */; };
|
||||
D6A4531629EF64BA00032932 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4531529EF64BA00032932 /* ShareViewController.swift */; };
|
||||
D6A4531929EF64BA00032932 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6A4531729EF64BA00032932 /* MainInterface.storyboard */; };
|
||||
D6A4531D29EF64BA00032932 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D6A4531329EF64BA00032932 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -256,6 +258,7 @@
|
|||
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366C2828444F00237D0E /* SavedInstance.swift */; };
|
||||
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366E2828452F00237D0E /* SavedHashtag.swift */; };
|
||||
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */; };
|
||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */; };
|
||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */; };
|
||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */; };
|
||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
|
||||
|
@ -615,6 +618,8 @@
|
|||
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = "<group>"; };
|
||||
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderButton.swift; sourceTree = "<group>"; };
|
||||
D6A3A3812956123A0036B6EF /* TimelinePosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePosition.swift; sourceTree = "<group>"; };
|
||||
D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D6A4531329EF64BA00032932 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D6A4531529EF64BA00032932 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
D6A4531829EF64BA00032932 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
|
@ -656,6 +661,7 @@
|
|||
D6B9366C2828444F00237D0E /* SavedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstance.swift; sourceTree = "<group>"; };
|
||||
D6B9366E2828452F00237D0E /* SavedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtag.swift; sourceTree = "<group>"; };
|
||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Helpers.swift"; sourceTree = "<group>"; };
|
||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = "<group>"; };
|
||||
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPageViewController.swift; sourceTree = "<group>"; };
|
||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = "<group>"; };
|
||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1290,6 +1296,8 @@
|
|||
D6A3BC872321F78000FD64D5 /* Account Cell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */,
|
||||
D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */,
|
||||
D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */,
|
||||
);
|
||||
path = "Account Cell";
|
||||
|
@ -1419,6 +1427,7 @@
|
|||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */,
|
||||
D62D67C42A97D8CD00167EE2 /* MultiColumnNavigationController.swift */,
|
||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
||||
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
|
||||
|
@ -1852,6 +1861,7 @@
|
|||
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
|
||||
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
|
||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
|
||||
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */,
|
||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
||||
|
@ -1977,6 +1987,7 @@
|
|||
D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */,
|
||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
|
||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||
D65B4B682977769E00DABDFB /* StatusActionAccountListViewController.swift in Sources */,
|
||||
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */,
|
||||
|
@ -2216,6 +2227,7 @@
|
|||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
||||
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
||||
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
|
||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||
|
|
|
@ -449,12 +449,16 @@ class MastodonController: ObservableObject {
|
|||
guard let lists = try? persistentContainer.viewContext.fetch(req) else {
|
||||
return []
|
||||
}
|
||||
return lists.map(\.apiList).sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
||||
return lists.map {
|
||||
List(id: $0.id, title: $0.title)
|
||||
}.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
||||
}
|
||||
|
||||
func getCachedList(id: String) -> List? {
|
||||
let req = ListMO.fetchRequest(id: id)
|
||||
return (try? persistentContainer.viewContext.fetch(req).first).map(\.apiList)
|
||||
return (try? persistentContainer.viewContext.fetch(req).first).flatMap {
|
||||
List(id: $0.id, title: $0.title)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
|
|
@ -47,7 +47,7 @@ class RenameListService {
|
|||
|
||||
private func updateList(with title: String) async {
|
||||
do {
|
||||
let req = List.update(list.id, title: title, replyPolicy: nil, exclusive: nil)
|
||||
let req = List.update(list.id, title: title)
|
||||
let (list, _) = try await mastodonController.run(req)
|
||||
mastodonController.renamedList(list)
|
||||
} catch {
|
||||
|
|
|
@ -25,22 +25,6 @@ public final class ListMO: NSManagedObject, ListProtocol {
|
|||
|
||||
@NSManaged public var id: String
|
||||
@NSManaged public var title: String
|
||||
@NSManaged private var replyPolicyString: String?
|
||||
@NSManaged private var exclusiveInternal: Bool
|
||||
|
||||
public var replyPolicy: List.ReplyPolicy? {
|
||||
get {
|
||||
replyPolicyString.flatMap(List.ReplyPolicy.init(rawValue:))
|
||||
}
|
||||
set {
|
||||
replyPolicyString = newValue?.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
public var exclusive: Bool? {
|
||||
get { exclusiveInternal }
|
||||
set { exclusiveInternal = newValue ?? false }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -53,16 +37,5 @@ extension ListMO {
|
|||
func updateFrom(apiList list: List) {
|
||||
self.id = list.id
|
||||
self.title = list.title
|
||||
self.replyPolicy = list.replyPolicy
|
||||
self.exclusive = list.exclusive
|
||||
}
|
||||
|
||||
var apiList: List {
|
||||
List(
|
||||
id: id,
|
||||
title: title,
|
||||
replyPolicy: replyPolicy,
|
||||
exclusive: exclusive
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,9 +62,7 @@
|
|||
<attribute name="url" attributeType="URI"/>
|
||||
</entity>
|
||||
<entity name="List" representedClassName="ListMO" syncable="YES">
|
||||
<attribute name="exclusiveInternal" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<attribute name="replyPolicyString" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
|
|
|
@ -112,7 +112,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
|
|||
case .list(id: let id):
|
||||
let req = ListMO.fetchRequest(id: id)
|
||||
if let list = try? mastodonController.persistentContainer.viewContext.fetch(req).first {
|
||||
return ListTimelineViewController(for: list.apiList, mastodonController: mastodonController)
|
||||
return ListTimelineViewController(for: List(id: id, title: list.title), mastodonController: mastodonController)
|
||||
} else {
|
||||
return TimelineViewController(for: timeline, mastodonController: mastodonController)
|
||||
}
|
||||
|
|
|
@ -10,21 +10,18 @@ import UIKit
|
|||
import Pachyderm
|
||||
import Combine
|
||||
|
||||
class EditListAccountsViewController: UIViewController, CollectionViewController {
|
||||
class EditListAccountsViewController: EnhancedTableViewController {
|
||||
|
||||
private var list: List
|
||||
private let mastodonController: MastodonController
|
||||
let mastodonController: MastodonController
|
||||
|
||||
private var state = State.unloaded
|
||||
var changedAccounts = false
|
||||
|
||||
private(set) var changedAccounts = false
|
||||
var dataSource: DataSource!
|
||||
var nextRange: RequestRange?
|
||||
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
var collectionView: UICollectionView! { view as? UICollectionView }
|
||||
private var nextRange: RequestRange?
|
||||
|
||||
private var searchResultsController: SearchResultsViewController!
|
||||
private var searchController: UISearchController!
|
||||
var searchResultsController: SearchResultsViewController!
|
||||
var searchController: UISearchController!
|
||||
|
||||
private var listRenamedCancellable: AnyCancellable?
|
||||
|
||||
|
@ -32,7 +29,7 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
self.list = list
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
super.init(style: .plain)
|
||||
|
||||
listChanged()
|
||||
|
||||
|
@ -49,45 +46,29 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
fatalError("init(coder:) has not been implemeneted")
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
||||
var config = sectionConfig
|
||||
switch dataSource.itemIdentifier(for: indexPath)! {
|
||||
case .loadingIndicator:
|
||||
config.topSeparatorVisibility = .hidden
|
||||
config.bottomSeparatorVisibility = .hidden
|
||||
case .account(id: _):
|
||||
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||
}
|
||||
return config
|
||||
}
|
||||
config.trailingSwipeActionsConfigurationProvider = { [unowned self] indexPath in
|
||||
switch dataSource.itemIdentifier(for: indexPath) {
|
||||
case .account(id: let id):
|
||||
let remove = UIContextualAction(style: .destructive, title: "Remove") { [unowned self] _, _, completion in
|
||||
Task {
|
||||
await self.removeAccount(id: id)
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
return UISwipeActionsConfiguration(actions: [remove])
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.delegate = self
|
||||
collectionView.allowsSelection = false
|
||||
collectionView.backgroundColor = .appGroupedBackground
|
||||
dataSource = createDataSource()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: "accountCell")
|
||||
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 66
|
||||
tableView.allowsSelection = false
|
||||
tableView.backgroundColor = .appGroupedBackground
|
||||
|
||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
guard case let .account(id) = item else { fatalError() }
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell
|
||||
cell.delegate = self
|
||||
cell.updateUI(accountID: id)
|
||||
cell.configurationUpdateHandler = { cell, state in
|
||||
cell.backgroundConfiguration = .appListGroupedCell(for: state)
|
||||
}
|
||||
return cell
|
||||
})
|
||||
dataSource.editListAccountsController = self
|
||||
|
||||
searchResultsController = SearchResultsViewController(mastodonController: mastodonController, scope: .people)
|
||||
searchResultsController.following = true
|
||||
searchResultsController.delegate = self
|
||||
|
@ -106,33 +87,6 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
}
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Rename", comment: "rename list button title"), style: .plain, target: self, action: #selector(renameButtonPressed))
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
||||
cell.indicator.startAnimating()
|
||||
}
|
||||
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, itemIdentifier in
|
||||
cell.delegate = self
|
||||
cell.updateUI(accountID: itemIdentifier)
|
||||
cell.configurationUpdateHandler = { cell, state in
|
||||
cell.backgroundConfiguration = .appListGroupedCell(for: state)
|
||||
}
|
||||
}
|
||||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||
switch itemIdentifier {
|
||||
case .loadingIndicator:
|
||||
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
||||
case .account(id: let id):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
clearSelectionOnAppear(animated: animated)
|
||||
|
||||
Task {
|
||||
await loadAccounts()
|
||||
|
@ -143,21 +97,10 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
title = String(format: NSLocalizedString("Edit %@", comment: "edit list screen title"), list.title)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadAccounts() async {
|
||||
guard state == .unloaded else { return }
|
||||
|
||||
state = .loading
|
||||
|
||||
async let results = try await mastodonController.run(List.getAccounts(list.id))
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems([.loadingIndicator])
|
||||
await dataSource.apply(snapshot)
|
||||
|
||||
func loadAccounts() async {
|
||||
do {
|
||||
let (accounts, pagination) = try await results
|
||||
let request = List.getAccounts(list.id)
|
||||
let (accounts, pagination) = try await mastodonController.run(request)
|
||||
self.nextRange = pagination?.older
|
||||
|
||||
await withCheckedContinuation { continuation in
|
||||
|
@ -166,61 +109,20 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
}
|
||||
}
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
if snapshot.indexOfSection(.accounts) == nil {
|
||||
snapshot.appendSections([.accounts])
|
||||
} else {
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .accounts))
|
||||
}
|
||||
snapshot.appendItems(accounts.map { .account(id: $0.id) })
|
||||
await dataSource.apply(snapshot)
|
||||
|
||||
state = .loaded
|
||||
} catch {
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { [unowned self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self.loadAccounts()
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
state = .unloaded
|
||||
await dataSource.apply(.init())
|
||||
}
|
||||
}
|
||||
|
||||
private func loadNextPage() async {
|
||||
guard state == .loaded,
|
||||
let nextRange else { return }
|
||||
|
||||
state = .loading
|
||||
|
||||
async let results = try await mastodonController.run(List.getAccounts(list.id, range: nextRange))
|
||||
|
||||
let origSnapshot = dataSource.snapshot()
|
||||
var snapshot = origSnapshot
|
||||
snapshot.appendItems([.loadingIndicator])
|
||||
await dataSource.apply(snapshot)
|
||||
|
||||
do {
|
||||
let (accounts, pagination) = try await results
|
||||
self.nextRange = pagination?.older
|
||||
|
||||
await withCheckedContinuation { continuation in
|
||||
mastodonController.persistentContainer.addAll(accounts: accounts) {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
|
||||
var snapshot = origSnapshot
|
||||
snapshot.appendItems(accounts.map { .account(id: $0.id) })
|
||||
await dataSource.apply(snapshot)
|
||||
|
||||
state = .loaded
|
||||
} catch {
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { [unowned self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self.loadNextPage()
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
state = .loaded
|
||||
await dataSource.apply(origSnapshot)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,6 +157,12 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Table view delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
|
||||
return .delete
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc func renameButtonPressed() {
|
||||
|
@ -263,31 +171,29 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
|
||||
}
|
||||
|
||||
extension EditListAccountsViewController {
|
||||
enum State {
|
||||
case unloaded
|
||||
case loading
|
||||
case loaded
|
||||
case loadingOlder
|
||||
}
|
||||
}
|
||||
|
||||
extension EditListAccountsViewController {
|
||||
enum Section: Hashable {
|
||||
case accounts
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case loadingIndicator
|
||||
case account(id: String)
|
||||
}
|
||||
}
|
||||
|
||||
extension EditListAccountsViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
if state == .loaded,
|
||||
indexPath.item == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
|
||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||
weak var editListAccountsController: EditListAccountsViewController?
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
||||
guard editingStyle == .delete,
|
||||
case let .account(id) = itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
await loadNextPage()
|
||||
await self.editListAccountsController?.removeAccount(id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
//
|
||||
// EnhancedTableViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 11/10/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
|
||||
class EnhancedTableViewController: UITableViewController {
|
||||
|
||||
var dragEnabled = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
if dragEnabled {
|
||||
tableView.dragDelegate = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Table View Delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if let cell = tableView.cellForRow(at: indexPath) as? SelectableTableViewCell {
|
||||
cell.didSelectCell()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension EnhancedTableViewController {
|
||||
|
||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
if let cell = tableView.cellForRow(at: indexPath) as? UITableViewCell & MenuPreviewProvider {
|
||||
let cellLocation = cell.convert(point, from: tableView)
|
||||
guard let (previewProvider, actionsProvider) = cell.getPreviewProviders(for: cellLocation, sourceViewController: self) else {
|
||||
return nil
|
||||
}
|
||||
let actionProvider: UIContextMenuActionProvider = { (_) in
|
||||
let suggested = self.getSuggestedContextMenuActions(tableView: tableView, indexPath: indexPath, point: point)
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: suggested + actionsProvider())
|
||||
}
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: previewProvider, actionProvider: actionProvider)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// todo: replace this with the UIKit suggested actions, if possible
|
||||
@objc open func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
|
||||
return []
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
if let viewController = animator.previewViewController {
|
||||
animator.preferredCommitStyle = .pop
|
||||
animator.addCompletion {
|
||||
if let customPresenting = viewController as? CustomPreviewPresenting {
|
||||
customPresenting.presentFromPreview(presenter: self)
|
||||
} else {
|
||||
self.show(viewController, sender: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension EnhancedTableViewController: UITableViewDragDelegate {
|
||||
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
guard let cell = tableView.cellForRow(at: indexPath) as? DraggableTableViewCell else {
|
||||
return []
|
||||
}
|
||||
return cell.dragItemsForBeginning(session: session)
|
||||
}
|
||||
}
|
||||
|
||||
extension EnhancedTableViewController: TabBarScrollableViewController {
|
||||
func tabBarScrollToTop() {
|
||||
tableView.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
extension EnhancedTableViewController: StatusBarTappableViewController {
|
||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||
tableView.scrollToTop()
|
||||
return .stop
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
//
|
||||
// AccountTableViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/5/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftSoup
|
||||
|
||||
class AccountTableViewCell: UITableViewCell {
|
||||
|
||||
weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)?
|
||||
var mastodonController: MastodonController! { delegate?.apiController }
|
||||
|
||||
@IBOutlet weak var avatarImageView: UIImageView!
|
||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
||||
@IBOutlet weak var usernameLabel: UILabel!
|
||||
@IBOutlet weak var noteLabel: EmojiLabel!
|
||||
|
||||
var accountID: String!
|
||||
|
||||
private var avatarRequest: ImageCache.Request?
|
||||
private var isGrayscale = false
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
avatarImageView.layer.masksToBounds = true
|
||||
avatarImageView.layer.cornerCurve = .continuous
|
||||
|
||||
usernameLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .light))
|
||||
usernameLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
noteLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15))
|
||||
noteLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPrefrences), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
@objc func updateUIForPrefrences() {
|
||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||
// this table view cell could be cached in a table view (e.g., SearchResultsViewController) for an account that's since been purged
|
||||
return
|
||||
}
|
||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
|
||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||
updateGrayscaleableUI(account: account)
|
||||
}
|
||||
}
|
||||
|
||||
func updateUI(accountID: String) {
|
||||
self.accountID = accountID
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||
fatalError("Missing cached account \(accountID)")
|
||||
}
|
||||
|
||||
usernameLabel.text = "@\(account.acct)"
|
||||
|
||||
updateGrayscaleableUI(account: account)
|
||||
updateUIForPrefrences()
|
||||
}
|
||||
|
||||
private func updateGrayscaleableUI(account: AccountMO) {
|
||||
isGrayscale = Preferences.shared.grayscaleImages
|
||||
|
||||
let accountID = self.accountID
|
||||
|
||||
avatarImageView.image = nil
|
||||
if let avatarURL = account.avatar {
|
||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||
guard let self = self else { return }
|
||||
self.avatarRequest = nil
|
||||
|
||||
guard let image = image,
|
||||
self.accountID == accountID,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let doc = try! SwiftSoup.parse(account.note)
|
||||
noteLabel.text = try! doc.text()
|
||||
noteLabel.setEmojis(account.emojis, identifier: account.id)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
avatarRequest?.cancel()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AccountTableViewCell: SelectableTableViewCell {
|
||||
func didSelectCell() {
|
||||
delegate?.selected(account: accountID)
|
||||
}
|
||||
}
|
||||
|
||||
extension AccountTableViewCell: MenuPreviewProvider {
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
guard let mastodonController = mastodonController else { return nil }
|
||||
return (
|
||||
content: { ProfileViewController(accountID: self.accountID, mastodonController: mastodonController) },
|
||||
actions: { self.delegate?.actionsForProfile(accountID: self.accountID, source: .view(self.avatarImageView)) ?? [] }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension AccountTableViewCell: DraggableTableViewCell {
|
||||
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID),
|
||||
let currentAccountID = mastodonController.accountInfo?.id else {
|
||||
return []
|
||||
}
|
||||
let provider = NSItemProvider(object: account.url as NSURL)
|
||||
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
||||
activity.displaysAuxiliaryScene = true
|
||||
provider.registerObject(activity, visibility: .all)
|
||||
return [UIDragItem(itemProvider: provider)]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="100" id="KGk-i7-Jjw" customClass="AccountTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Rp2-O5-Vew">
|
||||
<rect key="frame" x="16" y="8" width="50" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="Rp2-O5-Vew" secondAttribute="height" multiplier="1:1" id="1AQ-lU-ptd"/>
|
||||
<constraint firstAttribute="height" constant="50" id="NqI-m0-owe"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Iif-9m-vM5">
|
||||
<rect key="frame" x="74" y="11" width="230" height="78"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fhc-bZ-lkB" customClass="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="230" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JMo-QH-1is">
|
||||
<rect key="frame" x="0.0" y="20.5" width="230" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Note" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bNO-qR-YEe" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="38.5" width="230" height="39.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="Iif-9m-vM5" secondAttribute="bottom" id="dV0-Vm-DUb"/>
|
||||
<constraint firstItem="Iif-9m-vM5" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="ihr-er-kLO"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="Iif-9m-vM5" secondAttribute="trailing" id="q7a-DT-WPF"/>
|
||||
<constraint firstItem="Iif-9m-vM5" firstAttribute="leading" secondItem="Rp2-O5-Vew" secondAttribute="trailing" constant="8" id="sk1-KY-Ttj"/>
|
||||
<constraint firstItem="Rp2-O5-Vew" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="xpB-wY-5d6"/>
|
||||
<constraint firstItem="Rp2-O5-Vew" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="yd4-AU-qbj"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="avatarImageView" destination="Rp2-O5-Vew" id="3Gw-Xg-bd5"/>
|
||||
<outlet property="displayNameLabel" destination="Fhc-bZ-lkB" id="1b0-3k-KR8"/>
|
||||
<outlet property="noteLabel" destination="bNO-qR-YEe" id="4oO-c0-BOT"/>
|
||||
<outlet property="usernameLabel" destination="JMo-QH-1is" id="ElX-ua-xcQ"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="173.91304347826087" y="35.491071428571423"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
Loading…
Reference in New Issue