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 {
|
extension Client {
|
||||||
|
|
|
@ -12,12 +12,12 @@ import Pachyderm
|
||||||
@MainActor
|
@MainActor
|
||||||
class ToggleFollowHashtagService {
|
class ToggleFollowHashtagService {
|
||||||
|
|
||||||
private let hashtag: Hashtag
|
private let hashtagName: String
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
private let presenter: any TuskerNavigationDelegate
|
private let presenter: any TuskerNavigationDelegate
|
||||||
|
|
||||||
init(hashtag: Hashtag, presenter: any TuskerNavigationDelegate) {
|
init(hashtagName: String, presenter: any TuskerNavigationDelegate) {
|
||||||
self.hashtag = hashtag
|
self.hashtagName = hashtagName
|
||||||
self.mastodonController = presenter.apiController
|
self.mastodonController = presenter.apiController
|
||||||
self.presenter = presenter
|
self.presenter = presenter
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,9 @@ class ToggleFollowHashtagService {
|
||||||
func toggleFollow() async {
|
func toggleFollow() async {
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
var config: ToastConfiguration
|
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 {
|
do {
|
||||||
let req = Hashtag.unfollow(name: hashtag.name)
|
let req = Hashtag.unfollow(name: hashtagName)
|
||||||
_ = try await mastodonController.run(req)
|
_ = try await mastodonController.run(req)
|
||||||
|
|
||||||
context.delete(existing)
|
context.delete(existing)
|
||||||
|
@ -44,7 +44,7 @@ class ToggleFollowHashtagService {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
do {
|
do {
|
||||||
let req = Hashtag.follow(name: hashtag.name)
|
let req = Hashtag.follow(name: hashtagName)
|
||||||
let (hashtag, _) = try await mastodonController.run(req)
|
let (hashtag, _) = try await mastodonController.run(req)
|
||||||
|
|
||||||
_ = FollowedHashtag(hashtag: hashtag, context: context)
|
_ = FollowedHashtag(hashtag: hashtag, context: context)
|
||||||
|
|
|
@ -106,7 +106,9 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
|
||||||
|
|
||||||
private func timelineViewController(for timeline: Timeline, mastodonController: MastodonController) -> UIViewController {
|
private func timelineViewController(for timeline: Timeline, mastodonController: MastodonController) -> UIViewController {
|
||||||
switch timeline {
|
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):
|
case .list(id: let id):
|
||||||
let req = ListMO.fetchRequest(id: id)
|
let req = ListMO.fetchRequest(id: id)
|
||||||
if let list = try? mastodonController.persistentContainer.viewContext.fetch(req).first {
|
if let list = try? mastodonController.persistentContainer.viewContext.fetch(req).first {
|
||||||
|
|
|
@ -127,6 +127,11 @@ class MainSidebarViewController: UIViewController {
|
||||||
itemLastSelectedTimestamps[item] = Date()
|
itemLastSelectedTimestamps[item] = Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasItem(_ item: Item) -> Bool {
|
||||||
|
loadViewIfNeeded()
|
||||||
|
return dataSource.snapshot().itemIdentifiers.contains(item)
|
||||||
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
let listCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
let listCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
||||||
var config = cell.defaultContentConfiguration()
|
var config = cell.defaultContentConfiguration()
|
||||||
|
@ -212,10 +217,10 @@ class MainSidebarViewController: UIViewController {
|
||||||
let req = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
|
let req = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
|
||||||
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(req)) ?? []
|
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(req)) ?? []
|
||||||
var items = saved.map {
|
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 }) {
|
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 = items.uniques()
|
||||||
items.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
items.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
||||||
|
@ -315,8 +320,8 @@ class MainSidebarViewController: UIViewController {
|
||||||
return UserActivityManager.myProfileActivity(accountID: id)
|
return UserActivityManager.myProfileActivity(accountID: id)
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
return UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: id)
|
return UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: id)
|
||||||
case let .savedHashtag(tag):
|
case let .savedHashtag(name):
|
||||||
return UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: tag.name), accountID: id)
|
return UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: name), accountID: id)
|
||||||
case .savedInstance(_):
|
case .savedInstance(_):
|
||||||
// todo: show timeline activity doesn't work for public timelines
|
// todo: show timeline activity doesn't work for public timelines
|
||||||
return nil
|
return nil
|
||||||
|
@ -347,7 +352,7 @@ extension MainSidebarViewController {
|
||||||
case tab(MainTabBarViewController.Tab)
|
case tab(MainTabBarViewController.Tab)
|
||||||
case explore, bookmarks, favorites
|
case explore, bookmarks, favorites
|
||||||
case listsHeader, list(List), addList
|
case listsHeader, list(List), addList
|
||||||
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
|
case savedHashtagsHeader, savedHashtag(String), addSavedHashtag
|
||||||
case savedInstancesHeader, savedInstance(URL), addSavedInstance
|
case savedInstancesHeader, savedInstance(URL), addSavedInstance
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
|
@ -368,8 +373,8 @@ extension MainSidebarViewController {
|
||||||
return "New List..."
|
return "New List..."
|
||||||
case .savedHashtagsHeader:
|
case .savedHashtagsHeader:
|
||||||
return "Hashtags"
|
return "Hashtags"
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(name):
|
||||||
return hashtag.name
|
return name
|
||||||
case .addSavedHashtag:
|
case .addSavedHashtag:
|
||||||
return "Add Hashtag..."
|
return "Add Hashtag..."
|
||||||
case .savedInstancesHeader:
|
case .savedInstancesHeader:
|
||||||
|
|
|
@ -339,8 +339,8 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
exploreItem = .favorites
|
exploreItem = .favorites
|
||||||
case let listVC as ListTimelineViewController:
|
case let listVC as ListTimelineViewController:
|
||||||
exploreItem = .list(listVC.list)
|
exploreItem = .list(listVC.list)
|
||||||
case let hashtagVC as HashtagTimelineViewController where hashtagVC.isHashtagSaved:
|
case let hashtagVC as HashtagTimelineViewController where sidebar.hasItem(.savedHashtag(hashtagVC.hashtagName)):
|
||||||
exploreItem = .savedHashtag(hashtagVC.hashtag)
|
exploreItem = .savedHashtag(hashtagVC.hashtagName)
|
||||||
case let instanceVC as InstanceTimelineViewController:
|
case let instanceVC as InstanceTimelineViewController:
|
||||||
exploreItem = .savedInstance(instanceVC.instanceURL)
|
exploreItem = .savedInstance(instanceVC.instanceURL)
|
||||||
case is TrendsViewController:
|
case is TrendsViewController:
|
||||||
|
@ -428,8 +428,8 @@ fileprivate extension MainSidebarViewController.Item {
|
||||||
return FavoritesViewController(mastodonController: mastodonController)
|
return FavoritesViewController(mastodonController: mastodonController)
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
return ListTimelineViewController(for: list, mastodonController: mastodonController)
|
return ListTimelineViewController(for: list, mastodonController: mastodonController)
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(name):
|
||||||
return HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController)
|
return HashtagTimelineViewController(forNamed: name, mastodonController: mastodonController)
|
||||||
case let .savedInstance(url):
|
case let .savedInstance(url):
|
||||||
return InstanceTimelineViewController(for: url, parentMastodonController: mastodonController)
|
return InstanceTimelineViewController(for: url, parentMastodonController: mastodonController)
|
||||||
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
||||||
|
|
|
@ -11,23 +11,30 @@ import Pachyderm
|
||||||
|
|
||||||
class HashtagTimelineViewController: TimelineViewController {
|
class HashtagTimelineViewController: TimelineViewController {
|
||||||
|
|
||||||
let hashtag: Hashtag
|
let hashtagName: String
|
||||||
|
private var hashtag: Hashtag?
|
||||||
|
private var fetchHashtag: Task<Hashtag?, Never>?
|
||||||
|
|
||||||
var toggleSaveButton: UIBarButtonItem!
|
var toggleSaveButton: UIBarButtonItem!
|
||||||
|
|
||||||
var isHashtagSaved: Bool {
|
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)
|
return mastodonController.persistentContainer.viewContext.objectExists(for: req)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isHashtagFollowed: Bool {
|
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
|
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) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
@ -36,11 +43,38 @@ class HashtagTimelineViewController: TimelineViewController {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.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: [
|
let menu = UIMenu(children: [
|
||||||
// uncached so that the saved/followed updates every time
|
// uncached so that the saved/followed updates every time
|
||||||
UIDeferredMenuElement.uncached({ [unowned self] elementHandler in
|
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)
|
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), menu: menu)
|
||||||
|
@ -48,17 +82,28 @@ class HashtagTimelineViewController: TimelineViewController {
|
||||||
|
|
||||||
private func toggleSave() {
|
private func toggleSave() {
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
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)
|
context.delete(existing)
|
||||||
|
mastodonController.persistentContainer.save(context: context)
|
||||||
} else {
|
} 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() {
|
private func toggleFollow() {
|
||||||
Task {
|
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 name = hashtag.name.lowercased()
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
let existing = try? context.fetch(SavedHashtag.fetchRequest(name: name, account: mastodonController.accountInfo!)).first
|
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")
|
let saveImage = UIImage(systemName: existing != nil ? "minus" : "plus")
|
||||||
actionsSection = [
|
actionsSection = [
|
||||||
UIAction(title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", subtitle: saveSubtitle, image: saveImage, handler: { (_) in
|
UIAction(title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", subtitle: saveSubtitle, image: saveImage, handler: { (_) in
|
||||||
|
@ -147,11 +147,11 @@ extension MenuActionProvider {
|
||||||
]
|
]
|
||||||
if mastodonController.instanceFeatures.canFollowHashtags {
|
if mastodonController.instanceFeatures.canFollowHashtags {
|
||||||
let existing = mastodonController.followedHashtags.first(where: { $0.name.lowercased() == name })
|
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")
|
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
|
actionsSection.append(UIAction(title: existing != nil ? "Unfollow" : "Follow", subtitle: subtitle, image: image) { [unowned self] _ in
|
||||||
Task {
|
Task {
|
||||||
await ToggleFollowHashtagService(hashtag: hashtag, presenter: navigationDelegate!).toggleFollow()
|
await ToggleFollowHashtagService(hashtagName: hashtag.name, presenter: navigationDelegate!).toggleFollow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue