forked from shadowfacts/Tusker
Add follow/unfollow hashtag actions
This commit is contained in:
parent
ab8e498cee
commit
f1a39c2faa
|
@ -42,6 +42,14 @@ public class Hashtag: Codable {
|
|||
try container.encodeIfPresent(following, forKey: .following)
|
||||
}
|
||||
|
||||
public static func follow(name: String) -> Request<Hashtag> {
|
||||
return Request(method: .post, path: "/api/v1/tags/\(name)/follow")
|
||||
}
|
||||
|
||||
public static func unfollow(name: String) -> Request<Hashtag> {
|
||||
return Request(method: .post, path: "/api/v1/tags/\(name)/unfollow")
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case url
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F758F29353B4300C0B37F /* FileManager+Size.swift */; };
|
||||
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; };
|
||||
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; };
|
||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */; };
|
||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
||||
|
@ -418,6 +419,7 @@
|
|||
D61F758F29353B4300C0B37F /* FileManager+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Size.swift"; sourceTree = "<group>"; };
|
||||
D61F759129365C6C00C0B37F /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = "<group>"; };
|
||||
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.swift; sourceTree = "<group>"; };
|
||||
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleFollowHashtagService.swift; sourceTree = "<group>"; };
|
||||
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
||||
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
||||
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1529,6 +1531,7 @@
|
|||
D6F6A54F291F058600F496A8 /* CreateListService.swift */,
|
||||
D6F6A551291F098700F496A8 /* RenameListService.swift */,
|
||||
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */,
|
||||
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */,
|
||||
);
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1998,6 +2001,7 @@
|
|||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||
|
|
|
@ -333,7 +333,7 @@ class MastodonController: ObservableObject {
|
|||
|
||||
@MainActor
|
||||
private func loadFollowedHashtags() async {
|
||||
followedHashtags = (try? persistentContainer.viewContext.fetch(FollowedHashtag.fetchRequest())) ?? []
|
||||
updateFollowedHashtags()
|
||||
|
||||
let req = Client.getFollowedHashtags()
|
||||
if let (hashtags, _) = try? await run(req) {
|
||||
|
@ -345,6 +345,11 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func updateFollowedHashtags() {
|
||||
followedHashtags = (try? persistentContainer.viewContext.fetch(FollowedHashtag.fetchRequest())) ?? []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct ListComparator: SortComparator {
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// ToggleFollowHashtagService.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 11/29/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
@MainActor
|
||||
class ToggleFollowHashtagService {
|
||||
|
||||
private let hashtag: Hashtag
|
||||
private let mastodonController: MastodonController
|
||||
private let presenter: any TuskerNavigationDelegate
|
||||
|
||||
init(hashtag: Hashtag, presenter: any TuskerNavigationDelegate) {
|
||||
self.hashtag = hashtag
|
||||
self.mastodonController = presenter.apiController
|
||||
self.presenter = presenter
|
||||
}
|
||||
|
||||
func toggleFollow() async {
|
||||
let context = mastodonController.persistentContainer.viewContext
|
||||
var config: ToastConfiguration
|
||||
if let existing = mastodonController.followedHashtags.first(where: { $0.name == hashtag.name }) {
|
||||
do {
|
||||
let req = Hashtag.unfollow(name: hashtag.name)
|
||||
_ = try await mastodonController.run(req)
|
||||
|
||||
context.delete(existing)
|
||||
mastodonController.updateFollowedHashtags()
|
||||
|
||||
config = ToastConfiguration(title: "Unfollowed Hashtag")
|
||||
config.systemImageName = "checkmark"
|
||||
config.dismissAutomaticallyAfter = 2
|
||||
} catch {
|
||||
config = ToastConfiguration(from: error, with: "Error Unfollowing Hashtag", in: presenter) { [unowned self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self.toggleFollow()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
let req = Hashtag.follow(name: hashtag.name)
|
||||
let (hashtag, _) = try await mastodonController.run(req)
|
||||
|
||||
_ = FollowedHashtag(hashtag: hashtag, context: context)
|
||||
mastodonController.updateFollowedHashtags()
|
||||
|
||||
config = ToastConfiguration(title: "Followed Hashtag")
|
||||
config.systemImageName = "checkmark"
|
||||
config.dismissAutomaticallyAfter = 2
|
||||
} catch {
|
||||
config = ToastConfiguration(from: error, with: "Error Following Hashtag", in: presenter) { [unowned self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self.toggleFollow()
|
||||
}
|
||||
}
|
||||
}
|
||||
presenter.showToast(configuration: config, animated: true)
|
||||
mastodonController.persistentContainer.save(context: context)
|
||||
}
|
||||
|
||||
}
|
|
@ -14,18 +14,15 @@ class HashtagTimelineViewController: TimelineViewController {
|
|||
let hashtag: Hashtag
|
||||
|
||||
var toggleSaveButton: UIBarButtonItem!
|
||||
var toggleSaveButtonTitle: String {
|
||||
if isHashtagSaved {
|
||||
return NSLocalizedString("Unsave", comment: "unsave hashtag button")
|
||||
} else {
|
||||
return NSLocalizedString("Save", comment: "save hashtag button")
|
||||
}
|
||||
}
|
||||
|
||||
private var isHashtagSaved: Bool {
|
||||
mastodonController.persistentContainer.viewContext.objectExists(for: SavedHashtag.fetchRequest(name: hashtag.name))
|
||||
}
|
||||
|
||||
private var isHashtagFollowed: Bool {
|
||||
mastodonController.followedHashtags.contains(where: { $0.name == hashtag.name })
|
||||
}
|
||||
|
||||
init(for hashtag: Hashtag, mastodonController: MastodonController) {
|
||||
self.hashtag = hashtag
|
||||
|
||||
|
@ -39,19 +36,16 @@ class HashtagTimelineViewController: TimelineViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
toggleSaveButton = UIBarButtonItem(title: toggleSaveButtonTitle, style: .plain, target: self, action: #selector(toggleSaveButtonPressed))
|
||||
navigationItem.rightBarButtonItem = toggleSaveButton
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
|
||||
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!)))
|
||||
})
|
||||
])
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), menu: menu)
|
||||
}
|
||||
|
||||
@objc func savedHashtagsChanged() {
|
||||
toggleSaveButton.title = toggleSaveButtonTitle
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc func toggleSaveButtonPressed() {
|
||||
private func toggleSave() {
|
||||
let context = mastodonController.persistentContainer.viewContext
|
||||
if let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first {
|
||||
context.delete(existing)
|
||||
|
@ -60,5 +54,11 @@ class HashtagTimelineViewController: TimelineViewController {
|
|||
}
|
||||
mastodonController.persistentContainer.save(context: context)
|
||||
}
|
||||
|
||||
private func toggleFollow() {
|
||||
Task {
|
||||
await ToggleFollowHashtagService(hashtag: hashtag, presenter: self).toggleFollow()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -106,13 +106,16 @@ extension MenuActionProvider {
|
|||
}
|
||||
|
||||
func actionsForHashtag(_ hashtag: Hashtag, source: PopoverSource) -> [UIMenuElement] {
|
||||
let actionsSection: [UIMenuElement]
|
||||
var actionsSection: [UIMenuElement] = []
|
||||
if let mastodonController = mastodonController,
|
||||
mastodonController.loggedIn {
|
||||
let name = hashtag.name.lowercased()
|
||||
let context = mastodonController.persistentContainer.viewContext
|
||||
let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first
|
||||
let existing = try? context.fetch(SavedHashtag.fetchRequest(name: name)).first
|
||||
let saveSubtitle = "Saved hashtags appear in the Explore section of Tusker"
|
||||
let saveImage = UIImage(systemName: existing != nil ? "minus" : "plus")
|
||||
actionsSection = [
|
||||
createAction(identifier: "save", title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", systemImageName: "number", handler: { (_) in
|
||||
UIAction(title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", subtitle: saveSubtitle, image: saveImage, handler: { (_) in
|
||||
if let existing = existing {
|
||||
context.delete(existing)
|
||||
} else {
|
||||
|
@ -121,8 +124,16 @@ extension MenuActionProvider {
|
|||
mastodonController.persistentContainer.save(context: context)
|
||||
})
|
||||
]
|
||||
} else {
|
||||
actionsSection = []
|
||||
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 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let shareSection: [UIMenuElement]
|
||||
|
|
Loading…
Reference in New Issue