Move trending hashtags/links to Explore tab on iPad

This commit is contained in:
Shadowfacts 2022-06-30 19:26:28 -07:00
parent 700cc2c67c
commit 5cb25c8c1f
10 changed files with 553 additions and 46 deletions

View File

@ -289,6 +289,8 @@
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; }; D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; }; D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */; }; D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */; };
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */; };
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */; };
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E9CDA7281A427800BBC98E /* PostService.swift */; }; D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E9CDA7281A427800BBC98E /* PostService.swift */; };
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; }; D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; }; D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
@ -641,6 +643,8 @@
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; }; D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagCollectionViewCell.swift; sourceTree = "<group>"; }; D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagCollectionViewCell.swift; sourceTree = "<group>"; };
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkCardCollectionViewCell.swift; sourceTree = "<group>"; };
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingLinkCardCollectionViewCell.xib; sourceTree = "<group>"; };
D6E9CDA7281A427800BBC98E /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = "<group>"; }; D6E9CDA7281A427800BBC98E /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = "<group>"; };
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; }; D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; }; D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
@ -794,6 +798,8 @@
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */, D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */, D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */,
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */, D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */,
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */,
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */,
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */, D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */, D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */, D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
@ -1624,6 +1630,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */, D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */, D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */, D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */, D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */,
@ -1912,6 +1919,7 @@
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */, D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */, 04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */, D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */, D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */, D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
D6B81F442560390300F6E31D /* MenuController.swift in Sources */, D6B81F442560390300F6E31D /* MenuController.swift in Sources */,

View File

@ -48,7 +48,7 @@ class MastodonController: ObservableObject {
@Published private(set) var instanceFeatures = InstanceFeatures() @Published private(set) var instanceFeatures = InstanceFeatures()
private(set) var customEmojis: [Emoji]? private(set) var customEmojis: [Emoji]?
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]() private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
private var ownInstanceRequest: URLSessionTask? private var ownInstanceRequest: URLSessionTask?
var loggedIn: Bool { var loggedIn: Bool {
@ -159,15 +159,28 @@ class MastodonController: ObservableObject {
} }
func getOwnInstance(completion: ((Instance) -> Void)? = nil) { func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
getOwnInstanceInternal(retryAttempt: 0, completion: completion) getOwnInstanceInternal(retryAttempt: 0) {
if case let .success(instance) = $0 {
completion?(instance)
}
}
} }
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) { @MainActor
func getOwnInstance() async throws -> Instance {
return try await withCheckedThrowingContinuation({ continuation in
getOwnInstanceInternal(retryAttempt: 0) { result in
continuation.resume(with: result)
}
})
}
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<Instance, Client.Error>) -> Void)?) {
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks // this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
assert(Thread.isMainThread) assert(Thread.isMainThread)
if let instance = self.instance { if let instance = self.instance {
completion?(instance) completion?(.success(instance))
} else { } else {
if let completion = completion { if let completion = completion {
pendingOwnInstanceRequestCallbacks.append(completion) pendingOwnInstanceRequestCallbacks.append(completion)
@ -177,7 +190,7 @@ class MastodonController: ObservableObject {
let request = Client.getInstance() let request = Client.getInstance()
ownInstanceRequest = run(request) { (response) in ownInstanceRequest = run(request) { (response) in
switch response { switch response {
case .failure(_): case .failure(let error):
let delay: DispatchTimeInterval let delay: DispatchTimeInterval
switch retryAttempt { switch retryAttempt {
case 0: case 0:
@ -190,6 +203,10 @@ class MastodonController: ObservableObject {
delay = .seconds(60) delay = .seconds(60)
default: default:
// if we've failed four times, just give up :/ // if we've failed four times, just give up :/
for completion in self.pendingOwnInstanceRequestCallbacks {
completion(.failure(error))
}
self.pendingOwnInstanceRequestCallbacks = []
return return
} }
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
@ -204,7 +221,7 @@ class MastodonController: ObservableObject {
self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo) self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo)
for completion in self.pendingOwnInstanceRequestCallbacks { for completion in self.pendingOwnInstanceRequestCallbacks {
completion(instance) completion(.success(instance))
} }
self.pendingOwnInstanceRequestCallbacks = [] self.pendingOwnInstanceRequestCallbacks = []
} }

View File

@ -22,7 +22,7 @@ struct MenuController {
let data: Any let data: Any
if case let .tab(tab) = item { if case let .tab(tab) = item {
data = tab.rawValue data = tab.rawValue
} else if case .search = item { } else if case .explore = item {
data = "search" data = "search"
} else if case .bookmarks = item { } else if case .bookmarks = item {
data = "bookmarks" data = "bookmarks"
@ -42,7 +42,7 @@ struct MenuController {
static let sidebarItemKeyCommands: [UIKeyCommand] = [ static let sidebarItemKeyCommands: [UIKeyCommand] = [
sidebarCommand(item: .tab(.timelines), command: "1"), sidebarCommand(item: .tab(.timelines), command: "1"),
sidebarCommand(item: .tab(.notifications), command: "2"), sidebarCommand(item: .tab(.notifications), command: "2"),
sidebarCommand(item: .search, command: "3"), sidebarCommand(item: .explore, command: "3"),
sidebarCommand(item: .bookmarks, command: "4"), sidebarCommand(item: .bookmarks, command: "4"),
sidebarCommand(item: .tab(.myProfile), command: "5"), sidebarCommand(item: .tab(.myProfile), command: "5"),
] ]

View File

@ -34,6 +34,10 @@ struct InstanceFeatures {
instanceType != .pixelfed instanceType != .pixelfed
} }
var trends: Bool {
instanceType == .mastodon
}
var trendingStatusesAndLinks: Bool { var trendingStatusesAndLinks: Bool {
instanceType == .mastodon && version != nil && version! >= Version(3, 5, 0) instanceType == .mastodon && version != nil && version! >= Version(3, 5, 0)
} }

View File

@ -0,0 +1,141 @@
//
// TrendingLinkCardCollectionViewCell.swift
// Tusker
//
// Created by Shadowfacts on 6/29/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
private var card: Card?
private var isGrayscale = false
private var thumbnailRequest: ImageCache.Request?
@IBOutlet weak var thumbnailView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var providerLabel: UILabel!
@IBOutlet weak var activityLabel: UILabel!
@IBOutlet weak var historyView: TrendHistoryView!
override func awakeFromNib() {
super.awakeFromNib()
layer.shadowOpacity = 0.2
layer.shadowRadius = 8
layer.shadowOffset = .zero
layer.masksToBounds = false
updateLayerColors()
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
}
override func layoutSubviews() {
super.layoutSubviews()
contentView.layer.cornerRadius = 0.05 * bounds.width
thumbnailView.layer.cornerRadius = 0.05 * bounds.width
}
func updateUI(card: Card) {
self.card = card
self.thumbnailView.image = nil
updateGrayscaleableUI(card: card)
updateUIForPreferences()
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
titleLabel.text = title
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
providerLabel.text = provider
let sorted = card.history!.sorted(by: { $0.day < $1.day })
let lastTwo = sorted[(sorted.count - 2)...]
let accounts = lastTwo.map(\.accounts).reduce(0, +)
let uses = lastTwo.map(\.uses).reduce(0, +)
// U+2009 THIN SPACE
let activityStr = NSMutableAttributedString(string: "\(accounts.formatted())\u{2009}")
activityStr.append(NSAttributedString(attachment: NSTextAttachment(image: UIImage(systemName: "person")!)))
activityStr.append(NSAttributedString(string: ", \(uses.formatted())\u{2009}"))
activityStr.append(NSAttributedString(attachment: NSTextAttachment(image: UIImage(systemName: "square.text.square")!)))
activityLabel.attributedText = activityStr
historyView.setHistory(card.history)
historyView.isHidden = card.history == nil || card.history!.count < 2
}
@objc private func updateUIForPreferences() {
if isGrayscale != Preferences.shared.grayscaleImages,
let card {
updateGrayscaleableUI(card: card)
}
}
private func updateGrayscaleableUI(card: Card) {
isGrayscale = Preferences.shared.grayscaleImages
if let imageURL = card.image,
let url = URL(imageURL) {
thumbnailRequest = ImageCache.attachments.get(url, completion: { _, image in
guard let image,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) else {
return
}
DispatchQueue.main.async {
self.thumbnailView.image = transformedImage
}
})
if thumbnailRequest != nil {
loadBlurHash(card: card)
}
}
}
private func loadBlurHash(card: Card) {
guard let hash = card.blurhash else {
return
}
let imageViewSize = self.thumbnailView.bounds.size
AttachmentView.queue.async { [weak self] in
let size: CGSize
if let width = card.width, let height = card.height {
size = CGSize(width: width, height: height)
} else {
size = imageViewSize
}
guard let preview = UIImage(blurHash: hash, size: size) else {
return
}
DispatchQueue.main.async { [weak self] in
guard let self,
self.card?.url == card.url,
self.thumbnailView.image == nil else {
return
}
self.thumbnailView.image = preview
}
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateLayerColors()
}
private func updateLayerColors() {
if traitCollection.userInterfaceStyle == .dark {
// clippingView.layer.borderColor = UIColor.darkGray.withAlphaComponent(0.5).cgColor
layer.shadowColor = UIColor.darkGray.cgColor
} else {
// clippingView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
layer.shadowColor = UIColor.black.cgColor
}
}
}

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21179.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21169.4"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="collection view cell content view" 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"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="izA-ZZ-g7F" customClass="TrendingLinkCardCollectionViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="300" height="400"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Zb0-aW-Sen">
<rect key="frame" x="0.0" y="0.0" width="300" height="400"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="h3b-Mf-lD6">
<rect key="frame" x="0.0" y="0.0" width="300" height="225"/>
<constraints>
<constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" id="QDY-8a-LYC"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho3-cU-IGi">
<rect key="frame" x="16" y="330.66666666666674" width="268" height="20.333333333333314"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Provider" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="O9r-10-LDD">
<rect key="frame" x="16.000000000000004" y="355" width="57.333333333333343" height="18"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Activity" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ULe-Gd-t1S">
<rect key="frame" x="16" y="377" width="43" height="15"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LZj-Ii-63i" customClass="TrendHistoryView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="200" y="355" width="100" height="44"/>
<constraints>
<constraint firstAttribute="width" constant="100" id="cUc-p7-aLH"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="bottomMargin" secondItem="ULe-Gd-t1S" secondAttribute="bottom" id="6UL-8b-Aia"/>
<constraint firstItem="h3b-Mf-lD6" firstAttribute="top" secondItem="Zb0-aW-Sen" secondAttribute="top" id="EFg-Yr-vdt"/>
<constraint firstItem="Ho3-cU-IGi" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="Ga8-LQ-f4N"/>
<constraint firstItem="ULe-Gd-t1S" firstAttribute="top" secondItem="O9r-10-LDD" secondAttribute="bottom" constant="4" id="HPD-qN-k3z"/>
<constraint firstAttribute="bottom" secondItem="LZj-Ii-63i" secondAttribute="bottom" constant="1" id="HWu-In-Uem"/>
<constraint firstItem="O9r-10-LDD" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="Hz8-Bw-jpl"/>
<constraint firstAttribute="trailing" secondItem="LZj-Ii-63i" secondAttribute="trailing" id="J9c-CF-3EF"/>
<constraint firstItem="ULe-Gd-t1S" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="KEj-En-StX"/>
<constraint firstItem="Ho3-cU-IGi" firstAttribute="top" secondItem="h3b-Mf-lD6" secondAttribute="bottom" constant="4" id="PjW-V1-oDs"/>
<constraint firstItem="LZj-Ii-63i" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="O9r-10-LDD" secondAttribute="trailing" id="WNr-ZP-o9a"/>
<constraint firstItem="LZj-Ii-63i" firstAttribute="top" secondItem="Ho3-cU-IGi" secondAttribute="bottom" constant="4" id="fpM-Hp-Oyf"/>
<constraint firstAttribute="trailing" secondItem="h3b-Mf-lD6" secondAttribute="trailing" id="kBD-1R-bh7"/>
<constraint firstItem="LZj-Ii-63i" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ULe-Gd-t1S" secondAttribute="trailing" id="ruZ-p8-n0x"/>
<constraint firstAttribute="trailingMargin" secondItem="Ho3-cU-IGi" secondAttribute="trailing" id="ubj-f6-bXE"/>
<constraint firstItem="h3b-Mf-lD6" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leading" id="wF1-Gm-nVQ"/>
<constraint firstItem="O9r-10-LDD" firstAttribute="top" secondItem="Ho3-cU-IGi" secondAttribute="bottom" constant="4" id="yPq-dT-uib"/>
</constraints>
</collectionViewCellContentView>
<connections>
<outlet property="activityLabel" destination="ULe-Gd-t1S" id="wqe-G6-IB3"/>
<outlet property="historyView" destination="LZj-Ii-63i" id="MVF-az-uyA"/>
<outlet property="providerLabel" destination="O9r-10-LDD" id="xAF-NW-ymm"/>
<outlet property="thumbnailView" destination="h3b-Mf-lD6" id="4mF-bJ-ALY"/>
<outlet property="titleLabel" destination="Ho3-cU-IGi" id="ltu-ey-chT"/>
</connections>
<point key="canvasLocation" x="0.0" y="-13.507109004739336"/>
</collectionViewCell>
</objects>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@ -113,6 +113,10 @@ class TrendingLinkTableViewCell: UITableViewCell {
} }
@objc private func updateUIForPreferences() { @objc private func updateUIForPreferences() {
if isGrayscale != Preferences.shared.grayscaleImages,
let card {
updateGrayscaleableUI(card: card)
}
} }
private func updateGrayscaleableUI(card: Card) { private func updateGrayscaleableUI(card: Card) {

View File

@ -37,7 +37,7 @@ class MainSidebarViewController: UIViewController {
} }
var exploreTabItems: [Item] { var exploreTabItems: [Item] {
var items: [Item] = [.search, .bookmarks, .trendingStatuses, .trendingTags, .trendingLinks, .profileDirectory] var items: [Item] = [.explore, .bookmarks, .trendingStatuses, .profileDirectory]
let snapshot = dataSource.snapshot() let snapshot = dataSource.snapshot()
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) { for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
items.append(.list(list)) items.append(.list(list))
@ -154,7 +154,7 @@ class MainSidebarViewController: UIViewController {
snapshot.appendItems([ snapshot.appendItems([
.tab(.timelines), .tab(.timelines),
.tab(.notifications), .tab(.notifications),
.search, .explore,
.bookmarks, .bookmarks,
.tab(.myProfile) .tab(.myProfile)
], toSection: .tabs) ], toSection: .tabs)
@ -177,12 +177,10 @@ class MainSidebarViewController: UIViewController {
var discoverSnapshot = NSDiffableDataSourceSectionSnapshot<Item>() var discoverSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
discoverSnapshot.append([.discoverHeader]) discoverSnapshot.append([.discoverHeader])
discoverSnapshot.append([ discoverSnapshot.append([
.trendingTags,
.profileDirectory, .profileDirectory,
], to: .discoverHeader) ], to: .discoverHeader)
if mastodonController.instanceFeatures.trendingStatusesAndLinks { if mastodonController.instanceFeatures.trendingStatusesAndLinks {
discoverSnapshot.insert([.trendingStatuses], before: .trendingTags) discoverSnapshot.insert([.trendingStatuses], before: .profileDirectory)
discoverSnapshot.insert([.trendingLinks], after: .trendingTags)
} }
dataSource.apply(discoverSnapshot, to: .discover) dataSource.apply(discoverSnapshot, to: .discover)
} }
@ -345,7 +343,7 @@ class MainSidebarViewController: UIViewController {
return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode) return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode)
case .tab(.compose): case .tab(.compose):
return UserActivityManager.newPostActivity(accountID: id) return UserActivityManager.newPostActivity(accountID: id)
case .search: case .explore:
return UserActivityManager.searchActivity() return UserActivityManager.searchActivity()
case .bookmarks: case .bookmarks:
return UserActivityManager.bookmarksActivity() return UserActivityManager.bookmarksActivity()
@ -384,8 +382,8 @@ extension MainSidebarViewController {
} }
enum Item: Hashable { enum Item: Hashable {
case tab(MainTabBarViewController.Tab) case tab(MainTabBarViewController.Tab)
case search, bookmarks case explore, bookmarks
case discoverHeader, trendingStatuses, trendingTags, trendingLinks, profileDirectory case discoverHeader, trendingStatuses, profileDirectory
case listsHeader, list(List), addList case listsHeader, list(List), addList
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
case savedInstancesHeader, savedInstance(URL), addSavedInstance case savedInstancesHeader, savedInstance(URL), addSavedInstance
@ -394,18 +392,14 @@ extension MainSidebarViewController {
switch self { switch self {
case let .tab(tab): case let .tab(tab):
return tab.title return tab.title
case .search: case .explore:
return "Search" return "Explore"
case .bookmarks: case .bookmarks:
return "Bookmarks" return "Bookmarks"
case .discoverHeader: case .discoverHeader:
return "Discover" return "Discover"
case .trendingStatuses: case .trendingStatuses:
return "Trending Posts" return "Trending Posts"
case .trendingTags:
return "Trending Hashtags"
case .trendingLinks:
return "Trending Links"
case .profileDirectory: case .profileDirectory:
return "Profile Directory" return "Profile Directory"
case .listsHeader: case .listsHeader:
@ -433,16 +427,12 @@ extension MainSidebarViewController {
switch self { switch self {
case let .tab(tab): case let .tab(tab):
return tab.imageName return tab.imageName
case .search: case .explore:
return "magnifyingglass" return "magnifyingglass"
case .bookmarks: case .bookmarks:
return "bookmark" return "bookmark"
case .trendingStatuses: case .trendingStatuses:
return "doc.text.image" return "square.text.square"
case .trendingTags:
return "number"
case .trendingLinks:
return "link"
case .profileDirectory: case .profileDirectory:
return "person.2.fill" return "person.2.fill"
case .list(_): case .list(_):

View File

@ -100,7 +100,7 @@ class MainSplitViewController: UISplitViewController {
item = .tab(MainTabBarViewController.Tab(rawValue: index)!) item = .tab(MainTabBarViewController.Tab(rawValue: index)!)
} else if let str = command.propertyList as? String { } else if let str = command.propertyList as? String {
if str == "search" { if str == "search" {
item = .search item = .explore
} else if str == "bookmarks" { } else if str == "bookmarks" {
item = .bookmarks item = .bookmarks
} else { } else {
@ -171,7 +171,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
$0.1 > $1.1 $0.1 > $1.1
} }
if let mostRecentExploreItem = mostRecentExploreItem?.0, if let mostRecentExploreItem = mostRecentExploreItem?.0,
mostRecentExploreItem != .search { mostRecentExploreItem != .explore {
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
// Pop back to root, so we're appending to the Explore VC instead of some other VC // Pop back to root, so we're appending to the Explore VC instead of some other VC
exploreNav.popToRootViewController(animated: false) exploreNav.popToRootViewController(animated: false)
@ -188,7 +188,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// sidebar items that map 1 <-> 1 can be transferred directly // sidebar items that map 1 <-> 1 can be transferred directly
tabBarViewController.select(tab: tab) tabBarViewController.select(tab: tab)
case .search: case .explore:
// Search sidebar item maps to the Explore tab with the search controller/results visible // Search sidebar item maps to the Explore tab with the search controller/results visible
// The nav stack can't be copied directly, since the split VC uses a different SearchViewController // The nav stack can't be copied directly, since the split VC uses a different SearchViewController
// so that explore items aren't shown multiple times. // so that explore items aren't shown multiple times.
@ -217,11 +217,11 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
explore.resultsController.loadResults(from: search.resultsController) explore.resultsController.loadResults(from: search.resultsController)
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened // Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
transferNavigationStack(from: .search, to: exploreNav, dropFirst: true, append: true) transferNavigationStack(from: .explore, to: exploreNav, dropFirst: true, append: true)
tabBarViewController.select(tab: .explore) tabBarViewController.select(tab: .explore)
case .bookmarks, .trendingStatuses, .trendingTags, .trendingLinks, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_): case .bookmarks, .trendingStatuses, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
tabBarViewController.select(tab: .explore) tabBarViewController.select(tab: .explore)
// Make sure the Explore VC doesn't show it's search bar when it appears, in case the user was previously // Make sure the Explore VC doesn't show it's search bar when it appears, in case the user was previously
// in compact mode and performing a search. // in compact mode and performing a search.
@ -272,7 +272,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// For other items, the 2nd VC in the nav stack determines which sidebar item they map to. // For other items, the 2nd VC in the nav stack determines which sidebar item they map to.
// Search screen has special considerations, all others can be transferred directly. // Search screen has special considerations, all others can be transferred directly.
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) { if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) {
exploreItem = .search exploreItem = .explore
let searchVC = SearchViewController(mastodonController: mastodonController) let searchVC = SearchViewController(mastodonController: mastodonController)
searchVC.loadViewIfNeeded() searchVC.loadViewIfNeeded()
let explore = tabNavigationStack.first as! ExploreViewController let explore = tabNavigationStack.first as! ExploreViewController
@ -300,9 +300,9 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
case is TrendingStatusesViewController: case is TrendingStatusesViewController:
exploreItem = .trendingStatuses exploreItem = .trendingStatuses
case is TrendingHashtagsViewController: case is TrendingHashtagsViewController:
exploreItem = .trendingTags exploreItem = .explore
case is TrendingLinksViewController: case is TrendingLinksViewController:
exploreItem = .trendingLinks exploreItem = .explore
case is ProfileDirectoryViewController: case is ProfileDirectoryViewController:
exploreItem = .profileDirectory exploreItem = .profileDirectory
default: default:
@ -354,16 +354,12 @@ fileprivate extension MainSidebarViewController.Item {
switch self { switch self {
case let .tab(tab): case let .tab(tab):
return tab.createViewController(mastodonController) return tab.createViewController(mastodonController)
case .search: case .explore:
return SearchViewController(mastodonController: mastodonController) return SearchViewController(mastodonController: mastodonController)
case .bookmarks: case .bookmarks:
return BookmarksTableViewController(mastodonController: mastodonController) return BookmarksTableViewController(mastodonController: mastodonController)
case .trendingStatuses: case .trendingStatuses:
return TrendingStatusesViewController(mastodonController: mastodonController) return TrendingStatusesViewController(mastodonController: mastodonController)
case .trendingTags:
return TrendingHashtagsViewController(mastodonController: mastodonController)
case .trendingLinks:
return TrendingLinksViewController(mastodonController: mastodonController)
case .profileDirectory: case .profileDirectory:
return ProfileDirectoryViewController(mastodonController: mastodonController) return ProfileDirectoryViewController(mastodonController: mastodonController)
case let .list(list): case let .list(list):
@ -435,8 +431,8 @@ extension MainSplitViewController: TuskerRootViewController {
return return
} }
if sidebar.selectedItem != .search { if sidebar.selectedItem != .explore {
select(item: .search) select(item: .explore)
} }
guard let searchViewController = secondaryNavController.viewControllers.first as? SearchViewController else { guard let searchViewController = secondaryNavController.viewControllers.first as? SearchViewController else {

View File

@ -7,11 +7,16 @@
// //
import UIKit import UIKit
import Pachyderm
import SafariServices
class SearchViewController: UIViewController { class SearchViewController: UIViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
var resultsController: SearchResultsViewController! var resultsController: SearchResultsViewController!
var searchController: UISearchController! var searchController: UISearchController!
@ -22,7 +27,7 @@ class SearchViewController: UIViewController {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
title = NSLocalizedString("Search", comment: "search tab title") title = NSLocalizedString("Explore", comment: "explore tab title")
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -32,12 +37,46 @@ class SearchViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .systemBackground let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
switch sectionIdentifier {
case .trendingHashtags:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.headerMode = .supplementary
return .list(using: listConfig, layoutEnvironment: environment)
case .trendingLinks:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// todo: i really wish i could just say the height is automatic and let autolayout figure out what it needs to be
// using .estimated(whatever) constrains the height to exactly whatever
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(250), heightDimension: .estimated(280))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(8), top: nil, trailing: .fixed(8), bottom: nil)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
section.boundarySupplementaryItems = [
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
]
return section
default:
fatalError("unimplemented")
}
}
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.backgroundColor = .secondarySystemBackground
view.addSubview(collectionView)
dataSource = createDataSource()
resultsController = SearchResultsViewController(mastodonController: mastodonController) resultsController = SearchResultsViewController(mastodonController: mastodonController)
resultsController.exploreNavigationController = self.navigationController resultsController.exploreNavigationController = self.navigationController
searchController = UISearchController(searchResultsController: resultsController) searchController = UISearchController(searchResultsController: resultsController)
searchController.obscuresBackgroundDuringPresentation = false searchController.obscuresBackgroundDuringPresentation = true
searchController.searchBar.autocapitalizationType = .none searchController.searchBar.autocapitalizationType = .none
searchController.searchBar.delegate = resultsController searchController.searchBar.delegate = resultsController
searchController.hidesNavigationBarDuringPresentation = false searchController.hidesNavigationBarDuringPresentation = false
@ -48,6 +87,18 @@ class SearchViewController: UIViewController {
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked navigationItem.preferredSearchBarPlacement = .stacked
} }
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Task(priority: .userInitiated) {
if (try? await mastodonController.getOwnInstance()) != nil {
await applySnapshot()
}
}
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
@ -61,5 +112,214 @@ class SearchViewController: UIViewController {
searchControllerStatusOnAppearance = nil searchControllerStatusOnAppearance = nil
} }
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] (headerView, collectionView, indexPath) in
let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
var config = UIListContentConfiguration.groupedHeader()
config.text = section.title
headerView.contentConfiguration = config
}
let trendingHashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { (cell, indexPath, hashtag) in
cell.updateUI(hashtag: hashtag)
}
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
cell.updateUI(card: card)
}
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
switch item {
case let .tag(hashtag):
return collectionView.dequeueConfiguredReusableCell(using: trendingHashtagCell, for: indexPath, item: hashtag)
case let .link(card):
return collectionView.dequeueConfiguredReusableCell(using: trendingLinkCell, for: indexPath, item: card)
default:
fatalError("todo")
}
}
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
if elementKind == UICollectionView.elementKindSectionHeader {
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
} else {
return nil
}
}
return dataSource
}
@MainActor
private func applySnapshot() async {
guard mastodonController.instanceFeatures.trends,
!Preferences.shared.hideDiscover else {
await dataSource.apply(NSDiffableDataSourceSnapshot())
return
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
async let hashtags = try? mastodonController.run(hashtagsReq).0
let linksReq = Client.getTrendingLinks(limit: 10)
async let links = try? mastodonController.run(linksReq).0
if let hashtags = await hashtags {
snapshot.appendSections([.trendingHashtags])
snapshot.appendItems(hashtags.map { .tag($0) }, toSection: .trendingHashtags)
}
if let links = await links {
snapshot.appendSections([.trendingLinks])
snapshot.appendItems(links.map { .link($0) }, toSection: .trendingLinks)
}
await dataSource.apply(snapshot)
}
@objc private func preferencesChanged() {
Task {
await applySnapshot()
}
}
} }
extension SearchViewController {
enum Section {
case trendingHashtags
case trendingLinks
case trendingStatuses
case profileSuggestions
var title: String {
switch self {
case .trendingHashtags:
return "Trending Hashtags"
case .trendingLinks:
return "Trending Links"
case .trendingStatuses:
return "Trending Statuses"
case .profileSuggestions:
return "Suggested Accounts"
}
}
}
enum Item: Equatable, Hashable {
case status(String)
case tag(Hashtag)
case link(Card)
static func == (lhs: SearchViewController.Item, rhs: SearchViewController.Item) -> Bool {
switch (lhs, rhs) {
case let (.status(a), .status(b)):
return a == b
case let (.tag(a), .tag(b)):
return a == b
case let (.link(a), .link(b)):
return a.url == b.url
default:
return false
}
}
func hash(into hasher: inout Hasher) {
switch self {
case let .status(id):
hasher.combine("status")
hasher.combine(id)
case let .tag(tag):
hasher.combine("tag")
hasher.combine(tag.name)
case let .link(card):
hasher.combine("link")
hasher.combine(card.url)
}
}
}
}
extension SearchViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return
}
switch item {
case let .tag(hashtag):
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
case let .link(card):
if let url = URL(card.url) {
selected(url: url)
}
default:
fatalError("todo")
}
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return nil
}
switch item {
case let .tag(hashtag):
return UIContextMenuConfiguration(identifier: nil) {
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
} actionProvider: { (_) in
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.collectionView.cellForItem(at: indexPath)))
}
case let .link(card):
guard let url = URL(card.url) else {
return nil
}
return UIContextMenuConfiguration {
SFSafariViewController(url: url)
} actionProvider: { _ in
UIMenu(children: self.actionsForTrendingLink(card: card))
}
default:
fatalError("todo")
}
}
}
extension SearchViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return []
}
switch item {
case let .tag(hashtag):
let provider = NSItemProvider(object: hashtag.url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
}
return [UIDragItem(itemProvider: provider)]
case let .link(card):
guard let url = URL(card.url) else {
return []
}
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
default:
fatalError("todo")
}
}
}
extension SearchViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController }
}
extension SearchViewController: ToastableViewController {
}
extension SearchViewController: MenuActionProvider {
}