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)
|
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 {
|
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 */; };
|
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */; };
|
||||||
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */; };
|
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */; };
|
||||||
D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.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 */; };
|
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
|
||||||
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
|
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
|
||||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -895,6 +899,8 @@
|
||||||
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */,
|
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */,
|
||||||
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */,
|
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */,
|
||||||
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */,
|
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */,
|
||||||
|
D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */,
|
||||||
|
D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */,
|
||||||
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
||||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
||||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
||||||
|
@ -1806,6 +1812,7 @@
|
||||||
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
||||||
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
||||||
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
|
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
|
||||||
|
D601FA84297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib in Resources */,
|
||||||
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
|
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
|
||||||
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
||||||
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
||||||
|
@ -1994,6 +2001,7 @@
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
||||||
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */,
|
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */,
|
||||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
||||||
|
D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */,
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
||||||
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
||||||
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
||||||
|
|
|
@ -51,6 +51,10 @@ struct InstanceFeatures {
|
||||||
instanceType.isMastodon
|
instanceType.isMastodon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var profileSuggestions: Bool {
|
||||||
|
instanceType.isMastodon && hasMastodonVersion(3, 4, 0)
|
||||||
|
}
|
||||||
|
|
||||||
var trendingStatusesAndLinks: Bool {
|
var trendingStatusesAndLinks: Bool {
|
||||||
instanceType.isMastodon && hasMastodonVersion(3, 5, 0)
|
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")
|
Label("Delete", systemImage: "trash")
|
||||||
}.foregroundStyle(.red)
|
}
|
||||||
} previewIfAvailable: {
|
} previewIfAvailable: {
|
||||||
ComposeAttachmentImage(attachment: attachment, fullSize: true)
|
ComposeAttachmentImage(attachment: attachment, fullSize: true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,13 @@ struct PinnedTimelinesView: View {
|
||||||
.foregroundColor(Color(.lightGray))
|
.foregroundColor(Color(.lightGray))
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
pinnedTimelines.removeAll(where: { $0.id == timeline.id })
|
||||||
|
} label: {
|
||||||
|
Label("Remove Pinned Timeline", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onMove { indices, newOffset in
|
.onMove { indices, newOffset in
|
||||||
pinnedTimelines.move(fromOffsets: indices, toOffset: newOffset)
|
pinnedTimelines.move(fromOffsets: indices, toOffset: newOffset)
|
||||||
|
|
|
@ -64,8 +64,12 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
resultsController.exploreNavigationController = self.navigationController!
|
resultsController.exploreNavigationController = self.navigationController!
|
||||||
searchController = UISearchController(searchResultsController: resultsController)
|
searchController = UISearchController(searchResultsController: resultsController)
|
||||||
searchController.searchResultsUpdater = resultsController
|
searchController.searchResultsUpdater = resultsController
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
searchController.scopeBarActivation = .onSearchActivation
|
||||||
|
}
|
||||||
searchController.searchBar.autocapitalizationType = .none
|
searchController.searchBar.autocapitalizationType = .none
|
||||||
searchController.searchBar.delegate = resultsController
|
searchController.searchBar.delegate = resultsController
|
||||||
|
searchController.searchBar.scopeButtonTitles = SearchResultsViewController.Scope.allCases.map(\.title)
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
|
|
|
@ -11,7 +11,7 @@ import UIKit
|
||||||
class HashtagSearchResultsViewController: SearchResultsViewController {
|
class HashtagSearchResultsViewController: SearchResultsViewController {
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
super.init(mastodonController: mastodonController, resultTypes: [.hashtags])
|
super.init(mastodonController: mastodonController, scope: .hashtags)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
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 UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import WebURLFoundationExtras
|
||||||
|
import SwiftSoup
|
||||||
|
|
||||||
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
private var card: Card?
|
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 titleLabel: UILabel!
|
||||||
|
@IBOutlet weak var descriptionLabel: UILabel!
|
||||||
@IBOutlet weak var providerLabel: UILabel!
|
@IBOutlet weak var providerLabel: UILabel!
|
||||||
@IBOutlet weak var activityLabel: UILabel!
|
@IBOutlet weak var activityLabel: UILabel!
|
||||||
@IBOutlet weak var historyView: TrendHistoryView!
|
@IBOutlet weak var historyView: TrendHistoryView!
|
||||||
|
|
||||||
|
private var hoverGestureAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
thumbnailView.cache = .attachments
|
||||||
|
|
||||||
layer.shadowOpacity = 0.2
|
layer.shadowOpacity = 0.2
|
||||||
layer.shadowRadius = 8
|
layer.shadowRadius = 8
|
||||||
layer.shadowOffset = .zero
|
layer.shadowOffset = .zero
|
||||||
layer.masksToBounds = false
|
layer.masksToBounds = false
|
||||||
|
contentView.layer.cornerRadius = 12.5
|
||||||
updateLayerColors()
|
updateLayerColors()
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized)))
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
|
|
||||||
contentView.layer.cornerRadius = 0.05 * bounds.width
|
|
||||||
thumbnailView.layer.cornerRadius = 0.05 * bounds.width
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(card: Card) {
|
func updateUI(card: Card) {
|
||||||
self.card = card
|
self.card = card
|
||||||
self.thumbnailView.image = nil
|
self.thumbnailView.image = nil
|
||||||
|
|
||||||
updateGrayscaleableUI(card: card)
|
thumbnailView.update(for: card.image.flatMap { URL($0) }, blurhash: card.blurhash)
|
||||||
updateUIForPreferences()
|
|
||||||
|
|
||||||
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
titleLabel.text = title
|
titleLabel.text = title
|
||||||
|
@ -53,6 +51,10 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
|
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
providerLabel.text = provider
|
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 sorted = card.history!.sorted(by: { $0.day < $1.day })
|
||||||
let lastTwo = sorted[(sorted.count - 2)...]
|
let lastTwo = sorted[(sorted.count - 2)...]
|
||||||
let accounts = lastTwo.map(\.accounts).reduce(0, +)
|
let accounts = lastTwo.map(\.accounts).reduce(0, +)
|
||||||
|
@ -69,33 +71,6 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
historyView.isHidden = card.history == nil || card.history!.count < 2
|
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) {
|
private func loadBlurHash(card: Card) {
|
||||||
guard let hash = card.blurhash else {
|
guard let hash = card.blurhash else {
|
||||||
return
|
return
|
||||||
|
@ -134,12 +109,30 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
private func updateLayerColors() {
|
private func updateLayerColors() {
|
||||||
if traitCollection.userInterfaceStyle == .dark {
|
if traitCollection.userInterfaceStyle == .dark {
|
||||||
// clippingView.layer.borderColor = UIColor.darkGray.withAlphaComponent(0.5).cgColor
|
|
||||||
layer.shadowColor = UIColor.darkGray.cgColor
|
layer.shadowColor = UIColor.darkGray.cgColor
|
||||||
} else {
|
} else {
|
||||||
// clippingView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
|
|
||||||
layer.shadowColor = UIColor.black.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"?>
|
<?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"/>
|
<device id="retina6_0" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<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="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="collection view cell content view" 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"/>
|
<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="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
<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">
|
<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"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Zb0-aW-Sen">
|
<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"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="300" height="225"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" id="QDY-8a-LYC"/>
|
<constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" id="QDY-8a-LYC"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</imageView>
|
</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">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="LpU-m4-guC">
|
||||||
<rect key="frame" x="16" y="330.66666666666674" width="268" height="20.333333333333314"/>
|
<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"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</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">
|
</subviews>
|
||||||
<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>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="100" id="cUc-p7-aLH"/>
|
<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>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
|
<blurEffect style="prominent"/>
|
||||||
|
</visualEffectView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="bottomMargin" secondItem="ULe-Gd-t1S" secondAttribute="bottom" id="6UL-8b-Aia"/>
|
<constraint firstItem="cWo-9n-z42" firstAttribute="bottom" secondItem="h3b-Mf-lD6" secondAttribute="bottom" id="1iR-rH-KCl"/>
|
||||||
<constraint firstItem="h3b-Mf-lD6" firstAttribute="top" secondItem="Zb0-aW-Sen" secondAttribute="top" id="EFg-Yr-vdt"/>
|
<constraint firstItem="LpU-m4-guC" firstAttribute="top" secondItem="h3b-Mf-lD6" secondAttribute="bottom" id="4GW-Eu-47t"/>
|
||||||
<constraint firstItem="Ho3-cU-IGi" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="Ga8-LQ-f4N"/>
|
<constraint firstItem="h3b-Mf-lD6" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leading" id="8vO-xS-kkp"/>
|
||||||
<constraint firstItem="ULe-Gd-t1S" firstAttribute="top" secondItem="O9r-10-LDD" secondAttribute="bottom" constant="4" id="HPD-qN-k3z"/>
|
<constraint firstAttribute="bottom" secondItem="LpU-m4-guC" secondAttribute="bottom" constant="4" id="DUO-kl-ggb"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="LZj-Ii-63i" secondAttribute="bottom" constant="1" id="HWu-In-Uem"/>
|
<constraint firstAttribute="trailing" secondItem="cWo-9n-z42" secondAttribute="trailing" id="Deo-mN-Ir3"/>
|
||||||
<constraint firstItem="O9r-10-LDD" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="Hz8-Bw-jpl"/>
|
<constraint firstAttribute="trailing" secondItem="h3b-Mf-lD6" secondAttribute="trailing" id="Hjp-9H-VHN"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="LZj-Ii-63i" secondAttribute="trailing" id="J9c-CF-3EF"/>
|
<constraint firstAttribute="bottom" secondItem="LZj-Ii-63i" secondAttribute="bottom" constant="1" id="Lcx-ET-gnk"/>
|
||||||
<constraint firstItem="ULe-Gd-t1S" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="KEj-En-StX"/>
|
<constraint firstAttribute="trailing" secondItem="LpU-m4-guC" secondAttribute="trailing" constant="4" id="UQ8-eo-L1G"/>
|
||||||
<constraint firstItem="Ho3-cU-IGi" firstAttribute="top" secondItem="h3b-Mf-lD6" secondAttribute="bottom" constant="4" id="PjW-V1-oDs"/>
|
<constraint firstItem="cWo-9n-z42" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leading" id="ai7-Qf-ksj"/>
|
||||||
<constraint firstItem="LZj-Ii-63i" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="O9r-10-LDD" secondAttribute="trailing" id="WNr-ZP-o9a"/>
|
<constraint firstAttribute="trailing" secondItem="LZj-Ii-63i" secondAttribute="trailing" id="dHE-zD-VvZ"/>
|
||||||
<constraint firstItem="LZj-Ii-63i" firstAttribute="top" secondItem="Ho3-cU-IGi" secondAttribute="bottom" constant="4" id="fpM-Hp-Oyf"/>
|
<constraint firstItem="LpU-m4-guC" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leading" constant="4" id="rdC-Sg-fwV"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="h3b-Mf-lD6" secondAttribute="trailing" id="kBD-1R-bh7"/>
|
<constraint firstItem="h3b-Mf-lD6" firstAttribute="top" secondItem="Zb0-aW-Sen" secondAttribute="top" id="z5T-J0-5RD"/>
|
||||||
<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>
|
</constraints>
|
||||||
</collectionViewCellContentView>
|
</collectionViewCellContentView>
|
||||||
|
<size key="customSize" width="300" height="406"/>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="activityLabel" destination="ULe-Gd-t1S" id="wqe-G6-IB3"/>
|
<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="historyView" destination="LZj-Ii-63i" id="MVF-az-uyA"/>
|
||||||
<outlet property="providerLabel" destination="O9r-10-LDD" id="xAF-NW-ymm"/>
|
<outlet property="providerLabel" destination="O9r-10-LDD" id="xAF-NW-ymm"/>
|
||||||
<outlet property="thumbnailView" destination="h3b-Mf-lD6" id="4mF-bJ-ALY"/>
|
<outlet property="thumbnailView" destination="h3b-Mf-lD6" id="4mF-bJ-ALY"/>
|
||||||
<outlet property="titleLabel" destination="Ho3-cU-IGi" id="ltu-ey-chT"/>
|
<outlet property="titleLabel" destination="Ho3-cU-IGi" id="ltu-ey-chT"/>
|
||||||
</connections>
|
</connections>
|
||||||
<point key="canvasLocation" x="0.0" y="-13.507109004739336"/>
|
<point key="canvasLocation" x="0.0" y="-11.374407582938389"/>
|
||||||
</collectionViewCell>
|
</collectionViewCell>
|
||||||
</objects>
|
</objects>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
|
@ -65,7 +65,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
})
|
})
|
||||||
dataSource.editListAccountsController = self
|
dataSource.editListAccountsController = self
|
||||||
|
|
||||||
searchResultsController = SearchResultsViewController(mastodonController: mastodonController, resultTypes: [.accounts])
|
searchResultsController = SearchResultsViewController(mastodonController: mastodonController, scope: .people)
|
||||||
searchResultsController.following = true
|
searchResultsController.following = true
|
||||||
searchResultsController.delegate = self
|
searchResultsController.delegate = self
|
||||||
searchController = UISearchController(searchResultsController: searchResultsController)
|
searchController = UISearchController(searchResultsController: searchResultsController)
|
||||||
|
|
|
@ -41,7 +41,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
var exploreTabItems: [Item] {
|
var exploreTabItems: [Item] {
|
||||||
var items: [Item] = [.explore, .bookmarks, .trendingStatuses, .profileDirectory]
|
var items: [Item] = [.explore, .bookmarks, .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))
|
||||||
|
@ -195,9 +195,6 @@ class MainSidebarViewController: UIViewController {
|
||||||
discoverSnapshot.append([
|
discoverSnapshot.append([
|
||||||
.profileDirectory,
|
.profileDirectory,
|
||||||
], to: .discoverHeader)
|
], to: .discoverHeader)
|
||||||
if mastodonController.instanceFeatures.trendingStatusesAndLinks {
|
|
||||||
discoverSnapshot.insert([.trendingStatuses], before: .profileDirectory)
|
|
||||||
}
|
|
||||||
dataSource.apply(discoverSnapshot, to: .discover)
|
dataSource.apply(discoverSnapshot, to: .discover)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -388,7 +385,7 @@ extension MainSidebarViewController {
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case tab(MainTabBarViewController.Tab)
|
case tab(MainTabBarViewController.Tab)
|
||||||
case explore, bookmarks
|
case explore, bookmarks
|
||||||
case discoverHeader, trendingStatuses, profileDirectory
|
case discoverHeader, 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
|
||||||
|
@ -403,8 +400,6 @@ extension MainSidebarViewController {
|
||||||
return "Bookmarks"
|
return "Bookmarks"
|
||||||
case .discoverHeader:
|
case .discoverHeader:
|
||||||
return "Discover"
|
return "Discover"
|
||||||
case .trendingStatuses:
|
|
||||||
return "Trending Posts"
|
|
||||||
case .profileDirectory:
|
case .profileDirectory:
|
||||||
return "Profile Directory"
|
return "Profile Directory"
|
||||||
case .listsHeader:
|
case .listsHeader:
|
||||||
|
@ -436,8 +431,6 @@ extension MainSidebarViewController {
|
||||||
return "magnifyingglass"
|
return "magnifyingglass"
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return "bookmark"
|
return "bookmark"
|
||||||
case .trendingStatuses:
|
|
||||||
return "square.text.square"
|
|
||||||
case .profileDirectory:
|
case .profileDirectory:
|
||||||
return "person.2.fill"
|
return "person.2.fill"
|
||||||
case .list(_):
|
case .list(_):
|
||||||
|
|
|
@ -232,7 +232,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
|
|
||||||
tabBarViewController.select(tab: .explore)
|
tabBarViewController.select(tab: .explore)
|
||||||
|
|
||||||
case .bookmarks, .trendingStatuses, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
|
case .bookmarks, .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.
|
||||||
|
@ -277,6 +277,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
case .explore:
|
case .explore:
|
||||||
// The Explore tab is more complicated since it encapsulates a bunch of screens which have top-level sidebar items.
|
// 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
|
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
|
// 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)
|
exploreItem = .savedHashtag(hashtagVC.hashtag)
|
||||||
case let instanceVC as InstanceTimelineViewController:
|
case let instanceVC as InstanceTimelineViewController:
|
||||||
exploreItem = .savedInstance(instanceVC.instanceURL)
|
exploreItem = .savedInstance(instanceVC.instanceURL)
|
||||||
case is TrendingStatusesViewController:
|
case is TrendingStatusesViewController, is TrendingHashtagsViewController, is TrendingLinksViewController:
|
||||||
exploreItem = .trendingStatuses
|
|
||||||
case is TrendingHashtagsViewController:
|
|
||||||
exploreItem = .explore
|
|
||||||
case is TrendingLinksViewController:
|
|
||||||
exploreItem = .explore
|
exploreItem = .explore
|
||||||
|
// these three VCs are part of the root SearchViewController, so we don't need to transfer them
|
||||||
|
skipFirst = 2
|
||||||
case is ProfileDirectoryViewController:
|
case is ProfileDirectoryViewController:
|
||||||
exploreItem = .profileDirectory
|
exploreItem = .profileDirectory
|
||||||
default:
|
default:
|
||||||
fatalError("unhandled second-level explore screen: \(tabNavigationStack[1])")
|
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:
|
case .compose:
|
||||||
// The compose tab can't be activated, this is unreachable.
|
// The compose tab can't be activated, this is unreachable.
|
||||||
|
@ -376,8 +375,6 @@ fileprivate extension MainSidebarViewController.Item {
|
||||||
return SearchViewController(mastodonController: mastodonController)
|
return SearchViewController(mastodonController: mastodonController)
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return BookmarksTableViewController(mastodonController: mastodonController)
|
return BookmarksTableViewController(mastodonController: mastodonController)
|
||||||
case .trendingStatuses:
|
|
||||||
return TrendingStatusesViewController(mastodonController: mastodonController)
|
|
||||||
case .profileDirectory:
|
case .profileDirectory:
|
||||||
return ProfileDirectoryViewController(mastodonController: mastodonController)
|
return ProfileDirectoryViewController(mastodonController: mastodonController)
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
|
|
|
@ -23,6 +23,8 @@ class OnboardingViewController: UINavigationController {
|
||||||
|
|
||||||
var authenticationSession: ASWebAuthenticationSession?
|
var authenticationSession: ASWebAuthenticationSession?
|
||||||
|
|
||||||
|
private var clientInfo: (url: URL, id: String, secret: String)?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(rootViewController: instanceSelector)
|
super.init(rootViewController: instanceSelector)
|
||||||
}
|
}
|
||||||
|
@ -42,11 +44,17 @@ class OnboardingViewController: UINavigationController {
|
||||||
let mastodonController = MastodonController(instanceURL: instanceURL, transient: true)
|
let mastodonController = MastodonController(instanceURL: instanceURL, transient: true)
|
||||||
let clientID: String
|
let clientID: String
|
||||||
let clientSecret: String
|
let clientSecret: String
|
||||||
|
if let clientInfo, clientInfo.url == instanceURL {
|
||||||
|
clientID = clientInfo.id
|
||||||
|
clientSecret = clientInfo.secret
|
||||||
|
} else {
|
||||||
do {
|
do {
|
||||||
(clientID, clientSecret) = try await mastodonController.registerApp()
|
(clientID, clientSecret) = try await mastodonController.registerApp()
|
||||||
|
self.clientInfo = (instanceURL, clientID, clientSecret)
|
||||||
} catch {
|
} catch {
|
||||||
throw Error.registeringApp(error)
|
throw Error.registeringApp(error)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let authCode = try await getAuthorizationCode(instanceURL: instanceURL, clientID: clientID)
|
let authCode = try await getAuthorizationCode(instanceURL: instanceURL, clientID: clientID)
|
||||||
let accessToken: String
|
let accessToken: String
|
||||||
do {
|
do {
|
||||||
|
|
|
@ -38,17 +38,17 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
private var activityIndicator: UIActivityIndicatorView!
|
private var activityIndicator: UIActivityIndicatorView!
|
||||||
private var errorLabel: UILabel!
|
private var errorLabel: UILabel!
|
||||||
|
|
||||||
/// Types of results to search for. `nil` means all results will be included.
|
/// Types of results to search for.
|
||||||
var resultTypes: [SearchResultType]? = nil
|
var scope: Scope
|
||||||
/// Whether to limit results to accounts the users is following.
|
/// Whether to limit results to accounts the users is following.
|
||||||
var following: Bool? = nil
|
var following: Bool? = nil
|
||||||
|
|
||||||
let searchSubject = PassthroughSubject<String?, Never>()
|
let searchSubject = PassthroughSubject<String?, Never>()
|
||||||
var currentQuery: String?
|
var currentQuery: String?
|
||||||
|
|
||||||
init(mastodonController: MastodonController, resultTypes: [SearchResultType]? = nil) {
|
init(mastodonController: MastodonController, scope: Scope = .all) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self.resultTypes = resultTypes
|
self.scope = scope
|
||||||
|
|
||||||
super.init(style: .grouped)
|
super.init(style: .grouped)
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
activityIndicator.startAnimating()
|
activityIndicator.startAnimating()
|
||||||
errorLabel.isHidden = true
|
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
|
mastodonController.run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case let .success(results, _):
|
case let .success(results, _):
|
||||||
|
@ -178,17 +178,17 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
|
self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
|
||||||
let resultTypes = self.resultTypes
|
let resultTypes = self.scope.resultTypes
|
||||||
if !results.accounts.isEmpty && (resultTypes == nil || resultTypes!.contains(.accounts)) {
|
if !results.accounts.isEmpty && resultTypes.contains(.accounts) {
|
||||||
snapshot.appendSections([.accounts])
|
snapshot.appendSections([.accounts])
|
||||||
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
|
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
|
||||||
addAccounts(results.accounts)
|
addAccounts(results.accounts)
|
||||||
}
|
}
|
||||||
if !results.hashtags.isEmpty && (resultTypes == nil || resultTypes!.contains(.hashtags)) {
|
if !results.hashtags.isEmpty && resultTypes.contains(.hashtags) {
|
||||||
snapshot.appendSections([.hashtags])
|
snapshot.appendSections([.hashtags])
|
||||||
snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .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.appendSections([.statuses])
|
||||||
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
|
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
|
||||||
addStatuses(results.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 {
|
extension SearchResultsViewController {
|
||||||
enum Section: CaseIterable {
|
enum Section: CaseIterable {
|
||||||
case accounts
|
case accounts
|
||||||
|
@ -311,6 +346,11 @@ extension SearchResultsViewController: UISearchBarDelegate {
|
||||||
// perform a search immedaitely when the search button is pressed
|
// perform a search immedaitely when the search button is pressed
|
||||||
performSearch(query: searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines))
|
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 {
|
extension SearchResultsViewController: TuskerNavigationDelegate {
|
||||||
|
|
|
@ -11,11 +11,11 @@ import Pachyderm
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
class SearchViewController: UIViewController {
|
class SearchViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var collectionView: UICollectionView!
|
var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
var resultsController: SearchResultsViewController!
|
var resultsController: SearchResultsViewController!
|
||||||
|
@ -23,6 +23,8 @@ class SearchViewController: UIViewController {
|
||||||
|
|
||||||
var searchControllerStatusOnAppearance: Bool? = nil
|
var searchControllerStatusOnAppearance: Bool? = nil
|
||||||
|
|
||||||
|
private var loadTask: Task<Void, Never>?
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@ -47,10 +49,8 @@ class SearchViewController: UIViewController {
|
||||||
return .list(using: listConfig, layoutEnvironment: environment)
|
return .list(using: listConfig, layoutEnvironment: environment)
|
||||||
|
|
||||||
case .trendingLinks:
|
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)
|
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 groupSize = NSCollectionLayoutSize(widthDimension: .absolute(250), heightDimension: .estimated(280))
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||||
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(8), top: nil, trailing: .fixed(8), bottom: nil)
|
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(8), top: nil, trailing: .fixed(8), bottom: nil)
|
||||||
|
@ -59,10 +59,27 @@ class SearchViewController: UIViewController {
|
||||||
section.boundarySupplementaryItems = [
|
section.boundarySupplementaryItems = [
|
||||||
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
|
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
|
return section
|
||||||
|
|
||||||
default:
|
case .profileSuggestions:
|
||||||
fatalError("unimplemented")
|
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)
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||||
|
@ -79,8 +96,12 @@ class SearchViewController: UIViewController {
|
||||||
resultsController.exploreNavigationController = self.navigationController
|
resultsController.exploreNavigationController = self.navigationController
|
||||||
searchController = UISearchController(searchResultsController: resultsController)
|
searchController = UISearchController(searchResultsController: resultsController)
|
||||||
searchController.obscuresBackgroundDuringPresentation = true
|
searchController.obscuresBackgroundDuringPresentation = true
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
searchController.scopeBarActivation = .onSearchActivation
|
||||||
|
}
|
||||||
searchController.searchBar.autocapitalizationType = .none
|
searchController.searchBar.autocapitalizationType = .none
|
||||||
searchController.searchBar.delegate = resultsController
|
searchController.searchBar.delegate = resultsController
|
||||||
|
searchController.searchBar.scopeButtonTitles = SearchResultsViewController.Scope.allCases.map(\.title)
|
||||||
searchController.hidesNavigationBarDuringPresentation = false
|
searchController.hidesNavigationBarDuringPresentation = false
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
|
@ -96,7 +117,10 @@ class SearchViewController: UIViewController {
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
Task(priority: .userInitiated) {
|
clearSelectionOnAppear(animated: animated)
|
||||||
|
|
||||||
|
loadTask?.cancel()
|
||||||
|
loadTask = Task(priority: .userInitiated) {
|
||||||
if (try? await mastodonController.getOwnInstance()) != nil {
|
if (try? await mastodonController.getOwnInstance()) != nil {
|
||||||
await applySnapshot()
|
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
|
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
|
||||||
cell.updateUI(card: card)
|
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
|
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
|
||||||
switch item {
|
switch item {
|
||||||
|
@ -138,8 +171,11 @@ class SearchViewController: UIViewController {
|
||||||
case let .link(card):
|
case let .link(card):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: trendingLinkCell, for: indexPath, item: card)
|
return collectionView.dequeueConfiguredReusableCell(using: trendingLinkCell, for: indexPath, item: card)
|
||||||
|
|
||||||
default:
|
case let .status(id, state):
|
||||||
fatalError("todo")
|
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
|
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
||||||
|
@ -163,37 +199,89 @@ class SearchViewController: UIViewController {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
|
||||||
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
|
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
|
||||||
async let hashtags = try? mastodonController.run(hashtagsReq).0
|
let hashtags = try? await mastodonController.run(hashtagsReq).0
|
||||||
let linksReq = Client.getTrendingLinks(limit: 10)
|
|
||||||
async let links = try? mastodonController.run(linksReq).0
|
|
||||||
|
|
||||||
if let hashtags = await hashtags {
|
if let hashtags {
|
||||||
snapshot.appendSections([.trendingHashtags])
|
snapshot.appendSections([.trendingHashtags])
|
||||||
snapshot.appendItems(hashtags.map { .tag($0) }, toSection: .trendingHashtags)
|
snapshot.appendItems(hashtags.map { .tag($0) }, toSection: .trendingHashtags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 let links = await links {
|
||||||
|
if snapshot.sectionIdentifiers.contains(.profileSuggestions) {
|
||||||
|
snapshot.insertSections([.trendingLinks], beforeSection: .profileSuggestions)
|
||||||
|
} else {
|
||||||
snapshot.appendSections([.trendingLinks])
|
snapshot.appendSections([.trendingLinks])
|
||||||
|
}
|
||||||
snapshot.appendItems(links.map { .link($0) }, toSection: .trendingLinks)
|
snapshot.appendItems(links.map { .link($0) }, toSection: .trendingLinks)
|
||||||
}
|
}
|
||||||
|
|
||||||
await dataSource.apply(snapshot)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
@objc private func preferencesChanged() {
|
||||||
Task {
|
loadTask?.cancel()
|
||||||
|
loadTask = Task {
|
||||||
await applySnapshot()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchViewController {
|
extension SearchViewController {
|
||||||
enum Section {
|
enum Section {
|
||||||
case trendingHashtags
|
case trendingHashtags
|
||||||
case trendingLinks
|
case trendingLinks
|
||||||
case trendingStatuses
|
|
||||||
case profileSuggestions
|
case profileSuggestions
|
||||||
|
case trendingStatuses
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -202,25 +290,28 @@ extension SearchViewController {
|
||||||
case .trendingLinks:
|
case .trendingLinks:
|
||||||
return "Trending Links"
|
return "Trending Links"
|
||||||
case .trendingStatuses:
|
case .trendingStatuses:
|
||||||
return "Trending Statuses"
|
return "Trending Posts"
|
||||||
case .profileSuggestions:
|
case .profileSuggestions:
|
||||||
return "Suggested Accounts"
|
return "Suggested Accounts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enum Item: Equatable, Hashable {
|
enum Item: Equatable, Hashable {
|
||||||
case status(String)
|
case status(String, CollapseState)
|
||||||
case tag(Hashtag)
|
case tag(Hashtag)
|
||||||
case link(Card)
|
case link(Card)
|
||||||
|
case account(String, Suggestion.Source)
|
||||||
|
|
||||||
static func == (lhs: SearchViewController.Item, rhs: SearchViewController.Item) -> Bool {
|
static func == (lhs: SearchViewController.Item, rhs: SearchViewController.Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case let (.status(a), .status(b)):
|
case let (.status(a, _), .status(b, _)):
|
||||||
return a == b
|
return a == b
|
||||||
case let (.tag(a), .tag(b)):
|
case let (.tag(a), .tag(b)):
|
||||||
return a == b
|
return a == b
|
||||||
case let (.link(a), .link(b)):
|
case let (.link(a), .link(b)):
|
||||||
return a.url == b.url
|
return a.url == b.url
|
||||||
|
case let (.account(a, _), .account(b, _)):
|
||||||
|
return a == b
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -228,7 +319,7 @@ extension SearchViewController {
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
case let .status(id):
|
case let .status(id, _):
|
||||||
hasher.combine("status")
|
hasher.combine("status")
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
case let .tag(tag):
|
case let .tag(tag):
|
||||||
|
@ -237,6 +328,9 @@ extension SearchViewController {
|
||||||
case let .link(card):
|
case let .link(card):
|
||||||
hasher.combine("link")
|
hasher.combine("link")
|
||||||
hasher.combine(card.url)
|
hasher.combine(card.url)
|
||||||
|
case let .account(id, _):
|
||||||
|
hasher.combine("account")
|
||||||
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,11 +350,15 @@ extension SearchViewController: UICollectionViewDelegate {
|
||||||
selected(url: url)
|
selected(url: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
case let .status(id, state):
|
||||||
fatalError("todo")
|
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? {
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -286,9 +384,62 @@ extension SearchViewController: UICollectionViewDelegate {
|
||||||
UIMenu(children: self.actionsForTrendingLink(card: card))
|
UIMenu(children: self.actionsForTrendingLink(card: card))
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
case let .status(id, state):
|
||||||
fatalError("todo")
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,8 +466,26 @@ extension SearchViewController: UICollectionViewDragDelegate {
|
||||||
}
|
}
|
||||||
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
|
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
|
||||||
|
|
||||||
default:
|
case let .status(id, _):
|
||||||
fatalError("todo")
|
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: 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)
|
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() {
|
@objc func toggleSaveButtonPressed() {
|
||||||
let context = parentMastodonController!.persistentContainer.viewContext
|
let context = parentMastodonController!.persistentContainer.viewContext
|
||||||
let req = SavedInstance.fetchRequest(url: instanceURL, account: parentMastodonController!.accountInfo!)
|
let req = SavedInstance.fetchRequest(url: instanceURL, account: parentMastodonController!.accountInfo!)
|
||||||
|
|
|
@ -856,6 +856,17 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: true)
|
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 {
|
extension TimelineViewController {
|
||||||
|
|
|
@ -384,7 +384,7 @@ extension MenuActionProvider {
|
||||||
|
|
||||||
private func openInSafariAction(url: URL) -> UIAction {
|
private func openInSafariAction(url: URL) -> UIAction {
|
||||||
return createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { [weak self] (_) in
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -226,6 +226,7 @@ private let statusPathRegex = try! NSRegularExpression(
|
||||||
pattern:
|
pattern:
|
||||||
"(^/@[a-z0-9_]+/\\d{18})" // mastodon
|
"(^/@[a-z0-9_]+/\\d{18})" // mastodon
|
||||||
+ "|(^/notice/[a-z0-9]{18})" // pleroma
|
+ "|(^/notice/[a-z0-9]{18})" // pleroma
|
||||||
|
+ "|(^/notes/[a-z0-9]{10})" // misskey
|
||||||
+ "|(^/p/[a-z0-9_]+/\\d{18})" // pixelfed
|
+ "|(^/p/[a-z0-9_]+/\\d{18})" // pixelfed
|
||||||
+ "|(^/i/web/post/\\d{18})" // pixelfed web frontend
|
+ "|(^/i/web/post/\\d{18})" // pixelfed web frontend
|
||||||
+ "|(^/u/.+/h/[a-z0-9]{18})" // honk
|
+ "|(^/u/.+/h/[a-z0-9]{18})" // honk
|
||||||
|
|
|
@ -10,26 +10,32 @@ import UIKit
|
||||||
|
|
||||||
class CachedImageView: UIImageView {
|
class CachedImageView: UIImageView {
|
||||||
|
|
||||||
private let cache: ImageCache
|
var cache: ImageCache!
|
||||||
private var url: URL?
|
private var url: URL?
|
||||||
private var isGrayscale = false
|
private var isGrayscale = false
|
||||||
private var fetchTask: Task<Void, Error>?
|
private var fetchTask: Task<Void, Error>?
|
||||||
|
private var blurHashTask: DispatchWorkItem?
|
||||||
|
|
||||||
init(cache: ImageCache) {
|
init(cache: ImageCache) {
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
commonInit()
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
super.init(coder: coder)
|
||||||
|
commonInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(for url: URL?) {
|
private func commonInit() {
|
||||||
if url != self.url {
|
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.url = url
|
||||||
|
self.image = nil
|
||||||
|
updateBlurhash(blurhash, for: url)
|
||||||
updateImage()
|
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() {
|
private func updateImage() {
|
||||||
fetchTask?.cancel()
|
fetchTask?.cancel()
|
||||||
fetchTask = Task(priority: .high) {
|
|
||||||
self.image = nil
|
|
||||||
guard let url else {
|
guard let url else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchTask = Task(priority: .high) {
|
||||||
let (_, image) = await cache.get(url)
|
let (_, image) = await cache.get(url)
|
||||||
guard let image else {
|
guard let image else {
|
||||||
return
|
return
|
||||||
|
@ -59,6 +84,7 @@ class CachedImageView: UIImageView {
|
||||||
try Task.checkCancellation()
|
try Task.checkCancellation()
|
||||||
self.image = transformedImage
|
self.image = transformedImage
|
||||||
self.isGrayscale = Preferences.shared.grayscaleImages
|
self.isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
self.blurHashTask?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,9 @@ class TrendingHashtagCollectionViewCell: UICollectionViewCell {
|
||||||
backgroundColor = .systemBackground
|
backgroundColor = .systemBackground
|
||||||
|
|
||||||
hashtagLabel.font = .preferredFont(forTextStyle: .title2)
|
hashtagLabel.font = .preferredFont(forTextStyle: .title2)
|
||||||
|
hashtagLabel.adjustsFontForContentSizeCategory = true
|
||||||
peopleTodayLabel.font = .preferredFont(forTextStyle: .caption1)
|
peopleTodayLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||||
|
peopleTodayLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
let vStack = UIStackView(arrangedSubviews: [
|
let vStack = UIStackView(arrangedSubviews: [
|
||||||
hashtagLabel,
|
hashtagLabel,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
|
import SwiftSoup
|
||||||
|
|
||||||
class StatusCardView: UIView {
|
class StatusCardView: UIView {
|
||||||
|
|
||||||
|
@ -156,7 +157,7 @@ class StatusCardView: UIView {
|
||||||
titleLabel.text = title
|
titleLabel.text = title
|
||||||
titleLabel.isHidden = title.isEmpty
|
titleLabel.isHidden = title.isEmpty
|
||||||
|
|
||||||
let description = card.description.trimmingCharacters(in: .whitespacesAndNewlines)
|
let description = try! SwiftSoup.parseBodyFragment(card.description).text()
|
||||||
descriptionLabel.text = description
|
descriptionLabel.text = description
|
||||||
descriptionLabel.isHidden = description.isEmpty
|
descriptionLabel.isHidden = description.isEmpty
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ class TrendHistoryView: UIView {
|
||||||
borderLayer.path = path.cgPath.copy()!
|
borderLayer.path = path.cgPath.copy()!
|
||||||
borderLayer.strokeColor = tintColor.cgColor
|
borderLayer.strokeColor = tintColor.cgColor
|
||||||
borderLayer.fillColor = nil
|
borderLayer.fillColor = nil
|
||||||
borderLayer.lineWidth = 2
|
borderLayer.lineWidth = lineWidth
|
||||||
borderLayer.lineCap = .round
|
borderLayer.lineCap = .round
|
||||||
|
|
||||||
path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
|
path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
|
||||||
|
@ -109,12 +109,16 @@ class TrendHistoryView: UIView {
|
||||||
let fillColor = self.fillColor()
|
let fillColor = self.fillColor()
|
||||||
fillLayer.strokeColor = fillColor
|
fillLayer.strokeColor = fillColor
|
||||||
fillLayer.fillColor = fillColor
|
fillLayer.fillColor = fillColor
|
||||||
fillLayer.lineWidth = 2
|
fillLayer.lineWidth = lineWidth
|
||||||
|
|
||||||
layer.addSublayer(fillLayer)
|
layer.addSublayer(fillLayer)
|
||||||
layer.addSublayer(borderLayer)
|
layer.addSublayer(borderLayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var lineWidth: CGFloat {
|
||||||
|
(traitCollection.preferredContentSizeCategory > .large || UIAccessibility.isBoldTextEnabled) ? 4 : 2
|
||||||
|
}
|
||||||
|
|
||||||
// The non-transparent fill color.
|
// The non-transparent fill color.
|
||||||
// We blend with the view's background color ourselves so that final color is non-transparent,
|
// 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
|
// otherwise when the fill layer's border and fill overlap, there's a visibly darker patch
|
||||||
|
|
Loading…
Reference in New Issue