forked from zelo72/mastodon-ios
feat: finish user favourite action
This commit is contained in:
parent
5f1800b353
commit
b55790fee8
|
@ -150,6 +150,17 @@ public extension Toot {
|
||||||
self.repliesCount = repliesCount
|
self.repliesCount = repliesCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func update(liked: Bool, mastodonUser: MastodonUser) {
|
||||||
|
if liked {
|
||||||
|
if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
|
||||||
|
self.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).addObjects(from: [mastodonUser])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (self.favouritedBy ?? Set()).contains(mastodonUser) {
|
||||||
|
self.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).remove(mastodonUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
func didUpdate(at networkDate: Date) {
|
func didUpdate(at networkDate: Date) {
|
||||||
self.updatedAt = networkDate
|
self.updatedAt = networkDate
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
//
|
||||||
|
// ManagedObjectContextObjectsDidChange.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
public struct ManagedObjectContextObjectsDidChangeNotification {
|
||||||
|
|
||||||
|
public let notification: Notification
|
||||||
|
public let managedObjectContext: NSManagedObjectContext
|
||||||
|
|
||||||
|
public init?(notification: Notification) {
|
||||||
|
guard notification.name == .NSManagedObjectContextObjectsDidChange,
|
||||||
|
let managedObjectContext = notification.object as? NSManagedObjectContext else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.notification = notification
|
||||||
|
self.managedObjectContext = managedObjectContext
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ManagedObjectContextObjectsDidChangeNotification {
|
||||||
|
|
||||||
|
public var insertedObjects: Set<NSManagedObject> {
|
||||||
|
return objects(forKey: NSInsertedObjectsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var updatedObjects: Set<NSManagedObject> {
|
||||||
|
return objects(forKey: NSUpdatedObjectsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var deletedObjects: Set<NSManagedObject> {
|
||||||
|
return objects(forKey: NSDeletedObjectsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var refreshedObjects: Set<NSManagedObject> {
|
||||||
|
return objects(forKey: NSRefreshedObjectsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var invalidedObjects: Set<NSManagedObject> {
|
||||||
|
return objects(forKey: NSInvalidatedObjectsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var invalidatedAllObjects: Bool {
|
||||||
|
return notification.userInfo?[NSInvalidatedAllObjectsKey] != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ManagedObjectContextObjectsDidChangeNotification {
|
||||||
|
|
||||||
|
private func objects(forKey key: String) -> Set<NSManagedObject> {
|
||||||
|
return notification.userInfo?[key] as? Set<NSManagedObject> ?? Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
//
|
||||||
|
// ManagedObjectObserver.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final public class ManagedObjectObserver {
|
||||||
|
private init() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ManagedObjectObserver {
|
||||||
|
|
||||||
|
public static func observe(object: NSManagedObject) -> AnyPublisher<Change, Error> {
|
||||||
|
guard let context = object.managedObjectContext else {
|
||||||
|
return Fail(error: .noManagedObjectContext).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
return NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context)
|
||||||
|
.tryMap { notification in
|
||||||
|
guard let notification = ManagedObjectContextObjectsDidChangeNotification(notification: notification) else {
|
||||||
|
throw Error.notManagedObjectChangeNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
let changeType = ManagedObjectObserver.changeType(of: object, in: notification)
|
||||||
|
return Change(
|
||||||
|
changeType: changeType,
|
||||||
|
changeNotification: notification
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.mapError { error -> Error in
|
||||||
|
return (error as? Error) ?? .unknown(error)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ManagedObjectObserver {
|
||||||
|
private static func changeType(of object: NSManagedObject, in notification: ManagedObjectContextObjectsDidChangeNotification) -> ChangeType? {
|
||||||
|
let deleted = notification.deletedObjects.union(notification.invalidedObjects)
|
||||||
|
if notification.invalidatedAllObjects || deleted.contains(where: { $0 === object }) {
|
||||||
|
return .delete
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = notification.updatedObjects.union(notification.refreshedObjects)
|
||||||
|
if let object = updated.first(where: { $0 === object }) {
|
||||||
|
return .update(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ManagedObjectObserver {
|
||||||
|
public struct Change {
|
||||||
|
public let changeType: ChangeType?
|
||||||
|
public let changeNotification: ManagedObjectContextObjectsDidChangeNotification
|
||||||
|
|
||||||
|
init(changeType: ManagedObjectObserver.ChangeType?, changeNotification: ManagedObjectContextObjectsDidChangeNotification) {
|
||||||
|
self.changeType = changeType
|
||||||
|
self.changeNotification = changeNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public enum ChangeType {
|
||||||
|
case delete
|
||||||
|
case update(NSManagedObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Error: Swift.Error {
|
||||||
|
case unknown(Swift.Error)
|
||||||
|
case noManagedObjectContext
|
||||||
|
case notManagedObjectChangeNotification
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,6 +56,11 @@
|
||||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
||||||
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
|
||||||
|
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */; };
|
||||||
|
2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA025D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift */; };
|
||||||
|
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */; };
|
||||||
|
2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */; };
|
||||||
|
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
|
||||||
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
||||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
|
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
|
||||||
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
|
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
|
||||||
|
@ -222,6 +227,11 @@
|
||||||
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||||
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
|
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
|
||||||
|
2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProviderFacade.swift; sourceTree = "<group>"; };
|
||||||
|
2DF75BA025D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+TimelinePostTableViewCellDelegate.swift"; sourceTree = "<group>"; };
|
||||||
|
2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Favorite.swift"; sourceTree = "<group>"; };
|
||||||
|
2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectObserver.swift; sourceTree = "<group>"; };
|
||||||
|
2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextObjectsDidChange.swift; sourceTree = "<group>"; };
|
||||||
2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = "<group>"; };
|
2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -393,6 +403,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2D38F1FD25CD481700561493 /* StatusProvider.swift */,
|
2D38F1FD25CD481700561493 /* StatusProvider.swift */,
|
||||||
|
2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */,
|
||||||
|
2DF75BA025D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift */,
|
||||||
);
|
);
|
||||||
path = StatusProvider;
|
path = StatusProvider;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -521,6 +533,15 @@
|
||||||
path = Item;
|
path = Item;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
2DF75BB725D1473400694EC8 /* Stack */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */,
|
||||||
|
2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */,
|
||||||
|
);
|
||||||
|
path = Stack;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
3FE14AD363ED19AE7FF210A6 /* Frameworks */ = {
|
3FE14AD363ED19AE7FF210A6 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -656,6 +677,7 @@
|
||||||
DB45FB0925CA87BC005A8AC7 /* CoreData */,
|
DB45FB0925CA87BC005A8AC7 /* CoreData */,
|
||||||
2D61335625C1887F00CAE157 /* Persist */,
|
2D61335625C1887F00CAE157 /* Persist */,
|
||||||
2D61335D25C1894B00CAE157 /* APIService.swift */,
|
2D61335D25C1894B00CAE157 /* APIService.swift */,
|
||||||
|
2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */,
|
||||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */,
|
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */,
|
||||||
DB98336A25C9420100AD9700 /* APIService+App.swift */,
|
DB98336A25C9420100AD9700 /* APIService+App.swift */,
|
||||||
DB98337025C9443200AD9700 /* APIService+Authentication.swift */,
|
DB98337025C9443200AD9700 /* APIService+Authentication.swift */,
|
||||||
|
@ -692,6 +714,7 @@
|
||||||
DB89B9F025C10FD0008580ED /* CoreDataStack.h */,
|
DB89B9F025C10FD0008580ED /* CoreDataStack.h */,
|
||||||
DB89BA1125C1105C008580ED /* CoreDataStack.swift */,
|
DB89BA1125C1105C008580ED /* CoreDataStack.swift */,
|
||||||
DB89BA3525C1145C008580ED /* CoreData.xcdatamodeld */,
|
DB89BA3525C1145C008580ED /* CoreData.xcdatamodeld */,
|
||||||
|
2DF75BB725D1473400694EC8 /* Stack */,
|
||||||
DB89BA4025C1165F008580ED /* Protocol */,
|
DB89BA4025C1165F008580ED /* Protocol */,
|
||||||
DB89BA1725C1107F008580ED /* Extension */,
|
DB89BA1725C1107F008580ED /* Extension */,
|
||||||
DB89BA2C25C110B7008580ED /* Entity */,
|
DB89BA2C25C110B7008580ED /* Entity */,
|
||||||
|
@ -1207,6 +1230,7 @@
|
||||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||||
|
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
||||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
||||||
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||||
|
@ -1216,6 +1240,7 @@
|
||||||
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
||||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
||||||
|
2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */,
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||||
|
@ -1243,6 +1268,7 @@
|
||||||
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
|
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
|
||||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
||||||
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */,
|
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */,
|
||||||
|
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
|
||||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
||||||
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
||||||
|
@ -1283,6 +1309,7 @@
|
||||||
files = (
|
files = (
|
||||||
2DA7D05725CA693F00804E11 /* Application.swift in Sources */,
|
2DA7D05725CA693F00804E11 /* Application.swift in Sources */,
|
||||||
2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */,
|
2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */,
|
||||||
|
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */,
|
||||||
DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */,
|
DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */,
|
||||||
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
|
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
|
||||||
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */,
|
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */,
|
||||||
|
@ -1295,6 +1322,7 @@
|
||||||
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
|
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
|
||||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
|
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
|
||||||
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */,
|
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */,
|
||||||
|
2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */,
|
||||||
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */,
|
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */,
|
||||||
DB89BA1D25C1107F008580ED /* URL.swift in Sources */,
|
DB89BA1D25C1107F008580ED /* URL.swift in Sources */,
|
||||||
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */,
|
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */,
|
||||||
|
|
|
@ -34,17 +34,18 @@ extension TimelineSection {
|
||||||
// configure cell
|
// configure cell
|
||||||
managedObjectContext.performAndWait {
|
managedObjectContext.performAndWait {
|
||||||
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
TimelineSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot)
|
TimelineSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID)
|
||||||
}
|
}
|
||||||
cell.delegate = timelinePostTableViewCellDelegate
|
cell.delegate = timelinePostTableViewCellDelegate
|
||||||
return cell
|
return cell
|
||||||
case .toot(let objectID):
|
case .toot(let objectID):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelinePostTableViewCell.self), for: indexPath) as! TimelinePostTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelinePostTableViewCell.self), for: indexPath) as! TimelinePostTableViewCell
|
||||||
|
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
|
||||||
|
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
|
||||||
// configure cell
|
// configure cell
|
||||||
managedObjectContext.performAndWait {
|
managedObjectContext.performAndWait {
|
||||||
let toot = managedObjectContext.object(with: objectID) as! Toot
|
let toot = managedObjectContext.object(with: objectID) as! Toot
|
||||||
TimelineSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot)
|
TimelineSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID:requestUserID)
|
||||||
}
|
}
|
||||||
cell.delegate = timelinePostTableViewCellDelegate
|
cell.delegate = timelinePostTableViewCellDelegate
|
||||||
return cell
|
return cell
|
||||||
|
@ -69,7 +70,8 @@ extension TimelineSection {
|
||||||
static func configure(
|
static func configure(
|
||||||
cell: TimelinePostTableViewCell,
|
cell: TimelinePostTableViewCell,
|
||||||
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||||
toot: Toot
|
toot: Toot,
|
||||||
|
requestUserID: String
|
||||||
) {
|
) {
|
||||||
// set name username avatar
|
// set name username avatar
|
||||||
cell.timelinePostView.nameLabel.text = toot.author.displayName
|
cell.timelinePostView.nameLabel.text = toot.author.displayName
|
||||||
|
@ -81,6 +83,15 @@ extension TimelineSection {
|
||||||
)
|
)
|
||||||
// set text
|
// set text
|
||||||
cell.timelinePostView.activeTextLabel.config(content: toot.content)
|
cell.timelinePostView.activeTextLabel.config(content: toot.content)
|
||||||
|
|
||||||
|
// toolbar
|
||||||
|
let isLike = (toot.reblog ?? toot).favouritedBy.flatMap({ $0.contains(where: { $0.id == requestUserID }) }) ?? false
|
||||||
|
let favoriteCountTitle: String = {
|
||||||
|
let count = (toot.reblog ?? toot).favouritesCount.intValue
|
||||||
|
return TimelineSection.formattedNumberTitleForActionButton(count)
|
||||||
|
}()
|
||||||
|
cell.timelinePostView.actionToolbarContainer.starButton.setTitle(favoriteCountTitle, for: .normal)
|
||||||
|
cell.timelinePostView.actionToolbarContainer.isStarButtonHighlight = isLike
|
||||||
// set date
|
// set date
|
||||||
let createdAt = (toot.reblog ?? toot).createdAt
|
let createdAt = (toot.reblog ?? toot).createdAt
|
||||||
timestampUpdatePublisher
|
timestampUpdatePublisher
|
||||||
|
@ -88,6 +99,25 @@ extension TimelineSection {
|
||||||
cell.timelinePostView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
cell.timelinePostView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
|
// observe model change
|
||||||
|
ManagedObjectObserver.observe(object: toot.reblog ?? toot)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { _ in
|
||||||
|
// do nothing
|
||||||
|
} receiveValue: { change in
|
||||||
|
guard case let .update(object) = change.changeType,
|
||||||
|
let newToot = object as? Toot else { return }
|
||||||
|
let targetToot = newToot.reblog ?? newToot
|
||||||
|
|
||||||
|
let isLike = targetToot.favouritedBy.flatMap({ $0.contains(where: { $0.id == requestUserID }) }) ?? false
|
||||||
|
let favoriteCount = targetToot.favouritesCount.intValue
|
||||||
|
let favoriteCountTitle = TimelineSection.formattedNumberTitleForActionButton(favoriteCount)
|
||||||
|
cell.timelinePostView.actionToolbarContainer.starButton.setTitle(favoriteCountTitle, for: .normal)
|
||||||
|
cell.timelinePostView.actionToolbarContainer.isStarButtonHighlight = isLike
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: like count label for tweet %s did update: %ld", ((#file as NSString).lastPathComponent), #line, #function, targetToot.id, favoriteCount )
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// StatusProvider+TimelinePostTableViewCellDelegate.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
|
// MARK: - ActionToolbarContainerDelegate
|
||||||
|
extension TimelinePostTableViewCellDelegate where Self: StatusProvider {
|
||||||
|
|
||||||
|
func timelinePostTableViewCell(_ cell: TimelinePostTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) {
|
||||||
|
StatusProviderFacade.responseToStatusLikeAction(provider: self, cell: cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
//
|
||||||
|
// StatusProviderFacade.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
|
enum StatusProviderFacade {
|
||||||
|
|
||||||
|
}
|
||||||
|
extension StatusProviderFacade {
|
||||||
|
|
||||||
|
static func responseToStatusLikeAction(provider: StatusProvider) {
|
||||||
|
_responseToStatusLikeAction(
|
||||||
|
provider: provider,
|
||||||
|
toot: provider.toot()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func responseToStatusLikeAction(provider: StatusProvider, cell: UITableViewCell) {
|
||||||
|
_responseToStatusLikeAction(
|
||||||
|
provider: provider,
|
||||||
|
toot: provider.toot(for: cell, indexPath: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func _responseToStatusLikeAction(provider: StatusProvider, toot: Future<Toot?, Never>) {
|
||||||
|
// prepare authentication
|
||||||
|
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare current user infos
|
||||||
|
guard let _currentMastodonUser = provider.context.authenticationService.activeMastodonAuthentication.value?.user else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mastodonUserID = activeMastodonAuthenticationBox.userID
|
||||||
|
assert(_currentMastodonUser.id == mastodonUserID)
|
||||||
|
let mastodonUserObjectID = _currentMastodonUser.objectID
|
||||||
|
|
||||||
|
guard let context = provider.context else { return }
|
||||||
|
|
||||||
|
// haptic feedback generator
|
||||||
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
|
||||||
|
toot
|
||||||
|
.compactMap { toot -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in
|
||||||
|
guard let toot = toot else { return nil }
|
||||||
|
let favoriteKind: Mastodon.API.Favorites.FavoriteKind = {
|
||||||
|
let targetToot = (toot.reblog ?? toot)
|
||||||
|
let isLiked = targetToot.favouritedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
|
||||||
|
return isLiked ? .destroy : .create
|
||||||
|
}()
|
||||||
|
return (toot.objectID, favoriteKind)
|
||||||
|
}
|
||||||
|
.map { tootObjectID, favoriteKind -> AnyPublisher<(Toot.ID, Mastodon.API.Favorites.FavoriteKind), Error> in
|
||||||
|
return context.apiService.like(
|
||||||
|
tootObjectID: tootObjectID,
|
||||||
|
mastodonUserObjectID: mastodonUserObjectID,
|
||||||
|
favoriteKind: favoriteKind
|
||||||
|
)
|
||||||
|
.map { tootID in (tootID, favoriteKind) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
.switchToLatest()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.handleEvents { _ in
|
||||||
|
generator.prepare()
|
||||||
|
responseFeedbackGenerator.prepare()
|
||||||
|
} receiveOutput: { _, favoriteKind in
|
||||||
|
generator.impactOccurred()
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: [Like] update local toot like status to: %s", ((#file as NSString).lastPathComponent), #line, #function, favoriteKind == .create ? "like" : "unlike")
|
||||||
|
} receiveCompletion: { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
// TODO: handle error
|
||||||
|
break
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { tootID, favoriteKind in
|
||||||
|
return context.apiService.like(
|
||||||
|
statusID: tootID,
|
||||||
|
favoriteKind: favoriteKind,
|
||||||
|
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.switchToLatest()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak provider] completion in
|
||||||
|
guard let provider = provider else { return }
|
||||||
|
if provider.view.window != nil {
|
||||||
|
responseFeedbackGenerator.impactOccurred()
|
||||||
|
}
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: [Like] remote like request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
case .finished:
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: [Like] remote like request success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
.store(in: &provider.disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusProviderFacade {
|
||||||
|
enum Target {
|
||||||
|
case toot
|
||||||
|
case reblog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
// MARK: - StatusProvider
|
// MARK: - StatusProvider
|
||||||
extension PublicTimelineViewController {
|
extension PublicTimelineViewController: StatusProvider {
|
||||||
|
|
||||||
func toot() -> Future<Toot?, Never> {
|
func toot() -> Future<Toot?, Never> {
|
||||||
return Future { promise in promise(.success(nil)) }
|
return Future { promise in promise(.success(nil)) }
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Combine
|
||||||
|
|
||||||
|
|
||||||
protocol TimelinePostTableViewCellDelegate: class {
|
protocol TimelinePostTableViewCellDelegate: class {
|
||||||
|
func timelinePostTableViewCell(_ cell: TimelinePostTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class TimelinePostTableViewCell: UITableViewCell {
|
final class TimelinePostTableViewCell: UITableViewCell {
|
||||||
|
@ -61,6 +61,26 @@ extension TimelinePostTableViewCell {
|
||||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: timelinePostView.trailingAnchor),
|
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: timelinePostView.trailingAnchor),
|
||||||
contentView.bottomAnchor.constraint(equalTo: timelinePostView.bottomAnchor), // use action toolbar margin
|
contentView.bottomAnchor.constraint(equalTo: timelinePostView.bottomAnchor), // use action toolbar margin
|
||||||
])
|
])
|
||||||
|
|
||||||
|
timelinePostView.actionToolbarContainer.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
// MARK: - ActionToolbarContainerDelegate
|
||||||
|
extension TimelinePostTableViewCell: ActionToolbarContainerDelegate {
|
||||||
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {
|
||||||
|
|
||||||
|
}
|
||||||
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, retootButtonDidPressed sender: UIButton) {
|
||||||
|
|
||||||
|
}
|
||||||
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) {
|
||||||
|
delegate?.timelinePostTableViewCell(self, actionToolbarContainer: actionToolbarContainer, likeButtonDidPressed: sender)
|
||||||
|
}
|
||||||
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, bookmarkButtonDidPressed sender: UIButton) {
|
||||||
|
|
||||||
|
}
|
||||||
|
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, moreButtonDidPressed sender: UIButton) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,8 +26,8 @@ final class ActionToolbarContainer: UIView {
|
||||||
let bookmartButton = HitTestExpandedButton()
|
let bookmartButton = HitTestExpandedButton()
|
||||||
let moreButton = HitTestExpandedButton()
|
let moreButton = HitTestExpandedButton()
|
||||||
|
|
||||||
var isstarButtonHighlight: Bool = false {
|
var isStarButtonHighlight: Bool = false {
|
||||||
didSet { isstarButtonHighlightStateDidChange(to: isstarButtonHighlight) }
|
didSet { isStarButtonHighlightStateDidChange(to: isStarButtonHighlight) }
|
||||||
}
|
}
|
||||||
|
|
||||||
weak var delegate: ActionToolbarContainerDelegate?
|
weak var delegate: ActionToolbarContainerDelegate?
|
||||||
|
@ -164,7 +164,7 @@ extension ActionToolbarContainer {
|
||||||
return oldStyle != style
|
return oldStyle != style
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isstarButtonHighlightStateDidChange(to isHighlight: Bool) {
|
private func isStarButtonHighlightStateDidChange(to isHighlight: Bool) {
|
||||||
let tintColor = isHighlight ? Asset.Colors.systemOrange.color : Asset.Colors.Label.secondary.color
|
let tintColor = isHighlight ? Asset.Colors.systemOrange.color : Asset.Colors.Label.secondary.color
|
||||||
starButton.tintColor = tintColor
|
starButton.tintColor = tintColor
|
||||||
starButton.setTitleColor(tintColor, for: .normal)
|
starButton.setTitleColor(tintColor, for: .normal)
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
//
|
||||||
|
// APIService+Favorite.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import MastodonSDK
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import CommonOSLog
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
// make local state change only
|
||||||
|
func like(
|
||||||
|
tootObjectID: NSManagedObjectID,
|
||||||
|
mastodonUserObjectID: NSManagedObjectID,
|
||||||
|
favoriteKind: Mastodon.API.Favorites.FavoriteKind
|
||||||
|
) -> AnyPublisher<Toot.ID, Error> {
|
||||||
|
var _targetTootID: Toot.ID?
|
||||||
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
|
return managedObjectContext.performChanges {
|
||||||
|
let toot = managedObjectContext.object(with: tootObjectID) as! Toot
|
||||||
|
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
||||||
|
let targetToot = toot.reblog ?? toot
|
||||||
|
let targetTootID = targetToot.id
|
||||||
|
_targetTootID = targetTootID
|
||||||
|
|
||||||
|
targetToot.update(liked: favoriteKind == .create, mastodonUser: mastodonUser)
|
||||||
|
|
||||||
|
}
|
||||||
|
.tryMap { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
guard let targetTootID = _targetTootID else {
|
||||||
|
throw APIError.implicit(.badRequest)
|
||||||
|
}
|
||||||
|
return targetTootID
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
// send favorite request to remote
|
||||||
|
func like(
|
||||||
|
statusID: Mastodon.Entity.Status.ID,
|
||||||
|
favoriteKind: Mastodon.API.Favorites.FavoriteKind,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||||
|
return Mastodon.API.Favorites.favorites(domain: mastodonAuthenticationBox.domain, statusID: statusID, session: session, authorization: authorization, favoriteKind: favoriteKind)
|
||||||
|
.map { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
|
||||||
|
let log = OSLog.api
|
||||||
|
let entity = response.value
|
||||||
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
|
|
||||||
|
return managedObjectContext.performChanges {
|
||||||
|
let _requestMastodonUser: MastodonUser? = {
|
||||||
|
let request = MastodonUser.sortedFetchRequest
|
||||||
|
request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID)
|
||||||
|
request.fetchLimit = 1
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
do {
|
||||||
|
return try managedObjectContext.fetch(request).first
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
let _oldToot: Toot? = {
|
||||||
|
let request = Toot.sortedFetchRequest
|
||||||
|
request.predicate = Toot.predicate(domain: mastodonAuthenticationBox.domain, id: entity.id)
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)]
|
||||||
|
do {
|
||||||
|
return try managedObjectContext.fetch(request).first
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let requestMastodonUser = _requestMastodonUser,
|
||||||
|
let oldToot = _oldToot else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
APIService.CoreData.mergeToot(for: requestMastodonUser, old: oldToot, in: mastodonAuthenticationBox.domain, entity: entity, networkDate: response.networkDate)
|
||||||
|
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s like status to: %{public}s. now %ld likes", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.favourited.flatMap { $0 ? "like" : "unlike" } ?? "<nil>", entity.favouritesCount )
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.switchToLatest()
|
||||||
|
.handleEvents(receiveCompletion: { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
print(error)
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
func likeList(
|
||||||
|
limit: Int = 200,
|
||||||
|
userID: String,
|
||||||
|
maxID: String? = nil,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||||
|
|
||||||
|
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||||
|
let query = Mastodon.API.Favorites.ListQuery(limit: limit, minID: nil, maxID: maxID)
|
||||||
|
return Mastodon.API.Favorites.getFavoriteStatus(domain: mastodonAuthenticationBox.domain, session: session, authorization: mastodonAuthenticationBox.userAuthorization, query: query)
|
||||||
|
.map { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> in
|
||||||
|
let log = OSLog.api
|
||||||
|
|
||||||
|
return APIService.Persist.persistTimeline(
|
||||||
|
managedObjectContext: self.backgroundManagedObjectContext,
|
||||||
|
domain: mastodonAuthenticationBox.domain,
|
||||||
|
query: query as! TimelineQueryType,
|
||||||
|
response: response,
|
||||||
|
persistType: .likeList,
|
||||||
|
requestMastodonUserID: requestMastodonUserID,
|
||||||
|
log: log
|
||||||
|
)
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.switchToLatest()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,11 +73,11 @@ extension APIService.CoreData {
|
||||||
mentions: metions,
|
mentions: metions,
|
||||||
emojis: emojis,
|
emojis: emojis,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
favouritedBy: requestMastodonUser,
|
favouritedBy: (entity.favourited ?? false) ? mastodonUser : nil,
|
||||||
rebloggedBy: requestMastodonUser,
|
rebloggedBy: (entity.reblogged ?? false) ? mastodonUser : nil,
|
||||||
mutedBy: requestMastodonUser,
|
mutedBy: (entity.muted ?? false) ? mastodonUser : nil,
|
||||||
bookmarkedBy: requestMastodonUser,
|
bookmarkedBy: (entity.bookmarked ?? false) ? mastodonUser : nil,
|
||||||
pinnedBy: requestMastodonUser
|
pinnedBy: (entity.pinned ?? false) ? mastodonUser : nil
|
||||||
)
|
)
|
||||||
return (toot, true, isMastodonUserCreated)
|
return (toot, true, isMastodonUserCreated)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ extension APIService.Persist {
|
||||||
enum PersistTimelineType {
|
enum PersistTimelineType {
|
||||||
case `public`
|
case `public`
|
||||||
case home
|
case home
|
||||||
|
case likeList
|
||||||
}
|
}
|
||||||
|
|
||||||
static func persistTimeline(
|
static func persistTimeline(
|
||||||
|
@ -92,6 +93,7 @@ extension APIService.Persist {
|
||||||
switch persistType {
|
switch persistType {
|
||||||
case .public: return .publicTimeline
|
case .public: return .publicTimeline
|
||||||
case .home: return .homeTimeline
|
case .home: return .homeTimeline
|
||||||
|
case .likeList: return .favoriteTimeline
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
//
|
||||||
|
// Mastodon+API+Favorites.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/7.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Mastodon.API.Favorites {
|
||||||
|
static func favoritesStatusesEndpointURL(domain: String) -> URL {
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("favourites")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func favoriteByUserListsEndpointURL(domain: String, statusID: String) -> URL {
|
||||||
|
let pathComponent = "statuses/" + statusID + "/favourited_by"
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func favoriteActionEndpointURL(domain: String, statusID: String, favoriteKind: FavoriteKind) -> URL {
|
||||||
|
var actionString: String
|
||||||
|
switch favoriteKind {
|
||||||
|
case .create:
|
||||||
|
actionString = "/favourite"
|
||||||
|
case .destroy:
|
||||||
|
actionString = "/unfavourite"
|
||||||
|
}
|
||||||
|
let pathComponent = "statuses/" + statusID + actionString
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func favorites(domain: String, statusID: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization, favoriteKind: FavoriteKind) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||||
|
let url: URL = favoriteActionEndpointURL(domain: domain, statusID: statusID, favoriteKind: favoriteKind)
|
||||||
|
var request = Mastodon.API.post(url: url, query: nil, authorization: authorization)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func getFavoriteByUserLists(domain: String, statusID: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
|
||||||
|
let url = favoriteByUserListsEndpointURL(domain: domain, statusID: statusID)
|
||||||
|
let request = Mastodon.API.get(url: url, query: nil, authorization: authorization)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Account].self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func getFavoriteStatus(domain: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization, query: Mastodon.API.Favorites.ListQuery) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||||
|
let url = favoritesStatusesEndpointURL(domain: domain)
|
||||||
|
let request = Mastodon.API.get(url: url, query: query, authorization: authorization)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Status].self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Mastodon.API.Favorites {
|
||||||
|
enum FavoriteKind {
|
||||||
|
case create
|
||||||
|
case destroy
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ListQuery: GetQuery,TimelineQueryType {
|
||||||
|
|
||||||
|
public var limit: Int?
|
||||||
|
public var minID: String?
|
||||||
|
public var maxID: String?
|
||||||
|
public var sinceID: Mastodon.Entity.Status.ID?
|
||||||
|
|
||||||
|
public init(limit: Int? = nil, minID: String? = nil, maxID: String? = nil, sinceID: String? = nil) {
|
||||||
|
self.limit = limit
|
||||||
|
self.minID = minID
|
||||||
|
self.maxID = maxID
|
||||||
|
self.sinceID = sinceID
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryItems: [URLQueryItem]? {
|
||||||
|
var items: [URLQueryItem] = []
|
||||||
|
if let limit = self.limit {
|
||||||
|
items.append(URLQueryItem(name: "limit", value: String(limit)))
|
||||||
|
}
|
||||||
|
if let minID = self.minID {
|
||||||
|
items.append(URLQueryItem(name: "min_id", value: minID))
|
||||||
|
}
|
||||||
|
if let maxID = self.maxID {
|
||||||
|
items.append(URLQueryItem(name: "max_id", value: maxID))
|
||||||
|
}
|
||||||
|
if let sinceID = self.sinceID {
|
||||||
|
items.append(URLQueryItem(name: "since_id", value: sinceID))
|
||||||
|
}
|
||||||
|
guard !items.isEmpty else { return nil }
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -87,6 +87,7 @@ extension Mastodon.API {
|
||||||
public enum Instance { }
|
public enum Instance { }
|
||||||
public enum OAuth { }
|
public enum OAuth { }
|
||||||
public enum Timeline { }
|
public enum Timeline { }
|
||||||
|
public enum Favorites { }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.API {
|
extension Mastodon.API {
|
||||||
|
|
Loading…
Reference in New Issue