Language selection fixes (#2917)

* Fix duplicated language entries from system and app language sets.
Closes #2900

* Prefer modern language codes.
Closes #2903

* Synchronize per-account default posting language with server.
Closes #2902

* Allow users to post in languages android doesn't know about yet (e.g. toki pona)

* Always put the preselected language at the top of the list
This commit is contained in:
Levi Bard 2022-11-24 15:45:19 +01:00 committed by GitHub
commit 0126ee9500
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1043 additions and 18 deletions

View file

@ -22,6 +22,7 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.modernLanguageCode
import java.util.Locale
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) {
@ -29,7 +30,7 @@ class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : Ar
return (super.getView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
typeface = Typeface.DEFAULT_BOLD
text = super.getItem(position)?.language?.uppercase()
text = super.getItem(position)?.modernLanguageCode?.uppercase()
}
}

View file

@ -94,6 +94,7 @@ import com.keylesspalace.tusky.util.getMediaSize
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.highlightSpans
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.modernLanguageCode
import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
@ -538,13 +539,38 @@ class ComposeActivity :
private fun mergeLocaleListCompat(list: MutableList<Locale>, localeListCompat: LocaleListCompat) {
for (index in 0 until localeListCompat.size()) {
val locale = localeListCompat[index]
if (locale != null && !list.contains(locale)) {
if (locale != null && list.none { locale.language == it.language }) {
list.add(locale)
}
}
}
private fun setupLanguageSpinner(initialLanguage: String?) {
// Ensure that the locale whose code matches the given language is first in the list
private fun ensureLanguageIsFirst(locales: MutableList<Locale>, language: String) {
var currentLocaleIndex = locales.indexOfFirst { it.language == language }
if (currentLocaleIndex < 0) {
// Recheck against modern language codes
// This should only happen when replying or when the per-account post language is set
// to a modern code
currentLocaleIndex = locales.indexOfFirst { it.modernLanguageCode == language }
if (currentLocaleIndex < 0) {
// This can happen when:
// - Your per-account posting language is set to one android doesn't know (e.g. toki pona)
// - Replying to a post in a language android doesn't know
locales.add(0, Locale(language))
Log.w(TAG, "Attempting to use unknown language tag '$language'")
return
}
}
if (currentLocaleIndex > 0) {
// Move preselected locale to the top
locales.add(0, locales.removeAt(currentLocaleIndex))
}
}
private fun setupLanguageSpinner(initialLanguage: String) {
val locales = mutableListOf<Locale>()
mergeLocaleListCompat(locales, AppCompatDelegate.getApplicationLocales()) // configured app languages first
mergeLocaleListCompat(locales, LocaleListCompat.getDefault()) // then configured system languages
@ -556,33 +582,33 @@ class ComposeActivity :
it.variant.isNullOrEmpty()
}
)
ensureLanguageIsFirst(locales, initialLanguage)
var currentLocaleIndex = locales.indexOfFirst { it.language == initialLanguage }
if (currentLocaleIndex < 0) {
Log.e(TAG, "Error looking up language tag '$initialLanguage', falling back to english")
currentLocaleIndex = locales.indexOfFirst { it.language == "en" }
}
val context = this
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).language
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode
}
override fun onNothingSelected(parent: AdapterView<*>) {
parent.setSelection(locales.indexOfFirst { it.language == getInitialLanguage() })
parent.setSelection(0)
}
}
binding.composePostLanguageButton.apply {
adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, locales)
setSelection(currentLocaleIndex)
setSelection(0)
}
}
private fun getInitialLanguage(language: String? = null): String {
return if (language.isNullOrEmpty()) {
// Setting the application ui preference sets the default locale
AppCompatDelegate.getApplicationLocales()[0]?.language ?: Locale.getDefault().language
// Account-specific language set on the server
if (accountManager.activeAccount?.defaultPostLanguage?.isNotEmpty() == true) {
accountManager.activeAccount?.defaultPostLanguage!!
} else {
// Setting the application ui preference sets the default locale
AppCompatDelegate.getApplicationLocales()[0]?.language
?: Locale.getDefault().language
}
} else {
language
}

View file

@ -154,6 +154,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
}
// TODO language
preferenceCategory(R.string.pref_publishing) {
listPreference {
setTitle(R.string.pref_default_post_privacy)

View file

@ -59,6 +59,7 @@ data class AccountEntity(
var notificationLight: Boolean = true,
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
var defaultMediaSensitivity: Boolean = false,
var defaultPostLanguage: String = "",
var alwaysShowSensitiveMedia: Boolean = false,
var alwaysOpenSpoiler: Boolean = false,
var mediaPreviewEnabled: Boolean = true,

View file

@ -154,6 +154,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
it.displayName = account.name
it.profilePictureUrl = account.avatar
it.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC
it.defaultPostLanguage = account.source?.language ?: ""
it.defaultMediaSensitivity = account.source?.sensitive ?: false
it.emojis = account.emojis ?: emptyList()

View file

@ -31,7 +31,7 @@ import java.io.File;
*/
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class
}, version = 42)
}, version = 43)
public abstract class AppDatabase extends RoomDatabase {
public abstract AccountDao accountDao();
@ -610,4 +610,11 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_language` TEXT");
}
};
public static final Migration MIGRATION_42_43 = new Migration(42, 43) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultPostLanguage` TEXT NOT NULL DEFAULT ''");
}
};
}

View file

@ -66,7 +66,7 @@ class AppModule {
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38,
AppDatabase.MIGRATION_38_39, AppDatabase.MIGRATION_39_40, AppDatabase.MIGRATION_40_41,
AppDatabase.MIGRATION_41_42,
AppDatabase.MIGRATION_41_42, AppDatabase.MIGRATION_42_43,
)
.build()
}

View file

@ -52,7 +52,8 @@ data class AccountSource(
val privacy: Status.Visibility?,
val sensitive: Boolean?,
val note: String?,
val fields: List<StringField>?
val fields: List<StringField>?,
val language: String?,
)
data class Field(

View file

@ -0,0 +1,11 @@
package com.keylesspalace.tusky.util
import java.util.Locale
// When a language code has changed, `language` *explicitly* returns the obsolete version,
// but `toLanguageTag()` uses the current version
// https://developer.android.com/reference/java/util/Locale#getLanguage()
val Locale.modernLanguageCode: String
get() {
return this.toLanguageTag().split('-', limit = 2)[0]
}

View file

@ -458,6 +458,23 @@ class ComposeActivityTest {
assertEquals(language, activity.selectedLanguage)
}
@Test
fun modernLanguageCodeIsUsed() {
// https://github.com/tuskyapp/Tusky/issues/2903
// "ji" was deprecated in favor of "yi"
composeOptions = ComposeActivity.ComposeOptions(language = "ji")
setupActivity()
assertEquals("yi", activity.selectedLanguage)
}
@Test
fun unknownLanguageGivenInComposeOptionsIsRespected() {
val language = "zzz"
composeOptions = ComposeActivity.ComposeOptions(language = language)
setupActivity()
assertEquals(language, activity.selectedLanguage)
}
private fun clickUp() {
val menuItem = RoboMenuItem(android.R.id.home)
activity.onOptionsItemSelected(menuItem)