Cache account relationships in CoreData

This commit is contained in:
Shadowfacts 2020-10-12 18:20:57 -04:00
parent 2cfc0cf28a
commit 1a4517c43a
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
9 changed files with 167 additions and 18 deletions

View File

@ -18,6 +18,7 @@ public class Relationship: Decodable {
public let followRequested: Bool
public let domainBlocking: Bool
public let showingReblogs: Bool
public let endorsed: Bool?
private enum CodingKeys: String, CodingKey {
case id
@ -29,5 +30,6 @@ public class Relationship: Decodable {
case followRequested = "requested"
case domainBlocking = "domain_blocking"
case showingReblogs = "showing_reblogs"
case endorsed
}
}

View File

@ -260,6 +260,7 @@
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
@ -594,6 +595,7 @@
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
@ -893,6 +895,7 @@
D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */,
D60E2F232442372B005F8713 /* StatusMO.swift */,
D60E2F252442372B005F8713 /* AccountMO.swift */,
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */,
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
);
path = CoreData;
@ -1837,6 +1840,7 @@
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */,

View File

@ -29,10 +29,13 @@ class FollowAccountActivity: AccountActivity {
let request = Account.follow(account.id)
mastodonController.run(request) { (response) in
if case .failure(_) = response {
switch response {
case .failure(_):
// todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error)
fatalError()
case let .success(relationship, _):
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
}
}
}

View File

@ -29,10 +29,13 @@ class UnfollowAccountActivity: AccountActivity {
let request = Account.unfollow(account.id)
mastodonController.run(request) { (response) in
if case .failure(_) = response {
switch response {
case .failure(_):
// todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error)
fatalError()
case let .success(relationship, _):
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
}
}
}

View File

@ -26,6 +26,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
let statusSubject = PassthroughSubject<String, Never>()
let accountSubject = PassthroughSubject<String, Never>()
let relationshipSubject = PassthroughSubject<String, Never>()
init(for accountInfo: LocalData.UserAccountInfo?, transient: Bool = false) {
if transient {
@ -136,6 +137,40 @@ class MastodonCachePersistentStore: NSPersistentContainer {
}
}
func relationship(forAccount id: String, in context: NSManagedObjectContext? = nil) -> RelationshipMO? {
let context = context ?? viewContext
let request: NSFetchRequest<RelationshipMO> = RelationshipMO.fetchRequest()
request.predicate = NSPredicate(format: "accountID = %@", id)
request.fetchLimit = 1
if let result = try? context.fetch(request), let relationship = result.first {
return relationship
} else {
return nil
}
}
@discardableResult
private func upsert(relationship: Relationship) -> RelationshipMO {
if let relationshipMO = self.relationship(forAccount: relationship.id, in: self.backgroundContext) {
relationshipMO.updateFrom(apiRelationship: relationship, container: self)
return relationshipMO
} else {
let relationshipMO = RelationshipMO(apiRelationship: relationship, container: self, context: self.backgroundContext)
return relationshipMO
}
}
func addOrUpdate(relationship: Relationship, completion: ((RelationshipMO) -> Void)? = nil) {
backgroundContext.perform {
let relationshipMO = self.upsert(relationship: relationship)
if self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
completion?(relationshipMO)
self.relationshipSubject.send(relationship.id)
}
}
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
backgroundContext.perform {
accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }

View File

@ -0,0 +1,59 @@
//
// RelationshipMO.swift
// Tusker
//
// Created by Shadowfacts on 10/11/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import Foundation
import CoreData
import Pachyderm
@objc(RelationshipMO)
public final class RelationshipMO: NSManagedObject {
@nonobjc public class func fetchRequest() -> NSFetchRequest<RelationshipMO> {
return NSFetchRequest<RelationshipMO>(entityName: "Relationship")
}
@NSManaged public var accountID: String
@NSManaged public var blocking: Bool
@NSManaged public var domainBlocking: Bool
@NSManaged public var endorsed: Bool
@NSManaged public var followedBy: Bool
@NSManaged public var following: Bool
@NSManaged public var muting: Bool
@NSManaged public var mutingNotifications: Bool
@NSManaged public var requested: Bool
@NSManaged public var showingReblogs: Bool
@NSManaged public var account: AccountMO?
}
extension RelationshipMO {
convenience init(apiRelationship relationship: Relationship, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
self.init(context: context)
self.updateFrom(apiRelationship: relationship, container: container)
}
func updateFrom(apiRelationship relationship: Relationship, container: MastodonCachePersistentStore) {
guard let context = managedObjectContext else {
// we have been deleted, don't bother updating
return
}
self.accountID = relationship.id
self.blocking = relationship.blocking
self.domainBlocking = relationship.domainBlocking
self.endorsed = relationship.endorsed ?? false
self.followedBy = relationship.followedBy
self.following = relationship.following
self.muting = relationship.muting
self.mutingNotifications = relationship.mutingNotifications
self.requested = relationship.followRequested
self.showingReblogs = relationship.showingReblogs
self.account = container.account(for: relationship.id, in: context)
}
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17507" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="AccountMO" syncable="YES">
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" attributeType="URI"/>
@ -20,12 +20,26 @@
<attribute name="url" attributeType="URI"/>
<attribute name="username" attributeType="String"/>
<relationship name="movedTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
<relationship name="relationship" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Relationship" inverseName="account" inverseEntity="Relationship"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Relationship" representedClassName="RelationshipMO" syncable="YES">
<attribute name="accountID" optional="YES" attributeType="String"/>
<attribute name="blocking" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="domainBlocking" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="endorsed" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="followedBy" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="following" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="muting" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mutingNotifications" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="requested" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<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="Status" representedClassName="StatusMO" syncable="YES">
<attribute name="applicationName" optional="YES" attributeType="String"/>
<attribute name="attachmentsData" attributeType="Binary"/>
@ -59,7 +73,8 @@
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="328"/>
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="343"/>
<element name="Status" positionX="-63" positionY="-18" width="128" height="418"/>
<element name="Relationship" positionX="63" positionY="135" width="128" height="208"/>
</elements>
</model>

View File

@ -71,7 +71,13 @@ extension MenuPreviewProvider {
elementHandler([
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
let request = (following ? Account.unfollow : Account.follow)(accountID)
mastodonController.run(request) { (_) in
mastodonController.run(request) { (response) in
switch response {
case .failure(_):
fatalError()
case let .success(relationship, _):
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
}
}
})
])

View File

@ -23,7 +23,11 @@ class ProfileHeaderView: UIView {
return nib.instantiate(withOwner: nil, options: nil).first as! ProfileHeaderView
}
weak var delegate: ProfileHeaderViewDelegate?
weak var delegate: ProfileHeaderViewDelegate? {
didSet {
createObservers()
}
}
var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var headerImageView: UIImageView!
@ -41,10 +45,10 @@ class ProfileHeaderView: UIView {
var accountID: String!
var avatarRequest: ImageCache.Request?
var headerRequest: ImageCache.Request?
private var avatarRequest: ImageCache.Request?
private var headerRequest: ImageCache.Request?
private var accountUpdater: Cancellable?
private var cancellables = [AnyCancellable]()
deinit {
avatarRequest?.cancel()
@ -74,6 +78,22 @@ class ProfileHeaderView: UIView {
}
}
private func createObservers() {
cancellables = []
mastodonController.persistentContainer.accountSubject
.filter { [weak self] in $0 == self?.accountID }
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.updateUI(for: $0) }
.store(in: &cancellables)
mastodonController.persistentContainer.relationshipSubject
.filter { [weak self] in $0 == self?.accountID }
.receive(on: DispatchQueue.main)
.sink { [weak self] (_) in self?.updateRelationship() }
.store(in: &cancellables)
}
func updateUI(for accountID: String) {
self.accountID = accountID
@ -111,6 +131,9 @@ class ProfileHeaderView: UIView {
// don't show relationship label for the user's own account
if accountID != mastodonController.account?.id {
// while fetching the most up-to-date, show the current data (if any)
updateRelationship()
let request = Client.getRelationships(accounts: [accountID])
mastodonController.run(request) { [weak self] (response) in
guard let self = self,
@ -118,9 +141,7 @@ class ProfileHeaderView: UIView {
let relationship = results.first else {
return
}
DispatchQueue.main.async {
self.followsYouLabel.isHidden = !relationship.followedBy
}
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
}
}
@ -152,13 +173,14 @@ class ProfileHeaderView: UIView {
nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true
}
if accountUpdater == nil {
accountUpdater = mastodonController.persistentContainer.accountSubject
.filter { [weak self] in $0 == self?.accountID }
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.updateUI(for: $0) }
}
private func updateRelationship() {
guard let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else {
return
}
followsYouLabel.isHidden = !relationship.followedBy
}
@objc private func updateUIForPreferences() {