Compare commits

..

5 Commits

12 changed files with 104 additions and 182 deletions

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum DirectoryOrder: String { public enum DirectoryOrder: String, CaseIterable {
case active case active
case new case new
} }

View File

@ -286,7 +286,7 @@
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; }; D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; };
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; }; D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; }; D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; };
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = D6E343B9265AAD8C00C4AA01 /* Action.js */; }; D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = D6E343B9265AAD8C00C4AA01 /* Action.js */; };
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */; }; D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */; };
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; }; D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; };
@ -331,15 +331,15 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
D6E3438F2659849800C4AA01 /* Embed App Extensions */ = { D6E3438F2659849800C4AA01 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
dstPath = ""; dstPath = "";
dstSubfolderSpec = 13; dstSubfolderSpec = 13;
files = ( files = (
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed App Extensions */, D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */,
); );
name = "Embed App Extensions"; name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
D6F953E52125197500CF0F2B /* Embed Frameworks */ = { D6F953E52125197500CF0F2B /* Embed Frameworks */ = {
@ -1494,7 +1494,7 @@
D6D4DDCA212518A000E1C4BB /* Resources */, D6D4DDCA212518A000E1C4BB /* Resources */,
D6F953E52125197500CF0F2B /* Embed Frameworks */, D6F953E52125197500CF0F2B /* Embed Frameworks */,
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */, D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
D6E3438F2659849800C4AA01 /* Embed App Extensions */, D6E3438F2659849800C4AA01 /* Embed Foundation Extensions */,
D6F1F9E127B0677000CB7D88 /* ShellScript */, D6F1F9E127B0677000CB7D88 /* ShellScript */,
); );
buildRules = ( buildRules = (
@ -1576,7 +1576,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1250; LastSwiftUpdateCheck = 1250;
LastUpgradeCheck = 1250; LastUpgradeCheck = 1400;
ORGANIZATIONNAME = Shadowfacts; ORGANIZATIONNAME = Shadowfacts;
TargetAttributes = { TargetAttributes = {
D6D4DDCB212518A000E1C4BB = { D6D4DDCB212518A000E1C4BB = {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1250" LastUpgradeVersion = "1400"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1250" LastUpgradeVersion = "1400"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -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)
}
}
}

View File

@ -471,13 +471,3 @@ extension ComposeHostingController: ComposeDrawingViewControllerDelegate {
dismiss(animated: true) 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)
}
}
}

View File

@ -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
}
}

View File

@ -16,6 +16,9 @@ class ProfileDirectoryViewController: UIViewController {
private var collectionView: UICollectionView! private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var scope: Scope = .everywhere
private var order: DirectoryOrder = .active
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
@ -31,14 +34,10 @@ class ProfileDirectoryViewController: UIViewController {
title = NSLocalizedString("Profile Directory", comment: "profile directory title") title = NSLocalizedString("Profile Directory", comment: "profile directory title")
let configuration = UICollectionViewCompositionalLayoutConfiguration() // todo: it would be nice if there were a better "filter" icon
configuration.boundarySupplementaryItems = [ navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "scope"), menu: nil)
NSCollectionLayoutBoundarySupplementaryItem( updateFilterMenu()
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100)),
elementKind: "filter",
alignment: .top
)
]
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in
let itemHeight = NSCollectionLayoutDimension.absolute(200) let itemHeight = NSCollectionLayoutDimension.absolute(200)
let itemWidth: NSCollectionLayoutDimension let itemWidth: NSCollectionLayoutDimension
@ -60,19 +59,18 @@ class ProfileDirectoryViewController: UIViewController {
section.interGroupSpacing = 16 section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
return section return section
}, configuration: configuration) })
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .secondarySystemBackground collectionView.backgroundColor = .secondarySystemBackground
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell") collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
collectionView.register(ProfileDirectoryFilterView.self, forSupplementaryViewOfKind: "filter", withReuseIdentifier: "filter")
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
view.addSubview(collectionView) view.addSubview(collectionView)
dataSource = createDataSource() dataSource = createDataSource()
updateProfiles(local: true, order: .active) updateProfiles()
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
@ -82,26 +80,44 @@ class ProfileDirectoryViewController: UIViewController {
cell.updateUI(account: account) cell.updateUI(account: account)
return cell 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 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) let request = Client.getFeaturedProfiles(local: local, order: order)
mastodonController.run(request) { (response) in 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 return
} }
@ -188,3 +204,28 @@ extension ProfileDirectoryViewController: UICollectionViewDragDelegate {
return [UIDragItem(itemProvider: provider)] 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"
}
}
}

View File

@ -81,6 +81,9 @@ class InstanceSelectorTableViewController: UITableViewController {
searchController.searchBar.searchTextField.autocapitalizationType = .none searchController.searchBar.searchTextField.autocapitalizationType = .none
navigationItem.searchController = searchController navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false navigationItem.hidesSearchBarWhenScrolling = false
if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked
}
definesPresentationContext = true definesPresentationContext = true
urlHandler = urlCheckerSubject urlHandler = urlCheckerSubject

View File

@ -45,6 +45,9 @@ class SearchViewController: UIViewController {
navigationItem.searchController = searchController navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false navigationItem.hidesSearchBarWhenScrolling = false
if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked
}
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {

View File

@ -34,7 +34,9 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
override func viewDidLoad() { override func viewDidLoad() {
super.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.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 140 tableView.estimatedRowHeight = 140

View File

@ -171,7 +171,6 @@ class ProfileHeaderView: UIView {
} }
private func updateRelationship() { private func updateRelationship() {
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
guard let mastodonController = mastodonController, guard let mastodonController = mastodonController,
let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else { let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else {
return return
@ -181,7 +180,6 @@ class ProfileHeaderView: UIView {
} }
@objc private func updateUIForPreferences() { @objc private func updateUIForPreferences() {
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
guard let mastodonController = mastodonController, guard let mastodonController = mastodonController,
// nil if prefs changed before own account is loaded // nil if prefs changed before own account is loaded
let accountID = accountID, let accountID = accountID,