Skip to content

Commit

Permalink
Merge pull request #1155 from mastodon/ios-190-user-suggestions
Browse files Browse the repository at this point in the history
Make "Suggestions" use Entities (IOS-190)
  • Loading branch information
zeitschlag authored Nov 16, 2023
2 parents 1400b52 + a2aa9b7 commit 6e149cd
Show file tree
Hide file tree
Showing 23 changed files with 128 additions and 168 deletions.
19 changes: 15 additions & 4 deletions Mastodon/Coordinator/SceneCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -562,18 +562,29 @@ private extension SceneCoordinator {
//MARK: - Loading

public extension SceneCoordinator {

@MainActor
func showLoading() {
guard let rootViewController else { return }
showLoading(on: rootViewController)
}

@MainActor
func showLoading(on viewController: UIViewController?) {
guard let viewController else { return }

MBProgressHUD.showAdded(to: rootViewController.view, animated: true)
MBProgressHUD.showAdded(to: viewController.view, animated: true)
}

@MainActor
func hideLoading() {
guard let rootViewController else { return }
hideLoading(on: rootViewController)
}

@MainActor
func hideLoading(on viewController: UIViewController?) {
guard let viewController else { return }

MBProgressHUD.hide(for: rootViewController.view, animated: true)
MBProgressHUD.hide(for: viewController.view, animated: true)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ extension MastodonPickServerViewModel {

func chooseRandomServer() -> Mastodon.Entity.Server? {

let language = Locale.autoupdatingCurrent.languageCode?.lowercased() ?? "en"
let language = Locale.autoupdatingCurrent.language.languageCode?.identifier.lowercased() ?? "en"

let servers = indexedServers.value
guard servers.isNotEmpty else { return nil }
Expand Down
4 changes: 2 additions & 2 deletions Mastodon/Scene/SuggestionAccount/RecommendAccountItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//

import Foundation
import CoreDataStack
import MastodonSDK

enum RecommendAccountItem: Hashable {
case account(ManagedObjectRecord<MastodonUser>)
case account(Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
}
16 changes: 3 additions & 13 deletions Mastodon/Scene/SuggestionAccount/RecommendAccountSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,11 @@ extension RecommendAccountSection {
UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell
switch item {
case .account(let record):
cell.delegate = configuration.suggestionAccountTableViewCellDelegate
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
cell.configure(viewModel:
SuggestionAccountTableViewCell.ViewModel(
user: user,
followedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds,
blockedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds,
followRequestedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs)
)
}
case .account(let account, let relationship):
cell.delegate = configuration.suggestionAccountTableViewCellDelegate
cell.configure(account: account, relationship: relationship)
}
return cell
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency {
setupNavigationBarAppearance()
defer { setupNavigationBarBackgroundView() }


title = L10n.Scene.SuggestionAccount.title
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: UIBarButtonItem.SystemItem.done,
Expand Down Expand Up @@ -72,6 +71,8 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency {
navigationItem.largeTitleDisplayMode = .automatic

tableView.deselectRow(with: transitionCoordinator, animated: animated)

viewModel.updateSuggestions()
}

//MARK: - Actions
Expand All @@ -85,26 +86,23 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency {
// MARK: - UITableViewDelegate
extension SuggestionAccountViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return }
guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .account(let record):
guard let account = record.object(in: context.managedObjectContext) else { return }
let cachedProfileViewModel = CachedProfileViewModel(context: context, authContext: viewModel.authContext, mastodonUser: account)
_ = coordinator.present(
scene: .profile(viewModel: cachedProfileViewModel),
from: self,
transition: .show
)
case .account(let account, _):
Task { await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) }
}

tableView.deselectRow(at: indexPath, animated: true)
}

func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
guard let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SuggestionAccountTableViewFooter.reuseIdentifier) as? SuggestionAccountTableViewFooter else {
return nil
}

footerView.followAllButton.isEnabled = viewModel.userFetchedResultsController.records.isNotEmpty
footerView.followAllButton.isEnabled = viewModel.accounts.isNotEmpty

footerView.delegate = self
return footerView
Expand All @@ -125,8 +123,9 @@ extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegat

extension SuggestionAccountViewController: SuggestionAccountTableViewFooterDelegate {
func followAll(_ footerView: SuggestionAccountTableViewFooter) {
viewModel.followAllSuggestedAccounts(self) {
viewModel.followAllSuggestedAccounts(self, presentedOn: self.navigationController) {
DispatchQueue.main.async {
self.coordinator.hideLoading(on: self.navigationController)
self.dismiss(animated: true)
}
}
Expand Down
91 changes: 43 additions & 48 deletions Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
//

import Combine
import CoreData
import CoreDataStack
import GameplayKit
import MastodonSDK
import MastodonCore
import UIKit
Expand All @@ -25,7 +22,8 @@ final class SuggestionAccountViewModel: NSObject {
// input
let context: AppContext
let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController
@Published var accounts: [Mastodon.Entity.V2.SuggestionAccount]
var relationships: [Mastodon.Entity.Relationship]

var viewWillAppear = PassthroughSubject<Void, Never>()

Expand All @@ -38,51 +36,42 @@ final class SuggestionAccountViewModel: NSObject {
) {
self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
additionalPredicate: nil
)

accounts = []
relationships = []

super.init()

userFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain

// fetch recommended users
updateSuggestions()
}


func updateSuggestions() {
Task {
var userIDs: [MastodonUser.ID] = []
var suggestedAccounts: [Mastodon.Entity.V2.SuggestionAccount] = []
do {
let response = try await context.apiService.suggestionAccountV2(
query: .init(limit: 5),
authenticationBox: authContext.mastodonAuthenticationBox
)
userIDs = response.value.map { $0.account.id }
} catch let error as Mastodon.API.Error where error.httpResponseStatus == .notFound {
let response = try await context.apiService.suggestionAccount(
query: nil,
suggestedAccounts = response.value

guard suggestedAccounts.isNotEmpty else { return }

let accounts = suggestedAccounts.compactMap { $0.account }

let relationships = try await context.apiService.relationship(
forAccounts: accounts,
authenticationBox: authContext.mastodonAuthenticationBox
)
userIDs = response.value.map { $0.id }
).value

self.relationships = relationships
self.accounts = suggestedAccounts
} catch {

self.relationships = []
self.accounts = []
}

guard userIDs.isNotEmpty else { return }
userFetchedResultsController.userIDs = userIDs
}

// fetch relationship
userFetchedResultsController.$records
.removeDuplicates()
.sink { [weak self] records in
guard let _ = self else { return }
Task {
_ = try await context.apiService.relationship(
records: records,
authenticationBox: authContext.mastodonAuthenticationBox
)
}
}
.store(in: &disposeBag)
}

func setupDiffableDataSource(
Expand All @@ -98,35 +87,41 @@ final class SuggestionAccountViewModel: NSObject {
)
)

userFetchedResultsController.$records
$accounts
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
guard let tableViewDiffableDataSource = self.tableViewDiffableDataSource else { return }
.sink { [weak self] suggestedAccounts in
guard let self, let tableViewDiffableDataSource = self.tableViewDiffableDataSource else { return }

let accounts = suggestedAccounts.compactMap { $0.account }

let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in
guard let relationship = self.relationships.first(where: {$0.id == account.id }) else { return (account: account, relationship: nil)}

return (account: account, relationship: relationship)
}

var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, RecommendAccountItem>()
snapshot.appendSections([.main])
let items: [RecommendAccountItem] = records.map { RecommendAccountItem.account($0) }
let items: [RecommendAccountItem] = accountsWithRelationship.map { RecommendAccountItem.account($0.account, relationship: $0.relationship) }
snapshot.appendItems(items, toSection: .main)

tableViewDiffableDataSource.applySnapshotUsingReloadData(snapshot)
}
.store(in: &disposeBag)
}

func followAllSuggestedAccounts(_ dependency: NeedsDependency & AuthContextProvider, completion: (() -> Void)? = nil) {
func followAllSuggestedAccounts(_ dependency: NeedsDependency & AuthContextProvider, presentedOn: UIViewController?, completion: (() -> Void)? = nil) {

let userRecords = userFetchedResultsController.records.compactMap {
$0.object(in: dependency.context.managedObjectContext)?.asRecord
}
let tmpAccounts = accounts.compactMap { $0.account }

Task {
await dependency.coordinator.showLoading(on: presentedOn)
await withTaskGroup(of: Void.self, body: { taskGroup in
for user in userRecords {
for account in tmpAccounts {
taskGroup.addTask {
try? await DataSourceFacade.responseToUserViewButtonAction(
dependency: dependency,
user: user,
user: account,
buttonState: .follow
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,28 +85,17 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
disposeBag.removeAll()
}

func configure(viewModel: SuggestionAccountTableViewCell.ViewModel) {
userView.configure(user: viewModel.user, delegate: delegate)

if viewModel.blockedUsers.contains(viewModel.user.id) {
self.userView.setButtonState(.blocked)
} else if viewModel.followedUsers.contains(viewModel.user.id) {
self.userView.setButtonState(.unfollow)
} else if viewModel.followRequestedUsers.contains(viewModel.user.id) {
self.userView.setButtonState(.pending)
} else if viewModel.user.locked {
self.userView.setButtonState(.request)
} else {
self.userView.setButtonState(.follow)
}
func configure(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) {
userView.configure(with: account, relationship: relationship, delegate: delegate)
userView.updateButtonState(with: relationship, isMe: false)

let metaContent: MetaContent = {
do {
let mastodonContent = MastodonContent(content: viewModel.user.note ?? "", emojis: viewModel.user.emojis.asDictionary)
let mastodonContent = MastodonContent(content: account.note, emojis: account.emojis?.asDictionary ?? [:])
return try MastodonMetaContent.convert(document: mastodonContent)
} catch {
assertionFailure()
return PlaintextMetaContent(string: viewModel.user.note ?? "")
return PlaintextMetaContent(string: account.note)
}
}()

Expand Down
4 changes: 2 additions & 2 deletions Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ extension ThreadViewModel.LoadThreadState {
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)

await enter(state: NoMore.self)
enter(state: NoMore.self)

// assert(!Thread.isMainThread)
// await Task.sleep(1_000_000_000) // 1s delay to prevent UI render issue
Expand All @@ -88,7 +88,7 @@ extension ThreadViewModel.LoadThreadState {
}
)
} catch {
await enter(state: Fail.self)
enter(state: Fail.self)
}
} // end Task
}
Expand Down
1 change: 1 addition & 0 deletions MastodonSDK/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ let package = Package(
name: "MastodonSDK",
dependencies: [
.product(name: "NIOHTTP1", package: "swift-nio"),
"MastodonCommon"
]
),
.target(
Expand Down
12 changes: 0 additions & 12 deletions MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,6 @@ public final class CoreDataStack {
container.viewContext.automaticallyMergesChangesFromParent = true

callback()

#if DEBUG
do {
let storeURL = URL.storeURL(for: AppName.groupID, databaseName: "shared")
let data = try Data(contentsOf: storeURL)
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useMB]
formatter.countStyle = .file
let size = formatter.string(fromByteCount: Int64(data.count))
} catch {
}
#endif
})
}

Expand Down
Loading

0 comments on commit 6e149cd

Please sign in to comment.