Skip to content

Commit 62be42c

Browse files
committed
Add note compose language control
1 parent f535dec commit 62be42c

4 files changed

Lines changed: 149 additions & 1 deletion

File tree

app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeScreen.kt

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.Repeat
4040
import androidx.compose.material.icons.outlined.AutoFixHigh
4141
import androidx.compose.material.icons.outlined.FormatQuote
4242
import androidx.compose.material.icons.outlined.Group
43+
import androidx.compose.material.icons.outlined.Language
4344
import androidx.compose.material.icons.outlined.Lock
4445
import androidx.compose.material3.Button
4546
import androidx.compose.material3.ButtonDefaults
@@ -79,6 +80,7 @@ import androidx.compose.ui.graphics.SolidColor
7980
import androidx.compose.ui.layout.ContentScale
8081
import androidx.compose.ui.layout.boundsInWindow
8182
import androidx.compose.ui.layout.onGloballyPositioned
83+
import androidx.compose.ui.platform.LocalConfiguration
8284
import androidx.compose.ui.platform.LocalDensity
8385
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
8486
import androidx.compose.ui.res.stringResource
@@ -101,6 +103,7 @@ import pub.hackers.android.ui.theme.AppShapes
101103
import pub.hackers.android.ui.theme.LocalAppColors
102104
import pub.hackers.android.ui.theme.LocalAppTypography
103105
import kotlin.math.roundToInt
106+
import java.util.Locale
104107

105108
private data class MentionPopupPositionInputs(
106109
val scrollY: Int,
@@ -145,6 +148,7 @@ fun ComposeScreen(
145148
val snackbarHostState = remember { SnackbarHostState() }
146149
var showVisibilityMenu by remember { mutableStateOf(false) }
147150
var showQuotePolicyMenu by remember { mutableStateOf(false) }
151+
var showLanguageMenu by remember { mutableStateOf(false) }
148152
val quotePolicyLocked = uiState.visibility != PostVisibility.PUBLIC &&
149153
uiState.visibility != PostVisibility.UNLISTED
150154
val effectiveQuotePolicy = if (quotePolicyLocked) QuotePolicy.SELF else uiState.quotePolicy
@@ -254,7 +258,10 @@ fun ComposeScreen(
254258
attachment.altText.isNotBlank() &&
255259
attachment.error == null
256260
}
257-
val postEnabled = uiState.content.isNotBlank() && !uiState.isPosting && mediaReady
261+
val postEnabled = uiState.content.isNotBlank() &&
262+
uiState.language.isNotBlank() &&
263+
!uiState.isPosting &&
264+
mediaReady
258265

259266
Scaffold(
260267
contentWindowInsets = WindowInsets(0),
@@ -572,6 +579,64 @@ fun ComposeScreen(
572579
horizontalArrangement = Arrangement.End,
573580
verticalAlignment = Alignment.CenterVertically,
574581
) {
582+
val languageOptions = remember(uiState.language, uiState.suggestedLanguages) {
583+
(listOf(uiState.language) + uiState.suggestedLanguages)
584+
.map { it.trim() }
585+
.filter { it.isNotEmpty() }
586+
.distinct()
587+
}
588+
TextButton(
589+
onClick = { showLanguageMenu = true },
590+
enabled = !uiState.isPosting && languageOptions.isNotEmpty(),
591+
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
592+
) {
593+
Icon(
594+
imageVector = Icons.Outlined.Language,
595+
contentDescription = stringResource(R.string.compose_language_label),
596+
tint = colors.textSecondary,
597+
)
598+
Spacer(modifier = Modifier.width(4.dp))
599+
Text(
600+
text = uiState.language.ifBlank {
601+
stringResource(R.string.compose_language_label)
602+
},
603+
color = colors.textSecondary,
604+
style = typography.labelMedium,
605+
maxLines = 1,
606+
)
607+
Icon(
608+
imageVector = Icons.Default.KeyboardArrowDown,
609+
contentDescription = null,
610+
tint = colors.textSecondary,
611+
modifier = Modifier.size(18.dp),
612+
)
613+
614+
DropdownMenu(
615+
expanded = showLanguageMenu,
616+
onDismissRequest = { showLanguageMenu = false },
617+
) {
618+
languageOptions.forEach { language ->
619+
DropdownMenuItem(
620+
text = { Text(languageOptionLabel(language)) },
621+
leadingIcon = {
622+
if (language == uiState.language) {
623+
Icon(
624+
imageVector = Icons.Default.Check,
625+
contentDescription = null,
626+
)
627+
}
628+
},
629+
onClick = {
630+
viewModel.updateLanguage(language)
631+
showLanguageMenu = false
632+
},
633+
)
634+
}
635+
}
636+
}
637+
638+
Spacer(modifier = Modifier.width(8.dp))
639+
575640
Button(
576641
onClick = { viewModel.post() },
577642
enabled = postEnabled,
@@ -609,6 +674,29 @@ fun ComposeScreen(
609674
)
610675
}
611676
}
677+
678+
}
679+
680+
@Composable
681+
private fun languageOptionLabel(language: String): String {
682+
val configuration = LocalConfiguration.current
683+
val uiLocale = remember(configuration) {
684+
if (configuration.locales.size() > 0) {
685+
configuration.locales[0]
686+
} else {
687+
Locale.getDefault()
688+
}
689+
}
690+
return remember(language, uiLocale) {
691+
val locale = Locale.forLanguageTag(language)
692+
val nativeName = locale.getDisplayLanguage(locale).ifBlank { language }
693+
val uiName = locale.getDisplayLanguage(uiLocale).ifBlank { language }
694+
if (nativeName.equals(uiName, ignoreCase = true)) {
695+
"$nativeName ($language)"
696+
} else {
697+
"$nativeName ($uiName, $language)"
698+
}
699+
}
612700
}
613701

614702
@Composable

app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeViewModel.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ data class ComposeUiState(
4848
val content: String = "",
4949
val cursorPosition: Int = 0,
5050
val language: String = java.util.Locale.getDefault().language,
51+
val isLanguageManuallySet: Boolean = false,
52+
val suggestedLanguages: List<String> = emptyList(),
5153
val visibility: PostVisibility = PostVisibility.PUBLIC,
5254
val quotePolicy: QuotePolicy = QuotePolicy.EVERYONE,
5355
val replyToId: String? = null,
@@ -95,6 +97,8 @@ class ComposeViewModel @Inject constructor(
9597
}
9698
}
9799

100+
loadSuggestedLanguages()
101+
98102
// Setup debounced mention search
99103
viewModelScope.launch {
100104
mentionQueryFlow
@@ -129,6 +133,16 @@ class ComposeViewModel @Inject constructor(
129133
}
130134
}
131135

136+
private fun loadSuggestedLanguages() {
137+
viewModelScope.launch {
138+
runCatching {
139+
repository.getSuggestedFilterLanguages().onSuccess { languages ->
140+
_uiState.update { it.copy(suggestedLanguages = languages) }
141+
}
142+
}
143+
}
144+
}
145+
132146
private fun detectMentionTrigger(content: String, cursorPosition: Int) {
133147
// Get text before cursor
134148
val textBeforeCursor = content.take(cursorPosition)
@@ -295,7 +309,17 @@ class ComposeViewModel @Inject constructor(
295309
detectLanguage(content)
296310
}
297311

312+
fun updateLanguage(language: String) {
313+
_uiState.update {
314+
it.copy(
315+
language = language.trim().replace('_', '-'),
316+
isLanguageManuallySet = true,
317+
)
318+
}
319+
}
320+
298321
private fun detectLanguage(text: String) {
322+
if (_uiState.value.isLanguageManuallySet) return
299323
if (text.isBlank() || text.length < 20) return
300324

301325
try {

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<!-- Compose -->
4343
<string name="compose">Compose</string>
4444
<string name="compose_hint">What\'s on your mind?</string>
45+
<string name="compose_language_label">Language</string>
4546
<string name="post">Post</string>
4647
<string name="cancel">Cancel</string>
4748
<string name="compose_edit">Edit</string>

app/src/test/java/pub/hackers/android/ui/screens/compose/ComposeViewModelTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,41 @@ class ComposeViewModelTest {
157157
}
158158
}
159159

160+
@Test
161+
fun `updateLanguage normalizes tag and post uses selected language`() = runTest {
162+
val createdPost = samplePost(id = "created")
163+
coEvery {
164+
repository.createNote(
165+
content = "bonjour",
166+
language = "fr-CA",
167+
visibility = PostVisibility.PUBLIC,
168+
quotePolicy = QuotePolicy.EVERYONE,
169+
replyTargetId = null,
170+
quotedPostId = null,
171+
)
172+
} returns Result.success(createdPost)
173+
174+
val vm = newViewModel()
175+
advanceUntilIdle()
176+
177+
vm.updateContent("bonjour")
178+
vm.updateLanguage(" fr_CA ")
179+
vm.post()
180+
advanceUntilIdle()
181+
182+
assertEquals("fr-CA", vm.uiState.value.language)
183+
coVerify {
184+
repository.createNote(
185+
content = "bonjour",
186+
language = "fr-CA",
187+
visibility = PostVisibility.PUBLIC,
188+
quotePolicy = QuotePolicy.EVERYONE,
189+
replyTargetId = null,
190+
quotedPostId = null,
191+
)
192+
}
193+
}
194+
160195
@Test
161196
fun `post clamps quote policy to self for followers-only notes`() = runTest {
162197
val createdPost = samplePost(id = "created")

0 commit comments

Comments
 (0)