Compare commits

...

2 Commits

7 changed files with 89 additions and 31 deletions

View File

@ -483,6 +483,12 @@ public class Client {
])
}
// MARK: - Hashtags
/// Requires Mastodon 4.0.0+
public static func getHashtag(name: String) -> Request<Hashtag> {
return Request(method: .get, path: "/api/v1/tags/\(name)")
}
}
extension Client {

View File

@ -12,12 +12,12 @@ import Pachyderm
@MainActor
class ToggleFollowHashtagService {
private let hashtag: Hashtag
private let hashtagName: String
private let mastodonController: MastodonController
private let presenter: any TuskerNavigationDelegate
init(hashtag: Hashtag, presenter: any TuskerNavigationDelegate) {
self.hashtag = hashtag
init(hashtagName: String, presenter: any TuskerNavigationDelegate) {
self.hashtagName = hashtagName
self.mastodonController = presenter.apiController
self.presenter = presenter
}
@ -25,9 +25,9 @@ class ToggleFollowHashtagService {
func toggleFollow() async {
let context = mastodonController.persistentContainer.viewContext
var config: ToastConfiguration
if let existing = mastodonController.followedHashtags.first(where: { $0.name == hashtag.name }) {
if let existing = mastodonController.followedHashtags.first(where: { $0.name == hashtagName }) {
do {
let req = Hashtag.unfollow(name: hashtag.name)
let req = Hashtag.unfollow(name: hashtagName)
_ = try await mastodonController.run(req)
context.delete(existing)
@ -44,7 +44,7 @@ class ToggleFollowHashtagService {
}
} else {
do {
let req = Hashtag.follow(name: hashtag.name)
let req = Hashtag.follow(name: hashtagName)
let (hashtag, _) = try await mastodonController.run(req)
_ = FollowedHashtag(hashtag: hashtag, context: context)

View File

@ -106,7 +106,9 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
private func timelineViewController(for timeline: Timeline, mastodonController: MastodonController) -> UIViewController {
switch timeline {
// todo: hashtag controllers need whole objects which must be fetched asynchronously, and that endpoint only exists in mastodon v4
case .tag(hashtag: let name):
return HashtagTimelineViewController(forNamed: name, mastodonController: mastodonController)
case .list(id: let id):
let req = ListMO.fetchRequest(id: id)
if let list = try? mastodonController.persistentContainer.viewContext.fetch(req).first {

View File

@ -127,6 +127,11 @@ class MainSidebarViewController: UIViewController {
itemLastSelectedTimestamps[item] = Date()
}
func hasItem(_ item: Item) -> Bool {
loadViewIfNeeded()
return dataSource.snapshot().itemIdentifiers.contains(item)
}
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let listCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
var config = cell.defaultContentConfiguration()
@ -212,10 +217,10 @@ class MainSidebarViewController: UIViewController {
let req = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(req)) ?? []
var items = saved.map {
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
Item.savedHashtag($0.name)
}
for followed in followed where !saved.contains(where: { $0.name == followed.name }) {
items.append(.savedHashtag(Hashtag(name: followed.name, url: followed.url)))
items.append(.savedHashtag(followed.name))
}
items = items.uniques()
items.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
@ -315,8 +320,8 @@ class MainSidebarViewController: UIViewController {
return UserActivityManager.myProfileActivity(accountID: id)
case let .list(list):
return UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: id)
case let .savedHashtag(tag):
return UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: tag.name), accountID: id)
case let .savedHashtag(name):
return UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: name), accountID: id)
case .savedInstance(_):
// todo: show timeline activity doesn't work for public timelines
return nil
@ -347,7 +352,7 @@ extension MainSidebarViewController {
case tab(MainTabBarViewController.Tab)
case explore, bookmarks, favorites
case listsHeader, list(List), addList
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
case savedHashtagsHeader, savedHashtag(String), addSavedHashtag
case savedInstancesHeader, savedInstance(URL), addSavedInstance
var title: String {
@ -368,8 +373,8 @@ extension MainSidebarViewController {
return "New List..."
case .savedHashtagsHeader:
return "Hashtags"
case let .savedHashtag(hashtag):
return hashtag.name
case let .savedHashtag(name):
return name
case .addSavedHashtag:
return "Add Hashtag..."
case .savedInstancesHeader:

View File

@ -339,8 +339,8 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
exploreItem = .favorites
case let listVC as ListTimelineViewController:
exploreItem = .list(listVC.list)
case let hashtagVC as HashtagTimelineViewController where hashtagVC.isHashtagSaved:
exploreItem = .savedHashtag(hashtagVC.hashtag)
case let hashtagVC as HashtagTimelineViewController where sidebar.hasItem(.savedHashtag(hashtagVC.hashtagName)):
exploreItem = .savedHashtag(hashtagVC.hashtagName)
case let instanceVC as InstanceTimelineViewController:
exploreItem = .savedInstance(instanceVC.instanceURL)
case is TrendsViewController:
@ -428,8 +428,8 @@ fileprivate extension MainSidebarViewController.Item {
return FavoritesViewController(mastodonController: mastodonController)
case let .list(list):
return ListTimelineViewController(for: list, mastodonController: mastodonController)
case let .savedHashtag(hashtag):
return HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController)
case let .savedHashtag(name):
return HashtagTimelineViewController(forNamed: name, mastodonController: mastodonController)
case let .savedInstance(url):
return InstanceTimelineViewController(for: url, parentMastodonController: mastodonController)
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:

View File

@ -11,23 +11,30 @@ import Pachyderm
class HashtagTimelineViewController: TimelineViewController {
let hashtag: Hashtag
let hashtagName: String
private var hashtag: Hashtag?
private var fetchHashtag: Task<Hashtag?, Never>?
var toggleSaveButton: UIBarButtonItem!
var isHashtagSaved: Bool {
let req = SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)
let req = SavedHashtag.fetchRequest(name: hashtagName, account: mastodonController.accountInfo!)
return mastodonController.persistentContainer.viewContext.objectExists(for: req)
}
private var isHashtagFollowed: Bool {
mastodonController.followedHashtags.contains(where: { $0.name == hashtag.name })
mastodonController.followedHashtags.contains(where: { $0.name == hashtagName })
}
init(for hashtag: Hashtag, mastodonController: MastodonController) {
convenience init(for hashtag: Hashtag, mastodonController: MastodonController) {
self.init(forNamed: hashtag.name, mastodonController: mastodonController)
self.hashtag = hashtag
}
super.init(for: .tag(hashtag: hashtag.name), mastodonController: mastodonController)
init(forNamed name: String, mastodonController: MastodonController) {
self.hashtagName = name
super.init(for: .tag(hashtag: hashtagName), mastodonController: mastodonController)
}
required init?(coder aDecoder: NSCoder) {
@ -37,10 +44,37 @@ class HashtagTimelineViewController: TimelineViewController {
override func viewDidLoad() {
super.viewDidLoad()
if hashtag == nil {
fetchHashtag = Task {
let name = hashtagName.lowercased()
let hashtag: Hashtag?
if mastodonController.instanceFeatures.hasMastodonVersion(4, 0, 0) {
let req = Client.getHashtag(name: name)
hashtag = try? await mastodonController.run(req).0
} else {
let req = Client.search(query: "#\(name)", types: [.hashtags])
let results = try? await mastodonController.run(req).0
hashtag = results?.hashtags.first(where: { $0.name.lowercased() == name })
}
self.hashtag = hashtag
return hashtag
}
}
let menu = UIMenu(children: [
// uncached so that the saved/followed updates every time
UIDeferredMenuElement.uncached({ [unowned self] elementHandler in
elementHandler(actionsForHashtag(hashtag, source: .barButtonItem(self.navigationItem.rightBarButtonItem!)))
if let hashtag = self.hashtag {
elementHandler(actionsForHashtag(hashtag, source: .barButtonItem(self.navigationItem.rightBarButtonItem!)))
} else {
Task {
if let hashtag = await fetchHashtag?.value {
elementHandler(actionsForHashtag(hashtag, source: .barButtonItem(self.navigationItem.rightBarButtonItem!)))
} else {
elementHandler([])
}
}
}
})
])
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), menu: menu)
@ -48,17 +82,28 @@ class HashtagTimelineViewController: TimelineViewController {
private func toggleSave() {
let context = mastodonController.persistentContainer.viewContext
if let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)).first {
if let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtagName, account: mastodonController.accountInfo!)).first {
context.delete(existing)
mastodonController.persistentContainer.save(context: context)
} else {
_ = SavedHashtag(hashtag: hashtag, account: mastodonController.accountInfo!, context: context)
Task { @MainActor in
let hashtag: Hashtag?
if let tag = self.hashtag {
hashtag = tag
} else {
hashtag = await fetchHashtag?.value
}
if let hashtag {
_ = SavedHashtag(hashtag: hashtag, account: mastodonController.accountInfo!, context: context)
mastodonController.persistentContainer.save(context: context)
}
}
}
mastodonController.persistentContainer.save(context: context)
}
private func toggleFollow() {
Task {
await ToggleFollowHashtagService(hashtag: hashtag, presenter: self).toggleFollow()
await ToggleFollowHashtagService(hashtagName: hashtagName, presenter: self).toggleFollow()
}
}

View File

@ -133,7 +133,7 @@ extension MenuActionProvider {
let name = hashtag.name.lowercased()
let context = mastodonController.persistentContainer.viewContext
let existing = try? context.fetch(SavedHashtag.fetchRequest(name: name, account: mastodonController.accountInfo!)).first
let saveSubtitle = "Saved hashtags appear in the Explore section of Tusker"
let saveSubtitle = "Shown in the Explore section of Tusker"
let saveImage = UIImage(systemName: existing != nil ? "minus" : "plus")
actionsSection = [
UIAction(title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", subtitle: saveSubtitle, image: saveImage, handler: { (_) in
@ -147,11 +147,11 @@ extension MenuActionProvider {
]
if mastodonController.instanceFeatures.canFollowHashtags {
let existing = mastodonController.followedHashtags.first(where: { $0.name.lowercased() == name })
let subtitle = "Posts tagged with followed hashtags appear in your Home timeline"
let subtitle = "Posts appear in your Home timeline"
let image = UIImage(systemName: existing != nil ? "person.badge.minus" : "person.badge.plus")
actionsSection.append(UIAction(title: existing != nil ? "Unfollow" : "Follow", subtitle: subtitle, image: image) { [unowned self] _ in
Task {
await ToggleFollowHashtagService(hashtag: hashtag, presenter: navigationDelegate!).toggleFollow()
await ToggleFollowHashtagService(hashtagName: hashtag.name, presenter: navigationDelegate!).toggleFollow()
}
})
}