feat: add familiar followers list

This commit is contained in:
CMK 2022-05-17 18:49:29 +08:00
parent c603406d54
commit 57c40b9050
14 changed files with 309 additions and 5 deletions

View File

@ -289,6 +289,10 @@
DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = DB552D4E26BBD10C00E481F6 /* OrderedCollections */; };
DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */; };
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
DB5B549A2833A60400DEF8B2 /* FamiliarFollowersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B54992833A60400DEF8B2 /* FamiliarFollowersViewController.swift */; };
DB5B549D2833A67400DEF8B2 /* FamiliarFollowersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B549C2833A67400DEF8B2 /* FamiliarFollowersViewModel.swift */; };
DB5B549F2833A72500DEF8B2 /* FamiliarFollowersViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B549E2833A72500DEF8B2 /* FamiliarFollowersViewModel+Diffable.swift */; };
DB5B54A12833A89600DEF8B2 /* FamiliarFollowersViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B54A02833A89600DEF8B2 /* FamiliarFollowersViewController+DataSourceProvider.swift */; };
DB5B7295273112B100081888 /* FollowingListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B7294273112B100081888 /* FollowingListViewController.swift */; };
DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B7297273112C800081888 /* FollowingListViewModel.swift */; };
DB5B729C273113C200081888 /* FollowingListViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B729B273113C200081888 /* FollowingListViewModel+Diffable.swift */; };
@ -1042,6 +1046,10 @@
DB519B17281BCC2F00F0C99D /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Intents.stringsdict; sourceTree = "<group>"; };
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFilterService.swift; sourceTree = "<group>"; };
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
DB5B54992833A60400DEF8B2 /* FamiliarFollowersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FamiliarFollowersViewController.swift; sourceTree = "<group>"; };
DB5B549C2833A67400DEF8B2 /* FamiliarFollowersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FamiliarFollowersViewModel.swift; sourceTree = "<group>"; };
DB5B549E2833A72500DEF8B2 /* FamiliarFollowersViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FamiliarFollowersViewModel+Diffable.swift"; sourceTree = "<group>"; };
DB5B54A02833A89600DEF8B2 /* FamiliarFollowersViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FamiliarFollowersViewController+DataSourceProvider.swift"; sourceTree = "<group>"; };
DB5B7294273112B100081888 /* FollowingListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingListViewController.swift; sourceTree = "<group>"; };
DB5B7297273112C800081888 /* FollowingListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingListViewModel.swift; sourceTree = "<group>"; };
DB5B729B273113C200081888 /* FollowingListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewModel+Diffable.swift"; sourceTree = "<group>"; };
@ -2443,6 +2451,17 @@
path = View;
sourceTree = "<group>";
};
DB5B549B2833A60600DEF8B2 /* FamiliarFollowers */ = {
isa = PBXGroup;
children = (
DB5B54992833A60400DEF8B2 /* FamiliarFollowersViewController.swift */,
DB5B54A02833A89600DEF8B2 /* FamiliarFollowersViewController+DataSourceProvider.swift */,
DB5B549C2833A67400DEF8B2 /* FamiliarFollowersViewModel.swift */,
DB5B549E2833A72500DEF8B2 /* FamiliarFollowersViewModel+Diffable.swift */,
);
path = FamiliarFollowers;
sourceTree = "<group>";
};
DB5B7296273112B400081888 /* Following */ = {
isa = PBXGroup;
children = (
@ -2944,6 +2963,7 @@
DBE3CDF1261C6B3100430CC6 /* Favorite */,
DB6B74F0272FB55400C70B6E /* Follower */,
DB5B7296273112B400081888 /* Following */,
DB5B549B2833A60600DEF8B2 /* FamiliarFollowers */,
DBFEEC97279BDC6A004F81DD /* About */,
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
@ -4015,6 +4035,7 @@
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */,
DB5B549F2833A72500DEF8B2 /* FamiliarFollowersViewModel+Diffable.swift in Sources */,
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
DB8F7076279E954700E1225B /* DataSourceFacade+Follow.swift in Sources */,
DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */,
@ -4045,6 +4066,7 @@
DB98EB5327B0F9890082E365 /* ReportHeadlineTableViewCell.swift in Sources */,
DB5B729C273113C200081888 /* FollowingListViewModel+Diffable.swift in Sources */,
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
DB5B549D2833A67400DEF8B2 /* FamiliarFollowersViewModel.swift in Sources */,
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
DB5B729E273113F300081888 /* FollowingListViewModel+State.swift in Sources */,
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
@ -4064,6 +4086,7 @@
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
DB5B54A12833A89600DEF8B2 /* FamiliarFollowersViewController+DataSourceProvider.swift in Sources */,
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
DB73BF43271192BB00781945 /* InstanceService.swift in Sources */,
DB67D08427312970006A36CF /* APIService+Following.swift in Sources */,
@ -4239,6 +4262,7 @@
DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */,
DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */,
DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */,
DB5B549A2833A60400DEF8B2 /* FamiliarFollowersViewController.swift in Sources */,
DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */,
DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */,
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */,

View File

@ -114,7 +114,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>29</integer>
<integer>34</integer>
</dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict>
@ -129,12 +129,12 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>27</integer>
<integer>33</integer>
</dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>28</integer>
<integer>35</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -178,6 +178,7 @@ extension SceneCoordinator {
case favorite(viewModel: FavoriteViewModel)
case follower(viewModel: FollowerListViewModel)
case following(viewModel: FollowingListViewModel)
case familiarFollowers(viewModel: FamiliarFollowersViewModel)
// setting
case settings(viewModel: SettingsViewModel)
@ -445,6 +446,10 @@ private extension SceneCoordinator {
let _viewController = FollowingListViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .familiarFollowers(let viewModel):
let _viewController = FamiliarFollowersViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .report(let viewModel):
let _viewController = ReportViewController()
_viewController.viewModel = viewModel

View File

@ -36,3 +36,4 @@ func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith con

View File

@ -122,7 +122,11 @@ extension DiscoveryForYouViewController: UITableViewDelegate {
// MARK: - ProfileCardTableViewCellDelegate
extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
func profileCardTableViewCell(_ cell: ProfileCardTableViewCell, profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) {
func profileCardTableViewCell(
_ cell: ProfileCardTableViewCell,
profileCardView: ProfileCardView,
relationshipButtonDidPressed button: ProfileRelationshipActionButton
) {
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let indexPath = tableView.indexPath(for: cell) else { return }
guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
@ -135,6 +139,31 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
)
} // end Task
}
func profileCardTableViewCell(
_ cell: ProfileCardTableViewCell,
profileCardView: ProfileCardView,
familiarFollowersDashboardViewDidPressed view: FamiliarFollowersDashboardView
) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
guard let user = record.object(in: context.managedObjectContext) else { return }
let userID = user.id
let _familiarFollowers = viewModel.familiarFollowers.first(where: { $0.id == userID })
guard let familiarFollowers = _familiarFollowers else {
assertionFailure()
return
}
let familiarFollowersViewModel = FamiliarFollowersViewModel(context: context)
familiarFollowersViewModel.familiarFollowers = familiarFollowers
coordinator.present(
scene: .familiarFollowers(viewModel: familiarFollowersViewModel),
from: self,
transition: .show
)
}
}
// MARK: ScrollViewContainer

View File

@ -0,0 +1,34 @@
//
// FamiliarFollowersViewController+DataSourceProvider.swift
// Mastodon
//
// Created by MainasuK on 2022-5-17.
//
import UIKit
extension FamiliarFollowersViewController: 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)
}
}

View File

@ -0,0 +1,89 @@
//
// FamiliarFollowersViewController.swift
// Mastodon
//
// Created by MainasuK on 2022-5-17.
//
import os.log
import UIKit
import Combine
final class FamiliarFollowersViewController: UIViewController, NeedsDependency {
let logger = Logger(subsystem: "FamiliarFollowersViewController", category: "ViewController")
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
var viewModel: FamiliarFollowersViewModel!
lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none
tableView.backgroundColor = .clear
return tableView
}()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension FamiliarFollowersViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = "Followers you familiar"
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
tableView.delegate = self
viewModel.setupDiffableDataSource(
tableView: tableView,
userTableViewCellDelegate: self
)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.deselectRow(with: transitionCoordinator, animated: animated)
}
}
// MARK: - UITableViewDelegate
extension FamiliarFollowersViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:FamiliarFollowersViewController.AutoGenerateTableViewDelegate
// Generated using Sourcery
// DO NOT EDIT
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
aspectTableView(tableView, didSelectRowAt: indexPath)
}
// sourcery:end
}
// MARK: - UserTableViewCellDelegate
extension FamiliarFollowersViewController: UserTableViewCellDelegate { }

View File

@ -0,0 +1,39 @@
//
// FamiliarFollowersViewModel+Diffable.swift
// Mastodon
//
// Created by MainasuK on 2022-5-17.
//
import UIKit
extension FamiliarFollowersViewModel {
func setupDiffableDataSource(
tableView: UITableView,
userTableViewCellDelegate: UserTableViewCellDelegate?
) {
diffableDataSource = UserSection.diffableDataSource(
tableView: tableView,
context: context,
configuration: UserSection.Configuration(
userTableViewCellDelegate: userTableViewCellDelegate
)
)
userFetchedResultsController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = 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) }
snapshot.appendItems(items, toSection: .main)
diffableDataSource.apply(snapshot, animatingDifferences: false)
}
.store(in: &disposeBag)
}
}

View File

@ -0,0 +1,49 @@
//
// FamiliarFollowersViewModel.swift
// Mastodon
//
// Created by MainasuK on 2022-5-17.
//
import UIKit
import Combine
import MastodonSDK
import CoreDataStack
final class FamiliarFollowersViewModel {
var disposeBag = Set<AnyCancellable>()
// input
let context: AppContext
let userFetchedResultsController: UserFetchedResultsController
@Published var familiarFollowers: Mastodon.Entity.FamiliarFollowers?
// output
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
init(context: AppContext) {
self.context = context
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
additionalPredicate: nil
)
// end init
context.authenticationService.activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.domain, on: userFetchedResultsController)
.store(in: &disposeBag)
$familiarFollowers
.map { familiarFollowers -> [MastodonUser.ID] in
guard let familiarFollowers = familiarFollowers else { return [] }
return familiarFollowers.accounts.map { $0.id }
}
.assign(to: \.userIDs, on: userFetchedResultsController)
.store(in: &disposeBag)
}
}

View File

@ -9,6 +9,7 @@ import os.log
import UIKit
import GameplayKit
import Combine
import MastodonLocalization
final class FollowerListViewController: UIViewController, NeedsDependency {
@ -42,6 +43,8 @@ extension FollowerListViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = L10n.Scene.Profile.Dashboard.followers
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)

View File

@ -9,6 +9,7 @@ import os.log
import UIKit
import GameplayKit
import Combine
import MastodonLocalization
final class FollowingListViewController: UIViewController, NeedsDependency {
@ -42,6 +43,8 @@ extension FollowingListViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = L10n.Scene.Profile.Dashboard.following
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
@ -89,6 +92,12 @@ extension FollowingListViewController {
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.deselectRow(with: transitionCoordinator, animated: animated)
}
}
// MARK: - UITableViewDelegate

View File

@ -63,6 +63,8 @@ extension FamiliarFollowersDashboardView {
stackView.addArrangedSubview(descriptionMetaLabel)
descriptionMetaLabel.setContentHuggingPriority(.required - 1, for: .vertical)
descriptionMetaLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
descriptionMetaLabel.isUserInteractionEnabled = false
}
}

View File

@ -13,10 +13,13 @@ import MastodonAsset
public protocol ProfileCardViewDelegate: AnyObject {
func profileCardView(_ profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
func profileCardView(_ profileCardView: ProfileCardView, familiarFollowersDashboardViewDidPressed view: FamiliarFollowersDashboardView)
}
public final class ProfileCardView: UIView {
let logger = Logger(subsystem: "ProfileCardView", category: "View")
static let avatarSize = CGSize(width: 56, height: 56)
static let friendshipActionButtonSize = CGSize(width: 108, height: 34)
static let contentMargin: CGFloat = 16
@ -255,6 +258,10 @@ extension ProfileCardView {
])
relationshipActionButton.addTarget(self, action: #selector(ProfileCardView.relationshipActionButtonDidPressed(_:)), for: .touchUpInside)
let familiarFollowersDashboardViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
familiarFollowersDashboardViewTapGestureRecognizer.addTarget(self, action: #selector(ProfileCardView.familiarFollowersDashboardViewDidPressed(_:)))
familiarFollowersDashboardView.addGestureRecognizer(familiarFollowersDashboardViewTapGestureRecognizer)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
@ -287,8 +294,14 @@ extension ProfileCardView {
extension ProfileCardView {
@objc private func relationshipActionButtonDidPressed(_ sender: UIButton) {
os_log(.debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
assert(sender === relationshipActionButton)
delegate?.profileCardView(self, relationshipButtonDidPressed: relationshipActionButton)
}
@objc private func familiarFollowersDashboardViewDidPressed(_ sender: UITapGestureRecognizer) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
assert(sender.view === familiarFollowersDashboardView)
delegate?.profileCardView(self, familiarFollowersDashboardViewDidPressed: familiarFollowersDashboardView)
}
}

View File

@ -11,6 +11,7 @@ import Combine
public protocol ProfileCardTableViewCellDelegate: AnyObject {
func profileCardTableViewCell(_ cell: ProfileCardTableViewCell, profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
func profileCardTableViewCell(_ cell: ProfileCardTableViewCell, profileCardView: ProfileCardView, familiarFollowersDashboardViewDidPressed view: FamiliarFollowersDashboardView)
}
public final class ProfileCardTableViewCell: UITableViewCell {
@ -86,7 +87,13 @@ extension ProfileCardTableViewCell {
// MARK: - ProfileCardViewDelegate
extension ProfileCardTableViewCell: ProfileCardViewDelegate {
public func profileCardView(_ profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) {
delegate?.profileCardTableViewCell(self, profileCardView: profileCardView, relationshipButtonDidPressed: button)
}
public func profileCardView(_ profileCardView: ProfileCardView, familiarFollowersDashboardViewDidPressed view: FamiliarFollowersDashboardView) {
delegate?.profileCardTableViewCell(self, profileCardView: profileCardView, familiarFollowersDashboardViewDidPressed: view)
}
}