Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make "Followed Hashtags"-screen work with entities (IOS-186) #1149

Merged
merged 3 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Mastodon/Coordinator/SceneCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

init(appContext: AppContext, sceneCoordinator: SceneCoordinator, authContext: AuthContext, viewModel: FollowedTagsViewModel) {
self.context = appContext
self.coordinator = sceneCoordinator
self.authContext = authContext
self.viewModel = viewModel

refreshControl = UIRefreshControl()

lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.register(FollowedTagsTableViewCell.self, forCellReuseIdentifier: String(describing: FollowedTagsTableViewCell.self))
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)

navigationItem.titleView = titleView

view.backgroundColor = .secondarySystemBackground
title = L10n.Scene.FollowedTags.title

tableView.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .secondarySystemBackground
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
)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
//

import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
import MastodonCore

Expand All @@ -18,7 +15,7 @@ extension FollowedTagsViewModel {
}

enum Item: Hashable {
case hashtag(Tag)
case hashtag(Mastodon.Entity.Tag)
}

func tableViewDiffableDataSource(
Expand All @@ -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()
}
Expand All @@ -39,9 +36,7 @@ extension FollowedTagsViewModel {
}
}

func setupDiffableDataSource(
tableView: UITableView
) {
func setupDiffableDataSource(tableView: UITableView) {
diffableDataSource = tableViewDiffableDataSource(for: tableView)

var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
Expand Down
78 changes: 24 additions & 54 deletions Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,103 +5,73 @@
// 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>?

// 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down