Get (and show) account-entities on followings-list

This is a first step, for now we show the name to see if it works (and it does!), the other properties and functionality will follow.

Again, this includes some refactoring, like getting rid of Configuration
This commit is contained in:
Nathan Mattes 2023-10-19 16:16:18 +02:00
parent 1750ef83a6
commit a549534fcf
10 changed files with 108 additions and 99 deletions

View File

@ -8,9 +8,11 @@
import Foundation
import CoreData
import CoreDataStack
import MastodonSDK
enum UserItem: Hashable {
case user(record: ManagedObjectRecord<MastodonUser>)
case account(account: Mastodon.Entity.Account)
case bottomLoader
case bottomHeader(text: String)
}

View File

@ -19,15 +19,11 @@ enum UserSection: Hashable {
}
extension UserSection {
struct Configuration {
weak var userTableViewCellDelegate: UserTableViewCellDelegate?
}
static func diffableDataSource(
tableView: UITableView,
context: AppContext,
authContext: AuthContext,
configuration: Configuration
userTableViewCellDelegate: UserTableViewCellDelegate?
) -> UITableViewDiffableDataSource<UserSection, UserItem> {
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self))
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
@ -35,6 +31,12 @@ extension UserSection {
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
switch item {
case .account(let account):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
cell.configure(tableView: tableView, account: account, delegate: userTableViewCellDelegate)
return cell
case .user(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
context.managedObjectContext.performAndWait {
@ -50,7 +52,7 @@ extension UserSection {
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()
),
configuration: configuration
userTableViewCellDelegate: userTableViewCellDelegate
)
}
@ -60,13 +62,12 @@ extension UserSection {
cell.startAnimating()
return cell
case .bottomHeader(let text):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineFooterTableViewCell.self), for: indexPath) as! TimelineFooterTableViewCell
cell.messageLabel.text = text
return cell
} // end switch
} // end UITableViewDiffableDataSource
} // end static func tableViewDiffableDataSource { }
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineFooterTableViewCell.self), for: indexPath) as! TimelineFooterTableViewCell
cell.messageLabel.text = text
return cell
}
}
}
}
extension UserSection {
@ -77,13 +78,13 @@ extension UserSection {
tableView: UITableView,
cell: UserTableViewCell,
viewModel: UserTableViewCell.ViewModel,
configuration: Configuration
userTableViewCellDelegate: UserTableViewCellDelegate?
) {
cell.configure(
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
tableView: tableView,
viewModel: viewModel,
delegate: configuration.userTableViewCellDelegate
delegate: userTableViewCellDelegate
)
}

View File

@ -17,9 +17,7 @@ extension FamiliarFollowersViewModel {
tableView: tableView,
context: context,
authContext: authContext,
configuration: UserSection.Configuration(
userTableViewCellDelegate: userTableViewCellDelegate
)
userTableViewCellDelegate: userTableViewCellDelegate
)
userFetchedResultsController.$records

View File

@ -18,9 +18,7 @@ extension FollowerListViewModel {
tableView: tableView,
context: context,
authContext: authContext,
configuration: UserSection.Configuration(
userTableViewCellDelegate: userTableViewCellDelegate
)
userTableViewCellDelegate: userTableViewCellDelegate
)
// workaround to append loader wrong animation issue

View File

@ -19,9 +19,7 @@ extension FollowingListViewModel {
tableView: tableView,
context: context,
authContext: authContext,
configuration: UserSection.Configuration(
userTableViewCellDelegate: userTableViewCellDelegate
)
userTableViewCellDelegate: userTableViewCellDelegate
)
// workaround to append loader wrong animation issue
@ -31,30 +29,32 @@ extension FollowingListViewModel {
snapshot.appendItems([.bottomLoader], toSection: .main)
diffableDataSource?.applySnapshotUsingReloadData(snapshot)
userFetchedResultsController.$records
$accounts
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
.sink { [weak self] accounts in
guard let self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
snapshot.appendSections([.main])
let items = records.map { UserItem.user(record: $0) }
let items = accounts.map { UserItem.account(account: $0) }
snapshot.appendItems(items, toSection: .main)
if let currentState = self.stateMachine.currentState {
switch currentState {
case is State.Idle, is State.Loading, is State.Fail:
snapshot.appendItems([.bottomLoader], toSection: .main)
case is State.NoMore:
guard let userID = self.userID,
userID != self.authContext.mastodonAuthenticationBox.userID
else { break }
// display footer exclude self
let text = L10n.Scene.Following.footer
snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
default:
break
case is State.Loading:
snapshot.appendItems([.bottomLoader], toSection: .main)
case is State.NoMore:
guard let userID = self.userID,
userID != self.authContext.mastodonAuthenticationBox.userID
else { break }
// display footer exclude self
let text = L10n.Scene.Following.footer
snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
case is State.Idle, is State.Fail:
break
default:
break
}
}

View File

@ -11,7 +11,7 @@ import MastodonSDK
extension FollowingListViewModel {
class State: GKState {
let id = UUID()
weak var viewModel: FollowingListViewModel?
@ -32,10 +32,10 @@ extension FollowingListViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
guard let viewModel = viewModel else { return false }
switch stateClass {
case is Reloading.Type:
return viewModel.userID != nil
default:
return false
case is Reloading.Type:
return viewModel.userID != nil
default:
return false
}
}
}
@ -43,19 +43,19 @@ extension FollowingListViewModel.State {
class Reloading: FollowingListViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Loading.Type:
return true
default:
return false
case is Loading.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 }
guard let viewModel, let stateMachine else { return }
// reset
viewModel.userFetchedResultsController.userIDs = []
viewModel.accounts = []
stateMachine.enter(Loading.self)
}
@ -65,10 +65,10 @@ extension FollowingListViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Loading.Type:
return true
default:
return false
case is Loading.Type:
return true
default:
return false
}
}
@ -85,10 +85,10 @@ extension FollowingListViewModel.State {
class Idle: FollowingListViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Reloading.Type, is Loading.Type:
return true
default:
return false
case is Reloading.Type, is Loading.Type:
return true
default:
return false
}
}
}
@ -99,14 +99,14 @@ extension FollowingListViewModel.State {
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
case is Fail.Type:
return true
case is Idle.Type:
return true
case is NoMore.Type:
return true
default:
return false
}
}
@ -117,9 +117,9 @@ extension FollowingListViewModel.State {
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)
return
}
@ -131,15 +131,17 @@ extension FollowingListViewModel.State {
maxID: maxID,
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
var hasNewAppend = false
var userIDs = viewModel.userFetchedResultsController.userIDs
var accounts = viewModel.accounts
for user in response.value {
guard !userIDs.contains(user.id) else { continue }
userIDs.append(user.id)
guard accounts.contains(user) == false else { continue }
accounts.append(user)
hasNewAppend = true
}
let maxID = response.link?.maxID
if hasNewAppend, maxID != nil {
@ -147,28 +149,24 @@ extension FollowingListViewModel.State {
} else {
await enter(state: NoMore.self)
}
self.maxID = maxID
viewModel.userFetchedResultsController.userIDs = userIDs
viewModel.accounts = accounts
self.maxID = maxID
} catch {
await enter(state: Fail.self)
}
} // end Task
} // end func didEnter
}
}
}
class NoMore: FollowingListViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Reloading.Type:
return true
default:
return false
case is Reloading.Type:
return true
default:
return false
}
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
}
}
}

View File

@ -20,9 +20,9 @@ final class FollowingListViewModel {
// input
let context: AppContext
let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var accounts: [Mastodon.Entity.Account]
let listBatchFetchViewModel: ListBatchFetchViewModel
@Published var domain: String?
@Published var userID: String?
@ -49,14 +49,9 @@ final class FollowingListViewModel {
) {
self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: domain,
additionalPredicate: nil
)
self.domain = domain
self.userID = userID
// super.init()
self.accounts = []
self.listBatchFetchViewModel = ListBatchFetchViewModel()
}
}

View File

@ -20,9 +20,7 @@ extension UserListViewModel {
tableView: tableView,
context: context,
authContext: authContext,
configuration: UserSection.Configuration(
userTableViewCellDelegate: userTableViewCellDelegate
)
userTableViewCellDelegate: userTableViewCellDelegate
)
// workaround to append loader wrong animation issue

View File

@ -19,6 +19,7 @@ extension UserView {
public func configure(user: MastodonUser, delegate: UserViewDelegate?) {
self.delegate = delegate
viewModel.user = user
viewModel.account = nil
Publishers.CombineLatest(
user.publisher(for: \.avatar),
@ -67,5 +68,11 @@ extension UserView {
func configure(with account: Mastodon.Entity.Account) {
//TODO: Implement
viewModel.account = account
viewModel.user = nil
// username
let metaContent = PlaintextMetaContent(string: "@\(account.username)")
authorUsernameLabel.configure(content: metaContent)
}
}

View File

@ -31,6 +31,18 @@ extension UserTableViewCell {
extension UserTableViewCell {
func configure(
me: MastodonUser? = nil,
tableView: UITableView,
account: Mastodon.Entity.Account,
delegate: UserTableViewCellDelegate?
) {
//TODO: Implement
userView.configure(with: account)
}
//TODO: Duplicate
func configure(
me: MastodonUser? = nil,
tableView: UITableView,