Add Trending Links

This commit is contained in:
Shadowfacts 2022-04-02 11:36:45 -04:00
parent 240ccf23a4
commit 8473f32781
13 changed files with 404 additions and 62 deletions

View File

@ -388,6 +388,16 @@ public class Client {
return Request(method: .get, path: "/api/v1/trends/statuses", queryParameters: parameters)
}
public static func getTrendingLinks(limit: Int? = nil) -> Request<[Card]> {
let parameters: [Parameter]
if let limit = limit {
parameters = ["limit" => limit]
} else {
parameters = []
}
return Request(method: .get, path: "/api/v1/trends/links", queryParameters: parameters)
}
public static func getFeaturedProfiles(local: Bool, order: DirectoryOrder, offset: Int? = nil, limit: Int? = nil) -> Request<[Account]> {
var parameters = [
"order" => order.rawValue,

View File

@ -23,6 +23,8 @@ public class Card: Codable {
public let width: Int?
public let height: Int?
public let blurhash: String?
/// Only present when returned from the trending links endpoint
public let history: [History]?
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@ -40,6 +42,7 @@ public class Card: Codable {
self.width = try? container.decodeIfPresent(Int.self, forKey: .width)
self.height = try? container.decodeIfPresent(Int.self, forKey: .height)
self.blurhash = try? container.decodeIfPresent(String.self, forKey: .blurhash)
self.history = try? container.decodeIfPresent([History].self, forKey: .history)
}
public func encode(to encoder: Encoder) throws {
@ -67,6 +70,7 @@ public class Card: Codable {
case width
case height
case blurhash
case history
}
}

View File

@ -11,6 +11,7 @@ import Foundation
public class Hashtag: Codable {
public let name: String
public let url: URL
/// Only present when returned from the trending hashtags endpoint
public let history: [History]?
public init(name: String, url: URL) {
@ -26,53 +27,6 @@ public class Hashtag: Codable {
}
}
extension Hashtag {
public class History: Codable {
public let day: Date
public let uses: Int
public let accounts: Int
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let day = try? container.decode(Date.self, forKey: .day) {
self.day = day
} else if let unixTimestamp = try? container.decode(Double.self, forKey: .day) {
self.day = Date(timeIntervalSince1970: unixTimestamp)
} else if let str = try? container.decode(String.self, forKey: .day),
let unixTimestamp = Double(str) {
self.day = Date(timeIntervalSince1970: unixTimestamp)
} else {
throw DecodingError.dataCorruptedError(forKey: .day, in: container, debugDescription: "day must be either date or UNIX timestamp")
}
if let uses = try? container.decode(Int.self, forKey: .uses) {
self.uses = uses
} else if let str = try? container.decode(String.self, forKey: .uses),
let uses = Int(str) {
self.uses = uses
} else {
throw DecodingError.dataCorruptedError(forKey: .uses, in: container, debugDescription: "uses must either be int or string containing int")
}
if let accounts = try? container.decode(Int.self, forKey: .accounts) {
self.accounts = accounts
} else if let str = try? container.decode(String.self, forKey: .accounts),
let accounts = Int(str) {
self.accounts = accounts
} else {
throw DecodingError.dataCorruptedError(forKey: .accounts, in: container, debugDescription: "accounts must either be int or string containing int")
}
}
private enum CodingKeys: String, CodingKey {
case day
case uses
case accounts
}
}
}
extension Hashtag: Equatable, Hashable {
public static func ==(lhs: Hashtag, rhs: Hashtag) -> Bool {
return lhs.name == rhs.name

View File

@ -0,0 +1,54 @@
//
// History.swift
// Pachyderm
//
// Created by Shadowfacts on 4/2/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
public class History: Codable {
public let day: Date
public let uses: Int
public let accounts: Int
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let day = try? container.decode(Date.self, forKey: .day) {
self.day = day
} else if let unixTimestamp = try? container.decode(Double.self, forKey: .day) {
self.day = Date(timeIntervalSince1970: unixTimestamp)
} else if let str = try? container.decode(String.self, forKey: .day),
let unixTimestamp = Double(str) {
self.day = Date(timeIntervalSince1970: unixTimestamp)
} else {
throw DecodingError.dataCorruptedError(forKey: .day, in: container, debugDescription: "day must be either date or UNIX timestamp")
}
if let uses = try? container.decode(Int.self, forKey: .uses) {
self.uses = uses
} else if let str = try? container.decode(String.self, forKey: .uses),
let uses = Int(str) {
self.uses = uses
} else {
throw DecodingError.dataCorruptedError(forKey: .uses, in: container, debugDescription: "uses must either be int or string containing int")
}
if let accounts = try? container.decode(Int.self, forKey: .accounts) {
self.accounts = accounts
} else if let str = try? container.decode(String.self, forKey: .accounts),
let accounts = Int(str) {
self.accounts = accounts
} else {
throw DecodingError.dataCorruptedError(forKey: .accounts, in: container, debugDescription: "accounts must either be int or string containing int")
}
}
private enum CodingKeys: String, CodingKey {
case day
case uses
case accounts
}
}

View File

@ -22,7 +22,7 @@
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */; };
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */; };
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* HashtagHistoryView.swift */; };
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; };
@ -72,6 +72,9 @@
D6114E0927F3EA3D0080E273 /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */; };
D6114E0B27F3F6EA0080E273 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0A27F3F6EA0080E273 /* Endpoint.swift */; };
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */; };
D6114E0F27F897D70080E273 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0E27F897D70080E273 /* History.swift */; };
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; };
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */; };
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; };
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
D61AC1D3232E928600C54D2D /* InstanceSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D2232E928600C54D2D /* InstanceSelector.swift */; };
@ -435,7 +438,7 @@
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; };
D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagTableViewCell.swift; sourceTree = "<group>"; };
D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingHashtagTableViewCell.xib; sourceTree = "<group>"; };
D6093FB625BE0CF3004811E6 /* HashtagHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagHistoryView.swift; sourceTree = "<group>"; };
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = "<group>"; };
D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = "<group>"; };
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.swift; sourceTree = "<group>"; };
@ -486,6 +489,9 @@
D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = "<group>"; };
D6114E0A27F3F6EA0080E273 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; };
D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusesViewController.swift; sourceTree = "<group>"; };
D6114E0E27F897D70080E273 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = "<group>"; };
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkTableViewCell.swift; sourceTree = "<group>"; };
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
D61AC1D2232E928600C54D2D /* InstanceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelector.swift; sourceTree = "<group>"; };
@ -916,6 +922,7 @@
D61099E22144C38900432DC2 /* Emoji.swift */,
D61099EC2145664800432DC2 /* Filter.swift */,
D6109A0021456B0800432DC2 /* Hashtag.swift */,
D6114E0E27F897D70080E273 /* History.swift */,
D61099EE214566C000432DC2 /* Instance.swift */,
D61099F02145686D00432DC2 /* List.swift */,
D6109A062145756700432DC2 /* LoginSettings.swift */,
@ -944,7 +951,6 @@
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */,
D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */,
D6093FB625BE0CF3004811E6 /* HashtagHistoryView.swift */,
);
path = "Hashtag Cell";
sourceTree = "<group>";
@ -1027,6 +1033,8 @@
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */,
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */,
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */,
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
@ -1506,6 +1514,7 @@
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
D620483723D38190008A63EF /* StatusContentTextView.swift */,
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */,
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
@ -2082,6 +2091,7 @@
D6114E0B27F3F6EA0080E273 /* Endpoint.swift in Sources */,
D61099EF214566C000432DC2 /* Instance.swift in Sources */,
D61099D22144B2E600432DC2 /* Body.swift in Sources */,
D6114E0F27F897D70080E273 /* History.swift in Sources */,
D623A53F2635F6910095BD04 /* Poll.swift in Sources */,
D63569E023908A8D003DD353 /* StatusState.swift in Sources */,
D6109A0121456B0800432DC2 /* Hashtag.swift in Sources */,
@ -2115,7 +2125,7 @@
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */,
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */,
D6DEA0DE268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift in Sources */,
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
@ -2275,6 +2285,7 @@
D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */,
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */,
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
@ -2284,6 +2295,7 @@
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,

View File

@ -156,7 +156,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
private func addDiscoverSection(to snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
snapshot.insertSections([.discover], afterSection: .bookmarks)
// todo: check version
snapshot.appendItems([.trendingStatuses, .trendingTags, .profileDirectory], toSection: .discover)
snapshot.appendItems([.trendingStatuses, .trendingTags, .trendingLinks, .profileDirectory], toSection: .discover)
}
private func ownInstanceLoaded(_ instance: Instance) {
@ -300,6 +300,9 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
case .trendingTags:
show(TrendingHashtagsViewController(mastodonController: mastodonController), sender: nil)
case .trendingLinks:
show(TrendingLinksViewController(mastodonController: mastodonController), sender: nil)
case .profileDirectory:
show(ProfileDirectoryViewController(mastodonController: mastodonController), sender: nil)
@ -381,6 +384,7 @@ extension ExploreViewController {
case bookmarks
case trendingStatuses
case trendingTags
case trendingLinks
case profileDirectory
case list(List)
case addList
@ -397,6 +401,8 @@ extension ExploreViewController {
return NSLocalizedString("Trending Posts", comment: "trending statuses nav item title")
case .trendingTags:
return NSLocalizedString("Trending Hashtags", comment: "trending hashtags nav item title")
case .trendingLinks:
return NSLocalizedString("Trending Links", comment: "trending links nav item title")
case .profileDirectory:
return NSLocalizedString("Profile Directory", comment: "profile directory nav item title")
case let .list(list):
@ -422,7 +428,9 @@ extension ExploreViewController {
case .trendingStatuses:
name = "doc.text.image"
case .trendingTags:
name = "arrow.up.arrow.down"
name = "number"
case .trendingLinks:
name = "link"
case .profileDirectory:
name = "person.2.fill"
case .list(_):
@ -447,6 +455,8 @@ extension ExploreViewController {
return true
case (.trendingTags, .trendingTags):
return true
case (.trendingLinks, .trendingLinks):
return true
case (.profileDirectory, .profileDirectory):
return true
case let (.list(a), .list(b)):
@ -474,6 +484,8 @@ extension ExploreViewController {
hasher.combine("trendingStatuses")
case .trendingTags:
hasher.combine("trendingTags")
case .trendingLinks:
hasher.combine("trendingLinks")
case .profileDirectory:
hasher.combine("profileDirectory")
case let .list(list):

View File

@ -81,7 +81,6 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
} actionProvider: { (_) in
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.tableView.cellForRow(at: indexPath)))
}
}
override func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {

View File

@ -0,0 +1,165 @@
//
// TrendingLinkTableViewCell.swift
// Tusker
//
// Created by Shadowfacts on 4/2/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import WebURLFoundationExtras
class TrendingLinkTableViewCell: UITableViewCell {
private var card: Card?
private var isGrayscale = false
private var thumbnailRequest: ImageCache.Request?
private let thumbnailView = UIImageView()
private let titleLabel = UILabel()
private let providerLabel = UILabel()
private let activityLabel = UILabel()
private let historyView = TrendHistoryView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
thumbnailView.contentMode = .scaleAspectFill
thumbnailView.clipsToBounds = true
titleLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline).withSymbolicTraits(.traitBold)!, size: 0)
titleLabel.numberOfLines = 2
providerLabel.font = .preferredFont(forTextStyle: .subheadline)
activityLabel.font = .preferredFont(forTextStyle: .caption1)
let vStack = UIStackView(arrangedSubviews: [
titleLabel,
providerLabel,
activityLabel,
])
vStack.axis = .vertical
vStack.spacing = 4
let hStack = UIStackView(arrangedSubviews: [
thumbnailView,
vStack,
historyView,
])
hStack.axis = .horizontal
hStack.spacing = 4
hStack.alignment = .center
hStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(hStack)
NSLayoutConstraint.activate([
thumbnailView.heightAnchor.constraint(equalToConstant: 75),
thumbnailView.widthAnchor.constraint(equalTo: thumbnailView.heightAnchor),
historyView.widthAnchor.constraint(equalToConstant: 75),
historyView.heightAnchor.constraint(equalToConstant: 44),
hStack.leadingAnchor.constraint(equalToSystemSpacingAfter: safeAreaLayoutGuide.leadingAnchor, multiplier: 1),
safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: hStack.trailingAnchor, multiplier: 1),
hStack.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 1),
bottomAnchor.constraint(equalToSystemSpacingBelow: hStack.bottomAnchor, multiplier: 1),
])
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
thumbnailView.layer.cornerRadius = 0.05 * thumbnailView.bounds.width
}
func updateUI(card: Card) {
self.card = card
self.thumbnailView.image = nil
updateGrayscaleableUI(card: card)
updateUIForPreferences()
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
titleLabel.text = title
titleLabel.isHidden = title.isEmpty
let provider = card.providerName?.trimmingCharacters(in: .whitespacesAndNewlines)
providerLabel.text = provider
providerLabel.isHidden = provider?.isEmpty ?? true
if let history = card.history {
let sorted = history.sorted(by: { $0.day < $1.day })
let lastTwo = sorted[(sorted.count - 2)...]
let accounts = lastTwo.map(\.accounts).reduce(0, +)
let uses = lastTwo.map(\.uses).reduce(0, +)
let format = NSLocalizedString("trending hashtag info", comment: "trending hashtag posts and people")
activityLabel.text = String.localizedStringWithFormat(format, accounts, uses)
activityLabel.isHidden = false
} else {
activityLabel.isHidden = true
}
historyView.setHistory(card.history)
historyView.isHidden = card.history == nil || card.history!.count < 2
}
@objc private func updateUIForPreferences() {
}
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 = image,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) else {
return
}
DispatchQueue.main.async {
self.thumbnailView.image = transformedImage
}
})
if thumbnailRequest != nil {
loadBlurHash(card: card)
}
}
}
private func loadBlurHash(card: Card) {
guard let hash = card.blurhash else {
return
}
let imageViewSize = self.thumbnailView.bounds.size
AttachmentView.queue.async { [weak self] in
let size: CGSize
if let width = card.width, let height = card.height {
size = CGSize(width: width, height: height)
} else {
size = imageViewSize
}
guard let preview = UIImage(blurHash: hash, size: size) else {
return
}
DispatchQueue.main.async { [weak self] in
guard let self = self,
self.card?.url == card.url,
self.thumbnailView.image == nil else {
return
}
self.thumbnailView.image = preview
}
}
}
}

View File

@ -0,0 +1,109 @@
//
// TrendingLinksViewController.swift
// Tusker
//
// Created by Shadowfacts on 4/2/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import WebURLFoundationExtras
import SafariServices
class TrendingLinksViewController: EnhancedTableViewController {
weak var mastodonController: MastodonController!
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(style: .grouped)
dragEnabled = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("Trending Links", comment: "trending links screen title")
tableView.register(TrendingLinkTableViewCell.self, forCellReuseIdentifier: "trendingLinkCell")
tableView.estimatedRowHeight = 100
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingLinkCell", for: indexPath) as! TrendingLinkTableViewCell
cell.updateUI(card: item.card)
return cell
})
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let request = Client.getTrendingLinks()
Task {
guard let (links, _) = try? await mastodonController.run(request) else {
return
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.links])
snapshot.appendItems(links.map(Item.init))
dataSource.apply(snapshot)
}
}
// MARK: - Table View Delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath),
let url = URL(item.card.url) else {
return
}
selected(url: url)
}
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath),
let url = URL(item.card.url) else {
return nil
}
return UIContextMenuConfiguration(identifier: nil) {
return SFSafariViewController(url: url)
} actionProvider: { _ in
return UIMenu(children: self.actionsForTrendingLink(card: item.card))
}
}
}
extension TrendingLinksViewController {
enum Section {
case links
}
struct Item: Hashable {
let card: Card
static func ==(lhs: Item, rhs: Item) -> Bool {
return lhs.card.url == rhs.card.url
}
func hash(into hasher: inout Hasher) {
hasher.combine(card.url)
}
}
}
extension TrendingLinksViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController }
}
extension TrendingLinksViewController: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { self }
}

View File

@ -209,6 +209,29 @@ extension MenuPreviewProvider {
]
}
func actionsForTrendingLink(card: Card) -> [UIMenuElement] {
guard let url = URL(card.url) else {
return []
}
return [
openInSafariAction(url: url),
createAction(identifier: "postlink", title: "Post this Link", systemImageName: "square.and.pencil", handler: { [weak self] _ in
guard let self = self else { return }
let draft = self.mastodonController!.createDraft()
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
if !title.isEmpty {
draft.text += title
draft.text += ":\n"
}
draft.text += url.absoluteString
// prevents the draft from being saved automatically until the user makes a change
// also prevents it from being posted without being changed
draft.initialText = draft.text
self.navigationDelegate?.compose(editing: draft)
})
]
}
private func createAction(identifier: String, title: String, systemImageName: String?, handler: @escaping UIActionHandler) -> UIAction {
let image: UIImage?
if let name = systemImageName {

View File

@ -13,7 +13,7 @@ class TrendingHashtagTableViewCell: UITableViewCell {
@IBOutlet weak var hashtagLabel: UILabel!
@IBOutlet weak var peopleTodayLabel: UILabel!
@IBOutlet weak var historyView: HashtagHistoryView!
@IBOutlet weak var historyView: TrendHistoryView!
override func awakeFromNib() {
super.awakeFromNib()

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -37,7 +37,7 @@
</label>
</subviews>
</stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Xrw-2v-ybZ" customClass="HashtagHistoryView" customModule="Tusker" customModuleProvider="target">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Xrw-2v-ybZ" customClass="TrendHistoryView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="188" y="11" width="100" height="44"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>

View File

@ -1,5 +1,5 @@
//
// HashtagHistoryView.swift
// TrendHistoryView.swift
// Tusker
//
// Created by Shadowfacts on 1/24/21.
@ -9,9 +9,9 @@
import UIKit
import Pachyderm
class HashtagHistoryView: UIView {
class TrendHistoryView: UIView {
private var history: [Hashtag.History]?
private var history: [History]?
private let curveRadius: CGFloat = 10
@ -30,7 +30,7 @@ class HashtagHistoryView: UIView {
createLayers()
}
func setHistory(_ history: [Hashtag.History]?) {
func setHistory(_ history: [History]?) {
if let history = history {
self.history = history.sorted(by: { $0.day < $1.day })
} else {