Merge pull request #1149 from mastodon/remove_coredata/ios-186-followed-tags-screen
Make "Followed Hashtags"-screen work with entities (IOS-186)
This commit is contained in:
commit
d8a795ab26
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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<AnyCancellable>()
|
||||
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
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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<Section, Item>()
|
||||
|
@ -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<AnyCancellable>()
|
||||
let fetchedResultsController: FollowedTagsFetchedResultController
|
||||
private(set) var followedTags: [Mastodon.Entity.Tag]
|
||||
|
||||
private weak var tableView: UITableView?
|
||||
var diffableDataSource: UITableViewDiffableDataSource<Section, Item>?
|
||||
@ -23,85 +18,60 @@ final class FollowedTagsViewModel: NSObject {
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
|
||||
// output
|
||||
let presentHashtagTimeline = PassthroughSubject<HashtagTimelineViewModel, Never>()
|
||||
|
||||
|
||||
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<Section, Item>()
|
||||
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<Section, Item>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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<Mastodon.Entity.Tag>,
|
||||
domain: String,
|
||||
|
Loading…
x
Reference in New Issue
Block a user