Store followed hashtags
The followed hashtags may not load until after the timeline request completes, and we want to be able to show the hashtag indicator (or at least make a best effort attempt) immediately.
This commit is contained in:
parent
80f9800fd6
commit
97d5b955a0
|
@ -261,6 +261,10 @@ public class Client {
|
|||
return Request<Account>(method: .post, path: "/api/v1/follows", body: ParametersBody(["uri" => acct]))
|
||||
}
|
||||
|
||||
public static func getFollowedHashtags() -> Request<[Hashtag]> {
|
||||
return Request(method: .get, path: "/api/v1/followed_tags")
|
||||
}
|
||||
|
||||
// MARK: - Lists
|
||||
public static func getLists() -> Request<[List]> {
|
||||
return Request<[List]>(method: .get, path: "/api/v1/lists")
|
||||
|
|
|
@ -15,11 +15,14 @@ public class Hashtag: Codable {
|
|||
public let url: WebURL
|
||||
/// Only present when returned from the trending hashtags endpoint
|
||||
public let history: [History]?
|
||||
/// Only present on Mastodon >= 4 and when logged in
|
||||
public let following: Bool?
|
||||
|
||||
public init(name: String, url: URL) {
|
||||
self.name = name
|
||||
self.url = WebURL(url)!
|
||||
self.history = nil
|
||||
self.following = nil
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
|
@ -28,6 +31,7 @@ public class Hashtag: Codable {
|
|||
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
|
||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
||||
self.history = try container.decodeIfPresent([History].self, forKey: .history)
|
||||
self.following = try container.decodeIfPresent(Bool.self, forKey: .following)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
@ -35,12 +39,14 @@ public class Hashtag: Codable {
|
|||
try container.encode(name, forKey: .name)
|
||||
try container.encode(url, forKey: .url)
|
||||
try container.encodeIfPresent(history, forKey: .history)
|
||||
try container.encodeIfPresent(following, forKey: .following)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case url
|
||||
case history
|
||||
case following
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
D61F758E2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
|
@ -416,6 +417,7 @@
|
|||
D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusUpdatedNotificationTableViewCell.xib; 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>"; };
|
||||
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.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>"; };
|
||||
|
@ -881,6 +883,7 @@
|
|||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */,
|
||||
D6B9366E2828452F00237D0E /* SavedHashtag.swift */,
|
||||
D6B9366C2828444F00237D0E /* SavedInstance.swift */,
|
||||
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */,
|
||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
||||
);
|
||||
|
@ -1824,6 +1827,7 @@
|
|||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
||||
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */,
|
||||
D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */,
|
||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
||||
|
|
|
@ -84,6 +84,14 @@ struct InstanceFeatures {
|
|||
}
|
||||
}
|
||||
|
||||
var canFollowHashtags: Bool {
|
||||
if case .mastodon(_, .some(let version)) = instanceType {
|
||||
return version >= Version(4, 0, 0)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
|
||||
let ver = instance.version.lowercased()
|
||||
if ver.contains("glitch") {
|
||||
|
|
|
@ -49,6 +49,7 @@ class MastodonController: ObservableObject {
|
|||
@Published private(set) var instanceFeatures = InstanceFeatures()
|
||||
@Published private(set) var lists: [List] = []
|
||||
@Published private(set) var customEmojis: [Emoji]?
|
||||
@Published private(set) var followedHashtags: [FollowedHashtag] = []
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
@ -78,6 +79,15 @@ class MastodonController: ObservableObject {
|
|||
self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
$instanceFeatures
|
||||
.filter { [unowned self] in $0.canFollowHashtags && self.followedHashtags.isEmpty }
|
||||
.sink { [unowned self] _ in
|
||||
Task {
|
||||
await self.loadFollowedHashtags()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
@ -321,6 +331,20 @@ class MastodonController: ObservableObject {
|
|||
self.lists = new
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadFollowedHashtags() async {
|
||||
followedHashtags = (try? persistentContainer.viewContext.fetch(FollowedHashtag.fetchRequest())) ?? []
|
||||
|
||||
let req = Client.getFollowedHashtags()
|
||||
if let (hashtags, _) = try? await run(req) {
|
||||
self.persistentContainer.updateFollowedHashtags(hashtags) {
|
||||
if case .success(let hashtags) = $0 {
|
||||
self.followedHashtags = hashtags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct ListComparator: SortComparator {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// FollowedHashtag.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 11/29/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import Pachyderm
|
||||
import WebURLFoundationExtras
|
||||
|
||||
@objc(FollowedHashtag)
|
||||
public final class FollowedHashtag: NSManagedObject {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<FollowedHashtag> {
|
||||
return NSFetchRequest<FollowedHashtag>(entityName: "FollowedHashtag")
|
||||
}
|
||||
|
||||
@nonobjc public class func fetchRequest(name: String) -> NSFetchRequest<FollowedHashtag> {
|
||||
let req = NSFetchRequest<FollowedHashtag>(entityName: "FollowedHashtag")
|
||||
req.predicate = NSPredicate(format: "name = %@", name)
|
||||
return req
|
||||
}
|
||||
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var url: URL
|
||||
|
||||
}
|
||||
|
||||
extension FollowedHashtag {
|
||||
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
||||
self.init(context: context)
|
||||
self.name = hashtag.name
|
||||
self.url = URL(hashtag.url)!
|
||||
}
|
||||
}
|
|
@ -267,6 +267,38 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
}
|
||||
}
|
||||
|
||||
func updateFollowedHashtags(_ hashtags: [Hashtag], completion: @escaping (Result<[FollowedHashtag], Error>) -> Void) {
|
||||
viewContext.perform {
|
||||
do {
|
||||
var all = try self.viewContext.fetch(FollowedHashtag.fetchRequest())
|
||||
|
||||
let toDelete = all.filter { existing in !hashtags.contains(where: { $0.name == existing.name }) }.map(\.objectID)
|
||||
if !toDelete.isEmpty {
|
||||
try self.viewContext.execute(NSBatchDeleteRequest(objectIDs: toDelete))
|
||||
}
|
||||
|
||||
for hashtag in hashtags where !all.contains(where: { $0.name == hashtag.name}) {
|
||||
let mo = FollowedHashtag(hashtag: hashtag, context: self.viewContext)
|
||||
all.append(mo)
|
||||
}
|
||||
|
||||
self.save(context: self.viewContext)
|
||||
completion(.success(all))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasFollowedHashtag(_ hashtag: Hashtag) -> Bool {
|
||||
do {
|
||||
let req = FollowedHashtag.fetchRequest(name: name)
|
||||
return try viewContext.count(for: req) > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
||||
let changes = hasChangedSavedHashtagsOrInstances(notification)
|
||||
if changes.hashtags {
|
||||
|
|
|
@ -28,6 +28,10 @@
|
|||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="FollowedHashtag" representedClassName="FollowedHashtag" syncable="YES">
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="url" attributeType="URI"/>
|
||||
</entity>
|
||||
<entity name="Relationship" representedClassName="RelationshipMO" syncable="YES">
|
||||
<attribute name="accountID" optional="YES" attributeType="String"/>
|
||||
<attribute name="blocking" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
|
|
Loading…
Reference in New Issue