commit
db7b2528af
|
@ -1,5 +1,12 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D64" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D5029f" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
|
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||||
|
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="vapidKey" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="website" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="toots" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="application" inverseEntity="Toot"/>
|
||||||
|
</entity>
|
||||||
<entity name="Emoji" representedClassName=".Emoji" syncable="YES">
|
<entity name="Emoji" representedClassName=".Emoji" syncable="YES">
|
||||||
<attribute name="category" optional="YES" attributeType="String"/>
|
<attribute name="category" optional="YES" attributeType="String"/>
|
||||||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||||
|
@ -95,6 +102,7 @@
|
||||||
<attribute name="uri" attributeType="String"/>
|
<attribute name="uri" attributeType="String"/>
|
||||||
<attribute name="url" attributeType="String"/>
|
<attribute name="url" attributeType="String"/>
|
||||||
<attribute name="visibility" optional="YES" attributeType="String"/>
|
<attribute name="visibility" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="toots" inverseEntity="Application"/>
|
||||||
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="toots" inverseEntity="MastodonUser"/>
|
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="toots" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="bookmarked" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
|
<relationship name="bookmarked" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="emojis" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Emoji" inverseName="toot" inverseEntity="Emoji"/>
|
<relationship name="emojis" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Emoji" inverseName="toot" inverseEntity="Emoji"/>
|
||||||
|
@ -109,13 +117,14 @@
|
||||||
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="toot" inverseEntity="Tag"/>
|
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="toot" inverseEntity="Tag"/>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
|
<element name="Application" positionX="160" positionY="192" width="128" height="104"/>
|
||||||
<element name="Emoji" positionX="45" positionY="135" width="128" height="149"/>
|
<element name="Emoji" positionX="45" positionY="135" width="128" height="149"/>
|
||||||
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
||||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="104"/>
|
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="104"/>
|
||||||
|
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
||||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="284"/>
|
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="284"/>
|
||||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||||
<element name="Toot" positionX="0" positionY="0" width="128" height="494"/>
|
<element name="Toot" positionX="0" positionY="0" width="128" height="494"/>
|
||||||
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -0,0 +1,61 @@
|
||||||
|
//
|
||||||
|
// Application.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public final class Application: NSManagedObject {
|
||||||
|
public typealias ID = UUID
|
||||||
|
@NSManaged public private(set) var identifier: ID
|
||||||
|
@NSManaged public private(set) var createAt: Date
|
||||||
|
|
||||||
|
@NSManaged public private(set) var name: String
|
||||||
|
@NSManaged public private(set) var website: String?
|
||||||
|
@NSManaged public private(set) var vapidKey: String?
|
||||||
|
|
||||||
|
// one-to-many relationship
|
||||||
|
@NSManaged public private(set) var toots: Set<Toot>
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Application {
|
||||||
|
override func awakeFromInsert() {
|
||||||
|
super.awakeFromInsert()
|
||||||
|
identifier = UUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
static func insert(
|
||||||
|
into context: NSManagedObjectContext,
|
||||||
|
property: Property
|
||||||
|
) -> Application {
|
||||||
|
let app: Application = context.insertObject()
|
||||||
|
app.name = property.name
|
||||||
|
app.website = property.website
|
||||||
|
app.vapidKey = property.vapidKey
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Application {
|
||||||
|
struct Property {
|
||||||
|
public let name: String
|
||||||
|
public let website: String?
|
||||||
|
public let vapidKey: String?
|
||||||
|
|
||||||
|
public init(name: String, website: String?, vapidKey: String?) {
|
||||||
|
self.name = name
|
||||||
|
self.website = website
|
||||||
|
self.vapidKey = vapidKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Application: Managed {
|
||||||
|
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||||
|
return [NSSortDescriptor(keyPath: \Application.createAt, ascending: false)]
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ public final class Toot: NSManagedObject {
|
||||||
@NSManaged public private(set) var visibility: String?
|
@NSManaged public private(set) var visibility: String?
|
||||||
@NSManaged public private(set) var sensitive: Bool
|
@NSManaged public private(set) var sensitive: Bool
|
||||||
@NSManaged public private(set) var spoilerText: String?
|
@NSManaged public private(set) var spoilerText: String?
|
||||||
|
@NSManaged public private(set) var application: Application?
|
||||||
|
|
||||||
// Informational
|
// Informational
|
||||||
@NSManaged public private(set) var reblogsCount: NSNumber
|
@NSManaged public private(set) var reblogsCount: NSNumber
|
||||||
|
@ -88,6 +89,8 @@ public extension Toot {
|
||||||
toot.sensitive = property.sensitive
|
toot.sensitive = property.sensitive
|
||||||
toot.spoilerText = property.spoilerText
|
toot.spoilerText = property.spoilerText
|
||||||
|
|
||||||
|
toot.application = property.application
|
||||||
|
|
||||||
if let mentions = property.mentions {
|
if let mentions = property.mentions {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions)
|
toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions)
|
||||||
}
|
}
|
||||||
|
@ -123,11 +126,9 @@ public extension Toot {
|
||||||
if let bookmarkedBy = property.bookmarkedBy {
|
if let bookmarkedBy = property.bookmarkedBy {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(bookmarkedBy)
|
toot.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(bookmarkedBy)
|
||||||
}
|
}
|
||||||
|
if let pinnedBy = property.pinnedBy {
|
||||||
// TODO: not implement yet
|
toot.mutableSetValue(forKey: #keyPath(Toot.pinnedBy)).add(pinnedBy)
|
||||||
// if let pinnedBy = property.pinnedBy {
|
}
|
||||||
// toot.mutableSetValue(forKey: #keyPath(Toot.pinnedBy))
|
|
||||||
// }
|
|
||||||
|
|
||||||
toot.updatedAt = property.updatedAt
|
toot.updatedAt = property.updatedAt
|
||||||
toot.deletedAt = property.deletedAt
|
toot.deletedAt = property.deletedAt
|
||||||
|
@ -137,6 +138,28 @@ public extension Toot {
|
||||||
|
|
||||||
return toot
|
return toot
|
||||||
}
|
}
|
||||||
|
func update(reblogsCount: NSNumber) {
|
||||||
|
if self.reblogsCount.intValue != reblogsCount.intValue {
|
||||||
|
self.reblogsCount = reblogsCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func update(favouritesCount: NSNumber) {
|
||||||
|
if self.favouritesCount.intValue != favouritesCount.intValue {
|
||||||
|
self.favouritesCount = favouritesCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func update(repliesCount: NSNumber?) {
|
||||||
|
guard let count = repliesCount else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.repliesCount?.intValue != count.intValue {
|
||||||
|
self.repliesCount = repliesCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func didUpdate(at networkDate: Date) {
|
||||||
|
self.updatedAt = networkDate
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Toot {
|
public extension Toot {
|
||||||
|
@ -150,6 +173,7 @@ public extension Toot {
|
||||||
visibility: String?,
|
visibility: String?,
|
||||||
sensitive: Bool,
|
sensitive: Bool,
|
||||||
spoilerText: String?,
|
spoilerText: String?,
|
||||||
|
application: Application?,
|
||||||
mentions: [Mention]?,
|
mentions: [Mention]?,
|
||||||
emojis: [Emoji]?,
|
emojis: [Emoji]?,
|
||||||
tags: [Tag]?,
|
tags: [Tag]?,
|
||||||
|
@ -181,6 +205,7 @@ public extension Toot {
|
||||||
self.visibility = visibility
|
self.visibility = visibility
|
||||||
self.sensitive = sensitive
|
self.sensitive = sensitive
|
||||||
self.spoilerText = spoilerText
|
self.spoilerText = spoilerText
|
||||||
|
self.application = application
|
||||||
self.mentions = mentions
|
self.mentions = mentions
|
||||||
self.emojis = emojis
|
self.emojis = emojis
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
|
@ -215,6 +240,7 @@ public extension Toot {
|
||||||
public let visibility: String?
|
public let visibility: String?
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
public let spoilerText: String?
|
public let spoilerText: String?
|
||||||
|
public let application: Application?
|
||||||
|
|
||||||
public let mentions: [Mention]?
|
public let mentions: [Mention]?
|
||||||
public let emojis: [Emoji]?
|
public let emojis: [Emoji]?
|
||||||
|
|
|
@ -16,11 +16,14 @@
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
||||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; };
|
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; };
|
||||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; };
|
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; };
|
||||||
|
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */; };
|
||||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; };
|
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; };
|
||||||
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46976325C2A71500CF4AA9 /* UIIamge.swift */; };
|
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46976325C2A71500CF4AA9 /* UIIamge.swift */; };
|
||||||
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; };
|
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; };
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
|
||||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
|
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
|
||||||
|
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; };
|
||||||
|
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; };
|
||||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; };
|
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; };
|
||||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.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 */; };
|
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */; };
|
||||||
|
@ -32,6 +35,9 @@
|
||||||
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; };
|
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; };
|
||||||
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; };
|
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; };
|
||||||
2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; };
|
2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; };
|
||||||
|
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; };
|
||||||
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
||||||
|
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
|
||||||
3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */; };
|
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 */; };
|
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
||||||
|
@ -153,10 +159,13 @@
|
||||||
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
|
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
|
||||||
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
|
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
|
||||||
2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
|
2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
|
||||||
|
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+State.swift"; sourceTree = "<group>"; };
|
||||||
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = "<group>"; };
|
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = "<group>"; };
|
||||||
2D46976325C2A71500CF4AA9 /* UIIamge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIIamge.swift; sourceTree = "<group>"; };
|
2D46976325C2A71500CF4AA9 /* UIIamge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIIamge.swift; sourceTree = "<group>"; };
|
||||||
2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = "<group>"; };
|
2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = "<group>"; };
|
||||||
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
|
||||||
|
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||||
|
2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = "<group>"; };
|
||||||
2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = "<group>"; };
|
2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewModel.swift; sourceTree = "<group>"; };
|
2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewModel.swift; sourceTree = "<group>"; };
|
||||||
2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -168,6 +177,10 @@
|
||||||
2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||||
2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
|
2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
|
||||||
2D927F1325C7EDD9004F19B8 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
|
2D927F1325C7EDD9004F19B8 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
|
||||||
|
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||||
|
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
|
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
|
||||||
2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = "<group>"; };
|
2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -350,6 +363,14 @@
|
||||||
path = Persist;
|
path = Persist;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
2D69CFF225CA9E2200C3A1B2 /* Protocol */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */,
|
||||||
|
);
|
||||||
|
path = Protocol;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
2D76316325C14BAC00929FB9 /* PublicTimeline */ = {
|
2D76316325C14BAC00929FB9 /* PublicTimeline */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -357,6 +378,7 @@
|
||||||
2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */,
|
2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */,
|
||||||
2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */,
|
2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */,
|
||||||
2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */,
|
2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */,
|
||||||
|
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */,
|
||||||
);
|
);
|
||||||
path = PublicTimeline;
|
path = PublicTimeline;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -403,6 +425,8 @@
|
||||||
602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */,
|
602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */,
|
||||||
A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */,
|
A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */,
|
||||||
2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */,
|
2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */,
|
||||||
|
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
|
||||||
|
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = TableviewCell;
|
path = TableviewCell;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -500,6 +524,7 @@
|
||||||
children = (
|
children = (
|
||||||
DB427DE325BAA00100D1B89D /* Info.plist */,
|
DB427DE325BAA00100D1B89D /* Info.plist */,
|
||||||
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
||||||
|
2D69CFF225CA9E2200C3A1B2 /* Protocol */,
|
||||||
2D76319C25C151DE00929FB9 /* Diffiable */,
|
2D76319C25C151DE00929FB9 /* Diffiable */,
|
||||||
DB8AF52A25C13561002E6C99 /* State */,
|
DB8AF52A25C13561002E6C99 /* State */,
|
||||||
2D61335525C1886800CAE157 /* Service */,
|
2D61335525C1886800CAE157 /* Service */,
|
||||||
|
@ -549,6 +574,7 @@
|
||||||
DB45FB0925CA87BC005A8AC7 /* CoreData */ = {
|
DB45FB0925CA87BC005A8AC7 /* CoreData */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */,
|
||||||
DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */,
|
DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */,
|
||||||
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */,
|
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */,
|
||||||
);
|
);
|
||||||
|
@ -600,6 +626,7 @@
|
||||||
2D927F0D25C7E9C9004F19B8 /* History.swift */,
|
2D927F0D25C7E9C9004F19B8 /* History.swift */,
|
||||||
2D927F1325C7EDD9004F19B8 /* Emoji.swift */,
|
2D927F1325C7EDD9004F19B8 /* Emoji.swift */,
|
||||||
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */,
|
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */,
|
||||||
|
2DA7D05625CA693F00804E11 /* Application.swift */,
|
||||||
);
|
);
|
||||||
path = Entity;
|
path = Entity;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -673,6 +700,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */,
|
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */,
|
||||||
|
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */,
|
||||||
);
|
);
|
||||||
name = "Recovered References";
|
name = "Recovered References";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1041,12 +1069,15 @@
|
||||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
||||||
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */,
|
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */,
|
||||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||||
|
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
|
||||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||||
|
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
||||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
||||||
|
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
|
||||||
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
||||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||||
|
@ -1069,10 +1100,12 @@
|
||||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
||||||
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */,
|
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */,
|
||||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||||
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
||||||
DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */,
|
DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */,
|
||||||
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
||||||
2D7631A825C1535600929FB9 /* TimelinePostTableViewCell.swift in Sources */,
|
2D7631A825C1535600929FB9 /* TimelinePostTableViewCell.swift in Sources */,
|
||||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||||
|
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
||||||
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
|
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||||
|
@ -1100,6 +1133,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
2DA7D05725CA693F00804E11 /* Application.swift in Sources */,
|
||||||
2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */,
|
2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */,
|
||||||
DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */,
|
DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */,
|
||||||
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
|
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
|
||||||
|
|
|
@ -5,16 +5,17 @@
|
||||||
// Created by sxiaojian on 2021/1/27.
|
// Created by sxiaojian on 2021/1/27.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import MastodonSDK
|
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
/// Note: update Equatable when change case
|
/// Note: update Equatable when change case
|
||||||
enum Item {
|
enum Item {
|
||||||
|
|
||||||
// normal list
|
// normal list
|
||||||
case toot(objectID: NSManagedObjectID)
|
case toot(objectID: NSManagedObjectID)
|
||||||
|
|
||||||
|
case bottomLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Item: Equatable {
|
extension Item: Equatable {
|
||||||
|
@ -22,6 +23,10 @@ extension Item: Equatable {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.toot(let objectIDLeft), .toot(let objectIDRight)):
|
case (.toot(let objectIDLeft), .toot(let objectIDRight)):
|
||||||
return objectIDLeft == objectIDRight
|
return objectIDLeft == objectIDRight
|
||||||
|
case (.bottomLoader, .bottomLoader):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +36,8 @@ extension Item: Hashable {
|
||||||
switch self {
|
switch self {
|
||||||
case .toot(let objectID):
|
case .toot(let objectID):
|
||||||
hasher.combine(objectID)
|
hasher.combine(objectID)
|
||||||
|
case .bottomLoader:
|
||||||
|
hasher.combine(String(describing: Item.bottomLoader.self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,10 @@ extension TimelineSection {
|
||||||
}
|
}
|
||||||
cell.delegate = timelinePostTableViewCellDelegate
|
cell.delegate = timelinePostTableViewCellDelegate
|
||||||
return cell
|
return cell
|
||||||
|
case .bottomLoader:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
|
cell.activityIndicatorView.startAnimating()
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,15 @@ import Foundation
|
||||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||||
internal enum L10n {
|
internal enum L10n {
|
||||||
|
|
||||||
|
internal enum Common {
|
||||||
|
internal enum Controls {
|
||||||
|
internal enum Timeline {
|
||||||
|
/// Load More
|
||||||
|
internal static let loadMore = L10n.tr("Localizable", "Common.Controls.Timeline.LoadMore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
|
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
|
||||||
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
|
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
//
|
||||||
|
// LoadMoreConfigurableTableViewContainer.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import GameplayKit
|
||||||
|
|
||||||
|
/// The tableView container driven by state machines with "LoadMore" logic
|
||||||
|
protocol LoadMoreConfigurableTableViewContainer: UIViewController {
|
||||||
|
|
||||||
|
associatedtype BottomLoaderTableViewCell: UITableViewCell
|
||||||
|
associatedtype LoadingState: GKState
|
||||||
|
|
||||||
|
var loadMoreConfigurableTableView: UITableView { get }
|
||||||
|
var loadMoreConfigurableStateMachine: GKStateMachine { get }
|
||||||
|
func handleScrollViewDidScroll(_ scrollView: UIScrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LoadMoreConfigurableTableViewContainer {
|
||||||
|
func handleScrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard scrollView === loadMoreConfigurableTableView else { return }
|
||||||
|
|
||||||
|
let cells = loadMoreConfigurableTableView.visibleCells.compactMap { $0 as? BottomLoaderTableViewCell }
|
||||||
|
guard let loaderTableViewCell = cells.first else { return }
|
||||||
|
|
||||||
|
if let tabBar = tabBarController?.tabBar, let window = view.window {
|
||||||
|
let loaderTableViewCellFrameInWindow = loadMoreConfigurableTableView.convert(loaderTableViewCell.frame, to: nil)
|
||||||
|
let windowHeight = window.frame.height
|
||||||
|
let loaderAppear = (loaderTableViewCellFrameInWindow.origin.y + 0.8 * loaderTableViewCell.frame.height) < (windowHeight - tabBar.frame.height)
|
||||||
|
if loaderAppear {
|
||||||
|
loadMoreConfigurableStateMachine.enter(LoadingState.self)
|
||||||
|
} else {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadMoreConfigurableStateMachine.enter(LoadingState.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,3 +5,4 @@
|
||||||
Created by MainasuK Cirno on 2021/1/22.
|
Created by MainasuK Cirno on 2021/1/22.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
"Common.Controls.Timeline.LoadMore" = "Load More";
|
||||||
|
|
|
@ -38,6 +38,8 @@ extension PublicTimelineViewController {
|
||||||
let toot = managedObjectContext.object(with: objectID) as? Toot
|
let toot = managedObjectContext.object(with: objectID) as? Toot
|
||||||
promise(.success(toot))
|
promise(.success(toot))
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
promise(.success(nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,41 +5,56 @@
|
||||||
// Created by sxiaojian on 2021/1/27.
|
// Created by sxiaojian on 2021/1/27.
|
||||||
//
|
//
|
||||||
|
|
||||||
import os.log
|
|
||||||
import UIKit
|
|
||||||
import AVKit
|
import AVKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
final class PublicTimelineViewController: UIViewController, NeedsDependency, TimelinePostTableViewCellDelegate {
|
final class PublicTimelineViewController: UIViewController, NeedsDependency, TimelinePostTableViewCellDelegate {
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
var viewModel: PublicTimelineViewModel!
|
var viewModel: PublicTimelineViewModel!
|
||||||
|
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
|
||||||
lazy var tableView: UITableView = {
|
lazy var tableView: UITableView = {
|
||||||
let tableView = UITableView()
|
let tableView = UITableView()
|
||||||
tableView.register(TimelinePostTableViewCell.self, forCellReuseIdentifier: String(describing: TimelinePostTableViewCell.self))
|
tableView.register(TimelinePostTableViewCell.self, forCellReuseIdentifier: String(describing: TimelinePostTableViewCell.self))
|
||||||
|
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.separatorStyle = .none
|
tableView.separatorStyle = .none
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PublicTimelineViewController {
|
extension PublicTimelineViewController {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
refreshControl.addTarget(self, action: #selector(PublicTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
// bind refresh control
|
||||||
|
viewModel.isFetchingLatestTimeline
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isFetching in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if !isFetching {
|
||||||
|
UIView.animate(withDuration: 0.5) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
tableView.backgroundColor = Asset.Colors.tootDark.color
|
tableView.backgroundColor = Asset.Colors.tootDark.color
|
||||||
view.addSubview(tableView)
|
view.addSubview(tableView)
|
||||||
|
@ -60,29 +75,35 @@ extension PublicTimelineViewController {
|
||||||
timelinePostTableViewCellDelegate: self
|
timelinePostTableViewCellDelegate: self
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
viewModel.fetchLatest()
|
viewModel.stateMachine.enter(PublicTimelineViewModel.State.Loading.self)
|
||||||
.receive(on: DispatchQueue.main)
|
}
|
||||||
.sink { completion in
|
}
|
||||||
switch completion {
|
|
||||||
case .failure(let error):
|
// MARK: - UIScrollViewDelegate
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: fetch user timeline latest response error: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
extension PublicTimelineViewController {
|
||||||
case .finished:
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
break
|
handleScrollViewDidScroll(scrollView)
|
||||||
}
|
}
|
||||||
} receiveValue: { response in
|
|
||||||
let tootsIDs = response.value.map { $0.id }
|
}
|
||||||
self.viewModel.tootIDs.value = tootsIDs
|
|
||||||
}
|
// MARK: - Selector
|
||||||
.store(in: &viewModel.disposeBag)
|
|
||||||
|
extension PublicTimelineViewController {
|
||||||
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||||
|
guard viewModel.stateMachine.enter(PublicTimelineViewModel.State.Loading.self) else {
|
||||||
|
sender.endRefreshing()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
|
|
||||||
extension PublicTimelineViewController: UITableViewDelegate {
|
extension PublicTimelineViewController: UITableViewDelegate {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
|
||||||
|
@ -94,12 +115,9 @@ extension PublicTimelineViewController: UITableViewDelegate {
|
||||||
return ceil(frame.height)
|
return ceil(frame.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
|
@ -108,3 +126,11 @@ extension PublicTimelineViewController: UITableViewDelegate {
|
||||||
viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key))
|
viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// MARK: - LoadMoreConfigurableTableViewContainer
|
||||||
|
extension PublicTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
|
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||||
|
typealias LoadingState = PublicTimelineViewModel.State.LoadingMore
|
||||||
|
|
||||||
|
var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||||
|
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
|
||||||
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
// Created by sxiaojian on 2021/1/27.
|
// Created by sxiaojian on 2021/1/27.
|
||||||
//
|
//
|
||||||
|
|
||||||
import os.log
|
|
||||||
import UIKit
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
extension PublicTimelineViewModel {
|
extension PublicTimelineViewModel {
|
||||||
func setupDiffableDataSource(
|
func setupDiffableDataSource(
|
||||||
|
@ -20,26 +20,27 @@ extension PublicTimelineViewModel {
|
||||||
.autoconnect()
|
.autoconnect()
|
||||||
.share()
|
.share()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
diffableDataSource = TimelineSection.tableViewDiffableDataSource(
|
diffableDataSource = TimelineSection.tableViewDiffableDataSource(
|
||||||
for: tableView,
|
for: tableView,
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
managedObjectContext: fetchedResultsController.managedObjectContext,
|
managedObjectContext: fetchedResultsController.managedObjectContext,
|
||||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate)
|
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate
|
||||||
|
)
|
||||||
items.value = []
|
items.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - NSFetchedResultsControllerDelegate
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
|
|
||||||
extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
|
||||||
let indexes = tootIDs.value
|
let indexes = tootIDs.value
|
||||||
let toots = fetchedResultsController.fetchedObjects ?? []
|
let toots = fetchedResultsController.fetchedObjects ?? []
|
||||||
guard toots.count == indexes.count else { return }
|
guard toots.count == indexes.count else { return }
|
||||||
|
|
||||||
let items: [Item] = toots
|
let items: [Item] = toots
|
||||||
.compactMap { toot -> (Int, Toot)? in
|
.compactMap { toot -> (Int, Toot)? in
|
||||||
guard toot.deletedAt == nil else { return nil }
|
guard toot.deletedAt == nil else { return nil }
|
||||||
|
@ -49,5 +50,4 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||||
.map { Item.toot(objectID: $0.1.objectID) }
|
.map { Item.toot(objectID: $0.1.objectID) }
|
||||||
self.items.value = items
|
self.items.value = items
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
//
|
||||||
|
// PublicTimelineViewModel+State.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/2.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
import os.log
|
||||||
|
|
||||||
|
extension PublicTimelineViewModel {
|
||||||
|
class State: GKState {
|
||||||
|
weak var viewModel: PublicTimelineViewModel?
|
||||||
|
|
||||||
|
init(viewModel: PublicTimelineViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PublicTimelineViewModel.State {
|
||||||
|
class Initial: PublicTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: PublicTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Fail.Type:
|
||||||
|
return true
|
||||||
|
case is Idle.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
viewModel.fetchLatest()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: fetch user timeline latest response error: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
viewModel.isFetchingLatestTimeline.value = false
|
||||||
|
let tootsIDs = response.value.map { $0.id }
|
||||||
|
viewModel.tootIDs.value = tootsIDs
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: PublicTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type, is LoadingMore.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: PublicTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type, is LoadingMore.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadingMore: PublicTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Fail.Type:
|
||||||
|
return true
|
||||||
|
case is Idle.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,28 +5,39 @@
|
||||||
// Created by sxiaojian on 2021/1/27.
|
// Created by sxiaojian on 2021/1/27.
|
||||||
//
|
//
|
||||||
|
|
||||||
import os.log
|
import AlamofireImage
|
||||||
import UIKit
|
|
||||||
import GameplayKit
|
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import AlamofireImage
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
class PublicTimelineViewModel: NSObject {
|
class PublicTimelineViewModel: NSObject {
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let fetchedResultsController: NSFetchedResultsController<Toot>
|
let fetchedResultsController: NSFetchedResultsController<Toot>
|
||||||
|
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||||
weak var tableView: UITableView?
|
weak var tableView: UITableView?
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UITableViewDiffableDataSource<TimelineSection, Item>?
|
var diffableDataSource: UITableViewDiffableDataSource<TimelineSection, Item>?
|
||||||
|
|
||||||
|
lazy var stateMachine: GKStateMachine = {
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
State.Initial(viewModel: self),
|
||||||
|
State.Loading(viewModel: self),
|
||||||
|
State.Fail(viewModel: self),
|
||||||
|
State.Idle(viewModel: self),
|
||||||
|
State.LoadingMore(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(State.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
let tootIDs = CurrentValueSubject<[String], Never>([])
|
let tootIDs = CurrentValueSubject<[String], Never>([])
|
||||||
let items = CurrentValueSubject<[Item], Never>([])
|
let items = CurrentValueSubject<[Item], Never>([])
|
||||||
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||||
|
@ -49,7 +60,7 @@ class PublicTimelineViewModel: NSObject {
|
||||||
}()
|
}()
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.fetchedResultsController.delegate = self
|
fetchedResultsController.delegate = self
|
||||||
|
|
||||||
items
|
items
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -57,12 +68,19 @@ class PublicTimelineViewModel: NSObject {
|
||||||
.sink { [weak self] items in
|
.sink { [weak self] items in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: items did change", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s: items did change", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, Item>()
|
||||||
snapshot.appendSections([.main])
|
snapshot.appendSections([.main])
|
||||||
snapshot.appendItems(items)
|
snapshot.appendItems(items)
|
||||||
|
if let currentState = self.stateMachine.currentState {
|
||||||
|
switch currentState {
|
||||||
|
case is State.Idle, is State.LoadingMore, is State.Fail:
|
||||||
|
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty)
|
diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
@ -82,14 +100,16 @@ class PublicTimelineViewModel: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PublicTimelineViewModel {
|
extension PublicTimelineViewModel {
|
||||||
|
|
||||||
func fetchLatest() -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
func fetchLatest() -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||||
return context.apiService.publicTimeline(count: 20, domain: "mstdn.jp")
|
return context.apiService.publicTimeline(count: 20, domain: "mstdn.jp")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadMore() -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||||
|
return context.apiService.publicTimeline(count: 20, domain: "mstdn.jp")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
//
|
||||||
|
// TimelineBottomLoaderTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell {
|
||||||
|
override func _init() {
|
||||||
|
super._init()
|
||||||
|
|
||||||
|
activityIndicatorView.isHidden = false
|
||||||
|
activityIndicatorView.startAnimating()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
//
|
||||||
|
// TimelineLoaderTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class TimelineLoaderTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
static let cellHeight: CGFloat = 48
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let loadMoreButton: UIButton = {
|
||||||
|
let button = UIButton(type: .system)
|
||||||
|
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
button.setTitle(L10n.Common.Controls.Timeline.loadMore, for: .normal)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
let activityIndicatorView: UIActivityIndicatorView = {
|
||||||
|
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||||
|
activityIndicatorView.tintColor = .white
|
||||||
|
activityIndicatorView.hidesWhenStopped = true
|
||||||
|
return activityIndicatorView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
disposeBag.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func _init() {
|
||||||
|
selectionStyle = .none
|
||||||
|
backgroundColor = Asset.Colors.tootDark.color
|
||||||
|
loadMoreButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(loadMoreButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
loadMoreButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
|
||||||
|
loadMoreButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
|
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: loadMoreButton.trailingAnchor),
|
||||||
|
contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor, constant: 8),
|
||||||
|
loadMoreButton.heightAnchor.constraint(equalToConstant: TimelineLoaderTableViewCell.cellHeight - 2 * 8).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(activityIndicatorView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
loadMoreButton.isHidden = true
|
||||||
|
activityIndicatorView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
//
|
||||||
|
// APIService+CoreData+Toot.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import CommonOSLog
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService.CoreData {
|
||||||
|
|
||||||
|
static func createOrMergeTweet(
|
||||||
|
into managedObjectContext: NSManagedObjectContext,
|
||||||
|
for requestMastodonUser: MastodonUser,
|
||||||
|
entity: Mastodon.Entity.Toot,
|
||||||
|
domain: String,
|
||||||
|
networkDate: Date,
|
||||||
|
log: OSLog
|
||||||
|
) -> (Toot: Toot, isTweetCreated: Bool, isMastodonUserCreated: Bool) {
|
||||||
|
|
||||||
|
// build tree
|
||||||
|
let reblog = entity.reblog.flatMap { entity -> Toot in
|
||||||
|
let (toot, _, _) = createOrMergeTweet(into: managedObjectContext, for: requestMastodonUser, entity: entity,domain: domain, networkDate: networkDate, log: log)
|
||||||
|
return toot
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch old Toot
|
||||||
|
let oldTweet: Toot? = {
|
||||||
|
let request = Toot.sortedFetchRequest
|
||||||
|
request.predicate = Toot.predicate(idStr: entity.id)
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
do {
|
||||||
|
return try managedObjectContext.fetch(request).first
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if let oldTweet = oldTweet {
|
||||||
|
// merge old Toot
|
||||||
|
APIService.CoreData.mergeToot(for: requestMastodonUser, old: oldTweet,in: domain, entity: entity, networkDate: networkDate)
|
||||||
|
return (oldTweet, false, false)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
let (mastodonUser, isMastodonUserCreated) = createOrMergeMastodonUser(into: managedObjectContext, for: requestMastodonUser,in: domain, entity: entity.account, networkDate: networkDate, log: log)
|
||||||
|
let application = entity.application.flatMap { app -> Application? in
|
||||||
|
Application.insert(into: managedObjectContext, property: Application.Property(name: app.name, website: app.website, vapidKey: app.vapidKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
let metions = entity.mentions?.compactMap({ (mention) -> Mention in
|
||||||
|
Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url))
|
||||||
|
})
|
||||||
|
let emojis = entity.emojis?.compactMap({ (emoji) -> Emoji in
|
||||||
|
Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category))
|
||||||
|
})
|
||||||
|
let tags = entity.tags?.compactMap({ (tag) -> Tag in
|
||||||
|
let histories = tag.history?.compactMap({ (history) -> History in
|
||||||
|
History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts))
|
||||||
|
})
|
||||||
|
return Tag.insert(into: managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories))
|
||||||
|
})
|
||||||
|
let tootProperty = Toot.Property(
|
||||||
|
domain: domain,
|
||||||
|
id: entity.id,
|
||||||
|
uri: entity.uri,
|
||||||
|
createdAt: entity.createdAt,
|
||||||
|
content: entity.content,
|
||||||
|
visibility: entity.visibility?.rawValue,
|
||||||
|
sensitive: entity.sensitive ?? false,
|
||||||
|
spoilerText: entity.spoilerText,
|
||||||
|
application: application,
|
||||||
|
mentions: metions,
|
||||||
|
emojis: emojis,
|
||||||
|
tags: tags,
|
||||||
|
reblogsCount: NSNumber(value: entity.reblogsCount),
|
||||||
|
favouritesCount: NSNumber(value: entity.favouritesCount),
|
||||||
|
repliesCount: (entity.repliesCount != nil) ? NSNumber(value: entity.repliesCount!) : nil,
|
||||||
|
url: entity.uri,
|
||||||
|
inReplyToID: entity.inReplyToID,
|
||||||
|
inReplyToAccountID: entity.inReplyToAccountID,
|
||||||
|
reblog: reblog,
|
||||||
|
language: entity.language,
|
||||||
|
text: entity.text,
|
||||||
|
favouritedBy: (entity.favourited ?? false) ? mastodonUser : nil,
|
||||||
|
rebloggedBy: (entity.reblogged ?? false) ? mastodonUser : nil,
|
||||||
|
mutedBy: (entity.muted ?? false) ? mastodonUser : nil,
|
||||||
|
bookmarkedBy: (entity.bookmarked ?? false) ? mastodonUser : nil,
|
||||||
|
pinnedBy: (entity.pinned ?? false) ? mastodonUser : nil,
|
||||||
|
updatedAt: networkDate,
|
||||||
|
deletedAt: nil,
|
||||||
|
author: requestMastodonUser,
|
||||||
|
homeTimelineIndexes: nil)
|
||||||
|
let toot = Toot.insert(into: managedObjectContext, property: tootProperty, author: mastodonUser)
|
||||||
|
return (toot, true, isMastodonUserCreated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static func mergeToot(for requestMastodonUser: MastodonUser?, old toot: Toot,in domain: String, entity: Mastodon.Entity.Toot, networkDate: Date) {
|
||||||
|
guard networkDate > toot.updatedAt else { return }
|
||||||
|
|
||||||
|
// merge
|
||||||
|
if entity.favouritesCount != toot.favouritesCount.intValue {
|
||||||
|
toot.update(favouritesCount:NSNumber(value: entity.favouritesCount))
|
||||||
|
}
|
||||||
|
if let repliesCount = entity.repliesCount {
|
||||||
|
if (repliesCount != toot.repliesCount?.intValue) {
|
||||||
|
toot.update(repliesCount:NSNumber(value: repliesCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if entity.reblogsCount != toot.reblogsCount.intValue {
|
||||||
|
toot.update(reblogsCount:NSNumber(value: entity.reblogsCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// set updateAt
|
||||||
|
toot.didUpdate(at: networkDate)
|
||||||
|
|
||||||
|
// merge user
|
||||||
|
mergeMastodonUser(for: requestMastodonUser, old: toot.author, in: domain, entity: entity.account, networkDate: networkDate)
|
||||||
|
// merge indirect reblog & quote
|
||||||
|
if let reblog = toot.reblog, let reblogEntity = entity.reblog {
|
||||||
|
mergeToot(for: requestMastodonUser, old: reblog,in: domain, entity: reblogEntity, networkDate: networkDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,6 +27,9 @@ extension APIService.Persist {
|
||||||
let _ = toots.map {
|
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 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 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
|
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))
|
Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url))
|
||||||
})
|
})
|
||||||
|
@ -49,6 +52,7 @@ extension APIService.Persist {
|
||||||
visibility: $0.visibility?.rawValue,
|
visibility: $0.visibility?.rawValue,
|
||||||
sensitive: $0.sensitive ?? false,
|
sensitive: $0.sensitive ?? false,
|
||||||
spoilerText: $0.spoilerText,
|
spoilerText: $0.spoilerText,
|
||||||
|
application: application,
|
||||||
mentions: metions,
|
mentions: metions,
|
||||||
emojis: emojis,
|
emojis: emojis,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
|
Loading…
Reference in New Issue