From f5385b0a1d7646b629d8b76160902365b43818aa Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 10 Jun 2022 23:36:40 -0400 Subject: [PATCH] Use context menu for filter/sort on profile directory --- .../Pachyderm/Model/DirectoryOrder.swift | 2 +- Tusker.xcodeproj/project.pbxproj | 8 +- Tusker/Extensions/UIAction+Subtitle.swift | 19 +++ .../Compose/ComposeHostingController.swift | 10 -- .../Explore/ProfileDirectoryFilterView.swift | 134 ------------------ .../ProfileDirectoryViewController.swift | 93 ++++++++---- 6 files changed, 91 insertions(+), 175 deletions(-) create mode 100644 Tusker/Extensions/UIAction+Subtitle.swift delete mode 100644 Tusker/Screens/Explore/ProfileDirectoryFilterView.swift diff --git a/Pachyderm/Sources/Pachyderm/Model/DirectoryOrder.swift b/Pachyderm/Sources/Pachyderm/Model/DirectoryOrder.swift index 25a6fdb7..bb25aebb 100644 --- a/Pachyderm/Sources/Pachyderm/Model/DirectoryOrder.swift +++ b/Pachyderm/Sources/Pachyderm/Model/DirectoryOrder.swift @@ -8,7 +8,7 @@ import Foundation -public enum DirectoryOrder: String { +public enum DirectoryOrder: String, CaseIterable { case active case new } diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index fa74eef1..3e999cec 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; - D600613E25D07E170067FAD6 /* ProfileDirectoryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */; }; D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; }; D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */; }; D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */; }; @@ -283,6 +282,7 @@ D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; }; D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; }; D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; + D6E1EEF4285443EF00D20549 /* UIAction+Subtitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E1EEF3285443EF00D20549 /* UIAction+Subtitle.swift */; }; D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; }; D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; }; D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; }; @@ -366,7 +366,6 @@ 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = ""; }; - D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryFilterView.swift; sourceTree = ""; }; D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = ""; }; D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagTableViewCell.swift; sourceTree = ""; }; D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingHashtagTableViewCell.xib; sourceTree = ""; }; @@ -635,6 +634,7 @@ D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = ""; }; D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = ""; }; D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = ""; }; + D6E1EEF3285443EF00D20549 /* UIAction+Subtitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAction+Subtitle.swift"; sourceTree = ""; }; D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInTusker.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D6E343AA265AAD6B00C4AA01 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = ""; }; D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = ""; }; @@ -806,7 +806,6 @@ D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */, D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */, D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */, - D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */, ); path = Explore; sourceTree = ""; @@ -1124,6 +1123,7 @@ D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */, D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */, D62E9984279CA23900C26176 /* URLSession+Development.swift */, + D6E1EEF3285443EF00D20549 /* UIAction+Subtitle.swift */, ); path = Extensions; sourceTree = ""; @@ -1744,7 +1744,6 @@ D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */, D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */, D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */, - D600613E25D07E170067FAD6 /* ProfileDirectoryFilterView.swift in Sources */, D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */, @@ -1765,6 +1764,7 @@ D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */, D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, + D6E1EEF4285443EF00D20549 /* UIAction+Subtitle.swift in Sources */, D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */, D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */, D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */, diff --git a/Tusker/Extensions/UIAction+Subtitle.swift b/Tusker/Extensions/UIAction+Subtitle.swift new file mode 100644 index 00000000..690ed97f --- /dev/null +++ b/Tusker/Extensions/UIAction+Subtitle.swift @@ -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) + } + } +} diff --git a/Tusker/Screens/Compose/ComposeHostingController.swift b/Tusker/Screens/Compose/ComposeHostingController.swift index 602e4c69..6e7acee9 100644 --- a/Tusker/Screens/Compose/ComposeHostingController.swift +++ b/Tusker/Screens/Compose/ComposeHostingController.swift @@ -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) - } - } -} diff --git a/Tusker/Screens/Explore/ProfileDirectoryFilterView.swift b/Tusker/Screens/Explore/ProfileDirectoryFilterView.swift deleted file mode 100644 index 843dde27..00000000 --- a/Tusker/Screens/Explore/ProfileDirectoryFilterView.swift +++ /dev/null @@ -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 - } -} diff --git a/Tusker/Screens/Explore/ProfileDirectoryViewController.swift b/Tusker/Screens/Explore/ProfileDirectoryViewController.swift index 19ce4dd0..f35e9b13 100644 --- a/Tusker/Screens/Explore/ProfileDirectoryViewController.swift +++ b/Tusker/Screens/Explore/ProfileDirectoryViewController.swift @@ -16,6 +16,9 @@ class ProfileDirectoryViewController: UIViewController { private var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! + 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 { @@ -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" + } + } +}