Compare commits
2 Commits
dcd1b4ad94
...
ca03cf3b08
Author | SHA1 | Date |
---|---|---|
Shadowfacts | ca03cf3b08 | |
Shadowfacts | f0e530722f |
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
init(forNamed name: String, mastodonController: MastodonController) {
|
||||
self.hashtagName = name
|
||||
|
||||
super.init(for: .tag(hashtag: hashtag.name), mastodonController: mastodonController)
|
||||
super.init(for: .tag(hashtag: hashtagName), mastodonController: mastodonController)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
@ -36,11 +43,38 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue