diff --git a/Pachyderm/Client.swift b/Pachyderm/Client.swift index e45442c7..afad5ecf 100644 --- a/Pachyderm/Client.swift +++ b/Pachyderm/Client.swift @@ -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, diff --git a/Pachyderm/Model/Card.swift b/Pachyderm/Model/Card.swift index aea2bcf2..311ae631 100644 --- a/Pachyderm/Model/Card.swift +++ b/Pachyderm/Model/Card.swift @@ -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 } } diff --git a/Pachyderm/Model/Hashtag.swift b/Pachyderm/Model/Hashtag.swift index af868580..d5ea7ff4 100644 --- a/Pachyderm/Model/Hashtag.swift +++ b/Pachyderm/Model/Hashtag.swift @@ -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 diff --git a/Pachyderm/Model/History.swift b/Pachyderm/Model/History.swift new file mode 100644 index 00000000..4555c22c --- /dev/null +++ b/Pachyderm/Model/History.swift @@ -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 + } +} diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 4142e60e..14014342 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -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 = ""; }; D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagTableViewCell.swift; sourceTree = ""; }; D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingHashtagTableViewCell.xib; sourceTree = ""; }; - D6093FB625BE0CF3004811E6 /* HashtagHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagHistoryView.swift; sourceTree = ""; }; + D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = ""; }; D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = ""; }; D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = ""; }; D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.swift; sourceTree = ""; }; @@ -486,6 +489,9 @@ D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = ""; }; D6114E0A27F3F6EA0080E273 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusesViewController.swift; sourceTree = ""; }; + D6114E0E27F897D70080E273 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; + D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = ""; }; + D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkTableViewCell.swift; sourceTree = ""; }; D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = ""; }; D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = ""; }; D61AC1D2232E928600C54D2D /* InstanceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelector.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index b210ce43..5f9ba696 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -156,7 +156,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate { private func addDiscoverSection(to snapshot: inout NSDiffableDataSourceSnapshot) { 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): diff --git a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift index 135f6870..7f5bdfa2 100644 --- a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift +++ b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift @@ -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] { diff --git a/Tusker/Screens/Explore/TrendingLinkTableViewCell.swift b/Tusker/Screens/Explore/TrendingLinkTableViewCell.swift new file mode 100644 index 00000000..e3f39b6f --- /dev/null +++ b/Tusker/Screens/Explore/TrendingLinkTableViewCell.swift @@ -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 + } + } + } + +} diff --git a/Tusker/Screens/Explore/TrendingLinksViewController.swift b/Tusker/Screens/Explore/TrendingLinksViewController.swift new file mode 100644 index 00000000..3a0fac59 --- /dev/null +++ b/Tusker/Screens/Explore/TrendingLinksViewController.swift @@ -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! + + 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() + 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 } +} diff --git a/Tusker/Screens/Utilities/Previewing.swift b/Tusker/Screens/Utilities/Previewing.swift index 633c14fc..d60e5723 100644 --- a/Tusker/Screens/Utilities/Previewing.swift +++ b/Tusker/Screens/Utilities/Previewing.swift @@ -208,6 +208,29 @@ extension MenuPreviewProvider { UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection), ] } + + 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? diff --git a/Tusker/Views/Hashtag Cell/TrendingHashtagTableViewCell.swift b/Tusker/Views/Hashtag Cell/TrendingHashtagTableViewCell.swift index 60403543..88ce2085 100644 --- a/Tusker/Views/Hashtag Cell/TrendingHashtagTableViewCell.swift +++ b/Tusker/Views/Hashtag Cell/TrendingHashtagTableViewCell.swift @@ -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() diff --git a/Tusker/Views/Hashtag Cell/TrendingHashtagTableViewCell.xib b/Tusker/Views/Hashtag Cell/TrendingHashtagTableViewCell.xib index df56871a..9444a559 100644 --- a/Tusker/Views/Hashtag Cell/TrendingHashtagTableViewCell.xib +++ b/Tusker/Views/Hashtag Cell/TrendingHashtagTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -37,7 +37,7 @@ - + diff --git a/Tusker/Views/Hashtag Cell/HashtagHistoryView.swift b/Tusker/Views/TrendHistoryView.swift similarity index 96% rename from Tusker/Views/Hashtag Cell/HashtagHistoryView.swift rename to Tusker/Views/TrendHistoryView.swift index 07116acb..96c20a0f 100644 --- a/Tusker/Views/Hashtag Cell/HashtagHistoryView.swift +++ b/Tusker/Views/TrendHistoryView.swift @@ -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 {