Merge pull request #1137 from mastodon/remove_coredata/followers
IOS-174: Don't persist followers
This commit is contained in:
commit
093be8bbc8
|
@ -317,7 +317,6 @@
|
||||||
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */; };
|
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */; };
|
||||||
DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7442799056400455B82 /* HashtagTableViewCell.swift */; };
|
DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7442799056400455B82 /* HashtagTableViewCell.swift */; };
|
||||||
DB63F74727990B0600455B82 /* DataSourceFacade+Hashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */; };
|
DB63F74727990B0600455B82 /* DataSourceFacade+Hashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */; };
|
||||||
DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */; };
|
|
||||||
DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */; };
|
DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */; };
|
||||||
DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; };
|
DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; };
|
||||||
DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; };
|
DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; };
|
||||||
|
@ -1021,7 +1020,6 @@
|
||||||
DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewModel.swift; sourceTree = "<group>"; };
|
DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB63F7442799056400455B82 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
|
DB63F7442799056400455B82 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Hashtag.swift"; sourceTree = "<group>"; };
|
DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Hashtag.swift"; sourceTree = "<group>"; };
|
||||||
DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewController+DataSourceProvider.swift"; sourceTree = "<group>"; };
|
|
||||||
DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryUserCollectionViewCell.swift; sourceTree = "<group>"; };
|
DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryUserCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
|
DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2494,7 +2492,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */,
|
DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */,
|
||||||
DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */,
|
|
||||||
DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */,
|
DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */,
|
||||||
DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */,
|
DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */,
|
||||||
DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */,
|
DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */,
|
||||||
|
@ -3738,7 +3735,6 @@
|
||||||
DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */,
|
DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */,
|
||||||
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
|
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
|
||||||
2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */,
|
2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */,
|
||||||
DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */,
|
|
||||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
|
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
|
||||||
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */,
|
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */,
|
||||||
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
|
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
|
||||||
|
|
|
@ -223,7 +223,6 @@ extension SceneCoordinator {
|
||||||
func setup() {
|
func setup() {
|
||||||
let rootViewController: UIViewController
|
let rootViewController: UIViewController
|
||||||
|
|
||||||
do {
|
|
||||||
let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
|
let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
|
||||||
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
|
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
|
||||||
self.authContext = _authContext
|
self.authContext = _authContext
|
||||||
|
@ -240,6 +239,7 @@ extension SceneCoordinator {
|
||||||
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
|
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
|
||||||
rootViewController = splitViewController
|
rootViewController = splitViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
sceneDelegate.window?.rootViewController = rootViewController // base: main
|
sceneDelegate.window?.rootViewController = rootViewController // base: main
|
||||||
self.rootViewController = rootViewController
|
self.rootViewController = rootViewController
|
||||||
|
|
||||||
|
@ -252,14 +252,6 @@ extension SceneCoordinator {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
|
||||||
assertionFailure(error.localizedDescription)
|
|
||||||
Task {
|
|
||||||
try? await Task.sleep(nanoseconds: .second * 2)
|
|
||||||
setup() // entry #2: retry
|
|
||||||
} // end Task
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
@ -464,9 +456,8 @@ private extension SceneCoordinator {
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
case .follower(let viewModel):
|
case .follower(let viewModel):
|
||||||
let _viewController = FollowerListViewController()
|
let followerListViewController = FollowerListViewController(viewModel: viewModel, coordinator: self, context: appContext)
|
||||||
_viewController.viewModel = viewModel
|
viewController = followerListViewController
|
||||||
viewController = _viewController
|
|
||||||
case .following(let viewModel):
|
case .following(let viewModel):
|
||||||
let followingListViewController = FollowingListViewController(viewModel: viewModel, coordinator: self, context: appContext)
|
let followingListViewController = FollowingListViewController(viewModel: viewModel, coordinator: self, context: appContext)
|
||||||
viewController = followingListViewController
|
viewController = followingListViewController
|
||||||
|
|
|
@ -49,11 +49,12 @@ private extension DataSourceFacade {
|
||||||
authenticationBox: provider.authContext.mastodonAuthenticationBox
|
authenticationBox: provider.authContext.mastodonAuthenticationBox
|
||||||
).value
|
).value
|
||||||
|
|
||||||
guard let content = value.content else {
|
if value.content != nil {
|
||||||
|
return value
|
||||||
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
|
||||||
} catch {
|
} catch {
|
||||||
throw TranslationFailure.emptyOrInvalidResponse
|
throw TranslationFailure.emptyOrInvalidResponse
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,7 +150,7 @@ extension MastodonRegisterViewController {
|
||||||
return "en"
|
return "en"
|
||||||
}
|
}
|
||||||
let fallbackLanguageCode: String = {
|
let fallbackLanguageCode: String = {
|
||||||
let code = Locale.current.languageCode ?? "en"
|
let code = Locale.current.language.languageCode?.identifier ?? "en"
|
||||||
guard localCode[code] != nil else { return "en" }
|
guard localCode[code] != nil else { return "en" }
|
||||||
return code
|
return code
|
||||||
}()
|
}()
|
||||||
|
@ -161,7 +161,7 @@ extension MastodonRegisterViewController {
|
||||||
}
|
}
|
||||||
// prepare languageCode and validate then return fallback if needs
|
// prepare languageCode and validate then return fallback if needs
|
||||||
let local = Locale(identifier: identifier)
|
let local = Locale(identifier: identifier)
|
||||||
guard let languageCode = local.languageCode,
|
guard let languageCode = local.language.languageCode?.identifier,
|
||||||
localCode[languageCode] != nil
|
localCode[languageCode] != nil
|
||||||
else {
|
else {
|
||||||
return fallbackLanguageCode
|
return fallbackLanguageCode
|
||||||
|
@ -170,10 +170,10 @@ extension MastodonRegisterViewController {
|
||||||
let extendCodes: [String] = {
|
let extendCodes: [String] = {
|
||||||
let locales = Locale.preferredLanguages.map { Locale(identifier: $0) }
|
let locales = Locale.preferredLanguages.map { Locale(identifier: $0) }
|
||||||
return locales.compactMap { locale in
|
return locales.compactMap { locale in
|
||||||
guard let languageCode = locale.languageCode,
|
guard let languageCode = locale.language.languageCode?.identifier,
|
||||||
let regionCode = locale.regionCode
|
let regionIdentifier = locale.region?.identifier
|
||||||
else { return nil }
|
else { return nil }
|
||||||
return languageCode + "-" + regionCode
|
return languageCode + "-" + regionIdentifier
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
let _firstMatchExtendCode = extendCodes.first { code in
|
let _firstMatchExtendCode = extendCodes.first { code in
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
//
|
|
||||||
// FollowerListViewController+DataSourceProvider.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by MainasuK on 2022-1-20.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
extension FollowerListViewController: DataSourceProvider {
|
|
||||||
func item(from source: DataSourceItem.Source) async -> DataSourceItem? {
|
|
||||||
var _indexPath = source.indexPath
|
|
||||||
if _indexPath == nil, let cell = source.tableViewCell {
|
|
||||||
_indexPath = await self.indexPath(for: cell)
|
|
||||||
}
|
|
||||||
guard let indexPath = _indexPath else { return nil }
|
|
||||||
|
|
||||||
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch item {
|
|
||||||
case .user(let record):
|
|
||||||
return .user(record: record)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
|
||||||
return tableView.indexPath(for: cell)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,28 +11,53 @@ import Combine
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
import MastodonUI
|
import MastodonUI
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
import CoreDataStack
|
|
||||||
|
|
||||||
final class FollowerListViewController: UIViewController, NeedsDependency {
|
final class FollowerListViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext!
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator!
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
var viewModel: FollowerListViewModel!
|
var viewModel: FollowerListViewModel
|
||||||
|
|
||||||
lazy var tableView: UITableView = {
|
let tableView: UITableView
|
||||||
let tableView = UITableView()
|
let refreshControl: UIRefreshControl
|
||||||
|
|
||||||
|
init(viewModel: FollowerListViewModel, coordinator: SceneCoordinator, context: AppContext) {
|
||||||
|
|
||||||
|
self.context = context
|
||||||
|
self.coordinator = coordinator
|
||||||
|
self.viewModel = viewModel
|
||||||
|
|
||||||
|
tableView = UITableView()
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self))
|
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self))
|
||||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
tableView.register(TimelineFooterTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineFooterTableViewCell.self))
|
tableView.register(TimelineFooterTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineFooterTableViewCell.self))
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.separatorStyle = .none
|
tableView.separatorStyle = .none
|
||||||
tableView.backgroundColor = .clear
|
tableView.backgroundColor = .clear
|
||||||
return tableView
|
|
||||||
}()
|
|
||||||
|
|
||||||
|
refreshControl = UIRefreshControl()
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
title = L10n.Scene.Follower.title
|
||||||
|
|
||||||
|
view.backgroundColor = .secondarySystemBackground
|
||||||
|
|
||||||
|
view.addSubview(tableView)
|
||||||
|
tableView.pinToParent()
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.refreshControl?.addTarget(self, action: #selector(FollowerListViewController.refresh(_:)), for: .valueChanged)
|
||||||
|
|
||||||
|
viewModel.tableView = tableView
|
||||||
|
|
||||||
|
refreshControl.addTarget(self, action: #selector(FollowerListViewController.refresh(_:)), for: .valueChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FollowerListViewController {
|
extension FollowerListViewController {
|
||||||
|
@ -40,23 +65,13 @@ extension FollowerListViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
title = L10n.Scene.Follower.title
|
|
||||||
|
|
||||||
view.backgroundColor = .secondarySystemBackground
|
|
||||||
|
|
||||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
view.addSubview(tableView)
|
|
||||||
tableView.pinToParent()
|
|
||||||
|
|
||||||
tableView.delegate = self
|
|
||||||
viewModel.setupDiffableDataSource(
|
viewModel.setupDiffableDataSource(
|
||||||
tableView: tableView,
|
tableView: tableView,
|
||||||
userTableViewCellDelegate: self
|
userTableViewCellDelegate: self
|
||||||
)
|
)
|
||||||
|
|
||||||
// setup batch fetch
|
// setup batch fetch
|
||||||
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
|
viewModel.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 }
|
||||||
|
@ -75,6 +90,8 @@ extension FollowerListViewController {
|
||||||
self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self)
|
self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
tableView.refreshControl = UIRefreshControl()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -83,6 +100,12 @@ extension FollowerListViewController {
|
||||||
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//MARK: - Actions
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func refresh(_ sender: UIRefreshControl) {
|
||||||
|
viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AuthContextProvider
|
// MARK: - AuthContextProvider
|
||||||
|
@ -107,3 +130,52 @@ extension FollowerListViewController: UITableViewDelegate, AutoGenerateTableView
|
||||||
|
|
||||||
// MARK: - UserTableViewCellDelegate
|
// MARK: - UserTableViewCellDelegate
|
||||||
extension FollowerListViewController: UserTableViewCellDelegate {}
|
extension FollowerListViewController: UserTableViewCellDelegate {}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - DataSourceProvider
|
||||||
|
extension FollowerListViewController: DataSourceProvider {
|
||||||
|
func item(from source: DataSourceItem.Source) async -> DataSourceItem? {
|
||||||
|
var _indexPath = source.indexPath
|
||||||
|
if _indexPath == nil, let cell = source.tableViewCell {
|
||||||
|
_indexPath = await self.indexPath(for: cell)
|
||||||
|
}
|
||||||
|
guard let indexPath = _indexPath else { return nil }
|
||||||
|
|
||||||
|
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .account(let account, let relationship):
|
||||||
|
return .account(account: account, relationship: relationship)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||||
|
return tableView.indexPath(for: cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - UIScrollViewDelegate
|
||||||
|
|
||||||
|
extension FollowerListViewController: UIScrollViewDelegate {
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
|
||||||
|
if scrollView.isDragging || scrollView.isTracking { return }
|
||||||
|
|
||||||
|
let frame = scrollView.frame
|
||||||
|
let contentOffset = scrollView.contentOffset
|
||||||
|
let contentSize = scrollView.contentSize
|
||||||
|
|
||||||
|
let visibleBottomY = contentOffset.y + frame.height
|
||||||
|
let offset = 2 * frame.height
|
||||||
|
let fetchThrottleOffsetY = contentSize.height - offset
|
||||||
|
|
||||||
|
if visibleBottomY > fetchThrottleOffsetY {
|
||||||
|
viewModel.shouldFetch.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
extension FollowerListViewModel {
|
extension FollowerListViewModel {
|
||||||
func setupDiffableDataSource(
|
func setupDiffableDataSource(
|
||||||
|
@ -28,28 +29,37 @@ extension FollowerListViewModel {
|
||||||
snapshot.appendItems([.bottomLoader], toSection: .main)
|
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
diffableDataSource?.applySnapshotUsingReloadData(snapshot, completion: nil)
|
diffableDataSource?.applySnapshotUsingReloadData(snapshot, completion: nil)
|
||||||
|
|
||||||
userFetchedResultsController.$records
|
$accounts
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] records in
|
.sink { [weak self] accounts in
|
||||||
guard let self = self else { return }
|
guard let self else { return }
|
||||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
|
||||||
snapshot.appendSections([.main])
|
snapshot.appendSections([.main])
|
||||||
let items = records.map { UserItem.user(record: $0) }
|
|
||||||
|
let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in
|
||||||
|
guard let relationship = self.relationships.first(where: {$0.id == account.id }) else { return (account: account, relationship: nil)}
|
||||||
|
|
||||||
|
return (account: account, relationship: relationship)
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = accountsWithRelationship.map { UserItem.account(account: $0.account, relationship: $0.relationship) }
|
||||||
snapshot.appendItems(items, toSection: .main)
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
|
||||||
if let currentState = self.stateMachine.currentState {
|
if let currentState = self.stateMachine.currentState {
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case is State.Idle, is State.Loading, is State.Fail:
|
case is State.Loading:
|
||||||
snapshot.appendItems([.bottomLoader], toSection: .main)
|
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
case is State.NoMore:
|
case is State.NoMore:
|
||||||
guard let userID = self.userID,
|
guard let userID = self.userID,
|
||||||
userID != self.authContext.mastodonAuthenticationBox.userID
|
userID != self.authContext.mastodonAuthenticationBox.userID
|
||||||
else { break }
|
else { break }
|
||||||
// display hint footer exclude self
|
// display footer exclude self
|
||||||
let text = L10n.Scene.Follower.footer
|
let text = L10n.Scene.Following.footer
|
||||||
snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
|
snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
|
||||||
|
case is State.Idle, is State.Fail:
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import Foundation
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
import CoreDataStack
|
|
||||||
|
|
||||||
extension FollowerListViewModel {
|
extension FollowerListViewModel {
|
||||||
class State: GKState {
|
class State: GKState {
|
||||||
|
@ -61,7 +60,8 @@ extension FollowerListViewModel.State {
|
||||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
viewModel.userFetchedResultsController.userIDs = []
|
viewModel.accounts = []
|
||||||
|
viewModel.relationships = []
|
||||||
|
|
||||||
stateMachine.enter(Loading.self)
|
stateMachine.enter(Loading.self)
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,12 @@ extension FollowerListViewModel.State {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
|
viewModel?.tableView?.refreshControl?.endRefreshing()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Loading: FollowerListViewModel.State {
|
class Loading: FollowerListViewModel.State {
|
||||||
|
@ -123,45 +129,64 @@ extension FollowerListViewModel.State {
|
||||||
maxID = nil
|
maxID = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
guard let viewModel, let stateMachine else { return }
|
||||||
|
|
||||||
guard let userID = viewModel.userID, !userID.isEmpty else {
|
guard let userID = viewModel.userID, userID.isEmpty == false else {
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let response = try await viewModel.context.apiService.followers(
|
let accountResponse = try await viewModel.context.apiService.followers(
|
||||||
userID: userID,
|
userID: userID,
|
||||||
maxID: maxID,
|
maxID: maxID,
|
||||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if accountResponse.value.isEmpty {
|
||||||
|
await enter(state: NoMore.self)
|
||||||
|
|
||||||
|
viewModel.accounts = []
|
||||||
|
viewModel.relationships = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var hasNewAppend = false
|
var hasNewAppend = false
|
||||||
var userIDs = viewModel.userFetchedResultsController.userIDs
|
|
||||||
for user in response.value {
|
let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
|
||||||
guard !userIDs.contains(user.id) else { continue }
|
|
||||||
userIDs.append(user.id)
|
var accounts = viewModel.accounts
|
||||||
|
|
||||||
|
for user in accountResponse.value {
|
||||||
|
guard accounts.contains(user) == false else { continue }
|
||||||
|
accounts.append(user)
|
||||||
hasNewAppend = true
|
hasNewAppend = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxID = response.link?.maxID
|
var relationships = viewModel.relationships
|
||||||
|
|
||||||
if hasNewAppend && maxID != nil {
|
for relationship in newRelationships.value {
|
||||||
|
guard relationships.contains(relationship) == false else { continue }
|
||||||
|
relationships.append(relationship)
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxID = accountResponse.link?.maxID
|
||||||
|
|
||||||
|
if hasNewAppend, maxID != nil {
|
||||||
await enter(state: Idle.self)
|
await enter(state: Idle.self)
|
||||||
} else {
|
} else {
|
||||||
await enter(state: NoMore.self)
|
await enter(state: NoMore.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.accounts = accounts
|
||||||
|
viewModel.relationships = relationships
|
||||||
self.maxID = maxID
|
self.maxID = maxID
|
||||||
viewModel.userFetchedResultsController.userIDs = userIDs
|
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
await enter(state: Fail.self)
|
await enter(state: Fail.self)
|
||||||
}
|
}
|
||||||
} // end Task
|
}
|
||||||
} // end func didEnter
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoMore: FollowerListViewModel.State {
|
class NoMore: FollowerListViewModel.State {
|
||||||
|
@ -176,6 +201,8 @@ extension FollowerListViewModel.State {
|
||||||
|
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
super.didEnter(from: previousState)
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
|
viewModel?.tableView?.refreshControl?.endRefreshing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,16 @@ final class FollowerListViewModel {
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
let userFetchedResultsController: UserFetchedResultsController
|
@Published var accounts: [Mastodon.Entity.Account]
|
||||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
@Published var relationships: [Mastodon.Entity.Relationship]
|
||||||
|
|
||||||
@Published var domain: String?
|
@Published var domain: String?
|
||||||
@Published var userID: String?
|
@Published var userID: String?
|
||||||
|
|
||||||
|
let shouldFetch = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
var tableView: UITableView?
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
|
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
|
||||||
private(set) lazy var stateMachine: GKStateMachine = {
|
private(set) lazy var stateMachine: GKStateMachine = {
|
||||||
|
@ -48,13 +52,9 @@ final class FollowerListViewModel {
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.userFetchedResultsController = UserFetchedResultsController(
|
|
||||||
managedObjectContext: context.managedObjectContext,
|
|
||||||
domain: domain,
|
|
||||||
additionalPredicate: nil
|
|
||||||
)
|
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
self.userID = userID
|
self.userID = userID
|
||||||
// end init
|
self.accounts = []
|
||||||
|
self.relationships = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,7 +173,6 @@ extension FollowingListViewController: UIScrollViewDelegate {
|
||||||
if visibleBottomY > fetchThrottleOffsetY {
|
if visibleBottomY > fetchThrottleOffsetY {
|
||||||
viewModel.shouldFetch.send()
|
viewModel.shouldFetch.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue