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)
|
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 {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case name
|
case name
|
||||||
case url
|
case url
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F758F29353B4300C0B37F /* FileManager+Size.swift */; };
|
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F758F29353B4300C0B37F /* FileManager+Size.swift */; };
|
||||||
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; };
|
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; };
|
||||||
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.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 */; };
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1529,6 +1531,7 @@
|
||||||
D6F6A54F291F058600F496A8 /* CreateListService.swift */,
|
D6F6A54F291F058600F496A8 /* CreateListService.swift */,
|
||||||
D6F6A551291F098700F496A8 /* RenameListService.swift */,
|
D6F6A551291F098700F496A8 /* RenameListService.swift */,
|
||||||
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */,
|
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */,
|
||||||
|
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */,
|
||||||
);
|
);
|
||||||
path = API;
|
path = API;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1998,6 +2001,7 @@
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
|
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
||||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||||
|
|
|
@ -333,7 +333,7 @@ class MastodonController: ObservableObject {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadFollowedHashtags() async {
|
private func loadFollowedHashtags() async {
|
||||||
followedHashtags = (try? persistentContainer.viewContext.fetch(FollowedHashtag.fetchRequest())) ?? []
|
updateFollowedHashtags()
|
||||||
|
|
||||||
let req = Client.getFollowedHashtags()
|
let req = Client.getFollowedHashtags()
|
||||||
if let (hashtags, _) = try? await run(req) {
|
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 {
|
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
|
let hashtag: Hashtag
|
||||||
|
|
||||||
var toggleSaveButton: UIBarButtonItem!
|
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 {
|
private var isHashtagSaved: Bool {
|
||||||
mastodonController.persistentContainer.viewContext.objectExists(for: SavedHashtag.fetchRequest(name: hashtag.name))
|
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) {
|
init(for hashtag: Hashtag, mastodonController: MastodonController) {
|
||||||
self.hashtag = hashtag
|
self.hashtag = hashtag
|
||||||
|
|
||||||
|
@ -39,19 +36,16 @@ class HashtagTimelineViewController: TimelineViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
toggleSaveButton = UIBarButtonItem(title: toggleSaveButtonTitle, style: .plain, target: self, action: #selector(toggleSaveButtonPressed))
|
let menu = UIMenu(children: [
|
||||||
navigationItem.rightBarButtonItem = toggleSaveButton
|
// uncached so that the saved/followed updates every time
|
||||||
|
UIDeferredMenuElement.uncached({ [unowned self] elementHandler in
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
|
elementHandler(actionsForHashtag(hashtag, source: .barButtonItem(self.navigationItem.rightBarButtonItem!)))
|
||||||
|
})
|
||||||
|
])
|
||||||
|
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), menu: menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func savedHashtagsChanged() {
|
private func toggleSave() {
|
||||||
toggleSaveButton.title = toggleSaveButtonTitle
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Interaction
|
|
||||||
|
|
||||||
@objc func toggleSaveButtonPressed() {
|
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
if let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first {
|
if let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first {
|
||||||
context.delete(existing)
|
context.delete(existing)
|
||||||
|
@ -60,5 +54,11 @@ class HashtagTimelineViewController: TimelineViewController {
|
||||||
}
|
}
|
||||||
mastodonController.persistentContainer.save(context: context)
|
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] {
|
func actionsForHashtag(_ hashtag: Hashtag, source: PopoverSource) -> [UIMenuElement] {
|
||||||
let actionsSection: [UIMenuElement]
|
var actionsSection: [UIMenuElement] = []
|
||||||
if let mastodonController = mastodonController,
|
if let mastodonController = mastodonController,
|
||||||
mastodonController.loggedIn {
|
mastodonController.loggedIn {
|
||||||
|
let name = hashtag.name.lowercased()
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
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 = [
|
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 {
|
if let existing = existing {
|
||||||
context.delete(existing)
|
context.delete(existing)
|
||||||
} else {
|
} else {
|
||||||
|
@ -121,8 +124,16 @@ extension MenuActionProvider {
|
||||||
mastodonController.persistentContainer.save(context: context)
|
mastodonController.persistentContainer.save(context: context)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
} else {
|
if mastodonController.instanceFeatures.canFollowHashtags {
|
||||||
actionsSection = []
|
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]
|
let shareSection: [UIMenuElement]
|
||||||
|
|
Loading…
Reference in New Issue