forked from shadowfacts/Tusker
Move saved instances and hashtags to CoreData
This commit is contained in:
parent
ed0643c4ad
commit
d3187ce2c4
|
@ -237,6 +237,9 @@
|
|||
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
||||
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */; };
|
||||
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366A281EE77E00237D0E /* PollVoteButton.swift */; };
|
||||
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366C2828444F00237D0E /* SavedInstance.swift */; };
|
||||
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366E2828452F00237D0E /* SavedHashtag.swift */; };
|
||||
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */; };
|
||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */; };
|
||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */; };
|
||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */; };
|
||||
|
@ -579,6 +582,9 @@
|
|||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
||||
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarMyProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D6B9366A281EE77E00237D0E /* PollVoteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteButton.swift; sourceTree = "<group>"; };
|
||||
D6B9366C2828444F00237D0E /* SavedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstance.swift; sourceTree = "<group>"; };
|
||||
D6B9366E2828452F00237D0E /* SavedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtag.swift; sourceTree = "<group>"; };
|
||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Helpers.swift"; sourceTree = "<group>"; };
|
||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = "<group>"; };
|
||||
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPageViewController.swift; sourceTree = "<group>"; };
|
||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = "<group>"; };
|
||||
|
@ -847,7 +853,10 @@
|
|||
D60E2F232442372B005F8713 /* StatusMO.swift */,
|
||||
D60E2F252442372B005F8713 /* AccountMO.swift */,
|
||||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */,
|
||||
D6B9366E2828452F00237D0E /* SavedHashtag.swift */,
|
||||
D6B9366C2828444F00237D0E /* SavedInstance.swift */,
|
||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
||||
);
|
||||
path = CoreData;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1793,6 +1802,8 @@
|
|||
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */,
|
||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */,
|
||||
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
||||
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */,
|
||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
|
||||
|
@ -1847,6 +1858,7 @@
|
|||
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
|
||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
|
||||
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
import CrashReporter
|
||||
import CoreData
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
@ -26,6 +27,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
AudioSessionHelper.disable()
|
||||
AudioSessionHelper.setDefault()
|
||||
}
|
||||
|
||||
if let oldSavedData = SavedDataManager.load() {
|
||||
do {
|
||||
for account in oldSavedData.accountIDs {
|
||||
guard let account = LocalData.shared.getAccount(id: account) else {
|
||||
continue
|
||||
}
|
||||
let controller = MastodonController.getForAccount(account)
|
||||
try oldSavedData.migrateToCoreData(accountID: account.id, context: controller.persistentContainer.viewContext)
|
||||
if controller.persistentContainer.viewContext.hasChanges {
|
||||
try controller.persistentContainer.viewContext.save()
|
||||
}
|
||||
}
|
||||
try SavedDataManager.destroy()
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -50,6 +50,8 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
fatalError("Unable to load persistent store: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: viewContext)
|
||||
}
|
||||
|
||||
func status(for id: String, in context: NSManagedObjectContext? = nil) -> StatusMO? {
|
||||
|
@ -215,4 +217,43 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
||||
let changes = hasChangedSavedHashtagsOrInstances(notification)
|
||||
if changes.hashtags {
|
||||
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
|
||||
}
|
||||
if changes.instances {
|
||||
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func hasChangedSavedHashtagsOrInstances(_ notification: Foundation.Notification) -> (hashtags: Bool, instances: Bool) {
|
||||
var changes: (hashtags: Bool, instances: Bool) = (false, false)
|
||||
if let inserted = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> {
|
||||
for object in inserted {
|
||||
if object is SavedHashtag {
|
||||
changes.hashtags = true
|
||||
} else if object is SavedInstance {
|
||||
changes.instances = true
|
||||
}
|
||||
if changes.hashtags && changes.instances {
|
||||
return changes
|
||||
}
|
||||
}
|
||||
}
|
||||
if let deleted = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> {
|
||||
for object in deleted {
|
||||
if object is SavedHashtag {
|
||||
changes.hashtags = true
|
||||
} else if object is SavedInstance {
|
||||
changes.instances = true
|
||||
}
|
||||
if changes.hashtags && changes.instances {
|
||||
return changes
|
||||
}
|
||||
}
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// NSManagedObjectContext+Helpers.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/9/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
extension NSManagedObjectContext {
|
||||
func objectExists<T: NSFetchRequestResult>(for request: NSFetchRequest<T>) -> Bool {
|
||||
switch try? count(for: request) {
|
||||
case nil, 0, NSNotFound:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// SavedHashtag.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/8/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import Pachyderm
|
||||
|
||||
@objc(SavedHashtag)
|
||||
public final class SavedHashtag: NSManagedObject {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<SavedHashtag> {
|
||||
return NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
|
||||
}
|
||||
|
||||
@nonobjc public class func fetchRequest(name: String) -> NSFetchRequest<SavedHashtag> {
|
||||
let req = NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
|
||||
req.predicate = NSPredicate(format: "name = %@", name)
|
||||
return req
|
||||
}
|
||||
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var url: URL
|
||||
|
||||
}
|
||||
|
||||
extension SavedHashtag {
|
||||
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
||||
self.init(context: context)
|
||||
self.name = hashtag.name
|
||||
self.url = hashtag.url
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// SavedInstance.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/8/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(SavedInstance)
|
||||
public final class SavedInstance: NSManagedObject {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<SavedInstance> {
|
||||
return NSFetchRequest<SavedInstance>(entityName: "SavedInstance")
|
||||
}
|
||||
|
||||
@nonobjc public class func fetchRequest(url: URL) -> NSFetchRequest<SavedInstance> {
|
||||
let req = fetchRequest()
|
||||
req.predicate = NSPredicate(format: "url = %@", url as NSURL)
|
||||
return req
|
||||
}
|
||||
|
||||
@NSManaged public var url: URL
|
||||
|
||||
}
|
||||
|
||||
extension SavedInstance {
|
||||
convenience init(url: URL, context: NSManagedObjectContext) {
|
||||
self.init(context: context)
|
||||
self.url = url
|
||||
}
|
||||
}
|
|
@ -40,6 +40,23 @@
|
|||
<attribute name="showingReblogs" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="relationship" inverseEntity="Account"/>
|
||||
</entity>
|
||||
<entity name="SavedHashtag" representedClassName="SavedHashtag" syncable="YES">
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="url" attributeType="URI"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="name"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="SavedInstance" representedClassName="SavedInstance" syncable="YES">
|
||||
<attribute name="url" attributeType="URI"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="url"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Status" representedClassName="StatusMO" syncable="YES">
|
||||
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
||||
<attribute name="attachmentsData" attributeType="Binary"/>
|
||||
|
@ -77,7 +94,9 @@
|
|||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="343"/>
|
||||
<element name="Relationship" positionX="63" positionY="135" width="128" height="208"/>
|
||||
<element name="Relationship" positionX="63" positionY="135" width="128" height="194"/>
|
||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="449"/>
|
||||
<element name="SavedInstance" positionX="63" positionY="144" width="128" height="44"/>
|
||||
<element name="SavedHashtag" positionX="72" positionY="153" width="128" height="59"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -8,101 +8,68 @@
|
|||
|
||||
import Foundation
|
||||
import Pachyderm
|
||||
import CoreData
|
||||
|
||||
class SavedDataManager: Codable {
|
||||
private(set) static var shared: SavedDataManager = load()
|
||||
|
||||
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist")
|
||||
|
||||
static func save() {
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
let encoder = PropertyListEncoder()
|
||||
let data = try? encoder.encode(shared)
|
||||
try? data?.write(to: archiveURL, options: .noFileProtection)
|
||||
}
|
||||
}
|
||||
|
||||
static func load() -> SavedDataManager {
|
||||
static func load() -> SavedDataManager? {
|
||||
let decoder = PropertyListDecoder()
|
||||
if let data = try? Data(contentsOf: archiveURL),
|
||||
let savedHashtagsManager = try? decoder.decode(Self.self, from: data) {
|
||||
return savedHashtagsManager
|
||||
}
|
||||
return SavedDataManager()
|
||||
return nil
|
||||
}
|
||||
|
||||
static func destroy() throws {
|
||||
try FileManager.default.removeItem(at: archiveURL)
|
||||
}
|
||||
|
||||
private init() {}
|
||||
|
||||
private var savedHashtags: [String: [Hashtag]] = [:] {
|
||||
didSet {
|
||||
SavedDataManager.save()
|
||||
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
|
||||
private(set) var savedHashtags: [String: [Hashtag]] = [:]
|
||||
private(set) var savedInstances: [String: [URL]] = [:]
|
||||
|
||||
var accountIDs: Set<String> {
|
||||
var s = Set<String>()
|
||||
savedHashtags.keys.forEach { s.insert($0) }
|
||||
savedInstances.keys.forEach { s.insert($0) }
|
||||
return s
|
||||
}
|
||||
|
||||
private func save() {
|
||||
let encoder = PropertyListEncoder()
|
||||
let data = try? encoder.encode(self)
|
||||
try? data?.write(to: SavedDataManager.archiveURL, options: .noFileProtection)
|
||||
}
|
||||
|
||||
func migrateToCoreData(accountID: String, context: NSManagedObjectContext) throws {
|
||||
var changed = false
|
||||
|
||||
if let hashtags = savedHashtags[accountID] {
|
||||
let objects = hashtags.map {
|
||||
["url": $0.url, "name": $0.name]
|
||||
}
|
||||
let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects)
|
||||
try context.execute(hashtagsReq)
|
||||
savedHashtags.removeValue(forKey: accountID)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
private var savedInstances: [String: [URL]] = [:] {
|
||||
didSet {
|
||||
SavedDataManager.save()
|
||||
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
|
||||
|
||||
if let instances = savedInstances[accountID] {
|
||||
let objects = instances.map {
|
||||
["url": $0]
|
||||
}
|
||||
let instancesReq = NSBatchInsertRequest(entity: SavedInstance.entity(), objects: objects)
|
||||
try context.execute(instancesReq)
|
||||
savedInstances.removeValue(forKey: accountID)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
func sortedHashtags(for account: LocalData.UserAccountInfo) -> [Hashtag] {
|
||||
if let hashtags = savedHashtags[account.id] {
|
||||
return hashtags.sorted(by: { $0.name < $1.name })
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func isSaved(hashtag: Hashtag, for account: LocalData.UserAccountInfo) -> Bool {
|
||||
return savedHashtags[account.id]?.contains(hashtag) ?? false
|
||||
}
|
||||
|
||||
func add(hashtag: Hashtag, for account: LocalData.UserAccountInfo) {
|
||||
if isSaved(hashtag: hashtag, for: account) {
|
||||
return
|
||||
}
|
||||
if var saved = savedHashtags[account.id] {
|
||||
saved.append(hashtag)
|
||||
savedHashtags[account.id] = saved
|
||||
} else {
|
||||
savedHashtags[account.id] = [hashtag]
|
||||
}
|
||||
}
|
||||
|
||||
func remove(hashtag: Hashtag, for account: LocalData.UserAccountInfo) {
|
||||
guard isSaved(hashtag: hashtag, for: account) else { return }
|
||||
if var saved = savedHashtags[account.id] {
|
||||
saved.removeAll(where: { $0.name == hashtag.name })
|
||||
savedHashtags[account.id] = saved
|
||||
}
|
||||
}
|
||||
|
||||
func savedInstances(for account: LocalData.UserAccountInfo) -> [URL] {
|
||||
return savedInstances[account.id] ?? []
|
||||
}
|
||||
|
||||
func isSaved(instance url: URL, for account: LocalData.UserAccountInfo) -> Bool {
|
||||
return savedInstances[account.id]?.contains(url) ?? false
|
||||
}
|
||||
|
||||
func add(instance url: URL, for account: LocalData.UserAccountInfo) {
|
||||
if isSaved(instance: url, for: account) { return }
|
||||
if var saved = savedInstances[account.id] {
|
||||
saved.append(url)
|
||||
savedInstances[account.id] = saved
|
||||
} else {
|
||||
savedInstances[account.id] = [url]
|
||||
}
|
||||
}
|
||||
|
||||
func remove(instance url: URL, for account: LocalData.UserAccountInfo) {
|
||||
guard isSaved(instance: url, for: account) else { return }
|
||||
if var saved = savedInstances[account.id] {
|
||||
saved.removeAll(where: { $0 == url })
|
||||
savedInstances[account.id] = saved
|
||||
|
||||
if changed {
|
||||
save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -376,7 +376,8 @@ struct ComposeAutocompleteHashtagsView: View {
|
|||
}
|
||||
|
||||
private func updateHashtags(searchResults: [Hashtag], trendingTags: [Hashtag], query: String) {
|
||||
let savedTags = SavedDataManager.shared.sortedHashtags(for: mastodonController.accountInfo!)
|
||||
let savedTags = ((try? mastodonController.persistentContainer.viewContext.fetch(SavedHashtag.fetchRequest())) ?? [])
|
||||
.map { Hashtag(name: $0.name, url: $0.url) }
|
||||
|
||||
hashtags = (searchResults + savedTags + trendingTags)
|
||||
.map { (tag) -> (Hashtag, (matched: Bool, score: Int)) in
|
||||
|
|
|
@ -86,7 +86,9 @@ class AddSavedHashtagViewController: EnhancedTableViewController {
|
|||
}
|
||||
|
||||
private func selectHashtag(_ hashtag: Hashtag) {
|
||||
SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!)
|
||||
let context = mastodonController.persistentContainer.viewContext
|
||||
_ = SavedHashtag(hashtag: hashtag, context: context)
|
||||
try! context.save()
|
||||
presentingViewController!.dismiss(animated: true)
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import UIKit
|
||||
import Combine
|
||||
import Pachyderm
|
||||
import CoreData
|
||||
|
||||
class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||
|
||||
|
@ -134,8 +135,6 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
|||
}
|
||||
|
||||
private func applyInitialSnapshot() {
|
||||
let account = mastodonController.accountInfo!
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections(Section.allCases.filter { $0 != .discover })
|
||||
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
|
||||
|
@ -144,9 +143,15 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
|||
addDiscoverSection(to: &snapshot)
|
||||
}
|
||||
snapshot.appendItems([.addList], toSection: .lists)
|
||||
snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) }, toSection: .savedHashtags)
|
||||
let hashtags = fetchSavedHashtags().map {
|
||||
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
|
||||
}
|
||||
snapshot.appendItems(hashtags, toSection: .savedHashtags)
|
||||
snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
|
||||
snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) }, toSection: .savedInstances)
|
||||
let instances = fetchSavedInstances().map {
|
||||
Item.savedInstance($0.url)
|
||||
}
|
||||
snapshot.appendItems(instances, toSection: .savedInstances)
|
||||
snapshot.appendItems([.findInstance], toSection: .savedInstances)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
|
@ -190,20 +195,46 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func fetchSavedHashtags() -> [SavedHashtag] {
|
||||
let req = SavedHashtag.fetchRequest()
|
||||
req.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
|
||||
do {
|
||||
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func fetchSavedInstances() -> [SavedInstance] {
|
||||
let req = SavedInstance.fetchRequest()
|
||||
req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
|
||||
do {
|
||||
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func savedHashtagsChanged() {
|
||||
let account = mastodonController.accountInfo!
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
|
||||
snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) }, toSection: .savedHashtags)
|
||||
let hashtags = fetchSavedHashtags().map {
|
||||
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
|
||||
}
|
||||
snapshot.appendItems(hashtags, toSection: .savedHashtags)
|
||||
snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
@objc private func savedInstancesChanged() {
|
||||
let account = mastodonController.accountInfo!
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances))
|
||||
snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) }, toSection: .savedInstances)
|
||||
let instances = fetchSavedInstances().map {
|
||||
Item.savedInstance($0.url)
|
||||
}
|
||||
snapshot.appendItems(instances, toSection: .savedInstances)
|
||||
snapshot.appendItems([.findInstance], toSection: .savedInstances)
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
@ -249,13 +280,19 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
|||
}
|
||||
|
||||
func removeSavedHashtag(_ hashtag: Hashtag) {
|
||||
let account = mastodonController.accountInfo!
|
||||
SavedDataManager.shared.remove(hashtag: hashtag, for: account)
|
||||
let context = mastodonController.persistentContainer.viewContext
|
||||
if let hashtag = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first {
|
||||
context.delete(hashtag)
|
||||
try! context.save()
|
||||
}
|
||||
}
|
||||
|
||||
func removeSavedInstance(_ instanceURL: URL) {
|
||||
let account = mastodonController.accountInfo!
|
||||
SavedDataManager.shared.remove(instance: instanceURL, for: account)
|
||||
let context = mastodonController.persistentContainer.viewContext
|
||||
if let instance = try? context.fetch(SavedInstance.fetchRequest(url: instanceURL)).first {
|
||||
context.delete(instance)
|
||||
try! context.save()
|
||||
}
|
||||
}
|
||||
|
||||
private func trailingSwipeActionsForCell(at indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
|
|
@ -218,14 +218,38 @@ class MainSidebarViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func fetchSavedHashtags() -> [SavedHashtag] {
|
||||
let req = SavedHashtag.fetchRequest()
|
||||
req.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
|
||||
do {
|
||||
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func fetchSavedInstances() -> [SavedInstance] {
|
||||
let req = SavedInstance.fetchRequest()
|
||||
req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
|
||||
do {
|
||||
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func reloadSavedHashtags() {
|
||||
let selected = collectionView.indexPathsForSelectedItems?.first
|
||||
|
||||
var hashtagsSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||
hashtagsSnapshot.append([.savedHashtagsHeader])
|
||||
hashtagsSnapshot.expand([.savedHashtagsHeader])
|
||||
let sortedHashtags = SavedDataManager.shared.sortedHashtags(for: mastodonController.accountInfo!)
|
||||
hashtagsSnapshot.append(sortedHashtags.map { .savedHashtag($0) }, to: .savedHashtagsHeader)
|
||||
let hashtags = fetchSavedHashtags().map {
|
||||
Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
|
||||
}
|
||||
hashtagsSnapshot.append(hashtags, to: .savedHashtagsHeader)
|
||||
hashtagsSnapshot.append([.addSavedHashtag], to: .savedHashtagsHeader)
|
||||
self.dataSource.apply(hashtagsSnapshot, to: .savedHashtags, animatingDifferences: false) {
|
||||
if let selected = selected {
|
||||
|
@ -240,8 +264,10 @@ class MainSidebarViewController: UIViewController {
|
|||
var instancesSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||
instancesSnapshot.append([.savedInstancesHeader])
|
||||
instancesSnapshot.expand([.savedInstancesHeader])
|
||||
let sortedInstances = SavedDataManager.shared.savedInstances(for: mastodonController.accountInfo!)
|
||||
instancesSnapshot.append(sortedInstances.map { .savedInstance($0) }, to: .savedInstancesHeader)
|
||||
let instances = fetchSavedInstances().map {
|
||||
Item.savedInstance($0.url)
|
||||
}
|
||||
instancesSnapshot.append(instances, to: .savedInstancesHeader)
|
||||
instancesSnapshot.append([.addSavedInstance], to: .savedInstancesHeader)
|
||||
self.dataSource.apply(instancesSnapshot, to: .savedInstances, animatingDifferences: false) {
|
||||
if let selected = selected {
|
||||
|
@ -555,6 +581,7 @@ extension MainSidebarViewController: UICollectionViewDragDelegate {
|
|||
extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
|
||||
func didSaveInstance(url: URL) {
|
||||
dismiss(animated: true) {
|
||||
self.select(item: .savedInstance(url), animated: true)
|
||||
self.sidebarDelegate?.sidebar(self, didSelectItem: .savedInstance(url))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,13 +15,17 @@ class HashtagTimelineViewController: TimelineTableViewController {
|
|||
|
||||
var toggleSaveButton: UIBarButtonItem!
|
||||
var toggleSaveButtonTitle: String {
|
||||
if SavedDataManager.shared.isSaved(hashtag: hashtag, for: mastodonController.accountInfo!) {
|
||||
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))
|
||||
}
|
||||
|
||||
init(for hashtag: Hashtag, mastodonController: MastodonController) {
|
||||
self.hashtag = hashtag
|
||||
|
||||
|
@ -48,11 +52,13 @@ class HashtagTimelineViewController: TimelineTableViewController {
|
|||
// MARK: - Interaction
|
||||
|
||||
@objc func toggleSaveButtonPressed() {
|
||||
if SavedDataManager.shared.isSaved(hashtag: hashtag, for: mastodonController.accountInfo!) {
|
||||
SavedDataManager.shared.remove(hashtag: hashtag, for: mastodonController.accountInfo!)
|
||||
let context = mastodonController.persistentContainer.viewContext
|
||||
if let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first {
|
||||
context.delete(existing)
|
||||
} else {
|
||||
SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!)
|
||||
_ = SavedHashtag(hashtag: hashtag, context: context)
|
||||
}
|
||||
try! context.save()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,9 +22,13 @@ class InstanceTimelineViewController: TimelineTableViewController {
|
|||
let instanceURL: URL
|
||||
let instanceMastodonController: MastodonController
|
||||
|
||||
var toggleSaveButton: UIBarButtonItem!
|
||||
var toggleSaveButtonTitle: String {
|
||||
if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) {
|
||||
private var toggleSaveButton: UIBarButtonItem!
|
||||
|
||||
private var isInstanceSaved: Bool {
|
||||
parentMastodonController!.persistentContainer.viewContext.objectExists(for: SavedInstance.fetchRequest(url: instanceURL))
|
||||
}
|
||||
private var toggleSaveButtonTitle: String {
|
||||
if isInstanceSaved {
|
||||
return NSLocalizedString("Unsave", comment: "unsave instance button")
|
||||
} else {
|
||||
return NSLocalizedString("Save", comment: "save instance button")
|
||||
|
@ -81,13 +85,16 @@ class InstanceTimelineViewController: TimelineTableViewController {
|
|||
|
||||
// MARK: - Interaction
|
||||
@objc func toggleSaveButtonPressed() {
|
||||
if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) {
|
||||
SavedDataManager.shared.remove(instance: instanceURL, for: parentMastodonController!.accountInfo!)
|
||||
let context = parentMastodonController!.persistentContainer.viewContext
|
||||
let existing = try? context.fetch(SavedInstance.fetchRequest(url: instanceURL)).first
|
||||
if let existing = existing {
|
||||
context.delete(existing)
|
||||
delegate?.didUnsaveInstance(url: instanceURL)
|
||||
} else {
|
||||
SavedDataManager.shared.add(instance: instanceURL, for: parentMastodonController!.accountInfo!)
|
||||
_ = SavedInstance(url: instanceURL, context: context)
|
||||
delegate?.didSaveInstance(url: instanceURL)
|
||||
}
|
||||
try? context.save()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -97,18 +97,24 @@ extension MenuActionProvider {
|
|||
}
|
||||
|
||||
func actionsForHashtag(_ hashtag: Hashtag, sourceView: UIView?) -> [UIMenuElement] {
|
||||
let account = mastodonController!.accountInfo!
|
||||
let saved = SavedDataManager.shared.isSaved(hashtag: hashtag, for: account)
|
||||
|
||||
let actionsSection = [
|
||||
createAction(identifier: "save", title: saved ? "Unsave Hashtag" : "Save Hashtag", systemImageName: "number", handler: { (_) in
|
||||
if saved {
|
||||
SavedDataManager.shared.remove(hashtag: hashtag, for: account)
|
||||
} else {
|
||||
SavedDataManager.shared.add(hashtag: hashtag, for: account)
|
||||
}
|
||||
})
|
||||
]
|
||||
let actionsSection: [UIMenuElement]
|
||||
if let mastodonController = mastodonController,
|
||||
mastodonController.loggedIn {
|
||||
let context = mastodonController.persistentContainer.viewContext
|
||||
let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first
|
||||
actionsSection = [
|
||||
createAction(identifier: "save", title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", systemImageName: "number", handler: { (_) in
|
||||
if let existing = existing {
|
||||
context.delete(existing)
|
||||
} else {
|
||||
_ = SavedHashtag(hashtag: hashtag, context: context)
|
||||
}
|
||||
try! context.save()
|
||||
})
|
||||
]
|
||||
} else {
|
||||
actionsSection = []
|
||||
}
|
||||
|
||||
let shareSection = actionsForURL(hashtag.url, sourceView: sourceView)
|
||||
|
||||
|
|
Loading…
Reference in New Issue