forked from shadowfacts/Tusker
Compare commits
18 Commits
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 2ee34acbad | |
Shadowfacts | 6eee97759e | |
Shadowfacts | f88bf552af | |
Shadowfacts | d2c7664073 | |
Shadowfacts | e91249a876 | |
Shadowfacts | 1eab964c0b | |
Shadowfacts | 2933ac491b | |
Shadowfacts | 2958d2b1ac | |
Shadowfacts | 3262fe002b | |
Shadowfacts | 521e5ad5fc | |
Shadowfacts | 2b651b0bc4 | |
Shadowfacts | 99b3532e64 | |
Shadowfacts | 2ea8e9cf1e | |
Shadowfacts | e8b7446117 | |
Shadowfacts | a47b9c0c75 | |
Shadowfacts | a75862b5cc | |
Shadowfacts | 0738683ee3 | |
Shadowfacts | 155f4036f9 |
|
@ -438,6 +438,12 @@ public class Client {
|
|||
return Request<[Account]>(method: .get, path: "/api/v1/directory", queryParameters: parameters)
|
||||
}
|
||||
|
||||
public static func getSuggestions(limit: Int?) -> Request<[Suggestion]> {
|
||||
return Request(method: .get, path: "/api/v2/suggestions", queryParameters: [
|
||||
"limit" => limit,
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Client {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// Suggestion.swift
|
||||
// Pachyderm
|
||||
//
|
||||
// Created by Shadowfacts on 1/22/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Suggestion: Decodable {
|
||||
public let source: Source
|
||||
public let account: Account
|
||||
|
||||
public static func remove(accountID: String) -> Request<Empty> {
|
||||
return Request(method: .delete, path: "/api/v1/suggestions/\(accountID)")
|
||||
}
|
||||
}
|
||||
|
||||
extension Suggestion {
|
||||
public enum Source: String, Decodable {
|
||||
case staff
|
||||
case pastInteractions = "past_interactions"
|
||||
case global
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@
|
|||
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */; };
|
||||
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */; };
|
||||
D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */; };
|
||||
D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */; };
|
||||
D601FA84297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */; };
|
||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
|
||||
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
|
||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
|
||||
|
@ -417,6 +419,8 @@
|
|||
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandThreadCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfileCardCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SuggestedProfileCardCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; };
|
||||
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = "<group>"; };
|
||||
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -895,6 +899,8 @@
|
|||
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */,
|
||||
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */,
|
||||
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */,
|
||||
D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */,
|
||||
D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */,
|
||||
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
||||
|
@ -1806,6 +1812,7 @@
|
|||
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
||||
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
||||
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
|
||||
D601FA84297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib in Resources */,
|
||||
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
|
||||
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
||||
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
||||
|
@ -1994,6 +2001,7 @@
|
|||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
||||
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */,
|
||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
||||
D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */,
|
||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
||||
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
||||
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
||||
|
|
|
@ -51,6 +51,10 @@ struct InstanceFeatures {
|
|||
instanceType.isMastodon
|
||||
}
|
||||
|
||||
var profileSuggestions: Bool {
|
||||
instanceType.isMastodon && hasMastodonVersion(3, 4, 0)
|
||||
}
|
||||
|
||||
var trendingStatusesAndLinks: Bool {
|
||||
instanceType.isMastodon && hasMastodonVersion(3, 5, 0)
|
||||
}
|
||||
|
|
|
@ -37,9 +37,9 @@ struct ComposeAttachmentRow: View {
|
|||
}
|
||||
}
|
||||
|
||||
Button(action: self.removeAttachment) {
|
||||
Button(role: .destructive, action: self.removeAttachment) {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}.foregroundStyle(.red)
|
||||
}
|
||||
} previewIfAvailable: {
|
||||
ComposeAttachmentImage(attachment: attachment, fullSize: true)
|
||||
}
|
||||
|
|
|
@ -45,6 +45,13 @@ struct PinnedTimelinesView: View {
|
|||
.foregroundColor(Color(.lightGray))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
pinnedTimelines.removeAll(where: { $0.id == timeline.id })
|
||||
} label: {
|
||||
Label("Remove Pinned Timeline", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onMove { indices, newOffset in
|
||||
pinnedTimelines.move(fromOffsets: indices, toOffset: newOffset)
|
||||
|
|
|
@ -64,8 +64,12 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
|||
resultsController.exploreNavigationController = self.navigationController!
|
||||
searchController = UISearchController(searchResultsController: resultsController)
|
||||
searchController.searchResultsUpdater = resultsController
|
||||
if #available(iOS 16.0, *) {
|
||||
searchController.scopeBarActivation = .onSearchActivation
|
||||
}
|
||||
searchController.searchBar.autocapitalizationType = .none
|
||||
searchController.searchBar.delegate = resultsController
|
||||
searchController.searchBar.scopeButtonTitles = SearchResultsViewController.Scope.allCases.map(\.title)
|
||||
definesPresentationContext = true
|
||||
|
||||
navigationItem.searchController = searchController
|
||||
|
|
|
@ -11,7 +11,7 @@ import UIKit
|
|||
class HashtagSearchResultsViewController: SearchResultsViewController {
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
super.init(mastodonController: mastodonController, resultTypes: [.hashtags])
|
||||
super.init(mastodonController: mastodonController, scope: .hashtags)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
//
|
||||
// SuggestedProfileCardCollectionViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/23/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import SwiftUI
|
||||
|
||||
class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
weak var delegate: (any TuskerNavigationDelegate)?
|
||||
private var mastodonController: MastodonController! { delegate?.apiController }
|
||||
|
||||
private var accountID: String!
|
||||
private var source: Suggestion.Source!
|
||||
|
||||
@IBOutlet weak var headerImageView: CachedImageView!
|
||||
@IBOutlet weak var avatarContainerView: UIView!
|
||||
@IBOutlet weak var avatarImageView: CachedImageView!
|
||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||
@IBOutlet weak var usernameLabel: UILabel!
|
||||
@IBOutlet weak var noteTextView: StatusContentTextView!
|
||||
@IBOutlet weak var suggestionSourceButton: UIButton!
|
||||
|
||||
private var hoverGestureAnimator: UIViewPropertyAnimator?
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
layer.shadowOpacity = 0.2
|
||||
layer.shadowRadius = 8
|
||||
layer.shadowOffset = .zero
|
||||
layer.masksToBounds = false
|
||||
contentView.layer.cornerRadius = 12.5
|
||||
updateLayerColors()
|
||||
|
||||
headerImageView.cache = .headers
|
||||
|
||||
avatarContainerView.layer.masksToBounds = true
|
||||
avatarImageView.cache = .avatars
|
||||
avatarImageView.layer.masksToBounds = true
|
||||
|
||||
displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 24, weight: .semibold))
|
||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
usernameLabel.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 15, weight: .light))
|
||||
usernameLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
|
||||
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
||||
noteTextView.adjustsFontForContentSizeCategory = true
|
||||
noteTextView.textContainer.lineBreakMode = .byTruncatingTail
|
||||
|
||||
addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized)))
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
@objc private func preferencesChanged() {
|
||||
guard let accountID,
|
||||
let mastodonController,
|
||||
let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||
return
|
||||
}
|
||||
updateUIForPreferences(account: account)
|
||||
}
|
||||
|
||||
func updateUI(accountID: String, source: Suggestion.Source) {
|
||||
guard self.accountID != accountID,
|
||||
let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||
return
|
||||
}
|
||||
self.accountID = accountID
|
||||
self.source = source
|
||||
|
||||
updateUIForPreferences(account: account)
|
||||
|
||||
avatarImageView.update(for: account.avatar)
|
||||
headerImageView.update(for: account.header)
|
||||
usernameLabel.text = "@\(account.acct)"
|
||||
noteTextView.setTextFromHtml(account.note)
|
||||
|
||||
var config = UIButton.Configuration.plain()
|
||||
config.image = source.image
|
||||
suggestionSourceButton.configuration = config
|
||||
suggestionSourceButton.setNeedsUpdateConfiguration()
|
||||
}
|
||||
|
||||
private func updateUIForPreferences(account: AccountMO) {
|
||||
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
updateLayerColors()
|
||||
}
|
||||
|
||||
private func updateLayerColors() {
|
||||
if traitCollection.userInterfaceStyle == .dark {
|
||||
layer.shadowColor = UIColor.darkGray.cgColor
|
||||
} else {
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
||||
@IBAction func suggestionSourceButtonPressed(_ sender: Any) {
|
||||
guard let delegate,
|
||||
let source else {
|
||||
return
|
||||
}
|
||||
let view = SuggestionSourceView(mastodonController: mastodonController, source: source)
|
||||
let host = UIHostingController(rootView: view)
|
||||
let toPresent: UIViewController
|
||||
if traitCollection.horizontalSizeClass == .compact || traitCollection.verticalSizeClass == .compact {
|
||||
toPresent = UINavigationController(rootViewController: host)
|
||||
toPresent.modalPresentationStyle = .pageSheet
|
||||
let sheetPresentationController = toPresent.sheetPresentationController!
|
||||
sheetPresentationController.detents = [
|
||||
.medium()
|
||||
]
|
||||
} else {
|
||||
host.modalPresentationStyle = .popover
|
||||
let popoverPresentationController = host.popoverPresentationController!
|
||||
popoverPresentationController.sourceView = suggestionSourceButton
|
||||
host.preferredContentSize = host.sizeThatFits(in: CGSize(width: 400, height: CGFloat.infinity))
|
||||
toPresent = host
|
||||
}
|
||||
delegate.present(toPresent, animated: true)
|
||||
}
|
||||
|
||||
@objc private func hoverRecognized(_ recognizer: UIHoverGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began, .changed:
|
||||
hoverGestureAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut, animations: {
|
||||
self.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
|
||||
})
|
||||
hoverGestureAnimator!.startAnimation()
|
||||
case .ended:
|
||||
hoverGestureAnimator?.stopAnimation(true)
|
||||
hoverGestureAnimator?.addAnimations {
|
||||
self.transform = .identity
|
||||
}
|
||||
hoverGestureAnimator?.startAnimation()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension Suggestion.Source {
|
||||
var image: UIImage {
|
||||
switch self {
|
||||
case .global:
|
||||
return UIImage(systemName: "chart.line.uptrend.xyaxis")!
|
||||
case .pastInteractions:
|
||||
return UIImage(systemName: "clock.arrow.circlepath")!
|
||||
case .staff:
|
||||
return UIImage(systemName: "person.2")!
|
||||
}
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .global:
|
||||
return "Popular Recently"
|
||||
case .pastInteractions:
|
||||
return "Past Interactions"
|
||||
case .staff:
|
||||
return "Staff Recommendation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SuggestionSourceView: View {
|
||||
let mastodonController: MastodonController
|
||||
let source: Suggestion.Source
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Image(uiImage: source.image)
|
||||
Text(source.title)
|
||||
Spacer()
|
||||
}
|
||||
switch source {
|
||||
case .global:
|
||||
Text("This account is suggested for you because it has been highly active within the past 30 days.")
|
||||
case .pastInteractions:
|
||||
Text("This account is suggested for you because you have interacted with it before.")
|
||||
case .staff:
|
||||
Text("This account is recommended by the staff of \(mastodonController.accountInfo!.instanceURL.host!)")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.navigationTitle("Suggestion Reason")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<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="Z5E-Hf-n4L" customClass="SuggestedProfileCardCollectionViewCell" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="447" height="325"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Jcb-fI-gAO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="447" height="325"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="7fZ-qb-OUH" customClass="CachedImageView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="447" height="100"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="100" id="OYE-GJ-2eQ"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mYt-NG-Qs0">
|
||||
<rect key="frame" x="8" y="55" width="90" height="90"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="6Ev-Aa-3Mc" customClass="CachedImageView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="2" y="2" width="86" height="86"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="86" id="dpV-uh-zbU"/>
|
||||
<constraint firstAttribute="width" constant="86" id="lrh-qG-ETr"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="6Ev-Aa-3Mc" firstAttribute="centerY" secondItem="mYt-NG-Qs0" secondAttribute="centerY" id="IPQ-Ku-dNq"/>
|
||||
<constraint firstItem="6Ev-Aa-3Mc" firstAttribute="centerX" secondItem="mYt-NG-Qs0" secondAttribute="centerX" id="PJS-9F-5hw"/>
|
||||
<constraint firstAttribute="height" constant="90" id="lvP-pY-zQX"/>
|
||||
<constraint firstAttribute="width" constant="90" id="wav-YT-e4Y"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XCk-sZ-ujT" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="106" y="100" width="333" height="29"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bVM-lv-OK8">
|
||||
<rect key="frame" x="106" y="129" width="333" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FJh-fd-fo8" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="147" width="431" height="170"/>
|
||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fOC-AS-NkL" customClass="ProfileHeaderButton" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="9.9999999999999964" y="7.9999999999999964" width="51.666666666666657" height="51.666666666666657"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="fOC-AS-NkL" secondAttribute="height" multiplier="1:1" id="ab5-nM-00d"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="plain" image="clock.arrow.circlepath" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="suggestionSourceButtonPressed:" destination="Z5E-Hf-n4L" eventType="touchUpInside" id="gXT-zC-Kvl"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="FJh-fd-fo8" firstAttribute="top" secondItem="bVM-lv-OK8" secondAttribute="bottom" id="1Os-JD-p6O"/>
|
||||
<constraint firstAttribute="trailing" secondItem="XCk-sZ-ujT" secondAttribute="trailing" constant="8" id="FPN-6T-KIm"/>
|
||||
<constraint firstItem="7fZ-qb-OUH" firstAttribute="leading" secondItem="Jcb-fI-gAO" secondAttribute="leading" id="Nq6-3c-fs0"/>
|
||||
<constraint firstItem="fOC-AS-NkL" firstAttribute="leading" secondItem="6Ev-Aa-3Mc" secondAttribute="leading" id="Tr9-Il-Q4H"/>
|
||||
<constraint firstItem="bVM-lv-OK8" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="6Ev-Aa-3Mc" secondAttribute="bottom" id="Uad-gZ-GhS"/>
|
||||
<constraint firstItem="mYt-NG-Qs0" firstAttribute="leading" secondItem="Jcb-fI-gAO" secondAttribute="leading" constant="8" id="Wco-Rg-NEM"/>
|
||||
<constraint firstItem="fOC-AS-NkL" firstAttribute="top" secondItem="Jcb-fI-gAO" secondAttribute="top" constant="8" id="aqE-ad-qD8"/>
|
||||
<constraint firstAttribute="trailing" secondItem="7fZ-qb-OUH" secondAttribute="trailing" id="bBk-xa-K6q"/>
|
||||
<constraint firstAttribute="trailing" secondItem="bVM-lv-OK8" secondAttribute="trailing" constant="8" id="c7e-ha-JfL"/>
|
||||
<constraint firstItem="mYt-NG-Qs0" firstAttribute="centerY" secondItem="7fZ-qb-OUH" secondAttribute="bottom" id="cDW-a5-aLv"/>
|
||||
<constraint firstItem="7fZ-qb-OUH" firstAttribute="top" secondItem="Jcb-fI-gAO" secondAttribute="top" id="g1U-Ig-HW6"/>
|
||||
<constraint firstItem="FJh-fd-fo8" firstAttribute="leading" secondItem="Jcb-fI-gAO" secondAttribute="leading" constant="8" id="kE2-It-xgV"/>
|
||||
<constraint firstAttribute="trailing" secondItem="FJh-fd-fo8" secondAttribute="trailing" constant="8" id="ljC-j2-dB1"/>
|
||||
<constraint firstAttribute="bottom" secondItem="FJh-fd-fo8" secondAttribute="bottom" constant="8" id="nMc-OE-cEj"/>
|
||||
<constraint firstItem="bVM-lv-OK8" firstAttribute="top" secondItem="XCk-sZ-ujT" secondAttribute="bottom" id="neo-Eg-Y1D"/>
|
||||
<constraint firstItem="XCk-sZ-ujT" firstAttribute="top" secondItem="7fZ-qb-OUH" secondAttribute="bottom" id="nsb-mR-pJV"/>
|
||||
<constraint firstItem="bVM-lv-OK8" firstAttribute="leading" secondItem="mYt-NG-Qs0" secondAttribute="trailing" constant="8" id="tb5-R8-1wh"/>
|
||||
<constraint firstItem="XCk-sZ-ujT" firstAttribute="leading" secondItem="mYt-NG-Qs0" secondAttribute="trailing" constant="8" id="uQt-9z-Sci"/>
|
||||
</constraints>
|
||||
</collectionViewCellContentView>
|
||||
<size key="customSize" width="447" height="325"/>
|
||||
<connections>
|
||||
<outlet property="avatarContainerView" destination="mYt-NG-Qs0" id="7pR-Ml-smY"/>
|
||||
<outlet property="avatarImageView" destination="6Ev-Aa-3Mc" id="lec-fD-8F5"/>
|
||||
<outlet property="displayNameLabel" destination="XCk-sZ-ujT" id="UgE-Rm-sNi"/>
|
||||
<outlet property="headerImageView" destination="7fZ-qb-OUH" id="59z-43-WWQ"/>
|
||||
<outlet property="noteTextView" destination="FJh-fd-fo8" id="ciO-6u-r4d"/>
|
||||
<outlet property="suggestionSourceButton" destination="fOC-AS-NkL" id="8A0-RB-lLU"/>
|
||||
<outlet property="usernameLabel" destination="bVM-lv-OK8" id="hpN-xe-5vq"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-45.038167938931295" y="123.59154929577466"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="clock.arrow.circlepath" catalog="system" width="128" height="112"/>
|
||||
<systemColor name="labelColor">
|
||||
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="secondarySystemBackgroundColor">
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -8,44 +8,42 @@
|
|||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import WebURLFoundationExtras
|
||||
import SwiftSoup
|
||||
|
||||
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
private var card: Card?
|
||||
private var isGrayscale = false
|
||||
private var thumbnailRequest: ImageCache.Request?
|
||||
|
||||
@IBOutlet weak var thumbnailView: UIImageView!
|
||||
@IBOutlet weak var thumbnailView: CachedImageView!
|
||||
@IBOutlet weak var titleLabel: UILabel!
|
||||
@IBOutlet weak var descriptionLabel: UILabel!
|
||||
@IBOutlet weak var providerLabel: UILabel!
|
||||
@IBOutlet weak var activityLabel: UILabel!
|
||||
@IBOutlet weak var historyView: TrendHistoryView!
|
||||
|
||||
private var hoverGestureAnimator: UIViewPropertyAnimator?
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
thumbnailView.cache = .attachments
|
||||
|
||||
layer.shadowOpacity = 0.2
|
||||
layer.shadowRadius = 8
|
||||
layer.shadowOffset = .zero
|
||||
layer.masksToBounds = false
|
||||
contentView.layer.cornerRadius = 12.5
|
||||
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
|
||||
addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized)))
|
||||
}
|
||||
|
||||
func updateUI(card: Card) {
|
||||
self.card = card
|
||||
self.thumbnailView.image = nil
|
||||
|
||||
updateGrayscaleableUI(card: card)
|
||||
updateUIForPreferences()
|
||||
thumbnailView.update(for: card.image.flatMap { URL($0) }, blurhash: card.blurhash)
|
||||
|
||||
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
titleLabel.text = title
|
||||
|
@ -53,6 +51,10 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
|||
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
providerLabel.text = provider
|
||||
|
||||
let description = try! SwiftSoup.parseBodyFragment(card.description).text()
|
||||
descriptionLabel.text = description
|
||||
descriptionLabel.isHidden = description.isEmpty
|
||||
|
||||
let sorted = card.history!.sorted(by: { $0.day < $1.day })
|
||||
let lastTwo = sorted[(sorted.count - 2)...]
|
||||
let accounts = lastTwo.map(\.accounts).reduce(0, +)
|
||||
|
@ -69,33 +71,6 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
|||
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
|
||||
|
@ -134,12 +109,30 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
||||
@objc private func hoverRecognized(_ recognizer: UIHoverGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began, .changed:
|
||||
hoverGestureAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut, animations: {
|
||||
self.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
|
||||
})
|
||||
hoverGestureAnimator!.startAnimation()
|
||||
case .ended:
|
||||
hoverGestureAnimator?.stopAnimation(true)
|
||||
hoverGestureAnimator?.addAnimations {
|
||||
self.transform = .identity
|
||||
}
|
||||
hoverGestureAnimator?.startAnimation()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?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">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" 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"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<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"/>
|
||||
|
@ -12,71 +12,98 @@
|
|||
<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"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="406"/>
|
||||
<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"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="406"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="h3b-Mf-lD6">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="h3b-Mf-lD6" customClass="CachedImageView" customModule="Tusker" customModuleProvider="target">
|
||||
<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"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="LpU-m4-guC">
|
||||
<rect key="frame" x="4" y="225" width="292" height="177"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0hs-Zm-eWF">
|
||||
<rect key="frame" x="0.0" y="0.0" width="83.333333333333329" height="142.33333333333334"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Provider" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="O9r-10-LDD">
|
||||
<rect key="frame" x="0.0" y="146.33333333333331" width="44" height="13.333333333333343"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Activity" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ULe-Gd-t1S">
|
||||
<rect key="frame" x="0.0" y="163.66666666666669" width="39.333333333333336" height="13.333333333333343"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LZj-Ii-63i" customClass="TrendHistoryView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="200" y="361" width="100" height="44"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="LPx-cL-9b4"/>
|
||||
<constraint firstAttribute="height" constant="44" id="cGp-dq-laF"/>
|
||||
<constraint firstAttribute="width" constant="100" id="cUc-p7-aLH"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cWo-9n-z42">
|
||||
<rect key="frame" x="0.0" y="196.33333333333334" width="300" height="28.666666666666657"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ktv-3s-cp9">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="28.666666666666657"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" showsExpansionTextWhenTruncated="YES" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho3-cU-IGi">
|
||||
<rect key="frame" x="4" y="4" width="292" height="20.333333333333332"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Ho3-cU-IGi" firstAttribute="leading" secondItem="ktv-3s-cp9" secondAttribute="leading" constant="4" id="igP-RK-yCM"/>
|
||||
<constraint firstItem="Ho3-cU-IGi" firstAttribute="top" secondItem="ktv-3s-cp9" secondAttribute="top" constant="4" id="oPU-19-ZAy"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Ho3-cU-IGi" secondAttribute="bottom" constant="4" id="psK-H0-rhV"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Ho3-cU-IGi" secondAttribute="trailing" constant="4" id="v6n-yr-HLJ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<blurEffect style="prominent"/>
|
||||
</visualEffectView>
|
||||
</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"/>
|
||||
<constraint firstItem="cWo-9n-z42" firstAttribute="bottom" secondItem="h3b-Mf-lD6" secondAttribute="bottom" id="1iR-rH-KCl"/>
|
||||
<constraint firstItem="LpU-m4-guC" firstAttribute="top" secondItem="h3b-Mf-lD6" secondAttribute="bottom" id="4GW-Eu-47t"/>
|
||||
<constraint firstItem="h3b-Mf-lD6" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leading" id="8vO-xS-kkp"/>
|
||||
<constraint firstAttribute="bottom" secondItem="LpU-m4-guC" secondAttribute="bottom" constant="4" id="DUO-kl-ggb"/>
|
||||
<constraint firstAttribute="trailing" secondItem="cWo-9n-z42" secondAttribute="trailing" id="Deo-mN-Ir3"/>
|
||||
<constraint firstAttribute="trailing" secondItem="h3b-Mf-lD6" secondAttribute="trailing" id="Hjp-9H-VHN"/>
|
||||
<constraint firstAttribute="bottom" secondItem="LZj-Ii-63i" secondAttribute="bottom" constant="1" id="Lcx-ET-gnk"/>
|
||||
<constraint firstAttribute="trailing" secondItem="LpU-m4-guC" secondAttribute="trailing" constant="4" id="UQ8-eo-L1G"/>
|
||||
<constraint firstItem="cWo-9n-z42" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leading" id="ai7-Qf-ksj"/>
|
||||
<constraint firstAttribute="trailing" secondItem="LZj-Ii-63i" secondAttribute="trailing" id="dHE-zD-VvZ"/>
|
||||
<constraint firstItem="LpU-m4-guC" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leading" constant="4" id="rdC-Sg-fwV"/>
|
||||
<constraint firstItem="h3b-Mf-lD6" firstAttribute="top" secondItem="Zb0-aW-Sen" secondAttribute="top" id="z5T-J0-5RD"/>
|
||||
</constraints>
|
||||
</collectionViewCellContentView>
|
||||
<size key="customSize" width="300" height="406"/>
|
||||
<connections>
|
||||
<outlet property="activityLabel" destination="ULe-Gd-t1S" id="wqe-G6-IB3"/>
|
||||
<outlet property="descriptionLabel" destination="0hs-Zm-eWF" id="RSz-VV-OCw"/>
|
||||
<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"/>
|
||||
<point key="canvasLocation" x="0.0" y="-11.374407582938389"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
|
|
|
@ -65,7 +65,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
|||
})
|
||||
dataSource.editListAccountsController = self
|
||||
|
||||
searchResultsController = SearchResultsViewController(mastodonController: mastodonController, resultTypes: [.accounts])
|
||||
searchResultsController = SearchResultsViewController(mastodonController: mastodonController, scope: .people)
|
||||
searchResultsController.following = true
|
||||
searchResultsController.delegate = self
|
||||
searchController = UISearchController(searchResultsController: searchResultsController)
|
||||
|
|
|
@ -41,7 +41,7 @@ class MainSidebarViewController: UIViewController {
|
|||
}
|
||||
|
||||
var exploreTabItems: [Item] {
|
||||
var items: [Item] = [.explore, .bookmarks, .trendingStatuses, .profileDirectory]
|
||||
var items: [Item] = [.explore, .bookmarks, .profileDirectory]
|
||||
let snapshot = dataSource.snapshot()
|
||||
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
|
||||
items.append(.list(list))
|
||||
|
@ -195,9 +195,6 @@ class MainSidebarViewController: UIViewController {
|
|||
discoverSnapshot.append([
|
||||
.profileDirectory,
|
||||
], to: .discoverHeader)
|
||||
if mastodonController.instanceFeatures.trendingStatusesAndLinks {
|
||||
discoverSnapshot.insert([.trendingStatuses], before: .profileDirectory)
|
||||
}
|
||||
dataSource.apply(discoverSnapshot, to: .discover)
|
||||
}
|
||||
|
||||
|
@ -388,7 +385,7 @@ extension MainSidebarViewController {
|
|||
enum Item: Hashable {
|
||||
case tab(MainTabBarViewController.Tab)
|
||||
case explore, bookmarks
|
||||
case discoverHeader, trendingStatuses, profileDirectory
|
||||
case discoverHeader, profileDirectory
|
||||
case listsHeader, list(List), addList
|
||||
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
|
||||
case savedInstancesHeader, savedInstance(URL), addSavedInstance
|
||||
|
@ -403,8 +400,6 @@ extension MainSidebarViewController {
|
|||
return "Bookmarks"
|
||||
case .discoverHeader:
|
||||
return "Discover"
|
||||
case .trendingStatuses:
|
||||
return "Trending Posts"
|
||||
case .profileDirectory:
|
||||
return "Profile Directory"
|
||||
case .listsHeader:
|
||||
|
@ -436,8 +431,6 @@ extension MainSidebarViewController {
|
|||
return "magnifyingglass"
|
||||
case .bookmarks:
|
||||
return "bookmark"
|
||||
case .trendingStatuses:
|
||||
return "square.text.square"
|
||||
case .profileDirectory:
|
||||
return "person.2.fill"
|
||||
case .list(_):
|
||||
|
|
|
@ -232,7 +232,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
|||
|
||||
tabBarViewController.select(tab: .explore)
|
||||
|
||||
case .bookmarks, .trendingStatuses, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
|
||||
case .bookmarks, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
|
||||
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
|
||||
// in compact mode and performing a search.
|
||||
|
@ -277,6 +277,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
|||
case .explore:
|
||||
// The Explore tab is more complicated since it encapsulates a bunch of screens which have top-level sidebar items.
|
||||
|
||||
var skipFirst = 1
|
||||
var toPrepend: UIViewController? = nil
|
||||
|
||||
// If the tab navigation stack has only one item or the search controller is active, it corresponds to the Search item
|
||||
|
@ -308,19 +309,17 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
|||
exploreItem = .savedHashtag(hashtagVC.hashtag)
|
||||
case let instanceVC as InstanceTimelineViewController:
|
||||
exploreItem = .savedInstance(instanceVC.instanceURL)
|
||||
case is TrendingStatusesViewController:
|
||||
exploreItem = .trendingStatuses
|
||||
case is TrendingHashtagsViewController:
|
||||
exploreItem = .explore
|
||||
case is TrendingLinksViewController:
|
||||
case is TrendingStatusesViewController, is TrendingHashtagsViewController, is TrendingLinksViewController:
|
||||
exploreItem = .explore
|
||||
// these three VCs are part of the root SearchViewController, so we don't need to transfer them
|
||||
skipFirst = 2
|
||||
case is ProfileDirectoryViewController:
|
||||
exploreItem = .profileDirectory
|
||||
default:
|
||||
fatalError("unhandled second-level explore screen: \(tabNavigationStack[1])")
|
||||
}
|
||||
}
|
||||
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: 1, prepend: toPrepend)
|
||||
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: skipFirst, prepend: toPrepend)
|
||||
|
||||
case .compose:
|
||||
// The compose tab can't be activated, this is unreachable.
|
||||
|
@ -376,8 +375,6 @@ fileprivate extension MainSidebarViewController.Item {
|
|||
return SearchViewController(mastodonController: mastodonController)
|
||||
case .bookmarks:
|
||||
return BookmarksTableViewController(mastodonController: mastodonController)
|
||||
case .trendingStatuses:
|
||||
return TrendingStatusesViewController(mastodonController: mastodonController)
|
||||
case .profileDirectory:
|
||||
return ProfileDirectoryViewController(mastodonController: mastodonController)
|
||||
case let .list(list):
|
||||
|
|
|
@ -23,6 +23,8 @@ class OnboardingViewController: UINavigationController {
|
|||
|
||||
var authenticationSession: ASWebAuthenticationSession?
|
||||
|
||||
private var clientInfo: (url: URL, id: String, secret: String)?
|
||||
|
||||
init() {
|
||||
super.init(rootViewController: instanceSelector)
|
||||
}
|
||||
|
@ -42,10 +44,16 @@ class OnboardingViewController: UINavigationController {
|
|||
let mastodonController = MastodonController(instanceURL: instanceURL, transient: true)
|
||||
let clientID: String
|
||||
let clientSecret: String
|
||||
do {
|
||||
(clientID, clientSecret) = try await mastodonController.registerApp()
|
||||
} catch {
|
||||
throw Error.registeringApp(error)
|
||||
if let clientInfo, clientInfo.url == instanceURL {
|
||||
clientID = clientInfo.id
|
||||
clientSecret = clientInfo.secret
|
||||
} else {
|
||||
do {
|
||||
(clientID, clientSecret) = try await mastodonController.registerApp()
|
||||
self.clientInfo = (instanceURL, clientID, clientSecret)
|
||||
} catch {
|
||||
throw Error.registeringApp(error)
|
||||
}
|
||||
}
|
||||
let authCode = try await getAuthorizationCode(instanceURL: instanceURL, clientID: clientID)
|
||||
let accessToken: String
|
||||
|
|
|
@ -38,17 +38,17 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
private var activityIndicator: UIActivityIndicatorView!
|
||||
private var errorLabel: UILabel!
|
||||
|
||||
/// Types of results to search for. `nil` means all results will be included.
|
||||
var resultTypes: [SearchResultType]? = nil
|
||||
/// Types of results to search for.
|
||||
var scope: Scope
|
||||
/// Whether to limit results to accounts the users is following.
|
||||
var following: Bool? = nil
|
||||
|
||||
let searchSubject = PassthroughSubject<String?, Never>()
|
||||
var currentQuery: String?
|
||||
|
||||
init(mastodonController: MastodonController, resultTypes: [SearchResultType]? = nil) {
|
||||
init(mastodonController: MastodonController, scope: Scope = .all) {
|
||||
self.mastodonController = mastodonController
|
||||
self.resultTypes = resultTypes
|
||||
self.scope = scope
|
||||
|
||||
super.init(style: .grouped)
|
||||
|
||||
|
@ -153,7 +153,7 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
activityIndicator.startAnimating()
|
||||
errorLabel.isHidden = true
|
||||
|
||||
let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10, following: following)
|
||||
let request = Client.search(query: query, types: scope.resultTypes, resolve: true, limit: 10, following: following)
|
||||
mastodonController.run(request) { (response) in
|
||||
switch response {
|
||||
case let .success(results, _):
|
||||
|
@ -178,17 +178,17 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
|
||||
self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
|
||||
let resultTypes = self.resultTypes
|
||||
if !results.accounts.isEmpty && (resultTypes == nil || resultTypes!.contains(.accounts)) {
|
||||
let resultTypes = self.scope.resultTypes
|
||||
if !results.accounts.isEmpty && resultTypes.contains(.accounts) {
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
|
||||
addAccounts(results.accounts)
|
||||
}
|
||||
if !results.hashtags.isEmpty && (resultTypes == nil || resultTypes!.contains(.hashtags)) {
|
||||
if !results.hashtags.isEmpty && resultTypes.contains(.hashtags) {
|
||||
snapshot.appendSections([.hashtags])
|
||||
snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .hashtags)
|
||||
}
|
||||
if !results.statuses.isEmpty && (resultTypes == nil || resultTypes!.contains(.statuses)) {
|
||||
if !results.statuses.isEmpty && resultTypes.contains(.statuses) {
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
|
||||
addStatuses(results.statuses)
|
||||
|
@ -252,6 +252,41 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
|
||||
}
|
||||
|
||||
extension SearchResultsViewController {
|
||||
enum Scope: CaseIterable {
|
||||
case all
|
||||
case people
|
||||
case hashtags
|
||||
case posts
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return "All"
|
||||
case .people:
|
||||
return "People"
|
||||
case .hashtags:
|
||||
return "Hashtags"
|
||||
case .posts:
|
||||
return "Posts"
|
||||
}
|
||||
}
|
||||
|
||||
var resultTypes: [SearchResultType] {
|
||||
switch self {
|
||||
case .all:
|
||||
return [.accounts, .statuses, .hashtags]
|
||||
case .people:
|
||||
return [.accounts]
|
||||
case .hashtags:
|
||||
return [.hashtags]
|
||||
case .posts:
|
||||
return [.statuses]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultsViewController {
|
||||
enum Section: CaseIterable {
|
||||
case accounts
|
||||
|
@ -311,6 +346,11 @@ extension SearchResultsViewController: UISearchBarDelegate {
|
|||
// perform a search immedaitely when the search button is pressed
|
||||
performSearch(query: searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
||||
self.scope = Scope.allCases[selectedScope]
|
||||
performSearch(query: searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultsViewController: TuskerNavigationDelegate {
|
||||
|
|
|
@ -11,11 +11,11 @@ import Pachyderm
|
|||
import SafariServices
|
||||
import WebURLFoundationExtras
|
||||
|
||||
class SearchViewController: UIViewController {
|
||||
class SearchViewController: UIViewController, CollectionViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var collectionView: UICollectionView!
|
||||
var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
var resultsController: SearchResultsViewController!
|
||||
|
@ -23,6 +23,8 @@ class SearchViewController: UIViewController {
|
|||
|
||||
var searchControllerStatusOnAppearance: Bool? = nil
|
||||
|
||||
private var loadTask: Task<Void, Never>?
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
|
@ -47,10 +49,8 @@ class SearchViewController: UIViewController {
|
|||
return .list(using: listConfig, layoutEnvironment: environment)
|
||||
|
||||
case .trendingLinks:
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(280))
|
||||
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)
|
||||
|
@ -59,10 +59,27 @@ class SearchViewController: UIViewController {
|
|||
section.boundarySupplementaryItems = [
|
||||
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
|
||||
]
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0)
|
||||
return section
|
||||
|
||||
default:
|
||||
fatalError("unimplemented")
|
||||
case .profileSuggestions:
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(250))
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(350), heightDimension: .absolute(250))
|
||||
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)
|
||||
]
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0)
|
||||
return section
|
||||
|
||||
case .trendingStatuses:
|
||||
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||
listConfig.headerMode = .supplementary
|
||||
return .list(using: listConfig, layoutEnvironment: environment)
|
||||
}
|
||||
}
|
||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||
|
@ -79,8 +96,12 @@ class SearchViewController: UIViewController {
|
|||
resultsController.exploreNavigationController = self.navigationController
|
||||
searchController = UISearchController(searchResultsController: resultsController)
|
||||
searchController.obscuresBackgroundDuringPresentation = true
|
||||
if #available(iOS 16.0, *) {
|
||||
searchController.scopeBarActivation = .onSearchActivation
|
||||
}
|
||||
searchController.searchBar.autocapitalizationType = .none
|
||||
searchController.searchBar.delegate = resultsController
|
||||
searchController.searchBar.scopeButtonTitles = SearchResultsViewController.Scope.allCases.map(\.title)
|
||||
searchController.hidesNavigationBarDuringPresentation = false
|
||||
definesPresentationContext = true
|
||||
|
||||
|
@ -96,7 +117,10 @@ class SearchViewController: UIViewController {
|
|||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
Task(priority: .userInitiated) {
|
||||
clearSelectionOnAppear(animated: animated)
|
||||
|
||||
loadTask?.cancel()
|
||||
loadTask = Task(priority: .userInitiated) {
|
||||
if (try? await mastodonController.getOwnInstance()) != nil {
|
||||
await applySnapshot()
|
||||
}
|
||||
|
@ -129,6 +153,15 @@ class SearchViewController: UIViewController {
|
|||
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
|
||||
cell.updateUI(card: card)
|
||||
}
|
||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
|
||||
cell.delegate = self
|
||||
// TODO: filter trends
|
||||
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
|
||||
}
|
||||
let accountCell = UICollectionView.CellRegistration<SuggestedProfileCardCollectionViewCell, (String, Suggestion.Source)>(cellNib: UINib(nibName: "SuggestedProfileCardCollectionViewCell", bundle: .main)) { [unowned self] cell, indexPath, item in
|
||||
cell.delegate = self
|
||||
cell.updateUI(accountID: item.0, source: item.1)
|
||||
}
|
||||
|
||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
|
||||
switch item {
|
||||
|
@ -138,8 +171,11 @@ class SearchViewController: UIViewController {
|
|||
case let .link(card):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: trendingLinkCell, for: indexPath, item: card)
|
||||
|
||||
default:
|
||||
fatalError("todo")
|
||||
case let .status(id, state):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
|
||||
|
||||
case let .account(id, source):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source))
|
||||
}
|
||||
}
|
||||
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
||||
|
@ -161,30 +197,82 @@ class SearchViewController: UIViewController {
|
|||
}
|
||||
|
||||
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 {
|
||||
let hashtags = try? await mastodonController.run(hashtagsReq).0
|
||||
|
||||
if let 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)
|
||||
if mastodonController.instanceFeatures.profileSuggestions {
|
||||
let req = Client.getSuggestions(limit: 10)
|
||||
let suggestions = try? await mastodonController.run(req).0
|
||||
if let suggestions {
|
||||
snapshot.appendSections([.profileSuggestions])
|
||||
await mastodonController.persistentContainer.addAll(accounts: suggestions.map(\.account))
|
||||
snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) }, toSection: .profileSuggestions)
|
||||
}
|
||||
}
|
||||
|
||||
if mastodonController.instanceFeatures.trendingStatusesAndLinks {
|
||||
let linksReq = Client.getTrendingLinks(limit: 10)
|
||||
async let links = try? mastodonController.run(linksReq).0
|
||||
let statusesReq = Client.getTrendingStatuses(limit: 10)
|
||||
async let statuses = try? mastodonController.run(statusesReq).0
|
||||
|
||||
if let links = await links {
|
||||
if snapshot.sectionIdentifiers.contains(.profileSuggestions) {
|
||||
snapshot.insertSections([.trendingLinks], beforeSection: .profileSuggestions)
|
||||
} else {
|
||||
snapshot.appendSections([.trendingLinks])
|
||||
}
|
||||
snapshot.appendItems(links.map { .link($0) }, toSection: .trendingLinks)
|
||||
}
|
||||
|
||||
if let statuses = await statuses {
|
||||
await mastodonController.persistentContainer.addAll(statuses: statuses)
|
||||
snapshot.appendSections([.trendingStatuses])
|
||||
snapshot.appendItems(statuses.map { .status($0.id, .unknown) }, toSection: .trendingStatuses)
|
||||
}
|
||||
}
|
||||
|
||||
if !Task.isCancelled {
|
||||
await apply(snapshot: snapshot)
|
||||
}
|
||||
|
||||
await dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
@objc private func preferencesChanged() {
|
||||
Task {
|
||||
loadTask?.cancel()
|
||||
loadTask = Task {
|
||||
await applySnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>) async {
|
||||
await Task { @MainActor in
|
||||
self.dataSource.apply(snapshot)
|
||||
}.value
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func removeProfileSuggestion(accountID: String) async {
|
||||
let req = Suggestion.remove(accountID: accountID)
|
||||
do {
|
||||
_ = try await mastodonController.run(req)
|
||||
var snapshot = dataSource.snapshot()
|
||||
// the source here doesn't matter, since it's ignored by the equatable and hashable impls
|
||||
snapshot.deleteItems([.account(accountID, .global)])
|
||||
await apply(snapshot: snapshot)
|
||||
} catch {
|
||||
let config = ToastConfiguration(from: error, with: "Error Removing Suggestion", in: self) { [unowned self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
_ = await self.removeProfileSuggestion(accountID: accountID)
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -192,8 +280,8 @@ extension SearchViewController {
|
|||
enum Section {
|
||||
case trendingHashtags
|
||||
case trendingLinks
|
||||
case trendingStatuses
|
||||
case profileSuggestions
|
||||
case trendingStatuses
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
|
@ -202,25 +290,28 @@ extension SearchViewController {
|
|||
case .trendingLinks:
|
||||
return "Trending Links"
|
||||
case .trendingStatuses:
|
||||
return "Trending Statuses"
|
||||
return "Trending Posts"
|
||||
case .profileSuggestions:
|
||||
return "Suggested Accounts"
|
||||
}
|
||||
}
|
||||
}
|
||||
enum Item: Equatable, Hashable {
|
||||
case status(String)
|
||||
case status(String, CollapseState)
|
||||
case tag(Hashtag)
|
||||
case link(Card)
|
||||
case account(String, Suggestion.Source)
|
||||
|
||||
static func == (lhs: SearchViewController.Item, rhs: SearchViewController.Item) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.status(a), .status(b)):
|
||||
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
|
||||
case let (.account(a, _), .account(b, _)):
|
||||
return a == b
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
@ -228,7 +319,7 @@ extension SearchViewController {
|
|||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case let .status(id):
|
||||
case let .status(id, _):
|
||||
hasher.combine("status")
|
||||
hasher.combine(id)
|
||||
case let .tag(tag):
|
||||
|
@ -237,6 +328,9 @@ extension SearchViewController {
|
|||
case let .link(card):
|
||||
hasher.combine("link")
|
||||
hasher.combine(card.url)
|
||||
case let .account(id, _):
|
||||
hasher.combine("account")
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -256,11 +350,15 @@ extension SearchViewController: UICollectionViewDelegate {
|
|||
selected(url: url)
|
||||
}
|
||||
|
||||
default:
|
||||
fatalError("todo")
|
||||
case let .status(id, state):
|
||||
selected(status: id, state: state.copy())
|
||||
|
||||
case let .account(id, _):
|
||||
selected(account: id)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS, obsoleted: 16.0)
|
||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return nil
|
||||
|
@ -286,10 +384,63 @@ extension SearchViewController: UICollectionViewDelegate {
|
|||
UIMenu(children: self.actionsForTrendingLink(card: card))
|
||||
}
|
||||
|
||||
default:
|
||||
fatalError("todo")
|
||||
case let .status(id, state):
|
||||
guard let status = mastodonController.persistentContainer.status(for: id) else {
|
||||
return nil
|
||||
}
|
||||
let cell = collectionView.cellForItem(at: indexPath)!
|
||||
return UIContextMenuConfiguration {
|
||||
ConversationViewController(for: id, state: state.copy(), mastodonController: self.mastodonController)
|
||||
} actionProvider: { _ in
|
||||
UIMenu(children: self.actionsForStatus(status, source: .view(cell)))
|
||||
}
|
||||
|
||||
case let .account(id, _):
|
||||
let cell = collectionView.cellForItem(at: indexPath)!
|
||||
return UIContextMenuConfiguration {
|
||||
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
|
||||
} actionProvider: { _ in
|
||||
let dismiss = UIAction(title: "Remove Suggestion", image: UIImage(systemName: "trash"), attributes: .destructive) { [unowned self] _ in
|
||||
Task {
|
||||
await self.removeProfileSuggestion(accountID: id)
|
||||
}
|
||||
}
|
||||
return UIMenu(children: [UIMenu(options: .displayInline, children: [dismiss])] + self.actionsForProfile(accountID: id, source: .view(cell)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// implementing the highlightPreviewForItemAt method seems to prevent the old, single IndexPath variant of this method from being called on iOS 16
|
||||
@available(iOS 16.0, *)
|
||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard indexPaths.count == 1 else {
|
||||
return nil
|
||||
}
|
||||
return self.collectionView(collectionView, contextMenuConfigurationForItemAt: indexPaths[0], point: point)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfiguration configuration: UIContextMenuConfiguration, highlightPreviewForItemAt indexPath: IndexPath) -> UITargetedPreview? {
|
||||
switch dataSource.itemIdentifier(for: indexPath) {
|
||||
case .link(_), .account(_, _):
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
let params = UIPreviewParameters()
|
||||
params.visiblePath = UIBezierPath(roundedRect: cell.bounds, cornerRadius: cell.contentView.layer.cornerRadius)
|
||||
return UITargetedPreview(view: cell, parameters: params)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfiguration configuration: UIContextMenuConfiguration, dismissalPreviewForItemAt indexPath: IndexPath) -> UITargetedPreview? {
|
||||
return self.collectionView(collectionView, contextMenuConfiguration: configuration, highlightPreviewForItemAt: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchViewController: UICollectionViewDragDelegate {
|
||||
|
@ -315,8 +466,26 @@ extension SearchViewController: UICollectionViewDragDelegate {
|
|||
}
|
||||
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
|
||||
|
||||
default:
|
||||
fatalError("todo")
|
||||
case let .status(id, _):
|
||||
guard let status = mastodonController.persistentContainer.status(for: id),
|
||||
let url = status.url else {
|
||||
return []
|
||||
}
|
||||
let provider = NSItemProvider(object: url as NSURL)
|
||||
let activity = UserActivityManager.showConversationActivity(mainStatusID: id, accountID: mastodonController.accountInfo!.id)
|
||||
activity.displaysAuxiliaryScene = true
|
||||
provider.registerObject(activity, visibility: .all)
|
||||
return [UIDragItem(itemProvider: provider)]
|
||||
|
||||
case let .account(id, _):
|
||||
guard let account = mastodonController.persistentContainer.account(for: id) else {
|
||||
return []
|
||||
}
|
||||
let provider = NSItemProvider(object: account.url as NSURL)
|
||||
let activity = UserActivityManager.showProfileActivity(id: id, accountID: mastodonController.accountInfo!.id)
|
||||
activity.displaysAuxiliaryScene = true
|
||||
provider.registerObject(activity, visibility: .all)
|
||||
return [UIDragItem(itemProvider: provider)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -330,3 +499,17 @@ extension SearchViewController: ToastableViewController {
|
|||
|
||||
extension SearchViewController: MenuActionProvider {
|
||||
}
|
||||
|
||||
extension SearchViewController: StatusCollectionViewCellDelegate {
|
||||
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
|
||||
if let indexPath = collectionView.indexPath(for: cell) {
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
|
||||
dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
|
||||
// TODO: filtering
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,53 @@ class InstanceTimelineViewController: TimelineViewController {
|
|||
super.collectionView(collectionView, didSelectItemAt: indexPath)
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
// MARK: Timeline
|
||||
|
||||
override func handleLoadAllError(_ error: Swift.Error) async {
|
||||
switch (error as? Client.Error)?.type {
|
||||
case .mastodonError(422, _), .unexpectedStatus(422):
|
||||
collectionView.isHidden = true
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
let image = UIImageView(image: UIImage(systemName: "lock.fill"))
|
||||
image.tintColor = .secondaryLabel
|
||||
image.contentMode = .scaleAspectFit
|
||||
|
||||
let title = UILabel()
|
||||
title.textColor = .secondaryLabel
|
||||
title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||
title.adjustsFontForContentSizeCategory = true
|
||||
title.numberOfLines = 0
|
||||
title.textAlignment = .center
|
||||
title.text = "This instance requires an account to view."
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [
|
||||
image,
|
||||
title,
|
||||
])
|
||||
stack.axis = .vertical
|
||||
stack.alignment = .center
|
||||
stack.spacing = 8
|
||||
stack.isAccessibilityElement = true
|
||||
stack.accessibilityLabel = title.text!
|
||||
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(stack)
|
||||
NSLayoutConstraint.activate([
|
||||
image.widthAnchor.constraint(equalToConstant: 64),
|
||||
image.heightAnchor.constraint(equalToConstant: 64),
|
||||
|
||||
stack.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1),
|
||||
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
|
||||
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
||||
])
|
||||
|
||||
default:
|
||||
await super.handleLoadAllError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc func toggleSaveButtonPressed() {
|
||||
let context = parentMastodonController!.persistentContainer.viewContext
|
||||
let req = SavedInstance.fetchRequest(url: instanceURL, account: parentMastodonController!.accountInfo!)
|
||||
|
|
|
@ -856,6 +856,17 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
self.dataSource.apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
}
|
||||
|
||||
// this is only implemented here so it's overridable by InstanceTimelineViewController
|
||||
func handleLoadAllError(_ error: Swift.Error) async {
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
Task {
|
||||
await self?.controller.loadInitial()
|
||||
}
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineViewController {
|
||||
|
|
|
@ -384,7 +384,7 @@ extension MenuActionProvider {
|
|||
|
||||
private func openInSafariAction(url: URL) -> UIAction {
|
||||
return createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { [weak self] (_) in
|
||||
self?.navigationDelegate?.selected(url: url, allowUniversalLinks: false)
|
||||
self?.navigationDelegate?.selected(url: url, allowResolveStatuses: false, allowUniversalLinks: false)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -224,12 +224,13 @@ enum PopoverSource {
|
|||
|
||||
private let statusPathRegex = try! NSRegularExpression(
|
||||
pattern:
|
||||
"(^/@[a-z0-9_]+/\\d{18})" // mastodon
|
||||
+ "|(^/notice/[a-z0-9]{18})" // pleroma
|
||||
+ "|(^/p/[a-z0-9_]+/\\d{18})" // pixelfed
|
||||
+ "|(^/i/web/post/\\d{18})" // pixelfed web frontend
|
||||
+ "|(^/u/.+/h/[a-z0-9]{18})" // honk
|
||||
+ "|(^/@.+/statuses/[a-z0-9]{26})" // gotosocial
|
||||
"(^/@[a-z0-9_]+/\\d{18})" // mastodon
|
||||
+ "|(^/notice/[a-z0-9]{18})" // pleroma
|
||||
+ "|(^/notes/[a-z0-9]{10})" // misskey
|
||||
+ "|(^/p/[a-z0-9_]+/\\d{18})" // pixelfed
|
||||
+ "|(^/i/web/post/\\d{18})" // pixelfed web frontend
|
||||
+ "|(^/u/.+/h/[a-z0-9]{18})" // honk
|
||||
+ "|(^/@.+/statuses/[a-z0-9]{26})" // gotosocial
|
||||
,
|
||||
options: .caseInsensitive
|
||||
)
|
||||
|
|
|
@ -10,26 +10,32 @@ import UIKit
|
|||
|
||||
class CachedImageView: UIImageView {
|
||||
|
||||
private let cache: ImageCache
|
||||
var cache: ImageCache!
|
||||
private var url: URL?
|
||||
private var isGrayscale = false
|
||||
private var fetchTask: Task<Void, Error>?
|
||||
private var blurHashTask: DispatchWorkItem?
|
||||
|
||||
init(cache: ImageCache) {
|
||||
self.cache = cache
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
func update(for url: URL?) {
|
||||
if url != self.url {
|
||||
private func commonInit() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
func update(for url: URL?, blurhash: String? = nil) {
|
||||
if url != self.url || (url != nil && self.image == nil) {
|
||||
self.url = url
|
||||
self.image = nil
|
||||
updateBlurhash(blurhash, for: url)
|
||||
updateImage()
|
||||
}
|
||||
}
|
||||
|
@ -40,13 +46,32 @@ class CachedImageView: UIImageView {
|
|||
}
|
||||
}
|
||||
|
||||
private func updateBlurhash(_ blurhash: String?, for url: URL?) {
|
||||
blurHashTask?.cancel()
|
||||
|
||||
if let blurhash {
|
||||
let aspectRatio = self.bounds.width > 0 ? self.bounds.height / self.bounds.width : 1
|
||||
blurHashTask = DispatchWorkItem {
|
||||
let size = CGSize(width: 60, height: aspectRatio * 60)
|
||||
let image = UIImage(blurHash: blurhash, size: size)
|
||||
DispatchQueue.main.async {
|
||||
if self.image == nil && self.url == url && self.blurHashTask?.isCancelled == false {
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
}
|
||||
AttachmentView.queue.async(execute: blurHashTask!)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateImage() {
|
||||
fetchTask?.cancel()
|
||||
|
||||
guard let url else {
|
||||
return
|
||||
}
|
||||
|
||||
fetchTask = Task(priority: .high) {
|
||||
self.image = nil
|
||||
guard let url else {
|
||||
return
|
||||
}
|
||||
let (_, image) = await cache.get(url)
|
||||
guard let image else {
|
||||
return
|
||||
|
@ -59,6 +84,7 @@ class CachedImageView: UIImageView {
|
|||
try Task.checkCancellation()
|
||||
self.image = transformedImage
|
||||
self.isGrayscale = Preferences.shared.grayscaleImages
|
||||
self.blurHashTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,9 @@ class TrendingHashtagCollectionViewCell: UICollectionViewCell {
|
|||
backgroundColor = .systemBackground
|
||||
|
||||
hashtagLabel.font = .preferredFont(forTextStyle: .title2)
|
||||
hashtagLabel.adjustsFontForContentSizeCategory = true
|
||||
peopleTodayLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||
peopleTodayLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
let vStack = UIStackView(arrangedSubviews: [
|
||||
hashtagLabel,
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Pachyderm
|
||||
import SafariServices
|
||||
import WebURLFoundationExtras
|
||||
import SwiftSoup
|
||||
|
||||
class StatusCardView: UIView {
|
||||
|
||||
|
@ -156,7 +157,7 @@ class StatusCardView: UIView {
|
|||
titleLabel.text = title
|
||||
titleLabel.isHidden = title.isEmpty
|
||||
|
||||
let description = card.description.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let description = try! SwiftSoup.parseBodyFragment(card.description).text()
|
||||
descriptionLabel.text = description
|
||||
descriptionLabel.isHidden = description.isEmpty
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ class TrendHistoryView: UIView {
|
|||
borderLayer.path = path.cgPath.copy()!
|
||||
borderLayer.strokeColor = tintColor.cgColor
|
||||
borderLayer.fillColor = nil
|
||||
borderLayer.lineWidth = 2
|
||||
borderLayer.lineWidth = lineWidth
|
||||
borderLayer.lineCap = .round
|
||||
|
||||
path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
|
||||
|
@ -109,12 +109,16 @@ class TrendHistoryView: UIView {
|
|||
let fillColor = self.fillColor()
|
||||
fillLayer.strokeColor = fillColor
|
||||
fillLayer.fillColor = fillColor
|
||||
fillLayer.lineWidth = 2
|
||||
fillLayer.lineWidth = lineWidth
|
||||
|
||||
layer.addSublayer(fillLayer)
|
||||
layer.addSublayer(borderLayer)
|
||||
}
|
||||
|
||||
private var lineWidth: CGFloat {
|
||||
(traitCollection.preferredContentSizeCategory > .large || UIAccessibility.isBoldTextEnabled) ? 4 : 2
|
||||
}
|
||||
|
||||
// The non-transparent fill color.
|
||||
// We blend with the view's background color ourselves so that final color is non-transparent,
|
||||
// otherwise when the fill layer's border and fill overlap, there's a visibly darker patch
|
||||
|
|
Loading…
Reference in New Issue