diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
index e8d8dd60..c0e864dc 100644
--- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
+++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
@@ -27,9 +27,11 @@
+
+
-
+
@@ -120,11 +122,11 @@
-
+
-
\ No newline at end of file
+
diff --git a/CoreDataStack/Entity/HomeTimelineIndex.swift b/CoreDataStack/Entity/HomeTimelineIndex.swift
index 7c4e4eb7..93fa0fb5 100644
--- a/CoreDataStack/Entity/HomeTimelineIndex.swift
+++ b/CoreDataStack/Entity/HomeTimelineIndex.swift
@@ -13,9 +13,13 @@ final public class HomeTimelineIndex: NSManagedObject {
public typealias ID = String
@NSManaged public private(set) var identifier: ID
@NSManaged public private(set) var domain: String
- @NSManaged public private(set) var userIdentifier: String
+ @NSManaged public private(set) var userID: String
+
+ @NSManaged public private(set) var hasMore: Bool // default NO
@NSManaged public private(set) var createdAt: Date
+ @NSManaged public private(set) var deletedAt: Date?
+
// many-to-one relationship
@NSManaged public private(set) var toot: Toot
@@ -34,7 +38,7 @@ extension HomeTimelineIndex {
index.identifier = property.identifier
index.domain = property.domain
- index.userIdentifier = toot.author.identifier
+ index.userID = toot.author.id
index.createdAt = toot.createdAt
index.toot = toot
@@ -42,6 +46,17 @@ extension HomeTimelineIndex {
return index
}
+ public func update(hasMore: Bool) {
+ if self.hasMore != hasMore {
+ self.hasMore = hasMore
+ }
+ }
+
+ // internal method for Toot call
+ func softDelete() {
+ deletedAt = Date()
+ }
+
}
extension HomeTimelineIndex {
diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift
index 8ac22c0b..bcbfe5d2 100644
--- a/CoreDataStack/Entity/MastodonUser.swift
+++ b/CoreDataStack/Entity/MastodonUser.swift
@@ -37,9 +37,7 @@ final public class MastodonUser: NSManagedObject {
@NSManaged public private(set) var reblogged: Set?
@NSManaged public private(set) var muted: Set?
@NSManaged public private(set) var bookmarked: Set?
-
- @NSManaged public private(set) var retweets: Set?
-
+
}
extension MastodonUser {
diff --git a/CoreDataStack/Entity/Toot.swift b/CoreDataStack/Entity/Toot.swift
index c43d0d6c..6dab7010 100644
--- a/CoreDataStack/Entity/Toot.swift
+++ b/CoreDataStack/Entity/Toot.swift
@@ -10,6 +10,7 @@ import Foundation
public final class Toot: NSManagedObject {
public typealias ID = String
+
@NSManaged public private(set) var identifier: ID
@NSManaged public private(set) var domain: String
@@ -36,6 +37,8 @@ public final class Toot: NSManagedObject {
@NSManaged public private(set) var text: String?
// many-to-one relastionship
+ @NSManaged public private(set) var author: MastodonUser
+ @NSManaged public private(set) var reblog: Toot?
@NSManaged public private(set) var favouritedBy: MastodonUser?
@NSManaged public private(set) var rebloggedBy: MastodonUser?
@NSManaged public private(set) var mutedBy: MastodonUser?
@@ -43,29 +46,16 @@ public final class Toot: NSManagedObject {
// one-to-one relastionship
@NSManaged public private(set) var pinnedBy: MastodonUser?
+
+ // one-to-many relationship
+ @NSManaged public private(set) var reblogFrom: Set?
+ @NSManaged public private(set) var mentions: Set?
+ @NSManaged public private(set) var emojis: Set?
+ @NSManaged public private(set) var tags: Set?
+ @NSManaged public private(set) var homeTimelineIndexes: Set?
@NSManaged public private(set) var updatedAt: Date
@NSManaged public private(set) var deletedAt: Date?
-
- // one-to-many relationship
- @NSManaged public private(set) var reblogFrom: Set?
-
- // one-to-many relationship
- @NSManaged public private(set) var mentions: Set?
- // one-to-many relationship
- @NSManaged public private(set) var emojis: Set?
-
- // one-to-many relationship
- @NSManaged public private(set) var tags: Set?
-
- // many-to-one relastionship
- @NSManaged public private(set) var reblog: Toot?
-
- // many-to-one relationship
- @NSManaged public private(set) var author: MastodonUser
-
- // one-to-many relationship
- @NSManaged public private(set) var homeTimelineIndexes: Set?
}
public extension Toot {
@@ -73,7 +63,17 @@ public extension Toot {
static func insert(
into context: NSManagedObjectContext,
property: Property,
- author: MastodonUser
+ author: MastodonUser,
+ reblog: Toot?,
+ application: Application?,
+ mentions: [Mention]?,
+ emojis: [Emoji]?,
+ tags: [Tag]?,
+ favouritedBy: MastodonUser?,
+ rebloggedBy: MastodonUser?,
+ mutedBy: MastodonUser?,
+ bookmarkedBy: MastodonUser?,
+ pinnedBy: MastodonUser?
) -> Toot {
let toot: Toot = context.insertObject()
@@ -88,20 +88,7 @@ public extension Toot {
toot.visibility = property.visibility
toot.sensitive = property.sensitive
toot.spoilerText = property.spoilerText
-
- toot.application = property.application
-
- if let mentions = property.mentions {
- toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions)
- }
-
- if let emojis = property.emojis {
- toot.mutableSetValue(forKey: #keyPath(Toot.emojis)).addObjects(from: emojis)
- }
-
- if let tags = property.tags {
- toot.mutableSetValue(forKey: #keyPath(Toot.tags)).addObjects(from: tags)
- }
+ toot.application = application
toot.reblogsCount = property.reblogsCount
toot.favouritesCount = property.favouritesCount
@@ -110,31 +97,39 @@ public extension Toot {
toot.url = property.url
toot.inReplyToID = property.inReplyToID
toot.inReplyToAccountID = property.inReplyToAccountID
- toot.reblog = property.reblog
+
toot.language = property.language
toot.text = property.text
- if let favouritedBy = property.favouritedBy {
+ toot.author = author
+ toot.reblog = reblog
+
+ if let mentions = mentions {
+ toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions)
+ }
+ if let emojis = emojis {
+ toot.mutableSetValue(forKey: #keyPath(Toot.emojis)).addObjects(from: emojis)
+ }
+ if let tags = tags {
+ toot.mutableSetValue(forKey: #keyPath(Toot.tags)).addObjects(from: tags)
+ }
+ if let favouritedBy = favouritedBy {
toot.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(favouritedBy)
}
- if let rebloggedBy = property.rebloggedBy {
+ if let rebloggedBy = rebloggedBy {
toot.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).add(rebloggedBy)
}
- if let mutedBy = property.mutedBy {
+ if let mutedBy = mutedBy {
toot.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).add(mutedBy)
}
- if let bookmarkedBy = property.bookmarkedBy {
+ if let bookmarkedBy = bookmarkedBy {
toot.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(bookmarkedBy)
}
- if let pinnedBy = property.pinnedBy {
+ if let pinnedBy = pinnedBy {
toot.mutableSetValue(forKey: #keyPath(Toot.pinnedBy)).add(pinnedBy)
}
- toot.updatedAt = property.updatedAt
- toot.deletedAt = property.deletedAt
- toot.author = property.author
- toot.content = property.content
- toot.homeTimelineIndexes = property.homeTimelineIndexes
+ toot.updatedAt = property.networkDate
return toot
}
@@ -164,70 +159,6 @@ public extension Toot {
public extension Toot {
struct Property {
- public init(
- domain: String,
- id: String,
- uri: String,
- createdAt: Date,
- content: String,
- visibility: String?,
- sensitive: Bool,
- spoilerText: String?,
- application: Application?,
- mentions: [Mention]?,
- emojis: [Emoji]?,
- tags: [Tag]?,
- reblogsCount: NSNumber,
- favouritesCount: NSNumber,
- repliesCount: NSNumber?,
- url: String?,
- inReplyToID: Toot.ID?,
- inReplyToAccountID: MastodonUser.ID?,
- reblog: Toot?,
- language: String?,
- text: String?,
- favouritedBy: MastodonUser?,
- rebloggedBy: MastodonUser?,
- mutedBy: MastodonUser?,
- bookmarkedBy: MastodonUser?,
- pinnedBy: MastodonUser?,
- updatedAt: Date,
- deletedAt: Date?,
- author: MastodonUser,
- homeTimelineIndexes: Set?)
- {
- self.identifier = id + "@" + domain
- self.domain = domain
- self.id = id
- self.uri = uri
- self.createdAt = createdAt
- self.content = content
- self.visibility = visibility
- self.sensitive = sensitive
- self.spoilerText = spoilerText
- self.application = application
- self.mentions = mentions
- self.emojis = emojis
- self.tags = tags
- self.reblogsCount = reblogsCount
- self.favouritesCount = favouritesCount
- self.repliesCount = repliesCount
- self.url = url
- self.inReplyToID = inReplyToID
- self.inReplyToAccountID = inReplyToAccountID
- self.reblog = reblog
- self.language = language
- self.text = text
- self.favouritedBy = favouritedBy
- self.rebloggedBy = rebloggedBy
- self.mutedBy = mutedBy
- self.bookmarkedBy = bookmarkedBy
- self.pinnedBy = pinnedBy
- self.updatedAt = updatedAt
- self.deletedAt = deletedAt
- self.author = author
- self.homeTimelineIndexes = homeTimelineIndexes
- }
public let identifier: ID
public let domain: String
@@ -240,11 +171,7 @@ public extension Toot {
public let visibility: String?
public let sensitive: Bool
public let spoilerText: String?
- public let application: Application?
- public let mentions: [Mention]?
- public let emojis: [Emoji]?
- public let tags: [Tag]?
public let reblogsCount: NSNumber
public let favouritesCount: NSNumber
public let repliesCount: NSNumber?
@@ -252,22 +179,50 @@ public extension Toot {
public let url: String?
public let inReplyToID: Toot.ID?
public let inReplyToAccountID: MastodonUser.ID?
- public let reblog: Toot?
public let language: String? // (ISO 639 Part @1 two-letter language code)
public let text: String?
+
+ public let networkDate: Date
- public let favouritedBy: MastodonUser?
- public let rebloggedBy: MastodonUser?
- public let mutedBy: MastodonUser?
- public let bookmarkedBy: MastodonUser?
- public let pinnedBy: MastodonUser?
+ public init(
+ domain: String,
+ id: String,
+ uri: String,
+ createdAt: Date,
+ content: String,
+ visibility: String?,
+ sensitive: Bool,
+ spoilerText: String?,
+ reblogsCount: NSNumber,
+ favouritesCount: NSNumber,
+ repliesCount: NSNumber?,
+ url: String?,
+ inReplyToID: Toot.ID?,
+ inReplyToAccountID: MastodonUser.ID?,
+ language: String?,
+ text: String?,
+ networkDate: Date
+ ) {
+ self.identifier = id + "@" + domain
+ self.domain = domain
+ self.id = id
+ self.uri = uri
+ self.createdAt = createdAt
+ self.content = content
+ self.visibility = visibility
+ self.sensitive = sensitive
+ self.spoilerText = spoilerText
+ self.reblogsCount = reblogsCount
+ self.favouritesCount = favouritesCount
+ self.repliesCount = repliesCount
+ self.url = url
+ self.inReplyToID = inReplyToID
+ self.inReplyToAccountID = inReplyToAccountID
+ self.language = language
+ self.text = text
+ self.networkDate = networkDate
+ }
- public let updatedAt: Date
- public let deletedAt: Date?
-
- public let author: MastodonUser
-
- public let homeTimelineIndexes: Set?
}
}
@@ -277,20 +232,39 @@ extension Toot: Managed {
}
}
-public extension Toot {
- static func predicate(idStr: String) -> NSPredicate {
- return NSPredicate(format: "%K == %@", #keyPath(Toot.id), idStr)
+extension Toot {
+
+ static func predicate(domain: String) -> NSPredicate {
+ return NSPredicate(format: "%K == %@", #keyPath(Toot.domain), domain)
}
- static func predicate(idStrs: [String]) -> NSPredicate {
- return NSPredicate(format: "%K IN %@", #keyPath(Toot.id), idStrs)
+ static func predicate(id: String) -> NSPredicate {
+ return NSPredicate(format: "%K == %@", #keyPath(Toot.id), id)
}
- static func notDeleted() -> NSPredicate {
+ public static func predicate(domain: String, id: String) -> NSPredicate {
+ return NSCompoundPredicate(andPredicateWithSubpredicates: [
+ predicate(domain: domain),
+ predicate(id: id)
+ ])
+ }
+
+ static func predicate(ids: [String]) -> NSPredicate {
+ return NSPredicate(format: "%K IN %@", #keyPath(Toot.id), ids)
+ }
+
+ public static func predicate(domain: String, ids: [String]) -> NSPredicate {
+ return NSCompoundPredicate(andPredicateWithSubpredicates: [
+ predicate(domain: domain),
+ predicate(ids: ids)
+ ])
+ }
+
+ public static func notDeleted() -> NSPredicate {
return NSPredicate(format: "%K == nil", #keyPath(Toot.deletedAt))
}
- static func deleted() -> NSPredicate {
+ public static func deleted() -> NSPredicate {
return NSPredicate(format: "%K != nil", #keyPath(Toot.deletedAt))
}
}
diff --git a/CoreDataStack/Extension/Collection.swift b/CoreDataStack/Extension/Collection.swift
index 1b2574b5..a57737d1 100644
--- a/CoreDataStack/Extension/Collection.swift
+++ b/CoreDataStack/Extension/Collection.swift
@@ -3,7 +3,6 @@
// CoreDataStack
//
// Created by Cirno MainasuK on 2020-10-14.
-// Copyright © 2020 Twidere. All rights reserved.
//
import Foundation
diff --git a/CoreDataStack/Extension/NSManagedObjectContext.swift b/CoreDataStack/Extension/NSManagedObjectContext.swift
index 159d4bd1..e3f6600c 100644
--- a/CoreDataStack/Extension/NSManagedObjectContext.swift
+++ b/CoreDataStack/Extension/NSManagedObjectContext.swift
@@ -3,7 +3,6 @@
// CoreDataStack
//
// Created by Cirno MainasuK on 2020-8-10.
-// Copyright © 2020 Dimension. All rights reserved.
//
import os
diff --git a/CoreDataStack/Protocol/Managed.swift b/CoreDataStack/Protocol/Managed.swift
index 3d297779..4811b9c6 100644
--- a/CoreDataStack/Protocol/Managed.swift
+++ b/CoreDataStack/Protocol/Managed.swift
@@ -3,7 +3,6 @@
// CoreDataStack
//
// Created by Cirno MainasuK on 2020-8-6.
-// Copyright © 2020 Dimension. All rights reserved.
//
import Foundation
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index e67aded3..11403cdc 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -39,18 +39,17 @@
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
- 3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */; };
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; };
- 7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */; };
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */; };
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; };
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; };
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; };
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
+ DB084B5725CBC56C00F898ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Toot.swift */; };
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
@@ -67,6 +66,7 @@
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */; };
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; };
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; };
+ DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -186,10 +186,8 @@
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = ""; };
- 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = ""; };
9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = ""; };
- A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; };
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -198,6 +196,7 @@
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = ""; };
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = ""; };
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; };
+ DB084B5625CBC56C00F898ED /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = ""; };
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; };
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -220,6 +219,7 @@
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthentication.swift; sourceTree = ""; };
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = ""; };
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 = ""; };
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = ""; };
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
@@ -267,7 +267,6 @@
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
- 7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */,
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */,
);
@@ -286,7 +285,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */,
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -422,8 +420,6 @@
2D7631A625C1533800929FB9 /* TableviewCell */ = {
isa = PBXGroup;
children = (
- 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */,
- A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */,
2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */,
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
@@ -469,6 +465,15 @@
path = PinBased;
sourceTree = "";
};
+ DB084B5125CBC56300F898ED /* CoreDataStack */ = {
+ isa = PBXGroup;
+ children = (
+ DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
+ DB084B5625CBC56C00F898ED /* Toot.swift */,
+ );
+ path = CoreDataStack;
+ sourceTree = "";
+ };
DB3D0FF725BAA68500EAA174 /* Supporting Files */ = {
isa = PBXGroup;
children = (
@@ -559,14 +564,15 @@
DB45FB0425CA87B4005A8AC7 /* APIService */ = {
isa = PBXGroup;
children = (
- 2D61335625C1887F00CAE157 /* Persist */,
DB45FB0925CA87BC005A8AC7 /* CoreData */,
+ 2D61335625C1887F00CAE157 /* Persist */,
2D61335D25C1894B00CAE157 /* APIService.swift */,
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */,
DB98336A25C9420100AD9700 /* APIService+App.swift */,
DB98337025C9443200AD9700 /* APIService+Authentication.swift */,
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
+ DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
);
path = APIService;
sourceTree = "";
@@ -682,7 +688,7 @@
DB8AF56225C138BC002E6C99 /* Extension */ = {
isa = PBXGroup;
children = (
- DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
+ DB084B5125CBC56300F898ED /* CoreDataStack */,
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */,
@@ -1060,6 +1066,7 @@
buildActionMask = 2147483647;
files = (
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
+ DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
@@ -1088,6 +1095,7 @@
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */,
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
+ DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index 997b42f4..6f7ab589 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,12 +7,12 @@
CoreDataStack.xcscheme_^#shared#^_
orderHint
- 7
+ 8
Mastodon.xcscheme_^#shared#^_
orderHint
- 0
+ 1
SuppressBuildableAutocreation
diff --git a/Mastodon/Extension/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift
similarity index 100%
rename from Mastodon/Extension/MastodonUser.swift
rename to Mastodon/Extension/CoreDataStack/MastodonUser.swift
diff --git a/Mastodon/Extension/CoreDataStack/Toot.swift b/Mastodon/Extension/CoreDataStack/Toot.swift
new file mode 100644
index 00000000..2fab537e
--- /dev/null
+++ b/Mastodon/Extension/CoreDataStack/Toot.swift
@@ -0,0 +1,34 @@
+//
+// Toot.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021/2/4.
+//
+
+import Foundation
+import CoreDataStack
+import MastodonSDK
+
+extension Toot.Property {
+ init(entity: Mastodon.Entity.Status, domain: String, networkDate: Date) {
+ self.init(
+ domain: domain,
+ id: entity.id,
+ uri: entity.uri,
+ createdAt: entity.createdAt,
+ content: entity.content,
+ visibility: entity.visibility?.rawValue,
+ sensitive: entity.sensitive ?? false,
+ spoilerText: entity.spoilerText,
+ reblogsCount: NSNumber(value: entity.reblogsCount),
+ favouritesCount: NSNumber(value: entity.favouritesCount),
+ repliesCount: entity.repliesCount.flatMap { NSNumber(value: $0) },
+ url: entity.uri,
+ inReplyToID: entity.inReplyToID,
+ inReplyToAccountID: entity.inReplyToAccountID,
+ language: entity.language,
+ text: entity.text,
+ networkDate: networkDate
+ )
+ }
+}
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift
index 042d1582..73c5c783 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift
@@ -76,10 +76,6 @@ extension PublicTimelineViewController {
)
}
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- viewModel.stateMachine.enter(PublicTimelineViewModel.State.Loading.self)
- }
}
// MARK: - UIScrollViewDelegate
@@ -87,11 +83,9 @@ extension PublicTimelineViewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
handleScrollViewDidScroll(scrollView)
}
-
}
// MARK: - Selector
-
extension PublicTimelineViewController {
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
guard viewModel.stateMachine.enter(PublicTimelineViewModel.State.Loading.self) else {
@@ -102,7 +96,6 @@ extension PublicTimelineViewController {
}
// MARK: - UITableViewDelegate
-
extension PublicTimelineViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift
index 6e4e1069..a2766a00 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift
@@ -29,6 +29,7 @@ extension PublicTimelineViewModel {
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate
)
items.value = []
+ stateMachine.enter(PublicTimelineViewModel.State.Loading.self)
}
}
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift
index 3174015b..04bb4ec5 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift
@@ -51,8 +51,12 @@ extension PublicTimelineViewModel.State {
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
+ guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
+ stateMachine.enter(Fail.self)
+ return
+ }
- viewModel.fetchLatest()
+ viewModel.context.apiService.publicTimeline(domain: activeMastodonAuthenticationBox.domain)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
@@ -82,6 +86,14 @@ extension PublicTimelineViewModel.State {
return false
}
}
+
+ override func didEnter(from previousState: GKState?) {
+ super.didEnter(from: previousState)
+ guard let viewModel = viewModel else { return }
+
+ // trigger items update
+ viewModel.items.value = viewModel.items.value
+ }
}
class Idle: PublicTimelineViewModel.State {
@@ -110,29 +122,36 @@ extension PublicTimelineViewModel.State {
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
-
- viewModel.loadMore()
- .sink { completion in
- switch completion {
- case .failure(let error):
- stateMachine.enter(Fail.self)
- os_log("%{public}s[%{public}ld], %{public}s: load more fail: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
- case .finished:
- break
- }
- } receiveValue: { response in
- stateMachine.enter(Idle.self)
- var oldTootsIDs = viewModel.tootIDs.value
- for toot in response.value {
- if !oldTootsIDs.contains(toot.id) {
- oldTootsIDs.append(toot.id)
- }
- }
-
- viewModel.tootIDs.value = oldTootsIDs
-
+ guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
+ stateMachine.enter(Fail.self)
+ return
+ }
+ let maxID = viewModel.tootIDs.value.last
+ viewModel.context.apiService.publicTimeline(
+ domain: activeMastodonAuthenticationBox.domain,
+ maxID: maxID
+ )
+ .sink { completion in
+ switch completion {
+ case .failure(let error):
+ stateMachine.enter(Fail.self)
+ os_log("%{public}s[%{public}ld], %{public}s: load more fail: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
+ case .finished:
+ break
}
- .store(in: &viewModel.disposeBag)
+ } receiveValue: { response in
+ stateMachine.enter(Idle.self)
+ var oldTootsIDs = viewModel.tootIDs.value
+ for toot in response.value {
+ if !oldTootsIDs.contains(toot.id) {
+ oldTootsIDs.append(toot.id)
+ }
+ }
+
+ viewModel.tootIDs.value = oldTootsIDs
+
+ }
+ .store(in: &viewModel.disposeBag)
}
}
}
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift
index 115a35f2..fda4a240 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift
@@ -46,7 +46,7 @@ class PublicTimelineViewModel: NSObject {
self.context = context
self.fetchedResultsController = {
let fetchRequest = Toot.sortedFetchRequest
- fetchRequest.predicate = Toot.predicate(idStrs: [])
+ fetchRequest.predicate = Toot.predicate(domain: "", ids: [])
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.fetchBatchSize = 20
let controller = NSFetchedResultsController(
@@ -89,7 +89,8 @@ class PublicTimelineViewModel: NSObject {
.receive(on: DispatchQueue.main)
.sink { [weak self] ids in
guard let self = self else { return }
- self.fetchedResultsController.fetchRequest.predicate = Toot.predicate(idStrs: ids)
+ let domain = self.context.authenticationService.activeMastodonAuthenticationBox.value?.domain ?? ""
+ self.fetchedResultsController.fetchRequest.predicate = Toot.predicate(domain: domain, ids: ids)
do {
try self.fetchedResultsController.performFetch()
} catch {
@@ -105,11 +106,8 @@ class PublicTimelineViewModel: NSObject {
}
extension PublicTimelineViewModel {
- func fetchLatest() -> AnyPublisher, Error> {
- return context.apiService.publicTimeline(count: 20, domain: "mstdn.jp")
- }
func loadMore() -> AnyPublisher, Error> {
- return context.apiService.publicTimeline(count: 20, domain: "mstdn.jp")
+ return context.apiService.publicTimeline(domain: "mstdn.jp")
}
}
diff --git a/Mastodon/Service/APIService/APIService+HomeTimeline.swift b/Mastodon/Service/APIService/APIService+HomeTimeline.swift
new file mode 100644
index 00000000..b4160fad
--- /dev/null
+++ b/Mastodon/Service/APIService/APIService+HomeTimeline.swift
@@ -0,0 +1,66 @@
+//
+// APIService+HomeTimeline.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021/2/3.
+//
+
+import Foundation
+import Combine
+import CoreData
+import CoreDataStack
+import CommonOSLog
+import DateToolsSwift
+import MastodonSDK
+
+extension APIService {
+
+ func homeTimeline(
+ domain: String,
+ sinceID: Mastodon.Entity.Status.ID? = nil,
+ maxID: Mastodon.Entity.Status.ID? = nil,
+ limit: Int = 100,
+ local: Bool? = nil,
+ authorizationBox: AuthenticationService.MastodonAuthenticationBox
+ ) -> AnyPublisher, Error> {
+ let authorization = authorizationBox.userAuthorization
+ let requestMastodonUserID = authorizationBox.userID
+ let query = Mastodon.API.Timeline.HomeTimelineQuery(
+ maxID: maxID,
+ sinceID: sinceID,
+ minID: nil, // prefer sinceID
+ limit: limit,
+ local: local
+ )
+
+ return Mastodon.API.Timeline.home(
+ session: session,
+ domain: domain,
+ query: query,
+ authorization: authorization
+ )
+ .flatMap { response -> AnyPublisher, Error> in
+ return APIService.Persist.persistTimeline(
+ managedObjectContext: self.backgroundManagedObjectContext,
+ domain: domain,
+ query: query,
+ response: response,
+ persistType: .home,
+ requestMastodonUserID: requestMastodonUserID,
+ log: OSLog.api
+ )
+ .setFailureType(to: Error.self)
+ .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Toot]> in
+ switch result {
+ case .success:
+ return response
+ case .failure(let error):
+ throw error
+ }
+ }
+ .eraseToAnyPublisher()
+ }
+ .eraseToAnyPublisher()
+ }
+
+}
diff --git a/Mastodon/Service/APIService/APIService+PublicTimeline.swift b/Mastodon/Service/APIService/APIService+PublicTimeline.swift
index 3e75d246..aa45dbb9 100644
--- a/Mastodon/Service/APIService/APIService+PublicTimeline.swift
+++ b/Mastodon/Service/APIService/APIService+PublicTimeline.swift
@@ -9,6 +9,7 @@ import Foundation
import Combine
import CoreData
import CoreDataStack
+import CommonOSLog
import DateToolsSwift
import MastodonSDK
@@ -17,21 +18,35 @@ extension APIService {
static let publicTimelineRequestWindowInSec: TimeInterval = 15 * 60
func publicTimeline(
- count: Int = 20,
- domain: String
+ domain: String,
+ sinceID: Mastodon.Entity.Status.ID? = nil,
+ maxID: Mastodon.Entity.Status.ID? = nil,
+ limit: Int = 100
) -> AnyPublisher, Error> {
-
+ let query = Mastodon.API.Timeline.PublicTimelineQuery(
+ local: nil,
+ remote: nil,
+ onlyMedia: nil,
+ maxID: maxID,
+ sinceID: sinceID,
+ minID: nil, // prefer sinceID
+ limit: limit
+ )
+
return Mastodon.API.Timeline.public(
session: session,
domain: domain,
- query: Mastodon.API.Timeline.PublicTimelineQuery()
+ query: query
)
.flatMap { response -> AnyPublisher, Error> in
return APIService.Persist.persistTimeline(
- domain: domain,
managedObjectContext: self.backgroundManagedObjectContext,
+ domain: domain,
+ query: query,
response: response,
- persistType: Persist.PersistTimelineType.publicHomeTimeline
+ persistType: .public,
+ requestMastodonUserID: nil,
+ log: OSLog.api
)
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Toot]> in
diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift
index b4bdb28b..c9f5e325 100644
--- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift
+++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift
@@ -13,25 +13,26 @@ import MastodonSDK
extension APIService.CoreData {
- static func createOrMergeTweet(
+ static func createOrMergeToot(
into managedObjectContext: NSManagedObjectContext,
- for requestMastodonUser: MastodonUser,
+ for requestMastodonUser: MastodonUser?,
entity: Mastodon.Entity.Toot,
domain: String,
networkDate: Date,
log: OSLog
- ) -> (Toot: Toot, isTweetCreated: Bool, isMastodonUserCreated: Bool) {
+ ) -> (Toot: Toot, isTootCreated: Bool, isMastodonUserCreated: Bool) {
// build tree
let reblog = entity.reblog.flatMap { entity -> Toot in
- let (toot, _, _) = createOrMergeTweet(into: managedObjectContext, for: requestMastodonUser, entity: entity,domain: domain, networkDate: networkDate, log: log)
+ let (toot, _, _) = createOrMergeToot(into: managedObjectContext, for: requestMastodonUser, entity: entity, domain: domain, networkDate: networkDate, log: log)
return toot
}
// fetch old Toot
- let oldTweet: Toot? = {
+ let oldToot: Toot? = {
let request = Toot.sortedFetchRequest
- request.predicate = Toot.predicate(idStr: entity.id)
+ request.predicate = Toot.predicate(domain: domain, id: entity.id)
+ request.fetchLimit = 1
request.returnsObjectsAsFaults = false
do {
return try managedObjectContext.fetch(request).first
@@ -41,64 +42,47 @@ extension APIService.CoreData {
}
}()
- if let oldTweet = oldTweet {
+ if let oldToot = oldToot {
// merge old Toot
- APIService.CoreData.mergeToot(for: requestMastodonUser, old: oldTweet,in: domain, entity: entity, networkDate: networkDate)
- return (oldTweet, false, false)
+ APIService.CoreData.mergeToot(for: requestMastodonUser, old: oldToot,in: domain, entity: entity, 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 application = entity.application.flatMap { app -> Application? in
Application.insert(into: managedObjectContext, property: Application.Property(name: app.name, website: app.website, vapidKey: app.vapidKey))
}
-
- let metions = entity.mentions?.compactMap({ (mention) -> Mention in
+ let metions = entity.mentions?.compactMap { mention -> Mention in
Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url))
- })
- let emojis = entity.emojis?.compactMap({ (emoji) -> Emoji in
+ }
+ let emojis = entity.emojis?.compactMap { emoji -> Emoji in
Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category))
- })
- let tags = entity.tags?.compactMap({ (tag) -> Tag in
+ }
+ let tags = entity.tags?.compactMap { tag -> Tag in
let histories = tag.history?.compactMap({ (history) -> History in
History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts))
})
return Tag.insert(into: managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories))
- })
- let tootProperty = Toot.Property(
- domain: domain,
- id: entity.id,
- uri: entity.uri,
- createdAt: entity.createdAt,
- content: entity.content,
- visibility: entity.visibility?.rawValue,
- sensitive: entity.sensitive ?? false,
- spoilerText: entity.spoilerText,
+ }
+ let tootProperty = Toot.Property(entity: entity, domain: domain, networkDate: networkDate)
+ let toot = Toot.insert(
+ into: managedObjectContext,
+ property: tootProperty,
+ author: mastodonUser,
+ reblog: reblog,
application: application,
mentions: metions,
emojis: emojis,
tags: tags,
- reblogsCount: NSNumber(value: entity.reblogsCount),
- favouritesCount: NSNumber(value: entity.favouritesCount),
- repliesCount: (entity.repliesCount != nil) ? NSNumber(value: entity.repliesCount!) : nil,
- url: entity.uri,
- inReplyToID: entity.inReplyToID,
- inReplyToAccountID: entity.inReplyToAccountID,
- reblog: reblog,
- language: entity.language,
- text: entity.text,
- favouritedBy: (entity.favourited ?? false) ? mastodonUser : nil,
- rebloggedBy: (entity.reblogged ?? false) ? mastodonUser : nil,
- mutedBy: (entity.muted ?? false) ? mastodonUser : nil,
- bookmarkedBy: (entity.bookmarked ?? false) ? mastodonUser : nil,
- pinnedBy: (entity.pinned ?? false) ? mastodonUser : nil,
- updatedAt: networkDate,
- deletedAt: nil,
- author: requestMastodonUser,
- homeTimelineIndexes: nil)
- let toot = Toot.insert(into: managedObjectContext, property: tootProperty, author: mastodonUser)
+ favouritedBy: requestMastodonUser,
+ rebloggedBy: requestMastodonUser,
+ mutedBy: requestMastodonUser,
+ bookmarkedBy: requestMastodonUser,
+ pinnedBy: requestMastodonUser
+ )
return (toot, true, isMastodonUserCreated)
}
}
+
static func mergeToot(for requestMastodonUser: MastodonUser?, old toot: Toot,in domain: String, entity: Mastodon.Entity.Toot, networkDate: Date) {
guard networkDate > toot.updatedAt else { return }
@@ -114,8 +98,7 @@ extension APIService.CoreData {
if entity.reblogsCount != toot.reblogsCount.intValue {
toot.update(reblogsCount:NSNumber(value: entity.reblogsCount))
}
-
-
+
// set updateAt
toot.didUpdate(at: networkDate)
diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+Timeline.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+Timeline.swift
index ec38e599..61ff91de 100644
--- a/Mastodon/Service/APIService/Persist/APIService+Persist+Timeline.swift
+++ b/Mastodon/Service/APIService/Persist/APIService+Persist+Timeline.swift
@@ -6,6 +6,7 @@
//
import os.log
+import func QuartzCore.CACurrentMediaTime
import Foundation
import Combine
import CoreData
@@ -13,70 +14,190 @@ import CoreDataStack
import MastodonSDK
extension APIService.Persist {
+
enum PersistTimelineType {
- case publicHomeTimeline
+ case `public`
+ case home
}
+
static func persistTimeline(
- domain: String,
managedObjectContext: NSManagedObjectContext,
+ domain: String,
+ query: Mastodon.API.Timeline.TimelineQuery,
response: Mastodon.Response.Content<[Mastodon.Entity.Toot]>,
- persistType: PersistTimelineType
+ 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 toots = response.value
- let _ = toots.map {
- let userProperty = MastodonUser.Property(id: $0.account.id, domain: domain, acct: $0.account.acct, username: $0.account.username, displayName: $0.account.displayName,avatar: $0.account.avatar,avatarStatic: $0.account.avatarStatic, createdAt: $0.createdAt, networkDate: $0.createdAt)
- let author = MastodonUser.insert(into: managedObjectContext, property: userProperty)
- let application = $0.application.flatMap { app -> Application? in
- Application.insert(into: managedObjectContext, property: Application.Property(name: app.name, website: app.website, vapidKey: app.vapidKey))
- }
- let metions = $0.mentions?.compactMap({ (mention) -> Mention in
- Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url))
- })
- let emojis = $0.emojis?.compactMap({ (emoji) -> Emoji in
- Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category))
- })
-
- let tags = $0.tags?.compactMap({ (tag) -> Tag in
- let histories = tag.history?.compactMap({ (history) -> History in
- History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts))
- })
- return Tag.insert(into: managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories))
- })
- let tootProperty = Toot.Property(
- domain: domain,
- id: $0.id,
- uri: $0.uri,
- createdAt: $0.createdAt,
- content: $0.content,
- visibility: $0.visibility?.rawValue,
- sensitive: $0.sensitive ?? false,
- spoilerText: $0.spoilerText,
- application: application,
- mentions: metions,
- emojis: emojis,
- tags: tags,
- reblogsCount: NSNumber(value: $0.reblogsCount),
- favouritesCount: NSNumber(value: $0.favouritesCount),
- repliesCount: ($0.repliesCount != nil) ? NSNumber(value: $0.repliesCount!) : nil,
- url: $0.uri,
- inReplyToID: $0.inReplyToID,
- inReplyToAccountID: $0.inReplyToAccountID,
- reblog: nil, //TODO need fix
- language: $0.language,
- text: $0.text,
- favouritedBy: ($0.favourited ?? false) ? author : nil,
- rebloggedBy: ($0.reblogged ?? false) ? author : nil,
- mutedBy: ($0.muted ?? false) ? author : nil,
- bookmarkedBy: ($0.bookmarked ?? false) ? author : nil,
- pinnedBy: ($0.pinned ?? false) ? author : nil,
- updatedAt: response.networkDate,
- deletedAt: nil,
- author: author,
- homeTimelineIndexes: nil)
- Toot.insert(into: managedObjectContext, property: tootProperty, author: author)
+ 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
+ }
+ }()
+
+ 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:
@@ -91,3 +212,232 @@ extension APIService.Persist {
.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)
+ let _ = HomeTimelineIndex.insert(
+ into: managedObjectContext,
+ property: timelineIndexProperty,
+ toot: status
+ )
+ } else {
+ // enity already in home timeline
+ }
+ default:
+ break
+ }
+
+ return result
+ }
+
+ }
+
+}
+
diff --git a/Mastodon/Service/AuthenticationService.swift b/Mastodon/Service/AuthenticationService.swift
index 16969b8a..9fa411f2 100644
--- a/Mastodon/Service/AuthenticationService.swift
+++ b/Mastodon/Service/AuthenticationService.swift
@@ -62,6 +62,7 @@ class AuthenticationService: NSObject {
.map { authentication -> AuthenticationService.MastodonAuthenticationBox? in
guard let authentication = authentication else { return nil }
return AuthenticationService.MastodonAuthenticationBox(
+ domain: authentication.domain,
userID: authentication.userID,
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)
@@ -82,6 +83,7 @@ class AuthenticationService: NSObject {
extension AuthenticationService {
struct MastodonAuthenticationBox {
+ let domain: String
let userID: MastodonUser.ID
let appAuthorization: Mastodon.API.OAuth.Authorization
let userAuthorization: Mastodon.API.OAuth.Authorization
diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift
index 95f912b4..66d7d806 100644
--- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift
+++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift
@@ -13,6 +13,9 @@ extension Mastodon.API.Timeline {
static func publicTimelineEndpointURL(domain: String) -> URL {
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("timelines/public")
}
+ static func homeTimelineEndpointURL(domain: String) -> URL {
+ return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("timelines/home")
+ }
public static func `public`(
session: URLSession,
@@ -32,10 +35,37 @@ extension Mastodon.API.Timeline {
.eraseToAnyPublisher()
}
+ public static func home(
+ session: URLSession,
+ domain: String,
+ query: HomeTimelineQuery,
+ authorization: Mastodon.API.OAuth.Authorization
+ ) -> AnyPublisher, Error> {
+ let request = Mastodon.API.get(
+ url: homeTimelineEndpointURL(domain: domain),
+ query: query,
+ authorization: authorization
+ )
+ return session.dataTaskPublisher(for: request)
+ .tryMap { data, response in
+ let value = try Mastodon.API.decode(type: [Mastodon.Entity.Toot].self, from: data, response: response)
+ return Mastodon.Response.Content(value: value, response: response)
+ }
+ .eraseToAnyPublisher()
+ }
+
+}
+
+public protocol TimelineQueryType {
+ var maxID: Mastodon.Entity.Toot.ID? { get }
+ var sinceID: Mastodon.Entity.Toot.ID? { get }
}
extension Mastodon.API.Timeline {
- public struct PublicTimelineQuery: Codable, GetQuery {
+
+ public typealias TimelineQuery = TimelineQueryType
+
+ public struct PublicTimelineQuery: Codable, TimelineQuery, GetQuery {
public let local: Bool?
public let remote: Bool?
@@ -76,4 +106,38 @@ extension Mastodon.API.Timeline {
return items
}
}
+
+ public struct HomeTimelineQuery: Codable, TimelineQuery, GetQuery {
+ public let maxID: Mastodon.Entity.Toot.ID?
+ public let sinceID: Mastodon.Entity.Toot.ID?
+ public let minID: Mastodon.Entity.Toot.ID?
+ public let limit: Int?
+ public let local: Bool?
+
+ public init(
+ maxID: Mastodon.Entity.Toot.ID? = nil,
+ sinceID: Mastodon.Entity.Toot.ID? = nil,
+ minID: Mastodon.Entity.Toot.ID? = nil,
+ limit: Int? = nil,
+ local: Bool? = nil
+ ) {
+ self.maxID = maxID
+ self.sinceID = sinceID
+ self.minID = minID
+ self.limit = limit
+ self.local = local
+ }
+
+ var queryItems: [URLQueryItem]? {
+ var items: [URLQueryItem] = []
+ maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) }
+ sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) }
+ minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $0)) }
+ limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
+ local.flatMap { items.append(URLQueryItem(name: "local", value: $0.queryItemValue)) }
+ guard !items.isEmpty else { return nil }
+ return items
+ }
+ }
+
}
diff --git a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+TimelineTests.swift b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+TimelineTests.swift
new file mode 100644
index 00000000..ec6cb849
--- /dev/null
+++ b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+TimelineTests.swift
@@ -0,0 +1,71 @@
+//
+// MastodonSDK+API+TimelineTests.swift
+//
+//
+// Created by MainasuK Cirno on 2021/2/3.
+//
+
+import os.log
+import XCTest
+import Combine
+@testable import MastodonSDK
+
+extension MastodonSDKTests {
+
+ func testPublicTimeline() throws {
+ try _testPublicTimeline(domain: domain)
+ }
+
+ private func _testPublicTimeline(domain: String) throws {
+ let theExpectation = expectation(description: "Fetch Public Timeline")
+
+ let query = Mastodon.API.Timeline.PublicTimelineQuery()
+ Mastodon.API.Timeline.public(session: session, domain: domain, query: query)
+ .receive(on: DispatchQueue.main)
+ .sink { completion in
+ switch completion {
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ case .finished:
+ break
+ }
+ } receiveValue: { response in
+ XCTAssert(!response.value.isEmpty)
+ theExpectation.fulfill()
+ }
+ .store(in: &disposeBag)
+
+ wait(for: [theExpectation], timeout: 10.0)
+ }
+
+}
+
+extension MastodonSDKTests {
+
+ func testHomeTimeline() {
+ let domain = ""
+ let accessToken = ""
+ guard !domain.isEmpty, !accessToken.isEmpty else { return }
+
+ let query = Mastodon.API.Timeline.HomeTimelineQuery()
+ let authorization = Mastodon.API.OAuth.Authorization(accessToken: accessToken)
+ let theExpectation = expectation(description: "Fetch Home Timeline")
+ Mastodon.API.Timeline.home(session: session, domain: domain, query: query, authorization: authorization)
+ .receive(on: DispatchQueue.main)
+ .sink { completion in
+ switch completion {
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ case .finished:
+ break
+ }
+ } receiveValue: { response in
+ XCTAssert(!response.value.isEmpty)
+ theExpectation.fulfill()
+ }
+ .store(in: &disposeBag)
+
+ wait(for: [theExpectation], timeout: 10.0)
+ }
+
+}
diff --git a/MastodonSDK/Tests/MastodonSDKTests/MastodonSDKTests.swift b/MastodonSDK/Tests/MastodonSDKTests/MastodonSDKTests.swift
index 8996d1aa..b32261c3 100644
--- a/MastodonSDK/Tests/MastodonSDKTests/MastodonSDKTests.swift
+++ b/MastodonSDK/Tests/MastodonSDKTests/MastodonSDKTests.swift
@@ -14,33 +14,3 @@ final class MastodonSDKTests: XCTestCase {
}
}
-
-extension MastodonSDKTests {
-
- func testPublicTimeline() throws {
- try _testPublicTimeline(domain: domain)
- }
-
- private func _testPublicTimeline(domain: String) throws {
- let theExpectation = expectation(description: "Fetch Public Timeline")
-
- let query = Mastodon.API.Timeline.PublicTimelineQuery()
- Mastodon.API.Timeline.public(session: session, domain: domain, query: query)
- .receive(on: DispatchQueue.main)
- .sink { completion in
- switch completion {
- case .failure(let error):
- XCTFail(error.localizedDescription)
- case .finished:
- break
- }
- } receiveValue: { response in
- XCTAssert(!response.value.isEmpty)
- theExpectation.fulfill()
- }
- .store(in: &disposeBag)
-
- wait(for: [theExpectation], timeout: 10.0)
- }
-
-}