diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index ce454981b..e8d8dd605 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -1,6 +1,7 @@ + @@ -116,13 +117,14 @@ + + - \ No newline at end of file diff --git a/CoreDataStack/Entity/Toot.swift b/CoreDataStack/Entity/Toot.swift index 65971ffe9..c43d0d6c3 100644 --- a/CoreDataStack/Entity/Toot.swift +++ b/CoreDataStack/Entity/Toot.swift @@ -89,9 +89,8 @@ public extension Toot { toot.sensitive = property.sensitive toot.spoilerText = property.spoilerText - if let application = property.application { - toot.mutableSetValue(forKey: #keyPath(Toot.application)).add(application) - } + toot.application = property.application + if let mentions = property.mentions { toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions) } @@ -139,6 +138,28 @@ public extension Toot { return toot } + func update(reblogsCount: NSNumber) { + if self.reblogsCount.intValue != reblogsCount.intValue { + self.reblogsCount = reblogsCount + } + } + func update(favouritesCount: NSNumber) { + if self.favouritesCount.intValue != favouritesCount.intValue { + self.favouritesCount = favouritesCount + } + } + func update(repliesCount: NSNumber?) { + guard let count = repliesCount else { + return + } + if self.repliesCount?.intValue != count.intValue { + self.repliesCount = repliesCount + } + } + func didUpdate(at networkDate: Date) { + self.updatedAt = networkDate + } + } public extension Toot { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index eb61a8d76..e67aded30 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; + 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */; }; 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */; }; @@ -36,7 +37,6 @@ 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; }; 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; }; - 2DA7D05125CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */; }; 2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; }; 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; }; 3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */; }; @@ -165,6 +165,7 @@ 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; + 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = ""; }; 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewModel.swift; sourceTree = ""; }; 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; @@ -573,6 +574,7 @@ DB45FB0925CA87BC005A8AC7 /* CoreData */ = { isa = PBXGroup; children = ( + 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */, DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */, DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */, ); @@ -1067,6 +1069,7 @@ 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */, 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, + 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */, DB98338825C945ED00AD9700 /* Assets.swift in Sources */, 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */, DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */, diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift new file mode 100644 index 000000000..8f11fa214 --- /dev/null +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift @@ -0,0 +1,130 @@ +// +// APIService+CoreData+Toot.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/3. +// + +import Foundation +import CoreData +import CoreDataStack +import CommonOSLog +import MastodonSDK + +extension APIService.CoreData { + + static func createOrMergeTweet( + into managedObjectContext: NSManagedObjectContext, + for requestMastodonUser: MastodonUser, + entity: Mastodon.Entity.Toot, + domain: String, + networkDate: Date, + log: OSLog + ) -> (Toot: Toot, isTweetCreated: Bool, isMastodonUserCreated: Bool) { + + // build tree + let reblog = entity.reblog.flatMap { entity -> Toot in + let (toot, _, _) = createOrMergeTweet(into: managedObjectContext, for: requestMastodonUser, entity: entity,domain: domain, networkDate: networkDate, log: log) + return toot + } + + // fetch old Toot + let oldTweet: Toot? = { + let request = Toot.sortedFetchRequest + request.predicate = Toot.predicate(idStr: entity.id) + request.returnsObjectsAsFaults = false + do { + return try managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + + if let oldTweet = oldTweet { + // merge old Toot + APIService.CoreData.mergeToot(for: requestMastodonUser, old: oldTweet,in: domain, entity: entity, networkDate: networkDate) + return (oldTweet, false, false) + } else { + + let (mastodonUser, isMastodonUserCreated) = createOrMergeMastodonUser(into: managedObjectContext, for: requestMastodonUser,in: domain, entity: entity.account, networkDate: networkDate, log: log) + let application = entity.application.flatMap { (app) -> Application? in + Application.insert(into: managedObjectContext, property: Application.Property(name: app.name, website: app.website, vapidKey: app.vapidKey)) + } + + let metions = entity.mentions?.compactMap({ (mention) -> Mention in + Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url)) + }) + let emojis = entity.emojis?.compactMap({ (emoji) -> Emoji in + Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category)) + }) + let tags = entity.tags?.compactMap({ (tag) -> Tag in + let histories = tag.history?.compactMap({ (history) -> History in + History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts)) + }) + return Tag.insert(into: managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories)) + }) + let tootProperty = Toot.Property( + domain: domain, + id: entity.id, + uri: entity.uri, + createdAt: entity.createdAt, + content: entity.content, + visibility: entity.visibility?.rawValue, + sensitive: entity.sensitive ?? false, + spoilerText: entity.spoilerText, + application: application, + mentions: metions, + emojis: emojis, + tags: tags, + reblogsCount: NSNumber(value: entity.reblogsCount), + favouritesCount: NSNumber(value: entity.favouritesCount), + repliesCount: (entity.repliesCount != nil) ? NSNumber(value: entity.repliesCount!) : nil, + url: entity.uri, + inReplyToID: entity.inReplyToID, + inReplyToAccountID: entity.inReplyToAccountID, + reblog: reblog, + language: entity.language, + text: entity.text, + favouritedBy: (entity.favourited ?? false) ? mastodonUser : nil, + rebloggedBy: (entity.reblogged ?? false) ? mastodonUser : nil, + mutedBy: (entity.muted ?? false) ? mastodonUser : nil, + bookmarkedBy: (entity.bookmarked ?? false) ? mastodonUser : nil, + pinnedBy: (entity.pinned ?? false) ? mastodonUser : nil, + updatedAt: networkDate, + deletedAt: nil, + author: requestMastodonUser, + homeTimelineIndexes: nil) + let toot = Toot.insert(into: managedObjectContext, property: tootProperty, author: mastodonUser) + return (toot, true, isMastodonUserCreated) + } + } + static func mergeToot(for requestMastodonUser: MastodonUser?, old toot: Toot,in domain: String, entity: Mastodon.Entity.Toot, networkDate: Date) { + guard networkDate > toot.updatedAt else { return } + + // merge + if entity.favouritesCount != toot.favouritesCount.intValue { + toot.update(favouritesCount:NSNumber(value: entity.favouritesCount)) + } + if let repliesCount = entity.repliesCount { + if (repliesCount != toot.repliesCount?.intValue) { + toot.update(repliesCount:NSNumber(value: repliesCount)) + } + } + if entity.reblogsCount != toot.reblogsCount.intValue { + toot.update(reblogsCount:NSNumber(value: entity.reblogsCount)) + } + + + // set updateAt + toot.didUpdate(at: networkDate) + + // merge user + mergeMastodonUser(for: requestMastodonUser, old: toot.author, in: domain, entity: entity.account, networkDate: networkDate) + // merge indirect reblog & quote + if let reblog = entity.reblog { + mergeToot(for: requestMastodonUser, old: toot.reblog!,in: domain, entity: reblog, networkDate: networkDate) + } + } + +}