Merge pull request #1155 from mastodon/ios-190-user-suggestions
Make "Suggestions" use Entities (IOS-190)
This commit is contained in:
commit
6e149cd505
|
@ -562,18 +562,29 @@ private extension SceneCoordinator {
|
||||||
//MARK: - Loading
|
//MARK: - Loading
|
||||||
|
|
||||||
public extension SceneCoordinator {
|
public extension SceneCoordinator {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func showLoading() {
|
func showLoading() {
|
||||||
guard let rootViewController else { return }
|
showLoading(on: rootViewController)
|
||||||
|
}
|
||||||
|
|
||||||
MBProgressHUD.showAdded(to: rootViewController.view, animated: true)
|
@MainActor
|
||||||
|
func showLoading(on viewController: UIViewController?) {
|
||||||
|
guard let viewController else { return }
|
||||||
|
|
||||||
|
MBProgressHUD.showAdded(to: viewController.view, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func hideLoading() {
|
func hideLoading() {
|
||||||
guard let rootViewController else { return }
|
hideLoading(on: rootViewController)
|
||||||
|
}
|
||||||
|
|
||||||
MBProgressHUD.hide(for: rootViewController.view, animated: true)
|
@MainActor
|
||||||
|
func hideLoading(on viewController: UIViewController?) {
|
||||||
|
guard let viewController else { return }
|
||||||
|
|
||||||
|
MBProgressHUD.hide(for: viewController.view, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -203,7 +203,7 @@ extension MastodonPickServerViewModel {
|
||||||
|
|
||||||
func chooseRandomServer() -> Mastodon.Entity.Server? {
|
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
|
let servers = indexedServers.value
|
||||||
guard servers.isNotEmpty else { return nil }
|
guard servers.isNotEmpty else { return nil }
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreDataStack
|
import MastodonSDK
|
||||||
|
|
||||||
enum RecommendAccountItem: Hashable {
|
enum RecommendAccountItem: Hashable {
|
||||||
case account(ManagedObjectRecord<MastodonUser>)
|
case account(Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,21 +34,11 @@ extension RecommendAccountSection {
|
||||||
UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell
|
||||||
switch item {
|
switch item {
|
||||||
case .account(let record):
|
case .account(let account, let relationship):
|
||||||
cell.delegate = configuration.suggestionAccountTableViewCellDelegate
|
cell.delegate = configuration.suggestionAccountTableViewCellDelegate
|
||||||
context.managedObjectContext.performAndWait {
|
cell.configure(account: account, relationship: relationship)
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,6 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency {
|
||||||
setupNavigationBarAppearance()
|
setupNavigationBarAppearance()
|
||||||
defer { setupNavigationBarBackgroundView() }
|
defer { setupNavigationBarBackgroundView() }
|
||||||
|
|
||||||
|
|
||||||
title = L10n.Scene.SuggestionAccount.title
|
title = L10n.Scene.SuggestionAccount.title
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||||
barButtonSystemItem: UIBarButtonItem.SystemItem.done,
|
barButtonSystemItem: UIBarButtonItem.SystemItem.done,
|
||||||
|
@ -72,6 +71,8 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency {
|
||||||
navigationItem.largeTitleDisplayMode = .automatic
|
navigationItem.largeTitleDisplayMode = .automatic
|
||||||
|
|
||||||
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||||
|
|
||||||
|
viewModel.updateSuggestions()
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - Actions
|
//MARK: - Actions
|
||||||
|
@ -85,18 +86,15 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency {
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
extension SuggestionAccountViewController: UITableViewDelegate {
|
extension SuggestionAccountViewController: UITableViewDelegate {
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
|
||||||
guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return }
|
guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return }
|
||||||
guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
switch item {
|
switch item {
|
||||||
case .account(let record):
|
case .account(let account, _):
|
||||||
guard let account = record.object(in: context.managedObjectContext) else { return }
|
Task { await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) }
|
||||||
let cachedProfileViewModel = CachedProfileViewModel(context: context, authContext: viewModel.authContext, mastodonUser: account)
|
|
||||||
_ = coordinator.present(
|
|
||||||
scene: .profile(viewModel: cachedProfileViewModel),
|
|
||||||
from: self,
|
|
||||||
transition: .show
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||||
|
@ -104,7 +102,7 @@ extension SuggestionAccountViewController: UITableViewDelegate {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
footerView.followAllButton.isEnabled = viewModel.userFetchedResultsController.records.isNotEmpty
|
footerView.followAllButton.isEnabled = viewModel.accounts.isNotEmpty
|
||||||
|
|
||||||
footerView.delegate = self
|
footerView.delegate = self
|
||||||
return footerView
|
return footerView
|
||||||
|
@ -125,8 +123,9 @@ extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegat
|
||||||
|
|
||||||
extension SuggestionAccountViewController: SuggestionAccountTableViewFooterDelegate {
|
extension SuggestionAccountViewController: SuggestionAccountTableViewFooterDelegate {
|
||||||
func followAll(_ footerView: SuggestionAccountTableViewFooter) {
|
func followAll(_ footerView: SuggestionAccountTableViewFooter) {
|
||||||
viewModel.followAllSuggestedAccounts(self) {
|
viewModel.followAllSuggestedAccounts(self, presentedOn: self.navigationController) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.coordinator.hideLoading(on: self.navigationController)
|
||||||
self.dismiss(animated: true)
|
self.dismiss(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
|
||||||
import CoreDataStack
|
|
||||||
import GameplayKit
|
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
import UIKit
|
import UIKit
|
||||||
|
@ -25,7 +22,8 @@ final class SuggestionAccountViewModel: NSObject {
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
let userFetchedResultsController: UserFetchedResultsController
|
@Published var accounts: [Mastodon.Entity.V2.SuggestionAccount]
|
||||||
|
var relationships: [Mastodon.Entity.Relationship]
|
||||||
|
|
||||||
var viewWillAppear = PassthroughSubject<Void, Never>()
|
var viewWillAppear = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
@ -38,51 +36,42 @@ final class SuggestionAccountViewModel: NSObject {
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.userFetchedResultsController = UserFetchedResultsController(
|
|
||||||
managedObjectContext: context.managedObjectContext,
|
accounts = []
|
||||||
domain: nil,
|
relationships = []
|
||||||
additionalPredicate: nil
|
|
||||||
)
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
userFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain
|
updateSuggestions()
|
||||||
|
}
|
||||||
|
|
||||||
// fetch recommended users
|
|
||||||
|
func updateSuggestions() {
|
||||||
Task {
|
Task {
|
||||||
var userIDs: [MastodonUser.ID] = []
|
var suggestedAccounts: [Mastodon.Entity.V2.SuggestionAccount] = []
|
||||||
do {
|
do {
|
||||||
let response = try await context.apiService.suggestionAccountV2(
|
let response = try await context.apiService.suggestionAccountV2(
|
||||||
query: .init(limit: 5),
|
query: .init(limit: 5),
|
||||||
authenticationBox: authContext.mastodonAuthenticationBox
|
authenticationBox: authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
userIDs = response.value.map { $0.account.id }
|
suggestedAccounts = response.value
|
||||||
} catch let error as Mastodon.API.Error where error.httpResponseStatus == .notFound {
|
|
||||||
let response = try await context.apiService.suggestionAccount(
|
guard suggestedAccounts.isNotEmpty else { return }
|
||||||
query: nil,
|
|
||||||
|
let accounts = suggestedAccounts.compactMap { $0.account }
|
||||||
|
|
||||||
|
let relationships = try await context.apiService.relationship(
|
||||||
|
forAccounts: accounts,
|
||||||
authenticationBox: authContext.mastodonAuthenticationBox
|
authenticationBox: authContext.mastodonAuthenticationBox
|
||||||
)
|
).value
|
||||||
userIDs = response.value.map { $0.id }
|
|
||||||
|
self.relationships = relationships
|
||||||
|
self.accounts = suggestedAccounts
|
||||||
} catch {
|
} 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(
|
func setupDiffableDataSource(
|
||||||
|
@ -98,15 +87,22 @@ final class SuggestionAccountViewModel: NSObject {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
userFetchedResultsController.$records
|
$accounts
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] records in
|
.sink { [weak self] suggestedAccounts in
|
||||||
guard let self = self else { return }
|
guard let self, let tableViewDiffableDataSource = self.tableViewDiffableDataSource else { return }
|
||||||
guard 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>()
|
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, RecommendAccountItem>()
|
||||||
snapshot.appendSections([.main])
|
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)
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
|
||||||
tableViewDiffableDataSource.applySnapshotUsingReloadData(snapshot)
|
tableViewDiffableDataSource.applySnapshotUsingReloadData(snapshot)
|
||||||
|
@ -114,19 +110,18 @@ final class SuggestionAccountViewModel: NSObject {
|
||||||
.store(in: &disposeBag)
|
.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 {
|
let tmpAccounts = accounts.compactMap { $0.account }
|
||||||
$0.object(in: dependency.context.managedObjectContext)?.asRecord
|
|
||||||
}
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
await dependency.coordinator.showLoading(on: presentedOn)
|
||||||
await withTaskGroup(of: Void.self, body: { taskGroup in
|
await withTaskGroup(of: Void.self, body: { taskGroup in
|
||||||
for user in userRecords {
|
for account in tmpAccounts {
|
||||||
taskGroup.addTask {
|
taskGroup.addTask {
|
||||||
try? await DataSourceFacade.responseToUserViewButtonAction(
|
try? await DataSourceFacade.responseToUserViewButtonAction(
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
user: user,
|
user: account,
|
||||||
buttonState: .follow
|
buttonState: .follow
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,28 +85,17 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
|
||||||
disposeBag.removeAll()
|
disposeBag.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(viewModel: SuggestionAccountTableViewCell.ViewModel) {
|
func configure(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) {
|
||||||
userView.configure(user: viewModel.user, delegate: delegate)
|
userView.configure(with: account, relationship: relationship, delegate: delegate)
|
||||||
|
userView.updateButtonState(with: relationship, isMe: false)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
let metaContent: MetaContent = {
|
let metaContent: MetaContent = {
|
||||||
do {
|
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)
|
return try MastodonMetaContent.convert(document: mastodonContent)
|
||||||
} catch {
|
} catch {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return PlaintextMetaContent(string: viewModel.user.note ?? "")
|
return PlaintextMetaContent(string: account.note)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ extension ThreadViewModel.LoadThreadState {
|
||||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
|
|
||||||
await enter(state: NoMore.self)
|
enter(state: NoMore.self)
|
||||||
|
|
||||||
// assert(!Thread.isMainThread)
|
// assert(!Thread.isMainThread)
|
||||||
// await Task.sleep(1_000_000_000) // 1s delay to prevent UI render issue
|
// await Task.sleep(1_000_000_000) // 1s delay to prevent UI render issue
|
||||||
|
@ -88,7 +88,7 @@ extension ThreadViewModel.LoadThreadState {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
await enter(state: Fail.self)
|
enter(state: Fail.self)
|
||||||
}
|
}
|
||||||
} // end Task
|
} // end Task
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,7 @@ let package = Package(
|
||||||
name: "MastodonSDK",
|
name: "MastodonSDK",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "NIOHTTP1", package: "swift-nio"),
|
.product(name: "NIOHTTP1", package: "swift-nio"),
|
||||||
|
"MastodonCommon"
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
|
|
|
@ -94,18 +94,6 @@ public final class CoreDataStack {
|
||||||
container.viewContext.automaticallyMergesChangesFromParent = true
|
container.viewContext.automaticallyMergesChangesFromParent = true
|
||||||
|
|
||||||
callback()
|
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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,33 +9,6 @@ import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import MastodonMeta
|
import MastodonMeta
|
||||||
|
|
||||||
extension Mastodon.Entity.Account: Hashable {
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func == (lhs: Mastodon.Entity.Account, rhs: Mastodon.Entity.Account) -> Bool {
|
|
||||||
return lhs.id == rhs.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Mastodon.Entity.Account {
|
|
||||||
public func avatarImageURL() -> URL? {
|
|
||||||
let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar
|
|
||||||
return URL(string: string)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func avatarImageURLWithFallback(domain: String) -> URL {
|
|
||||||
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Mastodon.Entity.Account {
|
|
||||||
public var displayNameWithFallback: String {
|
|
||||||
return !displayName.isEmpty ? displayName : username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Mastodon.Entity.Account {
|
extension Mastodon.Entity.Account {
|
||||||
public var emojiMeta: MastodonContent.Emojis {
|
public var emojiMeta: MastodonContent.Emojis {
|
||||||
let isAnimated = !UserDefaults.shared.preferredStaticEmoji
|
let isAnimated = !UserDefaults.shared.preferredStaticEmoji
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
//
|
|
||||||
// Mastodon+Entity+Field.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021-5-25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import MastodonSDK
|
|
||||||
|
|
||||||
extension Mastodon.Entity.Field: Equatable {
|
|
||||||
public static func == (lhs: Mastodon.Entity.Field, rhs: Mastodon.Entity.Field) -> Bool {
|
|
||||||
return lhs.name == rhs.name &&
|
|
||||||
lhs.value == rhs.value &&
|
|
||||||
lhs.verifiedAt == rhs.verifiedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -50,7 +50,7 @@ extension APIService {
|
||||||
domain: domain,
|
domain: domain,
|
||||||
authorization: authorization).singleOutput()
|
authorization: authorization).singleOutput()
|
||||||
|
|
||||||
let responseHistory = try await Mastodon.API.Statuses.editHistory(
|
_ = try await Mastodon.API.Statuses.editHistory(
|
||||||
forStatusID: statusID,
|
forStatusID: statusID,
|
||||||
session: session,
|
session: session,
|
||||||
domain: domain,
|
domain: domain,
|
||||||
|
|
|
@ -238,7 +238,7 @@ extension NotificationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func authenticationBox(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationBox? {
|
private func authenticationBox(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationBox? {
|
||||||
guard let authenticationService = self.authenticationService else { return nil }
|
guard self.authenticationService != nil else { return nil }
|
||||||
let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == pushNotification.accessToken }
|
let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == pushNotification.accessToken }
|
||||||
guard let authentication = results.first else { return nil }
|
guard let authentication = results.first else { return nil }
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import MastodonCommon
|
||||||
|
|
||||||
extension Mastodon.Entity {
|
extension Mastodon.Entity {
|
||||||
|
|
||||||
|
@ -84,6 +85,24 @@ extension Mastodon.Entity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//MARK: - Hashable
|
||||||
|
extension Mastodon.Entity.Account: Hashable {
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
// The URL seems to be the only thing that doesn't change across instances.
|
||||||
|
hasher.combine(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - Equatable
|
||||||
|
extension Mastodon.Entity.Account: Equatable {
|
||||||
|
public static func == (lhs: Mastodon.Entity.Account, rhs: Mastodon.Entity.Account) -> Bool {
|
||||||
|
// The URL seems to be the only thing that doesn't change across instances.
|
||||||
|
return lhs.url == rhs.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - Convenience
|
||||||
extension Mastodon.Entity.Account {
|
extension Mastodon.Entity.Account {
|
||||||
public func acctWithDomainIfMissing(_ localDomain: String) -> String {
|
public func acctWithDomainIfMissing(_ localDomain: String) -> String {
|
||||||
guard acct.contains("@") else {
|
guard acct.contains("@") else {
|
||||||
|
@ -102,4 +121,18 @@ extension Mastodon.Entity.Account {
|
||||||
|
|
||||||
return components.host
|
return components.host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func avatarImageURL() -> URL? {
|
||||||
|
let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar
|
||||||
|
return URL(string: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func avatarImageURLWithFallback(domain: String) -> URL {
|
||||||
|
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
||||||
|
}
|
||||||
|
|
||||||
|
public var displayNameWithFallback: String {
|
||||||
|
return !displayName.isEmpty ? displayName : username
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
||||||
/// 2021/1/28
|
/// 2021/1/28
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// [Document](https://docs.joinmastodon.org/entities/emoji/)
|
/// [Document](https://docs.joinmastodon.org/entities/emoji/)
|
||||||
public struct Emoji: Codable, Sendable {
|
public struct Emoji: Codable, Sendable, Hashable {
|
||||||
public let shortcode: String
|
public let shortcode: String
|
||||||
public let url: String
|
public let url: String
|
||||||
public let staticURL: String
|
public let staticURL: String
|
||||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
||||||
/// 2021/1/28
|
/// 2021/1/28
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// [Document](https://docs.joinmastodon.org/entities/field/)
|
/// [Document](https://docs.joinmastodon.org/entities/field/)
|
||||||
public struct Field: Codable, Sendable {
|
public struct Field: Codable, Sendable, Hashable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let value: String
|
public let value: String
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
||||||
/// 2021/2/3
|
/// 2021/2/3
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// [Document](https://docs.joinmastodon.org/entities/source/)
|
/// [Document](https://docs.joinmastodon.org/entities/source/)
|
||||||
public struct Source: Codable, Sendable {
|
public struct Source: Codable, Sendable, Hashable {
|
||||||
|
|
||||||
// Base
|
// Base
|
||||||
public let note: String
|
public let note: String
|
||||||
|
@ -40,7 +40,7 @@ extension Mastodon.Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.Entity.Source {
|
extension Mastodon.Entity.Source {
|
||||||
public enum Privacy: RawRepresentable, Codable, Sendable {
|
public enum Privacy: RawRepresentable, Codable, Sendable, Hashable {
|
||||||
case `public`
|
case `public`
|
||||||
case unlisted
|
case unlisted
|
||||||
case `private`
|
case `private`
|
||||||
|
|
|
@ -9,7 +9,7 @@ import Foundation
|
||||||
|
|
||||||
extension Mastodon.Entity.V2 {
|
extension Mastodon.Entity.V2 {
|
||||||
|
|
||||||
public struct SuggestionAccount: Codable, Sendable {
|
public struct SuggestionAccount: Codable, Sendable, Hashable {
|
||||||
|
|
||||||
public let source: String
|
public let source: String
|
||||||
public let account: Mastodon.Entity.Account
|
public let account: Mastodon.Entity.Account
|
||||||
|
|
|
@ -31,7 +31,7 @@ extension ComposeContentViewModel: UITextViewDelegate {
|
||||||
switch textView {
|
switch textView {
|
||||||
case contentMetaText?.textView:
|
case contentMetaText?.textView:
|
||||||
// update model
|
// update model
|
||||||
guard let metaText = self.contentMetaText else {
|
guard self.contentMetaText != nil else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,7 +211,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
let recentLanguages = context.settingService.currentSetting.value?.recentLanguages ?? []
|
let recentLanguages = context.settingService.currentSetting.value?.recentLanguages ?? []
|
||||||
self.recentLanguages = recentLanguages
|
self.recentLanguages = recentLanguages
|
||||||
self.language = recentLanguages.first ?? Locale.current.languageCode ?? "en"
|
self.language = recentLanguages.first ?? Locale.current.language.languageCode?.identifier ?? "en"
|
||||||
super.init()
|
super.init()
|
||||||
// end init
|
// end init
|
||||||
|
|
||||||
|
@ -490,7 +490,7 @@ extension ComposeContentViewModel {
|
||||||
.flatMap { settings in
|
.flatMap { settings in
|
||||||
if let settings {
|
if let settings {
|
||||||
return settings.publisher(for: \.recentLanguages, options: .initial).eraseToAnyPublisher()
|
return settings.publisher(for: \.recentLanguages, options: .initial).eraseToAnyPublisher()
|
||||||
} else if let code = Locale.current.languageCode {
|
} else if let code = Locale.current.language.languageCode?.identifier {
|
||||||
return Just([code]).eraseToAnyPublisher()
|
return Just([code]).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
return Just([]).eraseToAnyPublisher()
|
return Just([]).eraseToAnyPublisher()
|
||||||
|
|
|
@ -32,7 +32,7 @@ extension ComposeContentToolbarView {
|
||||||
@Published var isAttachmentButtonEnabled = false
|
@Published var isAttachmentButtonEnabled = false
|
||||||
@Published var isPollButtonEnabled = false
|
@Published var isPollButtonEnabled = false
|
||||||
|
|
||||||
@Published var language = Locale.current.languageCode ?? "en"
|
@Published var language = Locale.current.language.languageCode?.identifier ?? "en"
|
||||||
@Published var recentLanguages: [String] = []
|
@Published var recentLanguages: [String] = []
|
||||||
|
|
||||||
@Published public var maxTextInputLimit = 500
|
@Published public var maxTextInputLimit = 500
|
||||||
|
|
|
@ -14,7 +14,7 @@ struct LanguagePicker: View {
|
||||||
let locales = Locale.availableIdentifiers.map(Locale.init(identifier:))
|
let locales = Locale.availableIdentifiers.map(Locale.init(identifier:))
|
||||||
var languages: [String: Language] = [:]
|
var languages: [String: Language] = [:]
|
||||||
for locale in locales {
|
for locale in locales {
|
||||||
if let code = locale.languageCode,
|
if let code = locale.language.languageCode?.identifier,
|
||||||
let endonym = locale.localizedString(forLanguageCode: code),
|
let endonym = locale.localizedString(forLanguageCode: code),
|
||||||
let exonym = Locale.current.localizedString(forLanguageCode: code) {
|
let exonym = Locale.current.localizedString(forLanguageCode: code) {
|
||||||
// don’t overwrite the “base” language
|
// don’t overwrite the “base” language
|
||||||
|
|
Loading…
Reference in New Issue