diff --git a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings index bc73a299..c386e4cb 100644 --- a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings @@ -281,6 +281,7 @@ "status.editor.drafts.navigation-title" = "Entwürfe"; "status.editor.error.upload" = "Fehler beim Hochladen"; "status.editor.language-select.navigation-title" = "Sprache auswählen"; +"status.editor.language-select.recently-used" = "Recently Used"; "status.editor.media.edit-image" = "Bild bearbeiten"; "status.editor.media.image-description" = "Bildbeschreibung"; "status.editor.mode.edit" = "Deinen Post bearbeiten"; diff --git a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings index 97df9111..ea31eadb 100644 --- a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings @@ -281,6 +281,7 @@ "status.editor.drafts.navigation-title" = "Drafts"; "status.editor.error.upload" = "Error uploading"; "status.editor.language-select.navigation-title" = "Select Language"; +"status.editor.language-select.recently-used" = "Recently Used"; "status.editor.media.edit-image" = "Edit Image"; "status.editor.media.image-description" = "Image description"; "status.editor.mode.edit" = "Editing your post"; diff --git a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings index 1eddfe0c..354fcccb 100644 --- a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings @@ -281,6 +281,7 @@ "status.editor.drafts.navigation-title" = "Borradores"; "status.editor.error.upload" = "Error subiendo"; "status.editor.language-select.navigation-title" = "Seleccionar idioma"; +"status.editor.language-select.recently-used" = "Recently Used"; "status.editor.media.edit-image" = "Editar Imagen"; "status.editor.media.image-description" = "Descripción de la imagen"; "status.editor.mode.edit" = "Editando tu publicación"; diff --git a/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings index 54175a4a..4a6b9557 100644 --- a/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings @@ -281,6 +281,7 @@ "status.editor.drafts.navigation-title" = "Bozze"; "status.editor.error.upload" = "Errore durante il caricamento"; "status.editor.language-select.navigation-title" = "Scegli la lingua"; +"status.editor.language-select.recently-used" = "Recently Used"; "status.editor.media.edit-image" = "Modifica l'immagine"; "status.editor.media.image-description" = "Descrizione dell'immagine"; "status.editor.mode.edit" = "Messaggio in modifica"; diff --git a/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings index 09c0f686..474c3ecd 100644 --- a/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings @@ -277,6 +277,7 @@ "status.editor.drafts.navigation-title" = "下書き"; "status.editor.error.upload" = "アップロードエラー"; "status.editor.language-select.navigation-title" = "言語設定"; +"status.editor.language-select.recently-used" = "Recently Used"; "status.editor.media.edit-image" = "イメージの編集"; "status.editor.media.image-description" = "イメージの説明文"; "status.editor.mode.edit" = "投稿を編集する"; diff --git a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings index f990893b..b845efe1 100644 --- a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings @@ -281,6 +281,7 @@ "status.editor.drafts.navigation-title" = "Concepten"; "status.editor.error.upload" = "Fout tijdens uploaden"; "status.editor.language-select.navigation-title" = "Taal selecteren"; +"status.editor.language-select.recently-used" = "Recently Used"; "status.editor.media.edit-image" = "Afbeelding bewerken"; "status.editor.media.image-description" = "Omschrijving"; "status.editor.mode.edit" = "Je post bewerken"; diff --git a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings index 687b9377..7c11a2a5 100644 --- a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings @@ -281,6 +281,7 @@ "status.editor.drafts.navigation-title" = "草稿"; "status.editor.error.upload" = "上传错误"; "status.editor.language-select.navigation-title" = "选择语言"; +"status.editor.language-select.recently-used" = "Recently Used"; "status.editor.media.edit-image" = "编辑图片"; "status.editor.media.image-description" = "图片描述"; "status.editor.mode.edit" = "正在编辑你的嘟文"; diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index e0252b24..6846af6d 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -16,6 +16,7 @@ public class UserPreferences: ObservableObject { @AppStorage("font_size_scale") public var fontSizeScale: Double = 1 @AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true @AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true + @AppStorage("recently_used_languages") public var recentlyUsedLanguages: [String] = [] public var pushNotificationsCount: Int { get { @@ -41,4 +42,13 @@ public class UserPreferences: ObservableObject { guard let client, client.isAuth else { return } serverPreferences = try? await client.get(endpoint: Accounts.preferences) } + + public func markLanguageAsSelected(isoCode: String) { + var copy = recentlyUsedLanguages + if let index = copy.firstIndex(of: isoCode) { + copy.remove(at: index) + } + copy.insert(isoCode, at: 0) + recentlyUsedLanguages = Array(copy.prefix(3)) + } } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift index 8b9d8431..f7a9b865 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift @@ -107,21 +107,17 @@ struct StatusEditorAccessoryView: View { private var languageSheetView: some View { NavigationStack { List { - ForEach(availableLanguages, id: \.0) { isoCode, nativeName, name in - HStack { - languageTextView(isoCode: isoCode, nativeName: nativeName, name: name) - .tag(isoCode) - Spacer() - if isoCode == viewModel.selectedLanguage { - Image(systemName: "checkmark") + if languageSearch.isEmpty { + if !recentlyUsedLanguages.isEmpty { + Section("Recently Used") { + languageSheetSection(languages: recentlyUsedLanguages) } } - .listRowBackground(theme.primaryBackgroundColor) - .contentShape(Rectangle()) - .onTapGesture { - viewModel.selectedLanguage = isoCode - isLanguageSheetDisplayed = false + Section { + languageSheetSection(languages: otherLanguages) } + } else { + languageSheetSection(languages: languageSearchResult(query: languageSearch)) } } .searchable(text: $languageSearch) @@ -137,6 +133,29 @@ struct StatusEditorAccessoryView: View { } } + private func languageSheetSection(languages: [Language]) -> some View { + ForEach(languages) { language in + HStack { + languageTextView( + isoCode: language.isoCode, + nativeName: language.nativeName, + name: language.localizedName + ).tag(language.isoCode) + Spacer() + if language.isoCode == viewModel.selectedLanguage { + Image(systemName: "checkmark") + } + } + .listRowBackground(theme.primaryBackgroundColor) + .contentShape(Rectangle()) + .onTapGesture { + viewModel.selectedLanguage = language.isoCode + viewModel.hasExplicitlySelectedLanguage = true + isLanguageSheetDisplayed = false + } + } + } + private var draftsSheetView: some View { NavigationStack { List { @@ -206,22 +225,43 @@ struct StatusEditorAccessoryView: View { .font(.scaledCallout) } - private var availableLanguages: [(String, String?, String?)] { + private struct Language: Identifiable, Equatable { + var id: String { isoCode } + + let isoCode: String + let nativeName: String? + let localizedName: String? + } + + private let allAvailableLanguages: [Language] = Locale.LanguageCode.isoLanguageCodes .filter { $0.identifier.count == 2 } // Mastodon only supports ISO 639-1 (two-letter) codes .map { lang in let nativeLocale = Locale(languageComponents: Locale.Language.Components(languageCode: lang)) - return ( - lang.identifier, - nativeLocale.localizedString(forLanguageCode: lang.identifier), - Locale.current.localizedString(forLanguageCode: lang.identifier) + return Language( + isoCode: lang.identifier, + nativeName: nativeLocale.localizedString(forLanguageCode: lang.identifier)?.capitalized, + localizedName: Locale.current.localizedString(forLanguageCode: lang.identifier)?.localizedCapitalized ) } - .filter { _, nativeLocale, _ in - guard !languageSearch.isEmpty else { - return true - } - return nativeLocale?.lowercased().hasPrefix(languageSearch.lowercased()) == true + + private var recentlyUsedLanguages: [Language] { + preferences.recentlyUsedLanguages.compactMap { isoCode in + allAvailableLanguages.first { $0.isoCode == isoCode } + } + } + + private var otherLanguages: [Language] { + allAvailableLanguages.filter { !preferences.recentlyUsedLanguages.contains($0.isoCode) } + } + + private func languageSearchResult(query: String) -> [Language] { + allAvailableLanguages.filter { language in + guard !languageSearch.isEmpty else { + return true } + return language.nativeName?.lowercased().hasPrefix(query.lowercased()) == true + || language.localizedName?.lowercased().hasPrefix(query.lowercased()) == true + } } } diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index db2b9b80..42420bc5 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -74,6 +74,7 @@ public struct StatusEditorView: View { viewModel.client = client viewModel.currentAccount = currentAccount.account viewModel.theme = theme + viewModel.preferences = preferences viewModel.prepareStatusText() if !client.isAuth { dismiss() diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index dc9a4aef..723419d1 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -13,6 +13,7 @@ public class StatusEditorViewModel: ObservableObject { var client: Client? var currentAccount: Account? var theme: Theme? + var preferences: UserPreferences? @Published var statusText = NSMutableAttributedString(string: "") { didSet { @@ -87,6 +88,7 @@ public class StatusEditorViewModel: ObservableObject { @Published var mentionsSuggestions: [Account] = [] @Published var tagsSuggestions: [Tag] = [] @Published var selectedLanguage: String? + var hasExplicitlySelectedLanguage: Bool = false private var currentSuggestionRange: NSRange? private var embeddedStatusURL: URL? { @@ -136,6 +138,9 @@ public class StatusEditorViewModel: ObservableObject { postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id, json: data)) } generator.notificationOccurred(.success) + if hasExplicitlySelectedLanguage, let selectedLanguage { + preferences?.markLanguageAsSelected(isoCode: selectedLanguage) + } isPosting = false return postStatus } catch let error {