diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index a7b32feb4..d74a4ccd8 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -448,9 +448,9 @@ private extension SceneCoordinator { _viewController.viewModel = viewModel viewController = _viewController case .followedTags(let viewModel): - let _viewController = FollowedTagsViewController() - _viewController.viewModel = viewModel - viewController = _viewController + guard let authContext else { return nil } + + viewController = FollowedTagsViewController(appContext: appContext, sceneCoordinator: self, authContext: authContext, viewModel: viewModel) case .favorite(let viewModel): let _viewController = FavoriteViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift index 6adb15a9c..3ae6b3ed8 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift @@ -6,23 +6,24 @@ // import UIKit -import CoreDataStack +import MastodonSDK final class FollowedTagsTableViewCell: UITableViewCell { + + static let reuseIdentifier = "FollowedTagsTableViewCell" + private var hashtagView: HashtagTimelineHeaderView! private let separatorLine = UIView.separatorLine - private weak var viewModel: FollowedTagsViewModel? - private weak var hashtag: Tag? + private var viewModel: FollowedTagsViewModel? + private var hashtag: Mastodon.Entity.Tag? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setup() } - required init?(coder: NSCoder) { - fatalError("Not implemented") - } - + required init?(coder: NSCoder) { fatalError("Not implemented") } + override func prepareForReuse() { hashtagView.removeFromSuperview() viewModel = nil @@ -67,7 +68,7 @@ private extension FollowedTagsTableViewCell { } extension FollowedTagsTableViewCell { - func populate(with tag: Tag) { + func populate(with tag: Mastodon.Entity.Tag) { self.hashtag = tag hashtagView.update(HashtagTimelineHeaderView.Data.from(tag)) } diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewController.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewController.swift index 4edec01d9..d92a4e977 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewController.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewController.swift @@ -5,61 +5,87 @@ // Created by Marcus Kida on 22.11.22. // -import os import UIKit -import Combine import MastodonAsset import MastodonCore import MastodonUI import MastodonLocalization final class FollowedTagsViewController: UIViewController, NeedsDependency { - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - - var disposeBag = Set() - var viewModel: FollowedTagsViewModel! + var context: AppContext! + var coordinator: SceneCoordinator! + let authContext: AuthContext + + var viewModel: FollowedTagsViewModel let titleView = DoubleTitleLabelNavigationBarTitleView() + let tableView: UITableView + let refreshControl: UIRefreshControl - lazy var tableView: UITableView = { - let tableView = UITableView() - tableView.register(FollowedTagsTableViewCell.self, forCellReuseIdentifier: String(describing: FollowedTagsTableViewCell.self)) + init(appContext: AppContext, sceneCoordinator: SceneCoordinator, authContext: AuthContext, viewModel: FollowedTagsViewModel) { + self.context = appContext + self.coordinator = sceneCoordinator + self.authContext = authContext + self.viewModel = viewModel + + refreshControl = UIRefreshControl() + + tableView = UITableView() + tableView.register(FollowedTagsTableViewCell.self, forCellReuseIdentifier: FollowedTagsTableViewCell.reuseIdentifier) + tableView.translatesAutoresizingMaskIntoConstraints = false tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.backgroundColor = .clear - return tableView - }() - -} + tableView.refreshControl = refreshControl -extension FollowedTagsViewController { - override func viewDidLoad() { - super.viewDidLoad() - - let _title = L10n.Scene.FollowedTags.title - title = _title - titleView.update(title: _title, subtitle: nil) + super.init(nibName: nil, bundle: nil) + + title = L10n.Scene.FollowedTags.title - navigationItem.titleView = titleView - view.backgroundColor = .secondarySystemBackground - - tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) tableView.pinToParent() + tableView.delegate = self + + refreshControl.addTarget(self, action: #selector(FollowedTagsViewController.refresh(_:)), for: .valueChanged) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewDidLoad() { + super.viewDidLoad() viewModel.setupTableView(tableView) - - viewModel.presentHashtagTimeline - .receive(on: DispatchQueue.main) - .sink { [weak self] hashtagTimelineViewModel in - guard let self = self else { return } - _ = self.coordinator.present( - scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), - from: self, - transition: .show - ) + } + + //MARK: - Actions + + @objc + func refresh(_ sender: UIRefreshControl) { + viewModel.fetchFollowedTags(completion: { + DispatchQueue.main.async { + self.refreshControl.endRefreshing() } - .store(in: &disposeBag) + }) + } +} + +extension FollowedTagsViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let object = viewModel.followedTags[indexPath.row] + + let hashtagTimelineViewModel = HashtagTimelineViewModel( + context: self.context, + authContext: self.authContext, + hashtag: object.name + ) + + _ = self.coordinator.present( + scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), + from: self, + transition: .show + ) + } } diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift index 91f2cc5e9..781bc353a 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift @@ -6,9 +6,6 @@ // import UIKit -import Combine -import CoreData -import CoreDataStack import MastodonSDK import MastodonCore @@ -18,7 +15,7 @@ extension FollowedTagsViewModel { } enum Item: Hashable { - case hashtag(Tag) + case hashtag(Mastodon.Entity.Tag) } func tableViewDiffableDataSource( @@ -27,7 +24,7 @@ extension FollowedTagsViewModel { UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in switch item { case let .hashtag(tag): - guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: FollowedTagsTableViewCell.self), for: indexPath) as? FollowedTagsTableViewCell else { + guard let cell = tableView.dequeueReusableCell(withIdentifier: FollowedTagsTableViewCell.reuseIdentifier, for: indexPath) as? FollowedTagsTableViewCell else { assertionFailure() return UITableViewCell() } @@ -39,9 +36,7 @@ extension FollowedTagsViewModel { } } - func setupDiffableDataSource( - tableView: UITableView - ) { + func setupDiffableDataSource(tableView: UITableView) { diffableDataSource = tableViewDiffableDataSource(for: tableView) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift index f73094a8c..0797f241f 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift @@ -5,17 +5,12 @@ // Created by Marcus Kida on 23.11.22. // -import os import UIKit -import Combine -import CoreData -import CoreDataStack import MastodonSDK import MastodonCore final class FollowedTagsViewModel: NSObject { - var disposeBag = Set() - let fetchedResultsController: FollowedTagsFetchedResultController + private(set) var followedTags: [Mastodon.Entity.Tag] private weak var tableView: UITableView? var diffableDataSource: UITableViewDiffableDataSource? @@ -23,85 +18,60 @@ final class FollowedTagsViewModel: NSObject { // input let context: AppContext let authContext: AuthContext - - // output - let presentHashtagTimeline = PassthroughSubject() - + init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.fetchedResultsController = FollowedTagsFetchedResultController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - user: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)! // fixme: - ) + self.followedTags = [] super.init() - - self.fetchedResultsController - .$records - .receive(on: DispatchQueue.main) - .sink { [weak self] records in - guard let self = self else { return } - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(records.map {.hashtag($0) }) - self.diffableDataSource?.apply(snapshot, animatingDifferences: true) - } - .store(in: &disposeBag) } } extension FollowedTagsViewModel { func setupTableView(_ tableView: UITableView) { - self.tableView = tableView setupDiffableDataSource(tableView: tableView) - tableView.delegate = self fetchFollowedTags() } - func fetchFollowedTags() { + func fetchFollowedTags(completion: (() -> Void)? = nil ) { Task { @MainActor in - try await context.apiService.getFollowedTags( - domain: authContext.mastodonAuthenticationBox.domain, - query: Mastodon.API.Account.FollowedTagsQuery(limit: nil), - authenticationBox: authContext.mastodonAuthenticationBox - ) + do { + followedTags = try await context.apiService.getFollowedTags( + domain: authContext.mastodonAuthenticationBox.domain, + query: Mastodon.API.Account.FollowedTagsQuery(limit: nil), + authenticationBox: authContext.mastodonAuthenticationBox + ).value + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + let items = followedTags.compactMap { Item.hashtag($0) } + snapshot.appendItems(items, toSection: .main) + + await diffableDataSource?.apply(snapshot) + } catch {} + + completion?() } } - func followOrUnfollow(_ tag: Tag) { + func followOrUnfollow(_ tag: Mastodon.Entity.Tag) { Task { @MainActor in - switch tag.following { - case true: + if tag.following ?? false { _ = try? await context.apiService.unfollowTag( for: tag.name, authenticationBox: authContext.mastodonAuthenticationBox ) - case false: + } else { _ = try? await context.apiService.followTag( for: tag.name, authenticationBox: authContext.mastodonAuthenticationBox ) } + fetchFollowedTags() } } } -extension FollowedTagsViewModel: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let object = fetchedResultsController.records[indexPath.row] - - let hashtagTimelineViewModel = HashtagTimelineViewModel( - context: self.context, - authContext: self.authContext, - hashtag: object.name - ) - - presentHashtagTimeline.send(hashtagTimelineViewModel) - } -} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index ad9561565..5e058c258 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -158,32 +158,15 @@ extension APIService { let domain = authenticationBox.domain let authorization = authenticationBox.userAuthorization - let response = try await Mastodon.API.Account.followedTags( + let followedTags = try await Mastodon.API.Account.followedTags( session: session, domain: domain, query: query, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - for entity in response.value { - _ = Persistence.Tag.createOrMerge( - in: managedObjectContext, - context: Persistence.Tag.PersistContext( - domain: domain, - entity: entity, - me: me, - networkDate: response.networkDate - ) - ) - } - } - - return response - } // end func + return followedTags + } } extension APIService { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift index 0a61fc687..008a7e44e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift @@ -66,6 +66,7 @@ extension APIService { } fileprivate extension APIService { + @available(*, deprecated, message: "We don't persist tags anymore") func persistTag( from response: Mastodon.Response.Content, domain: String,