Add language dropdown to compose view (#2651)

* Add UI for selecting post language

* Apply selected language when sending status

* Save/restore post language with drafts

* Fall back to english if the configured language isn't found in the locale list (no-NB)

* Remove comment about no_NB

* Move language dropdown to top of compose view

* Preserve language when redrafting

* Set default language to target post's language when replying

* Add Tusky license header to new source file

* Tweak language dropdown button width
This commit is contained in:
Levi Bard 2022-08-31 18:53:57 +02:00 committed by GitHub
commit 0041acf2d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1140 additions and 28 deletions

View file

@ -35,6 +35,7 @@ import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.PopupMenu
@ -65,6 +66,7 @@ import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.EmojiAdapter
import com.keylesspalace.tusky.adapter.LocaleAdapter
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
@ -244,6 +246,7 @@ class ComposeActivity :
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
}
setupLanguageSpinner(getInitialLanguage(composeOptions?.language))
setupComposeField(preferences, viewModel.startingText)
setupContentWarningField(composeOptions?.contentWarning)
setupPollView()
@ -476,6 +479,40 @@ class ComposeActivity :
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
}
private fun setupLanguageSpinner(initialLanguage: String?) {
val locales = Locale.getAvailableLocales()
.filter { it.country.isNullOrEmpty() && it.script.isNullOrEmpty() && it.variant.isNullOrEmpty() } // Only "base" languages, "en" but not "en_DK"
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
}
override fun onNothingSelected(parent: AdapterView<*>) {
parent.setSelection(locales.indexOfFirst { it.language == getInitialLanguage() })
}
}
binding.composePostLanguageButton.apply {
adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, locales)
setSelection(currentLocaleIndex)
}
}
private fun getInitialLanguage(language: String? = null): String {
return if (language.isNullOrEmpty()) {
// Setting the application ui preference sets the default locale
Locale.getDefault().language
} else {
language
}
}
private fun setupActionBar() {
setSupportActionBar(binding.toolbar)
supportActionBar?.run {
@ -793,6 +830,10 @@ class ComposeActivity :
return length
}
@VisibleForTesting
val selectedLanguage: String?
get() = viewModel.postLanguage
private fun updateVisibleCharactersLeft() {
val remainingLength = maximumTootCharacters - calculateTextLength()
binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
@ -1128,7 +1169,8 @@ class ComposeActivity :
var scheduledAt: String? = null,
var sensitive: Boolean? = null,
var poll: NewPoll? = null,
var modifiedInitialState: Boolean? = null
var modifiedInitialState: Boolean? = null,
var language: String? = null,
) : Parcelable
companion object {

View file

@ -68,6 +68,7 @@ class ComposeViewModel @Inject constructor(
private var replyingStatusAuthor: String? = null
private var replyingStatusContent: String? = null
internal var startingText: String? = null
internal var postLanguage: String? = null
private var draftId: Int = 0
private var scheduledTootId: String? = null
private var startingContentWarning: String = ""
@ -261,7 +262,8 @@ class ComposeViewModel @Inject constructor(
mediaDescriptions = mediaDescriptions,
poll = poll.value,
failedToSend = false,
scheduledAt = scheduledAt.value
scheduledAt = scheduledAt.value,
language = postLanguage,
)
}
@ -308,7 +310,8 @@ class ComposeViewModel @Inject constructor(
draftId = draftId,
idempotencyKey = randomAlphanumericString(16),
retries = 0,
mediaProcessed = mediaProcessed
mediaProcessed = mediaProcessed,
language = postLanguage,
)
serviceClient.sendToot(tootToSend)
@ -426,6 +429,7 @@ class ComposeViewModel @Inject constructor(
draftId = composeOptions?.draftId ?: 0
scheduledTootId = composeOptions?.scheduledTootId
startingText = composeOptions?.content
postLanguage = composeOptions?.language
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {

View file

@ -94,7 +94,8 @@ data class ConversationStatusEntity(
val expanded: Boolean,
val collapsed: Boolean,
val muted: Boolean,
val poll: Poll?
val poll: Poll?,
val language: String?,
) {
fun toViewData(): StatusViewData.Concrete {
@ -125,7 +126,8 @@ data class ConversationStatusEntity(
pinned = false,
muted = muted,
poll = poll,
card = null
card = null,
language = language,
),
isExpanded = expanded,
isShowingContent = showingHiddenContent,
@ -167,7 +169,8 @@ fun Status.toEntity() =
expanded = false,
collapsed = true,
muted = muted ?: false,
poll = poll
poll = poll,
language = language,
)
fun Conversation.toEntity(accountId: Long, order: Int) =

View file

@ -85,6 +85,7 @@ fun StatusViewData.Concrete.toConversationStatusEntity(
expanded = expanded,
collapsed = collapsed,
muted = muted,
poll = poll
poll = poll,
language = status.language,
)
}

View file

@ -61,7 +61,8 @@ class DraftHelper @Inject constructor(
mediaDescriptions: List<String?>,
poll: NewPoll?,
failedToSend: Boolean,
scheduledAt: String?
scheduledAt: String?,
language: String?,
) = withContext(Dispatchers.IO) {
val externalFilesDir = context.getExternalFilesDir("Tusky")
@ -118,7 +119,8 @@ class DraftHelper @Inject constructor(
attachments = attachments,
poll = poll,
failedToSend = failedToSend,
scheduledAt = scheduledAt
scheduledAt = scheduledAt,
language = language,
)
draftDao.insertOrReplace(draft)

View file

@ -107,7 +107,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
poll = draft.poll,
sensitive = draft.sensitive,
visibility = draft.visibility,
scheduledAt = draft.scheduledAt
scheduledAt = draft.scheduledAt,
language = draft.language,
)
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
@ -145,7 +146,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
poll = draft.poll,
sensitive = draft.sensitive,
visibility = draft.visibility,
scheduledAt = draft.scheduledAt
scheduledAt = draft.scheduledAt,
language = draft.language,
)
startActivity(ComposeActivity.startIntent(this, composeOptions))

View file

@ -365,6 +365,7 @@ public class NotificationHelper {
composeOptions.setReplyingStatusContent(citedText);
composeOptions.setMentionedUsernames(mentionedUsernames);
composeOptions.setModifiedInitialState(true);
composeOptions.setLanguage(actionableStatus.getLanguage());
Intent composeIntent = ComposeActivity.startIntent(
context,

View file

@ -216,7 +216,8 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
contentWarning = actionableStatus.spoilerText,
mentionedUsernames = mentionedUsernames,
replyingStatusAuthor = actionableStatus.account.localUsername,
replyingStatusContent = status.content.toString()
replyingStatusContent = status.content.toString(),
language = actionableStatus.language,
)
)
bottomSheetActivity?.startActivityWithSlideInAnimation(intent)
@ -461,7 +462,8 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
contentWarning = redraftStatus.spoilerText,
mediaAttachments = redraftStatus.attachments,
sensitive = redraftStatus.sensitive,
poll = redraftStatus.poll?.toNewPoll(status.createdAt)
poll = redraftStatus.poll?.toNewPoll(status.createdAt),
language = redraftStatus.language,
)
)
startActivity(intent)

View file

@ -99,7 +99,8 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
contentShowing = false,
pinned = false,
card = null,
repliesCount = 0
repliesCount = 0,
language = null,
)
}
@ -141,7 +142,8 @@ fun Status.toEntity(
contentCollapsed = contentCollapsed,
pinned = actionableStatus.pinned == true,
card = actionableStatus.card?.let(gson::toJson),
repliesCount = actionableStatus.repliesCount
repliesCount = actionableStatus.repliesCount,
language = actionableStatus.language,
)
}
@ -185,7 +187,8 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
muted = status.muted,
poll = poll,
card = card,
repliesCount = status.repliesCount
repliesCount = status.repliesCount,
language = status.language,
)
}
val status = if (reblog != null) {
@ -216,6 +219,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
poll = null,
card = null,
repliesCount = status.repliesCount,
language = status.language,
)
} else {
Status(
@ -245,6 +249,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
poll = poll,
card = card,
repliesCount = status.repliesCount,
language = status.language,
)
}
return StatusViewData.Concrete(