Merge pull request #1155 from mastodon/ios-190-user-suggestions

Make "Suggestions" use Entities (IOS-190)
This commit is contained in:
Nathan Mattes 2023-11-16 10:36:21 +01:00 committed by GitHub
commit 6e149cd505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 129 additions and 169 deletions

View File

@ -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)
} }
} }

View File

@ -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 }

View File

@ -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?)
} }

View File

@ -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
} }
} }
} }

View File

@ -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)
} }
} }

View File

@ -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
) )
} }

View File

@ -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)
} }
}() }()

View File

@ -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
} }

View File

@ -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(

View File

@ -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
}) })
} }

View File

@ -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

View File

@ -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
}
}

View File

@ -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,

View File

@ -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 }

View File

@ -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
}
} }

View File

@ -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

View File

@ -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

View File

@ -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`

View File

@ -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

View File

@ -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
} }

View File

@ -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()

View File

@ -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

View File

@ -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) {
// dont overwrite the base language // dont overwrite the base language