forked from shadowfacts/Tusker
parent
e0d97cd2a8
commit
d13b517128
|
@ -12,10 +12,11 @@ import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
import OSLog
|
import OSLog
|
||||||
import Sentry
|
import Sentry
|
||||||
|
import CloudKit
|
||||||
|
|
||||||
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore")
|
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore")
|
||||||
|
|
||||||
class MastodonCachePersistentStore: NSPersistentContainer {
|
class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
||||||
|
|
||||||
private static let managedObjectModel: NSManagedObjectModel = {
|
private static let managedObjectModel: NSManagedObjectModel = {
|
||||||
let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")!
|
let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")!
|
||||||
|
@ -46,6 +47,9 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
let relationshipSubject = PassthroughSubject<String, Never>()
|
let relationshipSubject = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
init(for accountInfo: LocalData.UserAccountInfo?, transient: Bool = false) {
|
init(for accountInfo: LocalData.UserAccountInfo?, transient: Bool = false) {
|
||||||
|
let group = DispatchGroup()
|
||||||
|
var instancesToMigrate: [URL]? = nil
|
||||||
|
var hashtagsToMigrate: [Hashtag]? = nil
|
||||||
if transient {
|
if transient {
|
||||||
super.init(name: "transient_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
|
super.init(name: "transient_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
|
||||||
|
|
||||||
|
@ -55,6 +59,24 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
} else {
|
} else {
|
||||||
super.init(name: "\(accountInfo!.persistenceKey)_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
|
super.init(name: "\(accountInfo!.persistenceKey)_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
|
||||||
|
|
||||||
|
var localStoreLocation = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||||
|
localStoreLocation.appendPathComponent("\(accountInfo!.persistenceKey)_cache.sqlite", isDirectory: false)
|
||||||
|
let localStoreDescription = NSPersistentStoreDescription(url: localStoreLocation)
|
||||||
|
localStoreDescription.configuration = "Local"
|
||||||
|
|
||||||
|
var cloudStoreLocation = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||||
|
cloudStoreLocation.appendPathComponent("cloud.sqlite", isDirectory: false)
|
||||||
|
let cloudStoreDescription = NSPersistentStoreDescription(url: cloudStoreLocation)
|
||||||
|
cloudStoreDescription.configuration = "Cloud"
|
||||||
|
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.space.vaccor.Tusker")
|
||||||
|
options.databaseScope = .private
|
||||||
|
cloudStoreDescription.cloudKitContainerOptions = options
|
||||||
|
|
||||||
|
persistentStoreDescriptions = [
|
||||||
|
cloudStoreDescription,
|
||||||
|
localStoreDescription,
|
||||||
|
]
|
||||||
|
|
||||||
// workaround for migrating from using id in name to persistenceKey
|
// workaround for migrating from using id in name to persistenceKey
|
||||||
// can be removed after a sufficient time has passed
|
// can be removed after a sufficient time has passed
|
||||||
if accountInfo!.id.contains("/") {
|
if accountInfo!.id.contains("/") {
|
||||||
|
@ -82,14 +104,64 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// migrate saved data from local store to cloud store
|
||||||
|
// this can be removed pre-app store release
|
||||||
|
var defaultPath = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||||
|
defaultPath.appendPathComponent("\(accountInfo!.persistenceKey)_cache.sqlite", isDirectory: false)
|
||||||
|
if FileManager.default.fileExists(atPath: defaultPath.path) {
|
||||||
|
group.enter()
|
||||||
|
let defaultDesc = NSPersistentStoreDescription(url: defaultPath)
|
||||||
|
defaultDesc.configuration = "Default"
|
||||||
|
let defaultPSC = NSPersistentContainer(name: "\(accountInfo!.persistenceKey)_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
|
||||||
|
defaultPSC.persistentStoreDescriptions = [defaultDesc]
|
||||||
|
defaultPSC.loadPersistentStores { _, error in
|
||||||
|
guard error == nil else {
|
||||||
|
group.leave()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defaultPSC.performBackgroundTask { context in
|
||||||
|
if let instances = try? context.fetch(SavedInstance.fetchRequestWithoutAccountForMigrating()) {
|
||||||
|
instancesToMigrate = instances.map(\.url)
|
||||||
|
instances.forEach(context.delete(_:))
|
||||||
|
}
|
||||||
|
if let hashtags = try? context.fetch(SavedHashtag.fetchRequestWithoutAccountForMigrating()) {
|
||||||
|
hashtagsToMigrate = hashtags.map { Hashtag(name: $0.name, url: $0.url) }
|
||||||
|
hashtags.forEach(context.delete(_:))
|
||||||
|
}
|
||||||
|
if context.hasChanges {
|
||||||
|
try? context.save()
|
||||||
|
}
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group.wait()
|
||||||
loadPersistentStores { (description, error) in
|
loadPersistentStores { (description, error) in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
logger.error("Unable to load persistent store: \(String(describing: error), privacy: .public)")
|
logger.error("Unable to load persistent store: \(String(describing: error), privacy: .public)")
|
||||||
fatalError("Unable to load persistent store")
|
fatalError("Unable to load persistent store")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if description.configuration == "Cloud" {
|
||||||
|
self.backgroundContext.perform {
|
||||||
|
instancesToMigrate?.forEach({ url in
|
||||||
|
_ = SavedInstance(url: url, account: accountInfo!, context: self.backgroundContext)
|
||||||
|
})
|
||||||
|
hashtagsToMigrate?.forEach({ hashtag in
|
||||||
|
_ = SavedHashtag(hashtag: hashtag, account: accountInfo!, context: self.backgroundContext)
|
||||||
|
})
|
||||||
|
self.save(context: self.backgroundContext)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// changes to the Cloud CD model in development need this to be uncommented to update the CK schema
|
||||||
|
// #if DEBUG
|
||||||
|
// try! initializeCloudKitSchema(options: [])
|
||||||
|
// #endif
|
||||||
|
|
||||||
viewContext.automaticallyMergesChangesFromParent = true
|
viewContext.automaticallyMergesChangesFromParent = true
|
||||||
viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||||
|
|
|
@ -14,24 +14,32 @@ import WebURLFoundationExtras
|
||||||
@objc(SavedHashtag)
|
@objc(SavedHashtag)
|
||||||
public final class SavedHashtag: NSManagedObject {
|
public final class SavedHashtag: NSManagedObject {
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<SavedHashtag> {
|
@nonobjc class func fetchRequestWithoutAccountForMigrating() -> NSFetchRequest<SavedHashtag> {
|
||||||
return NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
|
return NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
|
||||||
}
|
}
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest(name: String) -> NSFetchRequest<SavedHashtag> {
|
@nonobjc class func fetchRequest(account: LocalData.UserAccountInfo) -> NSFetchRequest<SavedHashtag> {
|
||||||
let req = NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
|
let req = NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
|
||||||
req.predicate = NSPredicate(format: "name LIKE[cd] %@", name)
|
req.predicate = NSPredicate(format: "accountID = %@", account.id)
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@nonobjc class func fetchRequest(name: String, account: LocalData.UserAccountInfo) -> NSFetchRequest<SavedHashtag> {
|
||||||
|
let req = NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
|
||||||
|
req.predicate = NSPredicate(format: "name LIKE[cd] %@ AND accountID = %@", name, account.id)
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged public var accountID: String
|
||||||
@NSManaged public var name: String
|
@NSManaged public var name: String
|
||||||
@NSManaged public var url: URL
|
@NSManaged public var url: URL
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SavedHashtag {
|
extension SavedHashtag {
|
||||||
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
convenience init(hashtag: Hashtag, account: LocalData.UserAccountInfo, context: NSManagedObjectContext) {
|
||||||
self.init(context: context)
|
self.init(context: context)
|
||||||
|
self.accountID = account.id
|
||||||
self.name = hashtag.name
|
self.name = hashtag.name
|
||||||
self.url = URL(hashtag.url)!
|
self.url = URL(hashtag.url)!
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,23 +12,31 @@ import CoreData
|
||||||
@objc(SavedInstance)
|
@objc(SavedInstance)
|
||||||
public final class SavedInstance: NSManagedObject {
|
public final class SavedInstance: NSManagedObject {
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<SavedInstance> {
|
@nonobjc class func fetchRequestWithoutAccountForMigrating() -> NSFetchRequest<SavedInstance> {
|
||||||
return NSFetchRequest<SavedInstance>(entityName: "SavedInstance")
|
return NSFetchRequest<SavedInstance>(entityName: "SavedInstance")
|
||||||
}
|
}
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest(url: URL) -> NSFetchRequest<SavedInstance> {
|
@nonobjc class func fetchRequest(account: LocalData.UserAccountInfo) -> NSFetchRequest<SavedInstance> {
|
||||||
let req = fetchRequest()
|
let req = NSFetchRequest<SavedInstance>(entityName: "SavedInstance")
|
||||||
req.predicate = NSPredicate(format: "url = %@", url as NSURL)
|
req.predicate = NSPredicate(format: "accountID = %@", account.id)
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@nonobjc class func fetchRequest(url: URL, account: LocalData.UserAccountInfo) -> NSFetchRequest<SavedInstance> {
|
||||||
|
let req = NSFetchRequest<SavedInstance>(entityName: "SavedInstance")
|
||||||
|
req.predicate = NSPredicate(format: "url = %@ AND accountID = %@", url as NSURL, account.id)
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged public var accountID: String
|
||||||
@NSManaged public var url: URL
|
@NSManaged public var url: URL
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SavedInstance {
|
extension SavedInstance {
|
||||||
convenience init(url: URL, context: NSManagedObjectContext) {
|
convenience init(url: URL, account: LocalData.UserAccountInfo, context: NSManagedObjectContext) {
|
||||||
self.init(context: context)
|
self.init(context: context)
|
||||||
|
self.accountID = account.id
|
||||||
self.url = url
|
self.url = url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21512" systemVersion="22A380" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22A380" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||||
<attribute name="acct" attributeType="String"/>
|
<attribute name="acct" attributeType="String"/>
|
||||||
<attribute name="avatar" optional="YES" attributeType="URI"/>
|
<attribute name="avatar" optional="YES" attributeType="URI"/>
|
||||||
|
@ -60,21 +60,13 @@
|
||||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="relationship" inverseEntity="Account"/>
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="relationship" inverseEntity="Account"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SavedHashtag" representedClassName="SavedHashtag" syncable="YES">
|
<entity name="SavedHashtag" representedClassName="SavedHashtag" syncable="YES">
|
||||||
<attribute name="name" attributeType="String"/>
|
<attribute name="accountID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="url" attributeType="URI"/>
|
<attribute name="name" optional="YES" attributeType="String"/>
|
||||||
<uniquenessConstraints>
|
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||||
<uniquenessConstraint>
|
|
||||||
<constraint value="name"/>
|
|
||||||
</uniquenessConstraint>
|
|
||||||
</uniquenessConstraints>
|
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SavedInstance" representedClassName="SavedInstance" syncable="YES">
|
<entity name="SavedInstance" representedClassName="SavedInstance" syncable="YES">
|
||||||
<attribute name="url" attributeType="URI"/>
|
<attribute name="accountID" optional="YES" attributeType="String"/>
|
||||||
<uniquenessConstraints>
|
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||||
<uniquenessConstraint>
|
|
||||||
<constraint value="url"/>
|
|
||||||
</uniquenessConstraint>
|
|
||||||
</uniquenessConstraints>
|
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Status" representedClassName="StatusMO" syncable="YES">
|
<entity name="Status" representedClassName="StatusMO" syncable="YES">
|
||||||
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
||||||
|
@ -117,4 +109,17 @@
|
||||||
<attribute name="timelineKind" attributeType="String" valueTransformerName="pachydermTimeline" customClassName="Tusker.TimelineContainer"/>
|
<attribute name="timelineKind" attributeType="String" valueTransformerName="pachydermTimeline" customClassName="Tusker.TimelineContainer"/>
|
||||||
<relationship name="statuses" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="Status" inverseName="timelines" inverseEntity="Status"/>
|
<relationship name="statuses" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="Status" inverseName="timelines" inverseEntity="Status"/>
|
||||||
</entity>
|
</entity>
|
||||||
|
<configuration name="Cloud" usedWithCloudKit="YES">
|
||||||
|
<memberEntity name="SavedHashtag"/>
|
||||||
|
<memberEntity name="SavedInstance"/>
|
||||||
|
</configuration>
|
||||||
|
<configuration name="Local">
|
||||||
|
<memberEntity name="Account"/>
|
||||||
|
<memberEntity name="Filter"/>
|
||||||
|
<memberEntity name="FilterKeyword"/>
|
||||||
|
<memberEntity name="FollowedHashtag"/>
|
||||||
|
<memberEntity name="Relationship"/>
|
||||||
|
<memberEntity name="Status"/>
|
||||||
|
<memberEntity name="TimelineState"/>
|
||||||
|
</configuration>
|
||||||
</model>
|
</model>
|
|
@ -2,37 +2,6 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>SentryDSN</key>
|
|
||||||
<string>$(SENTRY_DSN)</string>
|
|
||||||
<key>OSLogPreferences</key>
|
|
||||||
<dict>
|
|
||||||
<key>space.vaccor.Tusker</key>
|
|
||||||
<dict>
|
|
||||||
<key>DEFAULT-OPTIONS</key>
|
|
||||||
<dict>
|
|
||||||
<key>TTL</key>
|
|
||||||
<dict>
|
|
||||||
<key>Fault</key>
|
|
||||||
<integer>30</integer>
|
|
||||||
<key>Error</key>
|
|
||||||
<integer>30</integer>
|
|
||||||
<key>Debug</key>
|
|
||||||
<integer>15</integer>
|
|
||||||
<key>Info</key>
|
|
||||||
<integer>30</integer>
|
|
||||||
<key>Default</key>
|
|
||||||
<integer>30</integer>
|
|
||||||
</dict>
|
|
||||||
<key>Level</key>
|
|
||||||
<dict>
|
|
||||||
<key>Persist</key>
|
|
||||||
<string>Debug</string>
|
|
||||||
<key>Enable</key>
|
|
||||||
<string>Debug</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
|
@ -87,7 +56,7 @@
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>Post videos from the camera.</string>
|
<string>Post videos from the camera.</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>Save photos directly from other people's posts.</string>
|
<string>Save photos directly from other people's posts.</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Post photos from the photo library.</string>
|
<string>Post photos from the photo library.</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
|
@ -102,6 +71,37 @@
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.main-scene</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.main-scene</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>OSLogPreferences</key>
|
||||||
|
<dict>
|
||||||
|
<key>space.vaccor.Tusker</key>
|
||||||
|
<dict>
|
||||||
|
<key>DEFAULT-OPTIONS</key>
|
||||||
|
<dict>
|
||||||
|
<key>Level</key>
|
||||||
|
<dict>
|
||||||
|
<key>Enable</key>
|
||||||
|
<string>Debug</string>
|
||||||
|
<key>Persist</key>
|
||||||
|
<string>Debug</string>
|
||||||
|
</dict>
|
||||||
|
<key>TTL</key>
|
||||||
|
<dict>
|
||||||
|
<key>Debug</key>
|
||||||
|
<integer>15</integer>
|
||||||
|
<key>Default</key>
|
||||||
|
<integer>30</integer>
|
||||||
|
<key>Error</key>
|
||||||
|
<integer>30</integer>
|
||||||
|
<key>Fault</key>
|
||||||
|
<integer>30</integer>
|
||||||
|
<key>Info</key>
|
||||||
|
<integer>30</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>SentryDSN</key>
|
||||||
|
<string>$(SENTRY_DSN)</string>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
@ -140,6 +140,7 @@
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
|
<string>remote-notification</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
|
|
|
@ -400,7 +400,8 @@ struct ComposeAutocompleteHashtagsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateHashtags(searchResults: [Hashtag], trendingTags: [Hashtag], query: String) {
|
private func updateHashtags(searchResults: [Hashtag], trendingTags: [Hashtag], query: String) {
|
||||||
let savedTags = ((try? mastodonController.persistentContainer.viewContext.fetch(SavedHashtag.fetchRequest())) ?? [])
|
let req = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
|
||||||
|
let savedTags = ((try? mastodonController.persistentContainer.viewContext.fetch(req)) ?? [])
|
||||||
.map { Hashtag(name: $0.name, url: $0.url) }
|
.map { Hashtag(name: $0.name, url: $0.url) }
|
||||||
|
|
||||||
hashtags = (searchResults + savedTags + trendingTags)
|
hashtags = (searchResults + savedTags + trendingTags)
|
||||||
|
|
|
@ -206,7 +206,8 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func fetchHashtagItems(followed: [FollowedHashtag]) -> [Item] {
|
private func fetchHashtagItems(followed: [FollowedHashtag]) -> [Item] {
|
||||||
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(SavedHashtag.fetchRequest())) ?? []
|
let req = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
|
||||||
|
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(Hashtag(name: $0.name, url: $0.url))
|
||||||
}
|
}
|
||||||
|
@ -219,7 +220,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func fetchSavedInstances() -> [SavedInstance] {
|
private func fetchSavedInstances() -> [SavedInstance] {
|
||||||
let req = SavedInstance.fetchRequest()
|
let req = SavedInstance.fetchRequest(account: mastodonController.accountInfo!)
|
||||||
req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
|
req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
|
||||||
do {
|
do {
|
||||||
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
||||||
|
@ -278,7 +279,8 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
|
|
||||||
func removeSavedHashtag(_ hashtag: Hashtag) {
|
func removeSavedHashtag(_ hashtag: Hashtag) {
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
if let hashtag = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name)).first {
|
let req = SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)
|
||||||
|
if let hashtag = try? context.fetch(req).first {
|
||||||
context.delete(hashtag)
|
context.delete(hashtag)
|
||||||
try! context.save()
|
try! context.save()
|
||||||
}
|
}
|
||||||
|
@ -286,7 +288,8 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
|
|
||||||
func removeSavedInstance(_ instanceURL: URL) {
|
func removeSavedInstance(_ instanceURL: URL) {
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
if let instance = try? context.fetch(SavedInstance.fetchRequest(url: instanceURL)).first {
|
let req = SavedInstance.fetchRequest(url: instanceURL, account: mastodonController.accountInfo!)
|
||||||
|
if let instance = try? context.fetch(req).first {
|
||||||
context.delete(instance)
|
context.delete(instance)
|
||||||
try! context.save()
|
try! context.save()
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,7 +232,8 @@ class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func fetchHashtagItems(followed: [FollowedHashtag]) -> [Item] {
|
private func fetchHashtagItems(followed: [FollowedHashtag]) -> [Item] {
|
||||||
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(SavedHashtag.fetchRequest())) ?? []
|
let req = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
|
||||||
|
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(Hashtag(name: $0.name, url: $0.url))
|
||||||
}
|
}
|
||||||
|
@ -245,7 +246,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func fetchSavedInstances() -> [SavedInstance] {
|
private func fetchSavedInstances() -> [SavedInstance] {
|
||||||
let req = SavedInstance.fetchRequest()
|
let req = SavedInstance.fetchRequest(account: mastodonController.accountInfo!)
|
||||||
req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
|
req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
|
||||||
do {
|
do {
|
||||||
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
return try mastodonController.persistentContainer.viewContext.fetch(req)
|
||||||
|
|
|
@ -39,7 +39,7 @@ class OnboardingViewController: UINavigationController {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func tryLoginTo(instanceURL: URL) async throws {
|
private func tryLoginTo(instanceURL: URL) async throws {
|
||||||
let mastodonController = MastodonController(instanceURL: instanceURL)
|
let mastodonController = MastodonController(instanceURL: instanceURL, transient: true)
|
||||||
let clientID: String
|
let clientID: String
|
||||||
let clientSecret: String
|
let clientSecret: String
|
||||||
do {
|
do {
|
||||||
|
|
|
@ -16,7 +16,8 @@ class HashtagTimelineViewController: TimelineViewController {
|
||||||
var toggleSaveButton: UIBarButtonItem!
|
var toggleSaveButton: UIBarButtonItem!
|
||||||
|
|
||||||
private var isHashtagSaved: Bool {
|
private var isHashtagSaved: Bool {
|
||||||
mastodonController.persistentContainer.viewContext.objectExists(for: SavedHashtag.fetchRequest(name: hashtag.name))
|
let req = SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)
|
||||||
|
return mastodonController.persistentContainer.viewContext.objectExists(for: req)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isHashtagFollowed: Bool {
|
private var isHashtagFollowed: Bool {
|
||||||
|
@ -47,10 +48,10 @@ 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)).first {
|
if let existing = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)).first {
|
||||||
context.delete(existing)
|
context.delete(existing)
|
||||||
} else {
|
} else {
|
||||||
_ = SavedHashtag(hashtag: hashtag, context: context)
|
_ = SavedHashtag(hashtag: hashtag, account: mastodonController.accountInfo!, context: context)
|
||||||
}
|
}
|
||||||
mastodonController.persistentContainer.save(context: context)
|
mastodonController.persistentContainer.save(context: context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,8 @@ class InstanceTimelineViewController: TimelineViewController {
|
||||||
private var toggleSaveButton: UIBarButtonItem!
|
private var toggleSaveButton: UIBarButtonItem!
|
||||||
|
|
||||||
private var isInstanceSaved: Bool {
|
private var isInstanceSaved: Bool {
|
||||||
parentMastodonController!.persistentContainer.viewContext.objectExists(for: SavedInstance.fetchRequest(url: instanceURL))
|
let req = SavedInstance.fetchRequest(url: instanceURL, account: parentMastodonController!.accountInfo!)
|
||||||
|
return parentMastodonController!.persistentContainer.viewContext.objectExists(for: req)
|
||||||
}
|
}
|
||||||
private var toggleSaveButtonTitle: String {
|
private var toggleSaveButtonTitle: String {
|
||||||
if isInstanceSaved {
|
if isInstanceSaved {
|
||||||
|
@ -83,12 +84,13 @@ class InstanceTimelineViewController: TimelineViewController {
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
@objc func toggleSaveButtonPressed() {
|
@objc func toggleSaveButtonPressed() {
|
||||||
let context = parentMastodonController!.persistentContainer.viewContext
|
let context = parentMastodonController!.persistentContainer.viewContext
|
||||||
let existing = try? context.fetch(SavedInstance.fetchRequest(url: instanceURL)).first
|
let req = SavedInstance.fetchRequest(url: instanceURL, account: parentMastodonController!.accountInfo!)
|
||||||
|
let existing = try? context.fetch(req).first
|
||||||
if let existing = existing {
|
if let existing = existing {
|
||||||
context.delete(existing)
|
context.delete(existing)
|
||||||
delegate?.didUnsaveInstance(url: instanceURL)
|
delegate?.didUnsaveInstance(url: instanceURL)
|
||||||
} else {
|
} else {
|
||||||
_ = SavedInstance(url: instanceURL, context: context)
|
_ = SavedInstance(url: instanceURL, account: parentMastodonController!.accountInfo!, context: context)
|
||||||
delegate?.didSaveInstance(url: instanceURL)
|
delegate?.didSaveInstance(url: instanceURL)
|
||||||
}
|
}
|
||||||
mastodonController.persistentContainer.save(context: context)
|
mastodonController.persistentContainer.save(context: context)
|
||||||
|
|
|
@ -111,7 +111,7 @@ extension MenuActionProvider {
|
||||||
mastodonController.loggedIn {
|
mastodonController.loggedIn {
|
||||||
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)).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 = "Saved hashtags appear in the Explore section of Tusker"
|
||||||
let saveImage = UIImage(systemName: existing != nil ? "minus" : "plus")
|
let saveImage = UIImage(systemName: existing != nil ? "minus" : "plus")
|
||||||
actionsSection = [
|
actionsSection = [
|
||||||
|
@ -119,7 +119,7 @@ extension MenuActionProvider {
|
||||||
if let existing = existing {
|
if let existing = existing {
|
||||||
context.delete(existing)
|
context.delete(existing)
|
||||||
} else {
|
} else {
|
||||||
_ = SavedHashtag(hashtag: hashtag, context: context)
|
_ = SavedHashtag(hashtag: hashtag, account: mastodonController.accountInfo!, context: context)
|
||||||
}
|
}
|
||||||
mastodonController.persistentContainer.save(context: context)
|
mastodonController.persistentContainer.save(context: context)
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,6 +2,18 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>iCloud.space.vaccor.Tusker</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.icloud-services</key>
|
||||||
|
<array>
|
||||||
|
<string>CloudKit</string>
|
||||||
|
</array>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
|
|
Loading…
Reference in New Issue