Compare commits
3 Commits
cc10a13785
...
68dbbca9b8
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 68dbbca9b8 | |
Shadowfacts | 7f1577f4a4 | |
Shadowfacts | e52f032d01 |
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum DirectoryOrder: String {
|
||||
public enum DirectoryOrder: String, CaseIterable {
|
||||
case active
|
||||
case new
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// UIAction+Subtitle.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/10/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIAction {
|
||||
convenience init(title: String, subtitle: String?, image: UIImage?, state: UIAction.State, handler: @escaping UIActionHandler) {
|
||||
if #available(iOS 15.0, *) {
|
||||
self.init(title: title, subtitle: subtitle, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
|
||||
} else {
|
||||
self.init(title: title, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -471,13 +471,3 @@ extension ComposeHostingController: ComposeDrawingViewControllerDelegate {
|
|||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension UIAction {
|
||||
convenience init(title: String, subtitle: String?, image: UIImage?, state: UIAction.State, handler: @escaping UIActionHandler) {
|
||||
if #available(iOS 15.0, *) {
|
||||
self.init(title: title, subtitle: subtitle, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
|
||||
} else {
|
||||
self.init(title: title, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
//
|
||||
// ProfileDirectoryFilterView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 2/7/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class ProfileDirectoryFilterView: UICollectionReusableView {
|
||||
|
||||
var onFilterChanged: ((Scope, DirectoryOrder) -> Void)?
|
||||
|
||||
private var scope: UISegmentedControl!
|
||||
private var sort: UISegmentedControl!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
|
||||
commonInit()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
scope = UISegmentedControl(items: ["Instance", NSLocalizedString("Everywhere", comment: "everywhere profile directory scope")])
|
||||
scope.selectedSegmentIndex = 0
|
||||
scope.addTarget(self, action: #selector(filterChanged), for: .valueChanged)
|
||||
|
||||
sort = UISegmentedControl(items: [
|
||||
NSLocalizedString("Active", comment: "active profile directory sort"),
|
||||
NSLocalizedString("New", comment: "new profile directory sort"),
|
||||
])
|
||||
sort.selectedSegmentIndex = 0
|
||||
sort.addTarget(self, action: #selector(filterChanged), for: .valueChanged)
|
||||
|
||||
let fromLabel = UILabel()
|
||||
fromLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
fromLabel.text = NSLocalizedString("From", comment: "profile directory scope label")
|
||||
fromLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
fromLabel.textAlignment = .right
|
||||
|
||||
let sortLabel = UILabel()
|
||||
sortLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
sortLabel.text = NSLocalizedString("Sort By", comment: "profile directory sort label")
|
||||
sortLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
sortLabel.textAlignment = .right
|
||||
|
||||
let labelContainer = UIView()
|
||||
labelContainer.addSubview(sortLabel)
|
||||
labelContainer.addSubview(fromLabel)
|
||||
|
||||
let controlStack = UIStackView(arrangedSubviews: [sort, scope])
|
||||
controlStack.axis = .vertical
|
||||
controlStack.spacing = 8
|
||||
|
||||
let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
|
||||
let blurView = UIVisualEffectView(effect: blurEffect)
|
||||
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(blurView)
|
||||
|
||||
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect, style: .label)
|
||||
let vibrancyView = UIVisualEffectView(effect: vibrancyEffect)
|
||||
vibrancyView.translatesAutoresizingMaskIntoConstraints = false
|
||||
blurView.contentView.addSubview(vibrancyView)
|
||||
|
||||
let filterStack = UIStackView(arrangedSubviews: [
|
||||
labelContainer,
|
||||
controlStack,
|
||||
])
|
||||
filterStack.axis = .horizontal
|
||||
filterStack.spacing = 8
|
||||
filterStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
vibrancyView.contentView.addSubview(filterStack)
|
||||
|
||||
let separator = UIView()
|
||||
separator.backgroundColor = .separator
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(separator)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
fromLabel.leadingAnchor.constraint(equalTo: labelContainer.leadingAnchor),
|
||||
fromLabel.trailingAnchor.constraint(equalTo: labelContainer.trailingAnchor),
|
||||
fromLabel.centerYAnchor.constraint(equalTo: scope.centerYAnchor),
|
||||
|
||||
sortLabel.leadingAnchor.constraint(equalTo: labelContainer.leadingAnchor),
|
||||
sortLabel.trailingAnchor.constraint(equalTo: labelContainer.trailingAnchor),
|
||||
sortLabel.centerYAnchor.constraint(equalTo: sort.centerYAnchor),
|
||||
|
||||
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
|
||||
vibrancyView.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
|
||||
vibrancyView.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
|
||||
vibrancyView.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
|
||||
vibrancyView.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor),
|
||||
|
||||
filterStack.leadingAnchor.constraint(equalToSystemSpacingAfter: vibrancyView.contentView.leadingAnchor, multiplier: 1),
|
||||
vibrancyView.contentView.trailingAnchor.constraint(equalToSystemSpacingAfter: filterStack.trailingAnchor, multiplier: 1),
|
||||
filterStack.topAnchor.constraint(equalToSystemSpacingBelow: vibrancyView.contentView.topAnchor, multiplier: 1),
|
||||
vibrancyView.contentView.bottomAnchor.constraint(equalToSystemSpacingBelow: filterStack.bottomAnchor, multiplier: 1),
|
||||
|
||||
separator.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
separator.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
separator.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
separator.heightAnchor.constraint(equalToConstant: 0.5),
|
||||
])
|
||||
}
|
||||
|
||||
func updateUI(mastodonController: MastodonController) {
|
||||
scope.setTitle(mastodonController.accountInfo!.instanceURL.host!, forSegmentAt: 0)
|
||||
}
|
||||
|
||||
@objc private func filterChanged() {
|
||||
let scope = Scope(rawValue: self.scope.selectedSegmentIndex)!
|
||||
let order = sort.selectedSegmentIndex == 0 ? DirectoryOrder.active : .new
|
||||
onFilterChanged?(scope, order)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileDirectoryFilterView {
|
||||
enum Scope: Int, Equatable {
|
||||
case instance, everywhere
|
||||
}
|
||||
}
|
|
@ -16,6 +16,9 @@ class ProfileDirectoryViewController: UIViewController {
|
|||
private var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
private var scope: Scope = .everywhere
|
||||
private var order: DirectoryOrder = .active
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
|
@ -31,14 +34,10 @@ class ProfileDirectoryViewController: UIViewController {
|
|||
|
||||
title = NSLocalizedString("Profile Directory", comment: "profile directory title")
|
||||
|
||||
let configuration = UICollectionViewCompositionalLayoutConfiguration()
|
||||
configuration.boundarySupplementaryItems = [
|
||||
NSCollectionLayoutBoundarySupplementaryItem(
|
||||
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100)),
|
||||
elementKind: "filter",
|
||||
alignment: .top
|
||||
)
|
||||
]
|
||||
// todo: it would be nice if there were a better "filter" icon
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "scope"), menu: nil)
|
||||
updateFilterMenu()
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in
|
||||
let itemHeight = NSCollectionLayoutDimension.absolute(200)
|
||||
let itemWidth: NSCollectionLayoutDimension
|
||||
|
@ -60,19 +59,18 @@ class ProfileDirectoryViewController: UIViewController {
|
|||
section.interGroupSpacing = 16
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
|
||||
return section
|
||||
}, configuration: configuration)
|
||||
})
|
||||
|
||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
collectionView.backgroundColor = .secondarySystemBackground
|
||||
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
|
||||
collectionView.register(ProfileDirectoryFilterView.self, forSupplementaryViewOfKind: "filter", withReuseIdentifier: "filter")
|
||||
collectionView.delegate = self
|
||||
collectionView.dragDelegate = self
|
||||
view.addSubview(collectionView)
|
||||
|
||||
dataSource = createDataSource()
|
||||
updateProfiles(local: true, order: .active)
|
||||
updateProfiles()
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
|
@ -82,26 +80,44 @@ class ProfileDirectoryViewController: UIViewController {
|
|||
cell.updateUI(account: account)
|
||||
return cell
|
||||
}
|
||||
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
||||
guard elementKind == "filter" else {
|
||||
return nil
|
||||
}
|
||||
let filterView = collectionView.dequeueReusableSupplementaryView(ofKind: "filter", withReuseIdentifier: "filter", for: indexPath) as! ProfileDirectoryFilterView
|
||||
filterView.updateUI(mastodonController: self.mastodonController)
|
||||
filterView.onFilterChanged = { [weak self] (scope, order) in
|
||||
guard let self = self else { return }
|
||||
self.dataSource.apply(.init())
|
||||
self.updateProfiles(local: scope == .instance, order: order)
|
||||
}
|
||||
return filterView
|
||||
}
|
||||
return dataSource
|
||||
}
|
||||
|
||||
private func updateProfiles(local: Bool, order: DirectoryOrder) {
|
||||
private func updateFilterMenu() {
|
||||
let scopeMenu = UIMenu(options: .displayInline, children: [
|
||||
UIAction(title: "Everywhere", subtitle: "Users from the whole network", image: UIImage(systemName: "globe"), state: scope == .everywhere ? .on : .off, handler: { [unowned self] _ in
|
||||
self.scope = .everywhere
|
||||
self.updateFilterMenu()
|
||||
self.updateProfiles()
|
||||
}),
|
||||
UIAction(title: mastodonController.accountInfo!.instanceURL.host!, subtitle: "Only users from your instance", image: UIImage(systemName: "house"), state: scope == .instance ? .on : .off, handler: { [unowned self] _ in
|
||||
self.scope = .instance
|
||||
self.updateFilterMenu()
|
||||
self.updateProfiles()
|
||||
}),
|
||||
])
|
||||
let orderMenu = UIMenu(options: .displayInline, children: DirectoryOrder.allCases.map { order in
|
||||
UIAction(title: order.title, subtitle: order.subtitle, image: nil, state: self.order == order ? .on : .off) { [unowned self] _ in
|
||||
self.order = order
|
||||
self.updateFilterMenu()
|
||||
self.updateProfiles()
|
||||
}
|
||||
})
|
||||
navigationItem.rightBarButtonItem!.menu = UIMenu(children: [
|
||||
scopeMenu,
|
||||
orderMenu,
|
||||
])
|
||||
}
|
||||
|
||||
private func updateProfiles() {
|
||||
let scope = self.scope
|
||||
let order = self.order
|
||||
let local = scope == .everywhere
|
||||
let request = Client.getFeaturedProfiles(local: local, order: order)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(accounts, _) = response else {
|
||||
guard case let .success(accounts, _) = response,
|
||||
self.scope == scope,
|
||||
self.order == order else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -188,3 +204,28 @@ extension ProfileDirectoryViewController: UICollectionViewDragDelegate {
|
|||
return [UIDragItem(itemProvider: provider)]
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileDirectoryViewController {
|
||||
enum Scope: CaseIterable {
|
||||
case instance, everywhere
|
||||
}
|
||||
}
|
||||
|
||||
private extension DirectoryOrder {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .active:
|
||||
return "Active"
|
||||
case .new:
|
||||
return "New"
|
||||
}
|
||||
}
|
||||
var subtitle: String {
|
||||
switch self {
|
||||
case .active:
|
||||
return "Users who have posted lately"
|
||||
case .new:
|
||||
return "Recently joined users"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,9 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
searchController.searchBar.searchTextField.autocapitalizationType = .none
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
if #available(iOS 16.0, *) {
|
||||
navigationItem.preferredSearchBarPlacement = .stacked
|
||||
}
|
||||
definesPresentationContext = true
|
||||
|
||||
urlHandler = urlCheckerSubject
|
||||
|
|
|
@ -34,7 +34,9 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: self.cellProvider)
|
||||
dataSource = UITableViewDiffableDataSource(tableView: tableView) { [unowned self] (tableView, indexPath, item) in
|
||||
self.cellProvider(tableView, indexPath, item)
|
||||
}
|
||||
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 140
|
||||
|
|
|
@ -171,7 +171,6 @@ class ProfileHeaderView: UIView {
|
|||
}
|
||||
|
||||
private func updateRelationship() {
|
||||
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
|
||||
guard let mastodonController = mastodonController,
|
||||
let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else {
|
||||
return
|
||||
|
@ -181,7 +180,6 @@ class ProfileHeaderView: UIView {
|
|||
}
|
||||
|
||||
@objc private func updateUIForPreferences() {
|
||||
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
|
||||
guard let mastodonController = mastodonController,
|
||||
// nil if prefs changed before own account is loaded
|
||||
let accountID = accountID,
|
||||
|
|
Loading…
Reference in New Issue