forked from zelo72/mastodon-ios
feat: supports hashtag timeline pull-down-refresh
This commit is contained in:
parent
7a3145083a
commit
39ff50212b
|
@ -12,7 +12,7 @@
|
||||||
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */; };
|
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */; };
|
||||||
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */; };
|
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */; };
|
||||||
0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */; };
|
0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */; };
|
||||||
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20222C261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift */; };
|
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20222C261457EE000C64BF /* HashtagTimelineViewModel+State.swift */; };
|
||||||
0F20223926146553000C64BF /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; };
|
0F20223926146553000C64BF /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; };
|
||||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */; };
|
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */; };
|
||||||
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */; };
|
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */; };
|
||||||
|
@ -715,7 +715,7 @@
|
||||||
0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewModel.swift; sourceTree = "<group>"; };
|
0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewModel.swift; sourceTree = "<group>"; };
|
||||||
0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HashtagTimeline.swift"; sourceTree = "<group>"; };
|
0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HashtagTimeline.swift"; sourceTree = "<group>"; };
|
||||||
0F20222C261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+LoadOldestState.swift"; sourceTree = "<group>"; };
|
0F20222C261457EE000C64BF /* HashtagTimelineViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+State.swift"; sourceTree = "<group>"; };
|
||||||
0F20223826146553000C64BF /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
0F20223826146553000C64BF /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||||
0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
|
0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
|
||||||
0FAA101125E105390017CCDE /* PrimaryActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryActionButton.swift; sourceTree = "<group>"; };
|
0FAA101125E105390017CCDE /* PrimaryActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryActionButton.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1489,7 +1489,7 @@
|
||||||
DB0FCB9B27980AB6006C02E2 /* HashtagTimelineViewController+DataSourceProvider.swift */,
|
DB0FCB9B27980AB6006C02E2 /* HashtagTimelineViewController+DataSourceProvider.swift */,
|
||||||
0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */,
|
0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */,
|
||||||
0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */,
|
0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */,
|
||||||
0F20222C261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift */,
|
0F20222C261457EE000C64BF /* HashtagTimelineViewModel+State.swift */,
|
||||||
);
|
);
|
||||||
path = HashtagTimeline;
|
path = HashtagTimeline;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -4109,7 +4109,7 @@
|
||||||
DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */,
|
DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */,
|
||||||
DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */,
|
DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */,
|
||||||
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */,
|
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */,
|
||||||
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */,
|
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+State.swift in Sources */,
|
||||||
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */,
|
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */,
|
||||||
5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */,
|
5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */,
|
||||||
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */,
|
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */,
|
||||||
|
|
|
@ -114,7 +114,7 @@
|
||||||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>22</integer>
|
<integer>20</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -129,12 +129,12 @@
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>20</integer>
|
<integer>21</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>21</integer>
|
<integer>22</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
|
|
@ -46,6 +46,8 @@ final class HashtagTimelineViewController: UIViewController, NeedsDependency, Me
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
}
|
}
|
||||||
|
@ -89,13 +91,23 @@ extension HashtagTimelineViewController {
|
||||||
statusTableViewCellDelegate: self
|
statusTableViewCellDelegate: self
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
refreshControl.addTarget(self, action: #selector(HashtagTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
viewModel.didLoadLatest
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// setup batch fetch
|
// setup batch fetch
|
||||||
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
|
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
|
||||||
viewModel.listBatchFetchViewModel.shouldFetch
|
viewModel.listBatchFetchViewModel.shouldFetch
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.viewModel.loadOldestStateMachine.enter(HashtagTimelineViewModel.LoadOldestState.Loading.self)
|
self.viewModel.stateMachine.enter(HashtagTimelineViewModel.State.Loading.self)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
@ -144,6 +156,13 @@ extension HashtagTimelineViewController {
|
||||||
|
|
||||||
extension HashtagTimelineViewController {
|
extension HashtagTimelineViewController {
|
||||||
|
|
||||||
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||||
|
guard viewModel.stateMachine.enter(HashtagTimelineViewModel.State.Reloading.self) else {
|
||||||
|
sender.endRefreshing()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
@objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||||
|
|
|
@ -27,6 +27,8 @@ extension HashtagTimelineViewModel {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stateMachine.enter(State.Reloading.self)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<StatusSection, StatusItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<StatusSection, StatusItem>()
|
||||||
snapshot.appendSections([.main])
|
snapshot.appendSections([.main])
|
||||||
diffableDataSource?.apply(snapshot)
|
diffableDataSource?.apply(snapshot)
|
||||||
|
@ -43,14 +45,17 @@ extension HashtagTimelineViewModel {
|
||||||
let items = records.map { StatusItem.status(record: $0) }
|
let items = records.map { StatusItem.status(record: $0) }
|
||||||
snapshot.appendItems(items, toSection: .main)
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
|
||||||
if let currentState = self.loadOldestStateMachine.currentState {
|
if let currentState = self.stateMachine.currentState {
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case is LoadOldestState.Initial,
|
case is State.Initial,
|
||||||
is LoadOldestState.Loading,
|
is State.Reloading,
|
||||||
is LoadOldestState.Idle,
|
is State.Loading,
|
||||||
is LoadOldestState.Fail:
|
is State.Idle,
|
||||||
snapshot.appendItems([.bottomLoader], toSection: .main)
|
is State.Fail:
|
||||||
case is LoadOldestState.NoMore:
|
if !items.isEmpty {
|
||||||
|
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
|
}
|
||||||
|
case is State.NoMore:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// HashtagTimelineViewModel+LoadOldestState.swift
|
// HashtagTimelineViewModel+State.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by BradGao on 2021/3/31.
|
// Created by BradGao on 2021/3/31.
|
||||||
|
@ -11,7 +11,7 @@ import GameplayKit
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
|
||||||
extension HashtagTimelineViewModel {
|
extension HashtagTimelineViewModel {
|
||||||
class LoadOldestState: GKState, NamingState {
|
class State: GKState, NamingState {
|
||||||
|
|
||||||
let logger = Logger(subsystem: "HashtagTimelineViewModel.LoadOldestState", category: "StateMachine")
|
let logger = Logger(subsystem: "HashtagTimelineViewModel.LoadOldestState", category: "StateMachine")
|
||||||
|
|
||||||
|
@ -28,14 +28,14 @@ extension HashtagTimelineViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
let previousState = previousState as? HashtagTimelineViewModel.LoadOldestState
|
let previousState = previousState as? HashtagTimelineViewModel.State
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "<nil>")")
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "<nil>")")
|
||||||
|
|
||||||
viewModel?.loadOldestStateMachinePublisher.send(self)
|
viewModel?.loadOldestStateMachinePublisher.send(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func enter(state: LoadOldestState.Type) {
|
func enter(state: State.Type) {
|
||||||
stateMachine?.enter(state)
|
stateMachine?.enter(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,24 +45,96 @@ extension HashtagTimelineViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HashtagTimelineViewModel.LoadOldestState {
|
extension HashtagTimelineViewModel.State {
|
||||||
class Initial: HashtagTimelineViewModel.LoadOldestState {
|
class Initial: HashtagTimelineViewModel.State {
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
return stateClass == Loading.self
|
switch stateClass {
|
||||||
|
case is Reloading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Loading: HashtagTimelineViewModel.LoadOldestState {
|
class Reloading: HashtagTimelineViewModel.State {
|
||||||
var maxID: Status.ID?
|
|
||||||
|
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
return stateClass == Fail.self || stateClass == Idle.self || stateClass == NoMore.self
|
switch stateClass {
|
||||||
|
case is Loading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
super.didEnter(from: previousState)
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
stateMachine.enter(Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: HashtagTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let _ = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading 3s later…", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
stateMachine.enter(Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: HashtagTimelineViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Reloading.Type, is Loading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: HashtagTimelineViewModel.State {
|
||||||
|
var maxID: Status.ID?
|
||||||
|
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Fail.Type:
|
||||||
|
return true
|
||||||
|
case is Idle.Type:
|
||||||
|
return true
|
||||||
|
case is NoMore.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 }
|
||||||
|
|
||||||
|
switch previousState {
|
||||||
|
case is Reloading:
|
||||||
|
maxID = nil
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
|
@ -71,6 +143,8 @@ extension HashtagTimelineViewModel.LoadOldestState {
|
||||||
|
|
||||||
// TODO: only set large count when using Wi-Fi
|
// TODO: only set large count when using Wi-Fi
|
||||||
let maxID = self.maxID
|
let maxID = self.maxID
|
||||||
|
let isReloading = maxID == nil
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let response = try await viewModel.context.apiService.hashtagTimeline(
|
let response = try await viewModel.context.apiService.hashtagTimeline(
|
||||||
|
@ -79,24 +153,35 @@ extension HashtagTimelineViewModel.LoadOldestState {
|
||||||
hashtag: viewModel.hashtag,
|
hashtag: viewModel.hashtag,
|
||||||
authenticationBox: authenticationBox
|
authenticationBox: authenticationBox
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let newMaxID: String? = {
|
||||||
|
guard let maxID = response.link?.maxID else { return nil }
|
||||||
|
return maxID
|
||||||
|
}()
|
||||||
|
|
||||||
var hasMore = false
|
let hasMore: Bool = {
|
||||||
|
guard let newMaxID = newMaxID else { return false }
|
||||||
|
return newMaxID != maxID
|
||||||
|
}()
|
||||||
|
|
||||||
if let _maxID = response.link?.maxID,
|
self.maxID = newMaxID
|
||||||
_maxID != maxID
|
|
||||||
{
|
var hasNewStatusesAppend = false
|
||||||
self.maxID = _maxID
|
var statusIDs = isReloading ? [] : viewModel.fetchedResultsController.statusIDs.value
|
||||||
hasMore = true
|
for status in response.value {
|
||||||
|
guard !statusIDs.contains(status.id) else { continue }
|
||||||
|
statusIDs.append(status.id)
|
||||||
|
hasNewStatusesAppend = true
|
||||||
}
|
}
|
||||||
if hasMore {
|
|
||||||
|
if hasNewStatusesAppend, hasMore {
|
||||||
await enter(state: Idle.self)
|
await enter(state: Idle.self)
|
||||||
} else {
|
} else {
|
||||||
await enter(state: NoMore.self)
|
await enter(state: NoMore.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
let statusIDs = response.value.map { $0.id }
|
|
||||||
viewModel.fetchedResultsController.append(statusIDs: statusIDs)
|
viewModel.fetchedResultsController.append(statusIDs: statusIDs)
|
||||||
|
viewModel.didLoadLatest.send()
|
||||||
} catch {
|
} catch {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch statues failed: \(error.localizedDescription)")
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch statues failed: \(error.localizedDescription)")
|
||||||
await enter(state: Fail.self)
|
await enter(state: Fail.self)
|
||||||
|
@ -104,22 +189,15 @@ extension HashtagTimelineViewModel.LoadOldestState {
|
||||||
} // end Task
|
} // end Task
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Fail: HashtagTimelineViewModel.LoadOldestState {
|
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
|
||||||
return stateClass == Loading.self || stateClass == Idle.self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Idle: HashtagTimelineViewModel.LoadOldestState {
|
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
|
||||||
return stateClass == Loading.self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoMore: HashtagTimelineViewModel.LoadOldestState {
|
class NoMore: HashtagTimelineViewModel.State {
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
return false
|
switch stateClass {
|
||||||
|
case is Reloading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
|
@ -36,19 +36,20 @@ final class HashtagTimelineViewModel {
|
||||||
let didLoadLatest = PassthroughSubject<Void, Never>()
|
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
// bottom loader
|
// bottom loader
|
||||||
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
|
private(set) lazy var stateMachine: GKStateMachine = {
|
||||||
// exclude timeline middle fetcher state
|
// exclude timeline middle fetcher state
|
||||||
let stateMachine = GKStateMachine(states: [
|
let stateMachine = GKStateMachine(states: [
|
||||||
LoadOldestState.Initial(viewModel: self),
|
State.Initial(viewModel: self),
|
||||||
LoadOldestState.Loading(viewModel: self),
|
State.Reloading(viewModel: self),
|
||||||
LoadOldestState.Fail(viewModel: self),
|
State.Fail(viewModel: self),
|
||||||
LoadOldestState.Idle(viewModel: self),
|
State.Idle(viewModel: self),
|
||||||
LoadOldestState.NoMore(viewModel: self),
|
State.Loading(viewModel: self),
|
||||||
|
State.NoMore(viewModel: self),
|
||||||
])
|
])
|
||||||
stateMachine.enter(LoadOldestState.Initial.self)
|
stateMachine.enter(State.Initial.self)
|
||||||
return stateMachine
|
return stateMachine
|
||||||
}()
|
}()
|
||||||
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<LoadOldestState?, Never>(nil)
|
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<State?, Never>(nil)
|
||||||
|
|
||||||
init(context: AppContext, hashtag: String) {
|
init(context: AppContext, hashtag: String) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
Loading…
Reference in New Issue