From 807dfd9ea7b26eb811915b770d04a99bbfd18b41 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 10 Mar 2021 16:38:14 +0800 Subject: [PATCH 1/5] feat: profile persist logic. Add replyTo and replyFrom relationship for Toot --- .../CoreData.xcdatamodel/contents | 6 +- CoreDataStack/Entity/Toot.swift | 3 + Mastodon.xcodeproj/project.pbxproj | 16 +- .../APIService/APIService+Account.swift | 2 + .../APIService/APIService+Favorite.swift | 2 +- .../APIService/APIService+HomeTimeline.swift | 2 +- .../APIService+PublicTimeline.swift | 2 +- .../APIService+CoreData+MastodonUser.swift | 25 +- .../CoreData/APIService+CoreData+Toot.swift | 55 ++- .../APIService+Persist+PersistCache.swift | 66 +++ .../APIService+Persist+PersistMemo.swift | 226 +++++++++ .../Persist/APIService+Persist+Timeline.swift | 446 ------------------ .../Persist/APIService+Persist+Toot.swift | 251 ++++++++++ 13 files changed, 624 insertions(+), 478 deletions(-) create mode 100644 Mastodon/Service/APIService/Persist/APIService+Persist+PersistCache.swift create mode 100644 Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift delete mode 100644 Mastodon/Service/APIService/Persist/APIService+Persist+Timeline.swift create mode 100644 Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index be40ac57d..c9ebac45f 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -159,6 +159,8 @@ + + @@ -173,6 +175,6 @@ - + \ No newline at end of file diff --git a/CoreDataStack/Entity/Toot.swift b/CoreDataStack/Entity/Toot.swift index c5fcf4869..3a2c91775 100644 --- a/CoreDataStack/Entity/Toot.swift +++ b/CoreDataStack/Entity/Toot.swift @@ -39,6 +39,7 @@ public final class Toot: NSManagedObject { // many-to-one relastionship @NSManaged public private(set) var author: MastodonUser @NSManaged public private(set) var reblog: Toot? + @NSManaged public private(set) var replyTo: Toot? // many-to-many relastionship @NSManaged public private(set) var favouritedBy: Set? @@ -57,6 +58,7 @@ public final class Toot: NSManagedObject { @NSManaged public private(set) var tags: Set? @NSManaged public private(set) var homeTimelineIndexes: Set? @NSManaged public private(set) var mediaAttachments: Set? + @NSManaged public private(set) var replyFrom: Set? @NSManaged public private(set) var updatedAt: Date @NSManaged public private(set) var deletedAt: Date? @@ -70,6 +72,7 @@ public extension Toot { author: MastodonUser, reblog: Toot?, application: Application?, + replyTo: Toot?, poll: Poll?, mentions: [Mention]?, emojis: [Emoji]?, diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f2db77a66..5528d0234 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -56,7 +56,7 @@ 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; }; - 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; }; + 2D61335825C188A000CAE157 /* APIService+Persist+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Toot.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; @@ -149,6 +149,8 @@ DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; }; DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; }; DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; }; + DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; }; + DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; }; DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; @@ -293,7 +295,7 @@ 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = ""; }; 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = ""; }; 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = ""; }; - 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = ""; }; + 2D61335725C188A000CAE157 /* APIService+Persist+Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Toot.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = ""; }; 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; @@ -395,6 +397,8 @@ DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; }; + DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = ""; }; + DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = ""; }; DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -658,7 +662,9 @@ 2D61335625C1887F00CAE157 /* Persist */ = { isa = PBXGroup; children = ( - 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */, + 2D61335725C188A000CAE157 /* APIService+Persist+Toot.swift */, + DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */, + DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */, ); path = Persist; sourceTree = ""; @@ -1578,7 +1584,7 @@ 0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */, DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */, DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */, - 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */, + 2D61335825C188A000CAE157 /* APIService+Persist+Toot.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */, @@ -1605,6 +1611,7 @@ 2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */, 2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */, 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */, + DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */, 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */, 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */, @@ -1654,6 +1661,7 @@ DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */, + DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */, DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, diff --git a/Mastodon/Service/APIService/APIService+Account.swift b/Mastodon/Service/APIService/APIService+Account.swift index 2218bfa50..d8ea5cf4f 100644 --- a/Mastodon/Service/APIService/APIService+Account.swift +++ b/Mastodon/Service/APIService/APIService+Account.swift @@ -31,6 +31,7 @@ extension APIService { for: nil, in: domain, entity: account, + userCache: nil, networkDate: response.networkDate, log: log) let flag = isCreated ? "+" : "-" @@ -64,6 +65,7 @@ extension APIService { for: nil, in: domain, entity: account, + userCache: nil, networkDate: response.networkDate, log: log) let flag = isCreated ? "+" : "-" diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift index e1d5febe7..3eee6b6e1 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/Mastodon/Service/APIService/APIService+Favorite.swift @@ -136,7 +136,7 @@ extension APIService { .map { response -> AnyPublisher, Error> in let log = OSLog.api - return APIService.Persist.persistTimeline( + return APIService.Persist.persistToots( managedObjectContext: self.backgroundManagedObjectContext, domain: mastodonAuthenticationBox.domain, query: query, diff --git a/Mastodon/Service/APIService/APIService+HomeTimeline.swift b/Mastodon/Service/APIService/APIService+HomeTimeline.swift index 0112a9da7..125c05208 100644 --- a/Mastodon/Service/APIService/APIService+HomeTimeline.swift +++ b/Mastodon/Service/APIService/APIService+HomeTimeline.swift @@ -40,7 +40,7 @@ extension APIService { authorization: authorization ) .flatMap { response -> AnyPublisher, Error> in - return APIService.Persist.persistTimeline( + return APIService.Persist.persistToots( managedObjectContext: self.backgroundManagedObjectContext, domain: domain, query: query, diff --git a/Mastodon/Service/APIService/APIService+PublicTimeline.swift b/Mastodon/Service/APIService/APIService+PublicTimeline.swift index bfa3bb26e..288d1fd2e 100644 --- a/Mastodon/Service/APIService/APIService+PublicTimeline.swift +++ b/Mastodon/Service/APIService/APIService+PublicTimeline.swift @@ -39,7 +39,7 @@ extension APIService { query: query ) .flatMap { response -> AnyPublisher, Error> in - return APIService.Persist.persistTimeline( + return APIService.Persist.persistToots( managedObjectContext: self.backgroundManagedObjectContext, domain: domain, query: query, diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift index 4f35a54c1..512e224d2 100644 --- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift @@ -18,6 +18,7 @@ extension APIService.CoreData { for requestMastodonUser: MastodonUser?, in domain: String, entity: Mastodon.Entity.Account, + userCache: APIService.Persist.PersistCache?, networkDate: Date, log: OSLog ) -> (user: MastodonUser, isCreated: Bool) { @@ -29,15 +30,19 @@ extension APIService.CoreData { // fetch old mastodon user let oldMastodonUser: MastodonUser? = { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: domain, id: entity.id) - request.fetchLimit = 1 - request.returnsObjectsAsFaults = false - do { - return try managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil + if let userCache = userCache { + return userCache.dictionary[entity.id] + } else { + let request = MastodonUser.sortedFetchRequest + request.predicate = MastodonUser.predicate(domain: domain, id: entity.id) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } } }() @@ -57,7 +62,7 @@ extension APIService.CoreData { into: managedObjectContext, property: mastodonUserProperty ) - + userCache?.dictionary[entity.id] = mastodonUser os_signpost(.event, log: log, name: "update database - process entity: createOrMergeMastodonUser", signpostID: processEntityTaskSignpostID, "did insert new mastodon user %{public}s: name %s", mastodonUser.identifier, mastodonUser.username) return (mastodonUser, true) } diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift index 79fad947e..a4db46a8c 100644 --- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift @@ -16,29 +16,49 @@ extension APIService.CoreData { static func createOrMergeToot( into managedObjectContext: NSManagedObjectContext, for requestMastodonUser: MastodonUser?, - entity: Mastodon.Entity.Status, domain: String, + entity: Mastodon.Entity.Status, + tootCache: APIService.Persist.PersistCache?, + userCache: APIService.Persist.PersistCache?, networkDate: Date, log: OSLog ) -> (Toot: Toot, isTootCreated: Bool, isMastodonUserCreated: Bool) { - + let processEntityTaskSignpostID = OSSignpostID(log: log) + os_signpost(.begin, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "process toot %{public}s", entity.id) + defer { + os_signpost(.end, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "process toot %{public}s", entity.id) + } + // build tree let reblog = entity.reblog.flatMap { entity -> Toot in - let (toot, _, _) = createOrMergeToot(into: managedObjectContext, for: requestMastodonUser, entity: entity, domain: domain, networkDate: networkDate, log: log) + let (toot, _, _) = createOrMergeToot( + into: managedObjectContext, + for: requestMastodonUser, + domain: domain, + entity: entity, + tootCache: tootCache, + userCache: userCache, + networkDate: networkDate, + log: log + ) return toot } // fetch old Toot let oldToot: Toot? = { - let request = Toot.sortedFetchRequest - request.predicate = Toot.predicate(domain: domain, id: entity.id) - request.fetchLimit = 1 - request.returnsObjectsAsFaults = false - do { - return try managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil + if let tootCache = tootCache { + return tootCache.dictionary[entity.id] + } else { + let request = Toot.sortedFetchRequest + request.predicate = Toot.predicate(domain: domain, id: entity.id) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } } }() @@ -47,10 +67,16 @@ extension APIService.CoreData { APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate) return (oldToot, false, false) } else { - let (mastodonUser, isMastodonUserCreated) = createOrMergeMastodonUser(into: managedObjectContext, for: requestMastodonUser,in: domain, entity: entity.account, networkDate: networkDate, log: log) + let (mastodonUser, isMastodonUserCreated) = createOrMergeMastodonUser(into: managedObjectContext, for: requestMastodonUser,in: domain, entity: entity.account, userCache: userCache, 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 replyTo: Toot? = { + // could be nil if target replyTo toot's persist task in the queue + guard let inReplyToID = entity.inReplyToID, + let replyTo = tootCache?.dictionary[inReplyToID] else { return nil } + return replyTo + }() let poll = entity.poll.flatMap { poll -> Poll in let options = poll.options.enumerated().map { i, option -> PollOption in let votedBy: MastodonUser? = (poll.ownVotes ?? []).contains(i) ? requestMastodonUser : nil @@ -92,6 +118,7 @@ extension APIService.CoreData { author: mastodonUser, reblog: reblog, application: application, + replyTo: replyTo, poll: poll, mentions: metions, emojis: emojis, @@ -103,6 +130,8 @@ extension APIService.CoreData { bookmarkedBy: (entity.bookmarked ?? false) ? requestMastodonUser : nil, pinnedBy: (entity.pinned ?? false) ? requestMastodonUser : nil ) + tootCache?.dictionary[entity.id] = toot + os_signpost(.event, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "did insert new tweet %{public}s: %s", mastodonUser.identifier, entity.id) return (toot, true, isMastodonUserCreated) } } diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+PersistCache.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+PersistCache.swift new file mode 100644 index 000000000..16461494a --- /dev/null +++ b/Mastodon/Service/APIService/Persist/APIService+Persist+PersistCache.swift @@ -0,0 +1,66 @@ +// +// APIService+Persist+PersistCache.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-10. +// + +import Foundation +import CoreDataStack +import MastodonSDK + +extension APIService.Persist { + + class PersistCache { + var dictionary: [String : T] = [:] + } + +} + +extension APIService.Persist.PersistCache where T == Toot { + + static func ids(for toots: [Mastodon.Entity.Status]) -> Set { + var value = Set() + for toot in toots { + value = value.union(ids(for: toot)) + } + return value + } + + static func ids(for toot: Mastodon.Entity.Status) -> Set { + var value = Set() + value.insert(toot.id) + if let inReplyToID = toot.inReplyToID { + value.insert(inReplyToID) + } + if let reblog = toot.reblog { + value = value.union(ids(for: reblog)) + } + return value + } + +} + +extension APIService.Persist.PersistCache where T == MastodonUser { + + static func ids(for toots: [Mastodon.Entity.Status]) -> Set { + var value = Set() + for toot in toots { + value = value.union(ids(for: toot)) + } + return value + } + + static func ids(for toot: Mastodon.Entity.Status) -> Set { + var value = Set() + value.insert(toot.account.id) + if let inReplyToAccountID = toot.inReplyToAccountID { + value.insert(inReplyToAccountID) + } + if let reblog = toot.reblog { + value = value.union(ids(for: reblog)) + } + return value + } + +} diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift new file mode 100644 index 000000000..08696ec55 --- /dev/null +++ b/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift @@ -0,0 +1,226 @@ +// +// APIService+Persist+PersistMemo.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-10. +// + +import os.log +import Foundation +import CoreData +import CoreDataStack +import MastodonSDK + +extension APIService.Persist { + + class PersistMemo { + + let status: T + let children: [PersistMemo] + let memoType: MemoType + let statusProcessType: ProcessType + let authorProcessType: ProcessType + + enum MemoType { + case homeTimeline + case mentionTimeline + case userTimeline + case publicTimeline + case likeList + case searchList + case lookUp + + case reblog + + var flag: String { + switch self { + case .homeTimeline: return "H" + case .mentionTimeline: return "M" + case .userTimeline: return "U" + case .publicTimeline: return "P" + case .likeList: return "L" + case .searchList: return "S" + case .lookUp: return "LU" + case .reblog: return "R" + } + } + } + + enum ProcessType { + case create + case merge + + var flag: String { + switch self { + case .create: return "+" + case .merge: return "~" + } + } + } + + init( + status: T, + children: [PersistMemo], + memoType: MemoType, + statusProcessType: ProcessType, + authorProcessType: ProcessType + ) { + self.status = status + self.children = children + self.memoType = memoType + self.statusProcessType = statusProcessType + self.authorProcessType = authorProcessType + } + + } + +} + +extension APIService.Persist.PersistMemo { + + struct Counting { + var status = Counter() + var user = Counter() + + static func + (left: Counting, right: Counting) -> Counting { + return Counting( + status: left.status + right.status, + user: left.user + right.user + ) + } + + struct Counter { + var create = 0 + var merge = 0 + + static func + (left: Counter, right: Counter) -> Counter { + return Counter( + create: left.create + right.create, + merge: left.merge + right.merge + ) + } + } + } + + func count() -> Counting { + var counting = Counting() + + switch statusProcessType { + case .create: counting.status.create += 1 + case .merge: counting.status.merge += 1 + } + + switch authorProcessType { + case .create: counting.user.create += 1 + case .merge: counting.user.merge += 1 + } + + for child in children { + let childCounting = child.count() + counting = counting + childCounting + } + + return counting + } + +} + +extension APIService.Persist.PersistMemo where T == Toot, U == MastodonUser { + + static func createOrMergeToot( + into managedObjectContext: NSManagedObjectContext, + for requestMastodonUser: MastodonUser?, + requestMastodonUserID: MastodonUser.ID?, + domain: String, + entity: Mastodon.Entity.Status, + memoType: MemoType, + tootCache: APIService.Persist.PersistCache?, + userCache: APIService.Persist.PersistCache?, + networkDate: Date, + log: OSLog + ) -> APIService.Persist.PersistMemo { + let processEntityTaskSignpostID = OSSignpostID(log: log) + os_signpost(.begin, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "process toot %{public}s", entity.id) + defer { + os_signpost(.end, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "finish process toot %{public}s", entity.id) + } + + // build tree + let reblogMemo = entity.reblog.flatMap { entity -> APIService.Persist.PersistMemo in + createOrMergeToot( + into: managedObjectContext, + for: requestMastodonUser, + requestMastodonUserID: requestMastodonUserID, + domain: domain, + entity: entity, + memoType: .reblog, + tootCache: tootCache, + userCache: userCache, + networkDate: networkDate, + log: log + ) + } + let children = [reblogMemo].compactMap { $0 } + + + let (toot, isTootCreated, isMastodonUserCreated) = APIService.CoreData.createOrMergeToot( + into: managedObjectContext, + for: requestMastodonUser, + domain: domain, + entity: entity, + tootCache: tootCache, + userCache: userCache, + networkDate: networkDate, + log: log + ) + let memo = APIService.Persist.PersistMemo( + status: toot, + children: children, + memoType: memoType, + statusProcessType: isTootCreated ? .create : .merge, + authorProcessType: isMastodonUserCreated ? .create : .merge + ) + + switch (memo.statusProcessType, memoType) { + case (.create, .homeTimeline), (.merge, .homeTimeline): + let timelineIndex = toot.homeTimelineIndexes? + .first { $0.userID == requestMastodonUserID } + guard let requestMastodonUserID = requestMastodonUserID else { + assertionFailure() + break + } + if timelineIndex == nil { + // make it indexed + let timelineIndexProperty = HomeTimelineIndex.Property(domain: domain, userID: requestMastodonUserID) + let _ = HomeTimelineIndex.insert(into: managedObjectContext, property: timelineIndexProperty, toot: toot) + } else { + // enity already in home timeline + } + case (.create, .mentionTimeline), (.merge, .mentionTimeline): + break + // TODO: + default: + break + } + + return memo + } + + func log(indentLevel: Int = 0) -> String { + let indent = Array(repeating: " ", count: indentLevel).joined() + let preview = status.content.prefix(32).replacingOccurrences(of: "\n", with: " ") + let message = "\(indent)[\(statusProcessType.flag)\(memoType.flag)](\(status.id)) [\(authorProcessType.flag)](\(status.author.id))@\(status.author.username) ~> \(preview)" + + var childrenMessages: [String] = [] + for child in children { + childrenMessages.append(child.log(indentLevel: indentLevel + 1)) + } + let result = [[message] + childrenMessages] + .flatMap { $0 } + .joined(separator: "\n") + + return result + } + +} + diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+Timeline.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+Timeline.swift deleted file mode 100644 index 460cab023..000000000 --- a/Mastodon/Service/APIService/Persist/APIService+Persist+Timeline.swift +++ /dev/null @@ -1,446 +0,0 @@ -// -// APIService+Persist+Timeline.swift -// Mastodon -// -// Created by sxiaojian on 2021/1/27. -// - -import os.log -import func QuartzCore.CACurrentMediaTime -import Foundation -import Combine -import CoreData -import CoreDataStack -import MastodonSDK - -extension APIService.Persist { - - enum PersistTimelineType { - case `public` - case home - case likeList - } - - static func persistTimeline( - managedObjectContext: NSManagedObjectContext, - domain: String, - query: Mastodon.API.Timeline.TimelineQuery, - response: Mastodon.Response.Content<[Mastodon.Entity.Status]>, - persistType: PersistTimelineType, - requestMastodonUserID: MastodonUser.ID?, // could be nil when response from public endpoint - log: OSLog - ) -> AnyPublisher, Never> { - let toots = response.value - os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: persist %{public}ld toots…", ((#file as NSString).lastPathComponent), #line, #function, toots.count) - - return managedObjectContext.performChanges { - let contextTaskSignpostID = OSSignpostID(log: log) - let start = CACurrentMediaTime() - os_signpost(.begin, log: log, name: #function, signpostID: contextTaskSignpostID) - defer { - os_signpost(.end, log: .api, name: #function, signpostID: contextTaskSignpostID) - let end = CACurrentMediaTime() - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: persist cost %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start) - } - - // load request mastodon user - let requestMastodonUser: MastodonUser? = { - guard let requestMastodonUserID = requestMastodonUserID else { return nil } - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: domain, id: requestMastodonUserID) - request.fetchLimit = 1 - request.returnsObjectsAsFaults = false - do { - return try managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil - } - }() - - // load working set into context to avoid cache miss - let cacheTaskSignpostID = OSSignpostID(log: log) - os_signpost(.begin, log: log, name: "load toots into cache", signpostID: cacheTaskSignpostID) - let workingIDRecord = APIService.Persist.WorkingIDRecord.workingID(entities: toots) - - // contains toots and reblogs - let _tootCache: [Toot] = { - let request = Toot.sortedFetchRequest - let idSet = workingIDRecord.statusIDSet - .union(workingIDRecord.reblogIDSet) - let ids = Array(idSet) - request.predicate = Toot.predicate(domain: domain, ids: ids) - request.returnsObjectsAsFaults = false - request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)] - do { - return try managedObjectContext.fetch(request) - } catch { - assertionFailure(error.localizedDescription) - return [] - } - }() - os_signpost(.event, log: log, name: "load toots into cache", signpostID: cacheTaskSignpostID, "cached %{public}ld toots", _tootCache.count) - os_signpost(.end, log: log, name: "load toots into cache", signpostID: cacheTaskSignpostID) - - // remote timeline merge local timeline record set - // declare it before do working - let mergedOldTootsInTimeline = _tootCache.filter { - return $0.homeTimelineIndexes?.contains(where: { $0.userID == requestMastodonUserID }) ?? false - } - - let updateDatabaseTaskSignpostID = OSSignpostID(log: log) - let recordType: WorkingRecord.RecordType = { - switch persistType { - case .public: return .publicTimeline - case .home: return .homeTimeline - case .likeList: return .favoriteTimeline - } - }() - - var workingRecords: [WorkingRecord] = [] - os_signpost(.begin, log: log, name: "update database", signpostID: updateDatabaseTaskSignpostID) - for entity in toots { - let processEntityTaskSignpostID = OSSignpostID(log: log) - os_signpost(.begin, log: log, name: "update database - process entity", signpostID: processEntityTaskSignpostID, "process entity %{public}s", entity.id) - defer { - os_signpost(.end, log: log, name: "update database - process entity", signpostID: processEntityTaskSignpostID, "process entity %{public}s", entity.id) - } - let record = WorkingRecord.createOrMergeToot( - into: managedObjectContext, - for: requestMastodonUser, - domain: domain, - entity: entity, - recordType: recordType, - networkDate: response.networkDate, - log: log - ) - workingRecords.append(record) - } // end for… - os_signpost(.end, log: log, name: "update database", signpostID: updateDatabaseTaskSignpostID) - - // home & mention timeline tasks - switch persistType { - case .home: - // Task 1: update anchor hasMore - // update maxID anchor hasMore attribute when fetching on timeline - // do not use working records due to anchor toot is removable on the remote - var anchorToot: Toot? - if let maxID = query.maxID { - do { - // load anchor toot from database - let request = Toot.sortedFetchRequest - request.predicate = Toot.predicate(domain: domain, id: maxID) - request.returnsObjectsAsFaults = false - request.fetchLimit = 1 - anchorToot = try managedObjectContext.fetch(request).first - if persistType == .home { - let timelineIndex = anchorToot.flatMap { toot in - toot.homeTimelineIndexes?.first(where: { $0.userID == requestMastodonUserID }) - } - timelineIndex?.update(hasMore: false) - } else { - assertionFailure() - } - } catch { - assertionFailure(error.localizedDescription) - } - } - - // Task 2: set last toot hasMore when fetched toots not overlap with the timeline in the local database - let _oldestRecord = workingRecords - .sorted(by: { $0.status.createdAt < $1.status.createdAt }) - .first - if let oldestRecord = _oldestRecord { - if let anchorToot = anchorToot { - // using anchor. set hasMore when (overlap itself OR no overlap) AND oldest record NOT anchor - let isNoOverlap = mergedOldTootsInTimeline.isEmpty - let isOnlyOverlapItself = mergedOldTootsInTimeline.count == 1 && mergedOldTootsInTimeline.first?.id == anchorToot.id - let isAnchorEqualOldestRecord = oldestRecord.status.id == anchorToot.id - if (isNoOverlap || isOnlyOverlapItself) && !isAnchorEqualOldestRecord { - if persistType == .home { - let timelineIndex = oldestRecord.status.homeTimelineIndexes? - .first(where: { $0.userID == requestMastodonUserID }) - timelineIndex?.update(hasMore: true) - } else { - assertionFailure() - } - } - - } else if mergedOldTootsInTimeline.isEmpty { - // no anchor. set hasMore when no overlap - if persistType == .home { - let timelineIndex = oldestRecord.status.homeTimelineIndexes? - .first(where: { $0.userID == requestMastodonUserID }) - timelineIndex?.update(hasMore: true) - } - } - } else { - // empty working record. mark anchor hasMore in the task 1 - } - default: - break - } - - // print working record tree map - #if DEBUG - DispatchQueue.global(qos: .utility).async { - let logs = workingRecords - .map { record in record.log() } - .joined(separator: "\n") - os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: working status: \n%s", ((#file as NSString).lastPathComponent), #line, #function, logs) - let counting = workingRecords - .map { record in record.count() } - .reduce(into: WorkingRecord.Counting(), { result, next in result = result + next }) - let newTootsInTimeLineCount = workingRecords.reduce(0, { result, next in - return next.statusProcessType == .create ? result + 1 : result - }) - os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: toot: insert %{public}ldT(%{public}ldTRQ), merge %{public}ldT(%{public}ldTRQ)", ((#file as NSString).lastPathComponent), #line, #function, newTootsInTimeLineCount, counting.status.create, mergedOldTootsInTimeline.count, counting.status.merge) - os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: mastodon user: insert %{public}ld, merge %{public}ld", ((#file as NSString).lastPathComponent), #line, #function, counting.user.create, counting.user.merge) - } - #endif - } - .eraseToAnyPublisher() - .handleEvents(receiveOutput: { result in - switch result { - case .success: - break - case .failure(let error): - #if DEBUG - debugPrint(error) - #endif - assertionFailure(error.localizedDescription) - } - }) - .eraseToAnyPublisher() - } -} - -extension APIService.Persist { - - struct WorkingIDRecord { - var statusIDSet: Set - var reblogIDSet: Set - var userIDSet: Set - - enum RecordType { - case timeline - case reblog - } - - init(statusIDSet: Set = Set(), reblogIDSet: Set = Set(), userIDSet: Set = Set()) { - self.statusIDSet = statusIDSet - self.reblogIDSet = reblogIDSet - self.userIDSet = userIDSet - } - - mutating func union(record: WorkingIDRecord) { - statusIDSet = statusIDSet.union(record.statusIDSet) - reblogIDSet = reblogIDSet.union(record.reblogIDSet) - userIDSet = userIDSet.union(record.userIDSet) - } - - static func workingID(entities: [Mastodon.Entity.Status]) -> WorkingIDRecord { - var value = WorkingIDRecord() - for entity in entities { - let child = workingID(entity: entity, recordType: .timeline) - value.union(record: child) - } - return value - } - - private static func workingID(entity: Mastodon.Entity.Status, recordType: RecordType) -> WorkingIDRecord { - var value = WorkingIDRecord() - switch recordType { - case .timeline: value.statusIDSet = Set([entity.id]) - case .reblog: value.reblogIDSet = Set([entity.id]) - } - value.userIDSet = Set([entity.account.id]) - - if let reblog = entity.reblog { - let child = workingID(entity: reblog, recordType: .reblog) - value.union(record: child) - } - return value - } - } - - class WorkingRecord { - - let status: Toot - let children: [WorkingRecord] - let recordType: RecordType - let statusProcessType: ProcessType - let userProcessType: ProcessType - - init( - status: Toot, - children: [APIService.Persist.WorkingRecord], - recordType: APIService.Persist.WorkingRecord.RecordType, - tootProcessType: ProcessType, - userProcessType: ProcessType - ) { - self.status = status - self.children = children - self.recordType = recordType - self.statusProcessType = tootProcessType - self.userProcessType = userProcessType - } - - enum RecordType { - case publicTimeline - case homeTimeline - case mentionTimeline - case userTimeline - case favoriteTimeline - case searchTimeline - - case reblog - - var flag: String { - switch self { - case .publicTimeline: return "P" - case .homeTimeline: return "H" - case .mentionTimeline: return "M" - case .userTimeline: return "U" - case .favoriteTimeline: return "F" - case .searchTimeline: return "S" - case .reblog: return "R" - } - } - } - - enum ProcessType { - case create - case merge - - var flag: String { - switch self { - case .create: return "+" - case .merge: return "-" - } - } - } - - func log(indentLevel: Int = 0) -> String { - let indent = Array(repeating: " ", count: indentLevel).joined() - let tootPreview = status.content.prefix(32).replacingOccurrences(of: "\n", with: " ") - let message = "\(indent)[\(statusProcessType.flag)\(recordType.flag)](\(status.id)) [\(userProcessType.flag)](\(status.author.id))@\(status.author.username) ~> \(tootPreview)" - - var childrenMessages: [String] = [] - for child in children { - childrenMessages.append(child.log(indentLevel: indentLevel + 1)) - } - let result = [[message] + childrenMessages] - .flatMap { $0 } - .joined(separator: "\n") - - return result - } - - struct Counting { - var status = Counter() - var user = Counter() - - static func + (left: Counting, right: Counting) -> Counting { - return Counting( - status: left.status + right.status, - user: left.user + right.user - ) - } - - struct Counter { - var create = 0 - var merge = 0 - - static func + (left: Counter, right: Counter) -> Counter { - return Counter( - create: left.create + right.create, - merge: left.merge + right.merge - ) - } - } - } - - func count() -> Counting { - var counting = Counting() - - switch statusProcessType { - case .create: counting.status.create += 1 - case .merge: counting.status.merge += 1 - } - - switch userProcessType { - case .create: counting.user.create += 1 - case .merge: counting.user.merge += 1 - } - - for child in children { - let childCounting = child.count() - counting = counting + childCounting - } - - return counting - } - - // handle timelineIndex insert with APIService.Persist.createOrMergeToot - static func createOrMergeToot( - into managedObjectContext: NSManagedObjectContext, - for requestMastodonUser: MastodonUser?, - domain: String, - entity: Mastodon.Entity.Status, - recordType: RecordType, - networkDate: Date, - log: OSLog - ) -> WorkingRecord { - let processEntityTaskSignpostID = OSSignpostID(log: log) - os_signpost(.begin, log: log, name: "update database - process entity: createorMergeToot", signpostID: processEntityTaskSignpostID, "process toot %{public}s", entity.id) - defer { - os_signpost(.end, log: log, name: "update database - process entity: createorMergeToot", signpostID: processEntityTaskSignpostID, "finish process toot %{public}s", entity.id) - } - - // build tree - let reblogRecord: WorkingRecord? = entity.reblog.flatMap { entity -> WorkingRecord in - createOrMergeToot(into: managedObjectContext, for: requestMastodonUser, domain: domain, entity: entity, recordType: .reblog, networkDate: networkDate, log: log) - } - let children = [reblogRecord].compactMap { $0 } - - let (status, isTootCreated, isTootUserCreated) = APIService.CoreData.createOrMergeToot(into: managedObjectContext, for: requestMastodonUser, entity: entity, domain: domain, networkDate: networkDate, log: log) - - let result = WorkingRecord( - status: status, - children: children, - recordType: recordType, - tootProcessType: isTootCreated ? .create : .merge, - userProcessType: isTootUserCreated ? .create : .merge - ) - - switch (result.statusProcessType, recordType) { - case (.create, .homeTimeline), (.merge, .homeTimeline): - guard let requestMastodonUserID = requestMastodonUser?.id else { - assertionFailure("Request user is required for home timeline") - break - } - let timelineIndex = status.homeTimelineIndexes? - .first { $0.userID == requestMastodonUserID } - if timelineIndex == nil { - let timelineIndexProperty = HomeTimelineIndex.Property(domain: domain, userID: requestMastodonUserID) - - let _ = HomeTimelineIndex.insert( - into: managedObjectContext, - property: timelineIndexProperty, - toot: status - ) - } else { - // enity already in home timeline - } - default: - break - } - - return result - } - - } - -} - diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift new file mode 100644 index 000000000..1a3cd2985 --- /dev/null +++ b/Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift @@ -0,0 +1,251 @@ +// +// APIService+Persist+Toot.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/27. +// + +import os.log +import func QuartzCore.CACurrentMediaTime +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK + +extension APIService.Persist { + + enum PersistTimelineType { + case `public` + case home + case likeList + } + + static func persistToots( + managedObjectContext: NSManagedObjectContext, + domain: String, + query: Mastodon.API.Timeline.TimelineQuery, + response: Mastodon.Response.Content<[Mastodon.Entity.Status]>, + persistType: PersistTimelineType, + requestMastodonUserID: MastodonUser.ID?, // could be nil when response from public endpoint + log: OSLog + ) -> AnyPublisher, Never> { + return managedObjectContext.performChanges { + let toots = response.value + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: persist %{public}ld toots…", ((#file as NSString).lastPathComponent), #line, #function, toots.count) + + let contextTaskSignpostID = OSSignpostID(log: log) + let start = CACurrentMediaTime() + os_signpost(.begin, log: log, name: #function, signpostID: contextTaskSignpostID) + defer { + os_signpost(.end, log: .api, name: #function, signpostID: contextTaskSignpostID) + let end = CACurrentMediaTime() + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: persist cost %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start) + } + + // load request mastodon user + let requestMastodonUser: MastodonUser? = { + guard let requestMastodonUserID = requestMastodonUserID else { return nil } + let request = MastodonUser.sortedFetchRequest + request.predicate = MastodonUser.predicate(domain: domain, id: requestMastodonUserID) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + + // load working set into context to avoid cache miss + let cacheTaskSignpostID = OSSignpostID(log: log) + os_signpost(.begin, log: log, name: "load toots & users into cache", signpostID: cacheTaskSignpostID) + + // contains reblog + let tootCache: PersistCache = { + let cache = PersistCache() + let cacheIDs = PersistCache.ids(for: toots) + let cachedToots: [Toot] = { + let request = Toot.sortedFetchRequest + let ids = Array(cacheIDs) + request.predicate = Toot.predicate(domain: domain, ids: ids) + request.returnsObjectsAsFaults = false + request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)] + do { + return try managedObjectContext.fetch(request) + } catch { + assertionFailure(error.localizedDescription) + return [] + } + }() + for toot in cachedToots { + cache.dictionary[toot.id] = toot + } + os_signpost(.event, log: log, name: "load toot into cache", signpostID: cacheTaskSignpostID, "cached %{public}ld toots", cachedToots.count) + return cache + }() + + let userCache: PersistCache = { + let cache = PersistCache() + let cacheIDs = PersistCache.ids(for: toots) + let cachedMastodonUsers: [MastodonUser] = { + let request = MastodonUser.sortedFetchRequest + let ids = Array(cacheIDs) + request.predicate = MastodonUser.predicate(domain: domain, ids: ids) + //request.returnsObjectsAsFaults = false + do { + return try managedObjectContext.fetch(request) + } catch { + assertionFailure(error.localizedDescription) + return [] + } + }() + for mastodonuser in cachedMastodonUsers { + cache.dictionary[mastodonuser.id] = mastodonuser + } + os_signpost(.event, log: log, name: "load user into cache", signpostID: cacheTaskSignpostID, "cached %{public}ld users", cachedMastodonUsers.count) + return cache + }() + + os_signpost(.end, log: log, name: "load toots & users into cache", signpostID: cacheTaskSignpostID) + + // remote timeline merge local timeline record set + // declare it before persist + let mergedOldTootsInTimeline = tootCache.dictionary.values.filter { + return $0.homeTimelineIndexes?.contains(where: { $0.userID == requestMastodonUserID }) ?? false + } + + let updateDatabaseTaskSignpostID = OSSignpostID(log: log) + let memoType: PersistMemo.MemoType = { + switch persistType { + case .home: return .homeTimeline + case .public: return .publicTimeline + case .likeList: return .likeList + } + }() + + var persistMemos: [PersistMemo] = [] + os_signpost(.begin, log: log, name: "update database", signpostID: updateDatabaseTaskSignpostID) + for entity in toots { + let processEntityTaskSignpostID = OSSignpostID(log: log) + os_signpost(.begin, log: log, name: "update database - process entity", signpostID: processEntityTaskSignpostID, "process entity %{public}s", entity.id) + defer { + os_signpost(.end, log: log, name: "update database - process entity", signpostID: processEntityTaskSignpostID, "process entity %{public}s", entity.id) + } + let memo = PersistMemo.createOrMergeToot( + into: managedObjectContext, + for: requestMastodonUser, + requestMastodonUserID: requestMastodonUserID, + domain: domain, + entity: entity, + memoType: memoType, + tootCache: tootCache, + userCache: userCache, + networkDate: response.networkDate, + log: log + ) + persistMemos.append(memo) + } // end for… + os_signpost(.end, log: log, name: "update database", signpostID: updateDatabaseTaskSignpostID) + + // home timeline tasks + switch persistType { + case .home: + guard let requestMastodonUserID = requestMastodonUserID else { + assertionFailure() + return + } + // Task 1: update anchor hasMore + // update maxID anchor hasMore attribute when fetching on home timeline + // do not use working records due to anchor toot is removable on the remote + var anchorToot: Toot? + if let maxID = query.maxID { + do { + // load anchor toot from database + let request = Toot.sortedFetchRequest + request.predicate = Toot.predicate(domain: domain, id: maxID) + request.returnsObjectsAsFaults = false + request.fetchLimit = 1 + anchorToot = try managedObjectContext.fetch(request).first + if persistType == .home { + let timelineIndex = anchorToot.flatMap { toot in + toot.homeTimelineIndexes?.first(where: { $0.userID == requestMastodonUserID }) + } + timelineIndex?.update(hasMore: false) + } else { + assertionFailure() + } + } catch { + assertionFailure(error.localizedDescription) + } + } + + // Task 2: set last toot hasMore when fetched toots not overlap with the timeline in the local database + let _oldestMemo = persistMemos + .sorted(by: { $0.status.createdAt < $1.status.createdAt }) + .first + if let oldestMemo = _oldestMemo { + if let anchorToot = anchorToot { + // using anchor. set hasMore when (overlap itself OR no overlap) AND oldest record NOT anchor + let isNoOverlap = mergedOldTootsInTimeline.isEmpty + let isOnlyOverlapItself = mergedOldTootsInTimeline.count == 1 && mergedOldTootsInTimeline.first?.id == anchorToot.id + let isAnchorEqualOldestRecord = oldestMemo.status.id == anchorToot.id + if (isNoOverlap || isOnlyOverlapItself) && !isAnchorEqualOldestRecord { + if persistType == .home { + let timelineIndex = oldestMemo.status.homeTimelineIndexes? + .first(where: { $0.userID == requestMastodonUserID }) + timelineIndex?.update(hasMore: true) + } else { + assertionFailure() + } + } + + } else if mergedOldTootsInTimeline.isEmpty { + // no anchor. set hasMore when no overlap + if persistType == .home { + let timelineIndex = oldestMemo.status.homeTimelineIndexes? + .first(where: { $0.userID == requestMastodonUserID }) + timelineIndex?.update(hasMore: true) + } + } + } else { + // empty working record. mark anchor hasMore in the task 1 + } + default: + break + } + + // print working record tree map + #if DEBUG + DispatchQueue.global(qos: .utility).async { + let logs = persistMemos + .map { record in record.log() } + .joined(separator: "\n") + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: working status: \n%s", ((#file as NSString).lastPathComponent), #line, #function, logs) + let counting = persistMemos + .map { record in record.count() } + .reduce(into: PersistMemo.Counting(), { result, next in result = result + next }) + let newTweetsInTimeLineCount = persistMemos.reduce(0, { result, next in + return next.statusProcessType == .create ? result + 1 : result + }) + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: tweet: insert %{public}ldT(%{public}ldTRQ), merge %{public}ldT(%{public}ldTRQ)", ((#file as NSString).lastPathComponent), #line, #function, newTweetsInTimeLineCount, counting.status.create, mergedOldTootsInTimeline.count, counting.status.merge) + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: twitter user: insert %{public}ld, merge %{public}ld", ((#file as NSString).lastPathComponent), #line, #function, counting.user.create, counting.user.merge) + } + #endif + } + .eraseToAnyPublisher() + .handleEvents(receiveOutput: { result in + switch result { + case .success: + break + case .failure(let error): + #if DEBUG + debugPrint(error) + #endif + assertionFailure(error.localizedDescription) + } + }) + .eraseToAnyPublisher() + } +} From 75d39aabf04b1beae771f6fc30db9e542ac3836a Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 10 Mar 2021 19:12:53 +0800 Subject: [PATCH 2/5] feat: add reply to header for toot --- CoreDataStack/Entity/Toot.swift | 11 +++ Localization/app.json | 1 + Mastodon.xcodeproj/project.pbxproj | 12 +++ .../Diffiable/Section/StatusSection.swift | 43 ++++++++-- Mastodon/Generated/Strings.swift | 4 + ...Provider+StatusTableViewCellDelegate.swift | 2 +- ...der+UITableViewDataSourcePrefetching.swift | 50 +++++++++++ .../StatusProvider/StatusProvider.swift | 1 + .../Resources/en.lproj/Localizable.strings | 1 + ...imelineViewController+StatusProvider.swift | 14 ++++ .../HomeTimelineViewController.swift | 8 ++ .../MastodonPickServerViewController.swift | 2 +- ...rverViewModel+LoadIndexedServerState.swift | 4 +- .../MastodonPickServerViewModel.swift | 2 +- ...imelineViewController+StatusProvider.swift | 14 ++++ .../PublicTimelineViewController.swift | 8 ++ .../Scene/Share/View/Content/StatusView.swift | 32 +++++-- .../APIService/APIService+Status.swift | 54 ++++++++++++ .../Persist/APIService+Persist+Toot.swift | 7 +- .../Service/StatusPrefetchingService.swift | 84 +++++++++++++++++++ Mastodon/State/AppContext.swift | 6 ++ .../API/Mastodon+API+Statuses.swift | 53 ++++++++++++ .../MastodonSDK/API/Mastodon+API.swift | 1 + 23 files changed, 392 insertions(+), 22 deletions(-) create mode 100644 Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift create mode 100644 Mastodon/Service/APIService/APIService+Status.swift create mode 100644 Mastodon/Service/StatusPrefetchingService.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift diff --git a/CoreDataStack/Entity/Toot.swift b/CoreDataStack/Entity/Toot.swift index 3a2c91775..a2ad3b5a8 100644 --- a/CoreDataStack/Entity/Toot.swift +++ b/CoreDataStack/Entity/Toot.swift @@ -145,16 +145,19 @@ 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 @@ -163,6 +166,13 @@ public extension Toot { self.repliesCount = repliesCount } } + + func update(replyTo: Toot?) { + if self.replyTo != replyTo { + self.replyTo = replyTo + } + } + func update(liked: Bool, mastodonUser: MastodonUser) { if liked { if !(self.favouritedBy ?? Set()).contains(mastodonUser) { @@ -174,6 +184,7 @@ public extension Toot { } } } + func update(reblogged: Bool, mastodonUser: MastodonUser) { if reblogged { if !(self.rebloggedBy ?? Set()).contains(mastodonUser) { diff --git a/Localization/app.json b/Localization/app.json index 123655955..5d86f1f81 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -37,6 +37,7 @@ }, "status": { "user_boosted": "%s boosted", + "user_replied_to": "Replied to %s", "show_post": "Show Post", "status_content_warning": "content warning", "media_content_warning": "Tap to reveal that may be sensitive", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 5528d0234..6332da884 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -151,6 +151,9 @@ DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; }; DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; }; DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; }; + DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */; }; + DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */; }; + DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */; }; DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; @@ -399,6 +402,9 @@ DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; }; DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = ""; }; DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = ""; }; + DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDataSourcePrefetching.swift"; sourceTree = ""; }; + DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPrefetchingService.swift; sourceTree = ""; }; + DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status.swift"; sourceTree = ""; }; DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -606,6 +612,7 @@ 2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */, 2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */, DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */, + DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */, ); path = StatusProvider; sourceTree = ""; @@ -655,6 +662,7 @@ DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */, 2D206B8B25F6015000143C56 /* AudioPlayer.swift */, 2DA6054625F716A2006356F9 /* PlaybackState.swift */, + DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */, ); path = Service; sourceTree = ""; @@ -933,6 +941,7 @@ DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */, DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */, DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */, + DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */, ); path = APIService; sourceTree = ""; @@ -1566,6 +1575,7 @@ 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */, + DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */, DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */, @@ -1577,6 +1587,7 @@ 2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */, DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */, DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */, + DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */, 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */, 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */, DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */, @@ -1589,6 +1600,7 @@ DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */, DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */, + DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */, 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */, 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 489b9d3b8..a6b9397ad 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -79,12 +79,17 @@ extension StatusSection { statusItemAttribute: Item.StatusAttribute ) { // set header - cell.statusView.headerContainerStackView.isHidden = toot.reblog == nil - cell.statusView.headerInfoLabel.text = { - let author = toot.author - let name = author.displayName.isEmpty ? author.username : author.displayName - return L10n.Common.Controls.Status.userBoosted(name) - }() + StatusSection.configureHeader(cell: cell, toot: toot) + ManagedObjectObserver.observe(object: toot) + .receive(on: DispatchQueue.main) + .sink { _ in + // do nothing + } receiveValue: { change in + guard case .update(let object) = change.changeType, + let newToot = object as? Toot else { return } + StatusSection.configureHeader(cell: cell, toot: newToot) + } + .store(in: &cell.disposeBag) // set name username avatar cell.statusView.nameLabel.text = { @@ -225,7 +230,6 @@ extension StatusSection { guard case .update(let 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 = StatusSection.formattedNumberTitleForActionButton(favoriteCount) @@ -236,6 +240,31 @@ extension StatusSection { .store(in: &cell.disposeBag) } + static func configureHeader( + cell: StatusTableViewCell, + toot: Toot + ) { + if toot.reblog != nil { + cell.statusView.headerContainerStackView.isHidden = false + cell.statusView.headerInfoLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage) + cell.statusView.headerInfoLabel.text = { + let author = toot.author + let name = author.displayName.isEmpty ? author.username : author.displayName + return L10n.Common.Controls.Status.userBoosted(name) + }() + } else if let replyTo = toot.replyTo { + cell.statusView.headerContainerStackView.isHidden = false + cell.statusView.headerInfoLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) + cell.statusView.headerInfoLabel.text = { + let author = replyTo.author + let name = author.displayName.isEmpty ? author.username : author.displayName + return L10n.Common.Controls.Status.userRepliedTo(name) + }() + } else { + cell.statusView.headerContainerStackView.isHidden = true + } + } + static func configure( cell: StatusTableViewCell, poll: Poll?, diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 7c595918e..74595d5b6 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -80,6 +80,10 @@ internal enum L10n { internal static func userBoosted(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1)) } + /// Replied to %@ + internal static func userRepliedTo(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.UserRepliedTo", String(describing: p1)) + } internal enum Poll { /// Closed internal static let closed = L10n.tr("Localizable", "Common.Controls.Status.Poll.Closed") diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index cd4e5160d..12dfeadf5 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -119,7 +119,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { guard let diffableDataSource = cell.statusView.pollTableViewDataSource else { return } let item = diffableDataSource.itemIdentifier(for: indexPath) - guard case let .opion(objectID, attribute) = item else { return } + guard case let .opion(objectID, _) = item else { return } guard let option = managedObjectContext.object(with: objectID) as? PollOption else { return } let poll = option.poll diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift new file mode 100644 index 000000000..20f8e30a9 --- /dev/null +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift @@ -0,0 +1,50 @@ +// +// StatusProvider+UITableViewDataSourcePrefetching.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-10. +// + +import UIKit +import CoreData +import CoreDataStack + +extension StatusTableViewCellDelegate where Self: StatusProvider { + func handleTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + // prefetch reply toot + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + let domain = activeMastodonAuthenticationBox.domain + + var statusObjectIDs: [NSManagedObjectID] = [] + for item in items(indexPaths: indexPaths) { + switch item { + case .homeTimelineIndex(let objectID, _): + let homeTimelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex + statusObjectIDs.append(homeTimelineIndex.toot.objectID) + case .toot(let objectID, _): + statusObjectIDs.append(objectID) + default: + continue + } + } + + let backgroundManagedObjectContext = context.backgroundManagedObjectContext + backgroundManagedObjectContext.perform { [weak self] in + guard let self = self else { return } + for objectID in statusObjectIDs { + let toot = backgroundManagedObjectContext.object(with: objectID) as! Toot + guard let replyToID = toot.inReplyToID, toot.replyTo == nil else { + // skip + continue + } + self.context.statusPrefetchingService.prefetchReplyTo( + domain: domain, + statusObjectID: toot.objectID, + statusID: toot.id, + replyToStatusID: replyToID, + authorizationBox: activeMastodonAuthenticationBox + ) + } + } + } +} diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider.swift b/Mastodon/Protocol/StatusProvider/StatusProvider.swift index a0a7116fc..4ec0d1e16 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider.swift @@ -20,4 +20,5 @@ protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewControl var managedObjectContext: NSManagedObjectContext { get } var tableViewDiffableDataSource: UITableViewDiffableDataSource? { get } func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? + func items(indexPaths: [IndexPath]) -> [Item] } diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 54b69e274..a1a6db78e 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -31,6 +31,7 @@ "Common.Controls.Status.ShowPost" = "Show Post"; "Common.Controls.Status.StatusContentWarning" = "content warning"; "Common.Controls.Status.UserBoosted" = "%@ boosted"; +"Common.Controls.Status.UserRepliedTo" = "Replied to %@"; "Common.Controls.Timeline.LoadMore" = "Load More"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift index a0d9204ba..dc8eb4803 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift @@ -70,4 +70,18 @@ extension HomeTimelineViewController: StatusProvider { return item } + func items(indexPaths: [IndexPath]) -> [Item] { + guard let diffableDataSource = self.viewModel.diffableDataSource else { + assertionFailure() + return [] + } + + var items: [Item] = [] + for indexPath in indexPaths { + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { continue } + items.append(item) + } + return items + } + } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index b9d0f94e1..db285c2c3 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -103,6 +103,7 @@ extension HomeTimelineViewController { viewModel.tableView = tableView viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self tableView.delegate = self + tableView.prefetchDataSource = self viewModel.setupDiffableDataSource( for: tableView, dependency: self, @@ -239,6 +240,13 @@ extension HomeTimelineViewController: UITableViewDelegate { } +// MARK: - UITableViewDataSourcePrefetching +extension HomeTimelineViewController: UITableViewDataSourcePrefetching { + func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + handleTableView(tableView, prefetchRowsAt: indexPaths) + } +} + // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate { func navigationBar() -> UINavigationBar? { diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index f3e811b9f..37e8b0dab 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -375,7 +375,7 @@ extension MastodonPickServerViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { guard let diffableDataSource = viewModel.diffableDataSource else { return nil } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil } - guard case let .server(server) = item else { return nil } + guard case .server = item else { return nil } if tableView.indexPathForSelectedRow == indexPath { tableView.deselectRow(at: indexPath, animated: false) diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift index b9cdcc7e4..69f6a82fb 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift @@ -45,7 +45,7 @@ extension MastodonPickServerViewModel.LoadIndexedServerState { viewModel.context.apiService.servers(language: nil, category: nil) .sink { completion in switch completion { - case .failure(let error): + case .failure: // TODO: handle error stateMachine.enter(Fail.self) case .finished: @@ -84,7 +84,7 @@ extension MastodonPickServerViewModel.LoadIndexedServerState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = self.viewModel, let stateMachine = self.stateMachine else { return } + guard let viewModel = self.viewModel else { return } viewModel.isLoadingIndexedServers.value = false } } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index e136e86ce..09b6c327c 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -176,7 +176,7 @@ class MastodonPickServerViewModel: NSObject { switch result { case .success(let response): self.unindexedServers.send(response.value) - case .failure(let error): + case .failure: // TODO: What should be presented when user inputs invalid search text? self.unindexedServers.send([]) } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift index aceb83718..7d52a5764 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift @@ -70,4 +70,18 @@ extension PublicTimelineViewController: StatusProvider { return item } + func items(indexPaths: [IndexPath]) -> [Item] { + guard let diffableDataSource = self.viewModel.diffableDataSource else { + assertionFailure() + return [] + } + + var items: [Item] = [] + for indexPath in indexPaths { + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { continue } + items.append(item) + } + return items + } + } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift index 98d2dbd94..ddca93fe7 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift @@ -73,6 +73,7 @@ extension PublicTimelineViewController { viewModel.tableView = tableView viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self tableView.delegate = self + tableView.prefetchDataSource = self viewModel.setupDiffableDataSource( for: tableView, dependency: self, @@ -125,6 +126,13 @@ extension PublicTimelineViewController: UITableViewDelegate { } } +// MARK: - UITableViewDataSourcePrefetching +extension PublicTimelineViewController: UITableViewDataSourcePrefetching { + func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + handleTableView(tableView, prefetchRowsAt: indexPaths) + } +} + // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate extension PublicTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate { func navigationBar() -> UINavigationBar? { diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 2713647fe..f908ce5c1 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -24,6 +24,29 @@ final class StatusView: UIView { static let avatarImageCornerRadius: CGFloat = 4 static let contentWarningBlurRadius: CGFloat = 12 + static let boostIconImage: UIImage = { + let font = UIFont.systemFont(ofSize: 13, weight: .medium) + let configuration = UIImage.SymbolConfiguration(font: font) + let image = UIImage(systemName: "arrow.2.squarepath", withConfiguration: configuration)!.withTintColor(Asset.Colors.Label.secondary.color) + return image + }() + + static let replyIconImage: UIImage = { + let font = UIFont.systemFont(ofSize: 13, weight: .medium) + let configuration = UIImage.SymbolConfiguration(font: font) + let image = UIImage(systemName: "arrowshape.turn.up.left.fill", withConfiguration: configuration)!.withTintColor(Asset.Colors.Label.secondary.color) + return image + }() + + static func iconAttributedString(image: UIImage) -> NSAttributedString { + let attributedString = NSMutableAttributedString() + let imageTextAttachment = NSTextAttachment() + let imageAttribute = NSAttributedString(attachment: imageTextAttachment) + imageTextAttachment.image = image + attributedString.append(imageAttribute) + return attributedString + } + weak var delegate: StatusViewDelegate? var isStatusTextSensitive = false var pollTableViewDataSource: UITableViewDiffableDataSource? @@ -33,14 +56,7 @@ final class StatusView: UIView { let headerIconLabel: UILabel = { let label = UILabel() - let attributedString = NSMutableAttributedString() - let imageTextAttachment = NSTextAttachment() - let font = UIFont.systemFont(ofSize: 13, weight: .medium) - let configuration = UIImage.SymbolConfiguration(font: font) - imageTextAttachment.image = UIImage(systemName: "arrow.2.squarepath", withConfiguration: configuration)?.withTintColor(Asset.Colors.Label.secondary.color) - let imageAttribute = NSAttributedString(attachment: imageTextAttachment) - attributedString.append(imageAttribute) - label.attributedText = attributedString + label.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage) return label }() diff --git a/Mastodon/Service/APIService/APIService+Status.swift b/Mastodon/Service/APIService/APIService+Status.swift new file mode 100644 index 000000000..88ac064af --- /dev/null +++ b/Mastodon/Service/APIService/APIService+Status.swift @@ -0,0 +1,54 @@ +// +// APIService+Status.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-10. +// + +import Foundation +import Combine +import CoreData +import CoreDataStack +import CommonOSLog +import DateToolsSwift +import MastodonSDK + +extension APIService { + + func status( + domain: String, + statusID: Mastodon.Entity.Status.ID, + authorizationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = authorizationBox.userAuthorization + return Mastodon.API.Statuses.status( + session: session, + domain: domain, + statusID: statusID, + authorization: authorization + ) + .flatMap { response -> AnyPublisher, Error> in + return APIService.Persist.persistToots( + managedObjectContext: self.backgroundManagedObjectContext, + domain: domain, + query: nil, + response: response.map { [$0] }, + persistType: .lookUp, + requestMastodonUserID: nil, + log: OSLog.api + ) + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + +} diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift index 1a3cd2985..fb2c2e6c0 100644 --- a/Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift +++ b/Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift @@ -19,12 +19,13 @@ extension APIService.Persist { case `public` case home case likeList + case lookUp } static func persistToots( managedObjectContext: NSManagedObjectContext, domain: String, - query: Mastodon.API.Timeline.TimelineQuery, + query: Mastodon.API.Timeline.TimelineQuery?, response: Mastodon.Response.Content<[Mastodon.Entity.Status]>, persistType: PersistTimelineType, requestMastodonUserID: MastodonUser.ID?, // could be nil when response from public endpoint @@ -122,6 +123,7 @@ extension APIService.Persist { case .home: return .homeTimeline case .public: return .publicTimeline case .likeList: return .likeList + case .lookUp: return .lookUp } }() @@ -152,7 +154,8 @@ extension APIService.Persist { // home timeline tasks switch persistType { case .home: - guard let requestMastodonUserID = requestMastodonUserID else { + guard let query = query, + let requestMastodonUserID = requestMastodonUserID else { assertionFailure() return } diff --git a/Mastodon/Service/StatusPrefetchingService.swift b/Mastodon/Service/StatusPrefetchingService.swift new file mode 100644 index 000000000..d4332fe16 --- /dev/null +++ b/Mastodon/Service/StatusPrefetchingService.swift @@ -0,0 +1,84 @@ +// +// StatusPrefetchingService.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-10. +// + +import os.log +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK + +final class StatusPrefetchingService { + + typealias TaskID = String + + let workingQueue = DispatchQueue(label: "status-prefetching-service-working-queue") + + var disposeBag = Set() + private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:] + + weak var apiService: APIService? + + init(apiService: APIService) { + self.apiService = apiService + } + +} + +extension StatusPrefetchingService { + + func prefetchReplyTo( + domain: String, + statusObjectID: NSManagedObjectID, + statusID: Mastodon.Entity.Status.ID, + replyToStatusID: Mastodon.Entity.Status.ID, + authorizationBox: AuthenticationService.MastodonAuthenticationBox + ) { + workingQueue.async { [weak self] in + guard let self = self, let apiService = self.apiService else { return } + let taskID = domain + "@" + statusID + "->" + replyToStatusID + guard self.statusPrefetchingDisposeBagDict[taskID] == nil else { return } + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: prefetching replyTo: %s", ((#file as NSString).lastPathComponent), #line, #function, taskID) + + self.statusPrefetchingDisposeBagDict[taskID] = apiService.status( + domain: domain, + statusID: replyToStatusID, + authorizationBox: authorizationBox + ) + .sink(receiveCompletion: { [weak self] completion in + // remove task when completed + guard let self = self else { return } + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: prefeched replyTo: %s", ((#file as NSString).lastPathComponent), #line, #function, taskID) + self.statusPrefetchingDisposeBagDict[taskID] = nil + }, receiveValue: { [weak self] _ in + guard let self = self else { return } + let backgroundManagedObjectContext = apiService.backgroundManagedObjectContext + backgroundManagedObjectContext.performChanges { + guard let status = backgroundManagedObjectContext.object(with: statusObjectID) as? Toot else { return } + do { + let predicate = Toot.predicate(domain: domain, id: replyToStatusID) + let request = Toot.sortedFetchRequest + request.predicate = predicate + request.returnsObjectsAsFaults = false + request.fetchLimit = 1 + guard let replyTo = try backgroundManagedObjectContext.fetch(request).first else { return } + status.update(replyTo: replyTo) + } catch { + assertionFailure(error.localizedDescription) + } + } + .sink { _ in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update status replyTo: %s", ((#file as NSString).lastPathComponent), #line, #function, taskID) + } receiveValue: { _ in + // do nothing + } + .store(in: &self.disposeBag) + }) + } + } + +} diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 08918496b..ffa1321ab 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -24,6 +24,8 @@ class AppContext: ObservableObject { let apiService: APIService let authenticationService: AuthenticationService + let statusPrefetchingService: StatusPrefetchingService + let documentStore: DocumentStore private var documentStoreSubscription: AnyCancellable! @@ -46,6 +48,10 @@ class AppContext: ObservableObject { apiService: _apiService ) + statusPrefetchingService = StatusPrefetchingService( + apiService: _apiService + ) + documentStore = DocumentStore() documentStoreSubscription = documentStore.objectWillChange .receive(on: DispatchQueue.main) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift new file mode 100644 index 000000000..cb33bc09b --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift @@ -0,0 +1,53 @@ +// +// Mastodon+API+Statuses.swift +// +// +// Created by MainasuK Cirno on 2021-3-10. +// + +import Foundation +import Combine + +extension Mastodon.API.Statuses { + + static func viewStatusEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { + let pathComponent = "statuses/" + statusID + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent) + } + + /// View specific status + /// + /// View information about a status + /// + /// - Since: 0.0.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/3/10 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - statusID: id for status + /// - authorization: User token. Could be nil if status is public + /// - Returns: `AnyPublisher` contains `Status` nested in the response + public static func status( + session: URLSession, + domain: String, + statusID: Mastodon.Entity.Poll.ID, + authorization: Mastodon.API.OAuth.Authorization? + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: viewStatusEndpointURL(domain: domain, statusID: statusID), + query: nil, + 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() + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 073d926e9..5516a0492 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -95,6 +95,7 @@ extension Mastodon.API { public enum OAuth { } public enum Onboarding { } public enum Polls { } + public enum Statuses { } public enum Timeline { } public enum Favorites { } } From 1a3cff8a3abd6384df28c6d57cfd6da47598a704 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 Mar 2021 11:48:14 +0800 Subject: [PATCH 3/5] chore: add debug entries --- ...meTimelineViewController+DebugAction.swift | 115 ++++++++++++++---- 1 file changed, 88 insertions(+), 27 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 9f2b4e720..74ea5c3e5 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -45,34 +45,30 @@ extension HomeTimelineViewController { guard let self = self else { return } self.moveToTopGapAction(action) }), - UIAction(title: "First Reblog Toot", image: nil, attributes: [], handler: { [weak self] action in + UIAction(title: "First Replied Status", image: nil, attributes: [], handler: { [weak self] action in guard let self = self else { return } - self.moveToFirstReblogToot(action) + self.moveToFirstRepliedStatus(action) }), - UIAction(title: "First Poll Toot", image: nil, attributes: [], handler: { [weak self] action in + UIAction(title: "First Reblog Status", image: nil, attributes: [], handler: { [weak self] action in guard let self = self else { return } - self.moveToFirstPollToot(action) + self.moveToFirstReblogStatus(action) }), - UIAction(title: "First Audio Toot", image: nil, attributes: [], handler: { [weak self] action in + UIAction(title: "First Poll Status", image: nil, attributes: [], handler: { [weak self] action in guard let self = self else { return } - self.moveToFirstAudioToot(action) + self.moveToFirstPollStatus(action) + }), + UIAction(title: "First Audio Status", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.moveToFirstAudioStatus(action) + }), + UIAction(title: "First Video Status", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.moveToFirstVideoStatus(action) + }), + UIAction(title: "First GIF Toot", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.moveToFirstGIFStatus(action) }), -// UIAction(title: "First Reply Toot", image: nil, attributes: [], handler: { [weak self] action in -// guard let self = self else { return } -// self.moveToFirstReplyToot(action) -// }), -// UIAction(title: "First Reply Reblog", image: nil, attributes: [], handler: { [weak self] action in -// guard let self = self else { return } -// self.moveToFirstReplyReblog(action) -// }), -// UIAction(title: "First Video Toot", image: nil, attributes: [], handler: { [weak self] action in -// guard let self = self else { return } -// self.moveToFirstVideoToot(action) -// }), -// UIAction(title: "First GIF Toot", image: nil, attributes: [], handler: { [weak self] action in -// guard let self = self else { return } -// self.moveToFirstGIFToot(action) -// }), ] ) } @@ -109,7 +105,7 @@ extension HomeTimelineViewController { } } - @objc private func moveToFirstReblogToot(_ sender: UIAction) { + @objc private func moveToFirstReblogStatus(_ sender: UIAction) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshotTransitioning = diffableDataSource.snapshot() let item = snapshotTransitioning.itemIdentifiers.first(where: { item in @@ -125,11 +121,11 @@ extension HomeTimelineViewController { tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) tableView.blinkRow(at: IndexPath(row: index, section: 0)) } else { - print("Not found reblog toot") + print("Not found reblog status") } } - @objc private func moveToFirstPollToot(_ sender: UIAction) { + @objc private func moveToFirstPollStatus(_ sender: UIAction) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshotTransitioning = diffableDataSource.snapshot() let item = snapshotTransitioning.itemIdentifiers.first(where: { item in @@ -146,11 +142,34 @@ extension HomeTimelineViewController { tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) tableView.blinkRow(at: IndexPath(row: index, section: 0)) } else { - print("Not found poll toot") + print("Not found poll status") } } - @objc private func moveToFirstAudioToot(_ sender: UIAction) { + @objc private func moveToFirstRepliedStatus(_ sender: UIAction) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + let snapshotTransitioning = diffableDataSource.snapshot() + let item = snapshotTransitioning.itemIdentifiers.first(where: { item in + switch item { + case .homeTimelineIndex(let objectID, _): + let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex + guard homeTimelineIndex.toot.inReplyToID != nil else { + return false + } + return true + default: + return false + } + }) + if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) + tableView.blinkRow(at: IndexPath(row: index, section: 0)) + } else { + print("Not found replied status") + } + } + + @objc private func moveToFirstAudioStatus(_ sender: UIAction) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshotTransitioning = diffableDataSource.snapshot() let item = snapshotTransitioning.itemIdentifiers.first(where: { item in @@ -171,6 +190,48 @@ extension HomeTimelineViewController { } } + @objc private func moveToFirstVideoStatus(_ sender: UIAction) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + let snapshotTransitioning = diffableDataSource.snapshot() + let item = snapshotTransitioning.itemIdentifiers.first(where: { item in + switch item { + case .homeTimelineIndex(let objectID, _): + let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex + let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot + return toot.mediaAttachments?.contains(where: { $0.type == .video }) ?? false + default: + return false + } + }) + if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) + tableView.blinkRow(at: IndexPath(row: index, section: 0)) + } else { + print("Not found video status") + } + } + + @objc private func moveToFirstGIFStatus(_ sender: UIAction) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + let snapshotTransitioning = diffableDataSource.snapshot() + let item = snapshotTransitioning.itemIdentifiers.first(where: { item in + switch item { + case .homeTimelineIndex(let objectID, _): + let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex + let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot + return toot.mediaAttachments?.contains(where: { $0.type == .gifv }) ?? false + default: + return false + } + }) + if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) + tableView.blinkRow(at: IndexPath(row: index, section: 0)) + } else { + print("Not found GIF status") + } + } + @objc private func dropRecentTootsAction(_ sender: UIAction, count: Int) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshotTransitioning = diffableDataSource.snapshot() From 21f41247475a44b69f212c432b36c30d82e8a89d Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 Mar 2021 11:48:33 +0800 Subject: [PATCH 4/5] fix: header icon missing issue --- Mastodon/Diffiable/Section/StatusSection.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 744e873cf..f6d0b0273 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -311,7 +311,7 @@ extension StatusSection { ) { if toot.reblog != nil { cell.statusView.headerContainerStackView.isHidden = false - cell.statusView.headerInfoLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage) + cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage) cell.statusView.headerInfoLabel.text = { let author = toot.author let name = author.displayName.isEmpty ? author.username : author.displayName @@ -319,7 +319,7 @@ extension StatusSection { }() } else if let replyTo = toot.replyTo { cell.statusView.headerContainerStackView.isHidden = false - cell.statusView.headerInfoLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) + cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) cell.statusView.headerInfoLabel.text = { let author = replyTo.author let name = author.displayName.isEmpty ? author.username : author.displayName From 4873d8649b7187b193e4c956848caea31adf8efa Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 17 Mar 2021 11:33:25 +0800 Subject: [PATCH 5/5] chore: renaming status --- Mastodon.xcodeproj/project.pbxproj | 51 ++++++++----------- .../APIService/APIService+Favorite.swift | 2 +- .../APIService/APIService+HomeTimeline.swift | 2 +- .../APIService+PublicTimeline.swift | 2 +- .../APIService/APIService+Status.swift | 2 +- ...swift => APIService+CoreData+Status.swift} | 6 +-- .../APIService+Persist+PersistMemo.swift | 2 +- ....swift => APIService+Persist+Status.swift} | 4 +- .../Entity/Mastodon+Entity+Status.swift | 2 +- 9 files changed, 33 insertions(+), 40 deletions(-) rename Mastodon/Service/APIService/CoreData/{APIService+CoreData+Toot.swift => APIService+CoreData+Status.swift} (98%) rename Mastodon/Service/APIService/Persist/{APIService+Persist+Toot.swift => APIService+Persist+Status.swift} (99%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 60754172b..d2d581fd3 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -57,13 +57,13 @@ 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; }; - 2D61335825C188A000CAE157 /* APIService+Persist+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Toot.swift */; }; + 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 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 */; }; + 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Status.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 */; }; @@ -147,11 +147,11 @@ DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; }; DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; }; DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; }; - DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; }; DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; }; DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; }; DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */; }; + DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; @@ -220,12 +220,12 @@ DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */; }; DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; }; DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; }; + DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; }; DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; }; DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; }; + DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; }; DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; }; - DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; }; - DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; }; DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; }; DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; }; @@ -332,12 +332,12 @@ 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = ""; }; 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = ""; }; 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = ""; }; - 2D61335725C188A000CAE157 /* APIService+Persist+Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Toot.swift"; sourceTree = ""; }; + 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Status.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = ""; }; 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningOverlayView.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 = ""; }; + 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Status.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Status.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 = ""; }; @@ -433,11 +433,11 @@ DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = ""; }; DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = ""; }; - DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayerContainerView+MediaTypeIndicotorView.swift"; sourceTree = ""; }; DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = ""; }; DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = ""; }; DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = ""; }; DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CustomEmoji.swift"; sourceTree = ""; }; + DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayerContainerView+MediaTypeIndicotorView.swift"; sourceTree = ""; }; DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = ""; }; DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = ""; }; @@ -505,12 +505,12 @@ DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = ""; }; DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; + DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = ""; }; DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = ""; }; DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; + DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = ""; }; - DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = ""; }; - DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = ""; }; DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = ""; }; DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = ""; }; @@ -743,7 +743,7 @@ 2D61335625C1887F00CAE157 /* Persist */ = { isa = PBXGroup; children = ( - 2D61335725C188A000CAE157 /* APIService+Persist+Toot.swift */, + 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */, DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */, DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */, ); @@ -1028,7 +1028,7 @@ DB45FB0925CA87BC005A8AC7 /* CoreData */ = { isa = PBXGroup; children = ( - 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */, + 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Status.swift */, DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */, DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */, ); @@ -1229,21 +1229,15 @@ DB084B5125CBC56300F898ED /* CoreDataStack */, DB6C8C0525F0921200AAA452 /* MastodonSDK */, DB44384E25E8C1FA008912A2 /* CALayer.swift */, - DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, - 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, - DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, - 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */, 2DF123A625C3B0210020F248 /* ActiveLabel.swift */, 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */, DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */, - 2D939AB425EDD8A90076FA61 /* String.swift */, DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, DB68A06225E905E000CFDF14 /* UIApplication.swift */, DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */, 2D42FF8E25C8228A004A627A /* UIButton.swift */, DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */, DB4481B825EE289600BEFB67 /* UITableView.swift */, - 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, 0FAA101B25E10E760017CCDE /* UIFont.swift */, 2D939AB425EDD8A90076FA61 /* String.swift */, 2D206B7F25F5F45E00143C56 /* UIImage.swift */, @@ -1251,7 +1245,6 @@ 2D206B9125F60EA700143C56 /* UIControl.swift */, 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */, - 2D46976325C2A71500CF4AA9 /* UIIamge.swift */, 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, @@ -1326,6 +1319,14 @@ path = ViewModel; sourceTree = ""; }; + DB9E0D6925EDFFE500CFDD76 /* Helper */ = { + isa = PBXGroup; + children = ( + 2D42FF6A25C817D2004A627A /* TootContent.swift */, + ); + path = Helper; + sourceTree = ""; + }; DBA9B90325F1D4420012E7B6 /* Control */ = { isa = PBXGroup; children = ( @@ -1335,14 +1336,6 @@ path = Control; sourceTree = ""; }; - DB9E0D6925EDFFE500CFDD76 /* Helper */ = { - isa = PBXGroup; - children = ( - 2D42FF6A25C817D2004A627A /* TootContent.swift */, - ); - path = Helper; - sourceTree = ""; - }; DBABE3F125ECAC4E00879EE5 /* View */ = { isa = PBXGroup; children = ( @@ -1757,10 +1750,10 @@ 0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */, DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */, DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */, - 2D61335825C188A000CAE157 /* APIService+Persist+Toot.swift in Sources */, + 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, - 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */, + 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */, DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */, DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */, DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */, diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift index 4150e796e..2eacff573 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/Mastodon/Service/APIService/APIService+Favorite.swift @@ -141,7 +141,7 @@ extension APIService { .map { response -> AnyPublisher, Error> in let log = OSLog.api - return APIService.Persist.persistToots( + return APIService.Persist.persistStatus( managedObjectContext: self.backgroundManagedObjectContext, domain: mastodonAuthenticationBox.domain, query: query, diff --git a/Mastodon/Service/APIService/APIService+HomeTimeline.swift b/Mastodon/Service/APIService/APIService+HomeTimeline.swift index 125c05208..2fbb45e0b 100644 --- a/Mastodon/Service/APIService/APIService+HomeTimeline.swift +++ b/Mastodon/Service/APIService/APIService+HomeTimeline.swift @@ -40,7 +40,7 @@ extension APIService { authorization: authorization ) .flatMap { response -> AnyPublisher, Error> in - return APIService.Persist.persistToots( + return APIService.Persist.persistStatus( managedObjectContext: self.backgroundManagedObjectContext, domain: domain, query: query, diff --git a/Mastodon/Service/APIService/APIService+PublicTimeline.swift b/Mastodon/Service/APIService/APIService+PublicTimeline.swift index 288d1fd2e..cd02526d6 100644 --- a/Mastodon/Service/APIService/APIService+PublicTimeline.swift +++ b/Mastodon/Service/APIService/APIService+PublicTimeline.swift @@ -39,7 +39,7 @@ extension APIService { query: query ) .flatMap { response -> AnyPublisher, Error> in - return APIService.Persist.persistToots( + return APIService.Persist.persistStatus( managedObjectContext: self.backgroundManagedObjectContext, domain: domain, query: query, diff --git a/Mastodon/Service/APIService/APIService+Status.swift b/Mastodon/Service/APIService/APIService+Status.swift index 88ac064af..d02b04796 100644 --- a/Mastodon/Service/APIService/APIService+Status.swift +++ b/Mastodon/Service/APIService/APIService+Status.swift @@ -28,7 +28,7 @@ extension APIService { authorization: authorization ) .flatMap { response -> AnyPublisher, Error> in - return APIService.Persist.persistToots( + return APIService.Persist.persistStatus( managedObjectContext: self.backgroundManagedObjectContext, domain: domain, query: nil, diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift similarity index 98% rename from Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift rename to Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift index a4db46a8c..28bfd9727 100644 --- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift @@ -1,5 +1,5 @@ // -// APIService+CoreData+Toot.swift +// APIService+CoreData+Status.swift // Mastodon // // Created by sxiaojian on 2021/2/3. @@ -13,7 +13,7 @@ import MastodonSDK extension APIService.CoreData { - static func createOrMergeToot( + static func createOrMergeStatus( into managedObjectContext: NSManagedObjectContext, for requestMastodonUser: MastodonUser?, domain: String, @@ -31,7 +31,7 @@ extension APIService.CoreData { // build tree let reblog = entity.reblog.flatMap { entity -> Toot in - let (toot, _, _) = createOrMergeToot( + let (toot, _, _) = createOrMergeStatus( into: managedObjectContext, for: requestMastodonUser, domain: domain, diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift index 08696ec55..b74bb7771 100644 --- a/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift +++ b/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift @@ -163,7 +163,7 @@ extension APIService.Persist.PersistMemo where T == Toot, U == MastodonUser { let children = [reblogMemo].compactMap { $0 } - let (toot, isTootCreated, isMastodonUserCreated) = APIService.CoreData.createOrMergeToot( + let (toot, isTootCreated, isMastodonUserCreated) = APIService.CoreData.createOrMergeStatus( into: managedObjectContext, for: requestMastodonUser, domain: domain, diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift similarity index 99% rename from Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift rename to Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift index fb2c2e6c0..2944e66a5 100644 --- a/Mastodon/Service/APIService/Persist/APIService+Persist+Toot.swift +++ b/Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift @@ -1,5 +1,5 @@ // -// APIService+Persist+Toot.swift +// APIService+Persist+Status.swift // Mastodon // // Created by sxiaojian on 2021/1/27. @@ -22,7 +22,7 @@ extension APIService.Persist { case lookUp } - static func persistToots( + static func persistStatus( managedObjectContext: NSManagedObjectContext, domain: String, query: Mastodon.API.Timeline.TimelineQuery?, diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift index 31a8806a7..490429fce 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift @@ -1,5 +1,5 @@ // -// Mastodon+Entity+Toot.swift +// Mastodon+Entity+Status.swift // // // Created by MainasuK Cirno on 2021/1/27.