Add fixes to country picker.

This commit is contained in:
Michelle Tang
2025-02-05 14:19:59 -05:00
committed by Greyson Parrelli
parent e840efcecc
commit 02e7c035aa
8 changed files with 121 additions and 34 deletions

View File

@@ -113,6 +113,9 @@ class RegistrationViewModel : ViewModel() {
val phoneNumber: Phonenumber.PhoneNumber? val phoneNumber: Phonenumber.PhoneNumber?
get() = store.value.phoneNumber get() = store.value.phoneNumber
val country: Country?
get() = store.value.country
fun maybePrefillE164(context: Context) { fun maybePrefillE164(context: Context) {
Log.v(TAG, "maybePrefillE164()") Log.v(TAG, "maybePrefillE164()")
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) { if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {

View File

@@ -15,8 +15,10 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -24,8 +26,13 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
@@ -34,6 +41,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -42,6 +50,7 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.signal.core.ui.Dividers import org.signal.core.ui.Dividers
import org.signal.core.ui.IconButtons.IconButton
import org.signal.core.ui.Previews import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview import org.signal.core.ui.SignalPreview
@@ -236,21 +245,43 @@ fun SearchBar(
hint: String = stringResource(R.string.CountryCodeFragment__search_by), hint: String = stringResource(R.string.CountryCodeFragment__search_by),
onSearch: (String) -> Unit = {} onSearch: (String) -> Unit = {}
) { ) {
val focusRequester = remember { FocusRequester() }
var showKeyboard by remember { mutableStateOf(false) }
TextField( TextField(
value = text, value = text,
onValueChange = { onSearch(it) }, onValueChange = { onSearch(it) },
placeholder = { Text(hint) }, placeholder = { Text(hint) },
// TODO(michelle): Add keyboard switch to dialpad trailingIcon = {
// trailingIcon = { IconButton(onClick = {
// Icon( showKeyboard = !showKeyboard
// imageVector = ImageVector.vectorResource(R.drawable.symbol_number_pad_24), focusRequester.requestFocus()
// contentDescription = "Search icon" }) {
// ) if (showKeyboard) {
// }, Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_keyboard_24),
contentDescription = null
)
} else {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_number_pad_24),
contentDescription = null
)
}
}
},
keyboardOptions = KeyboardOptions(
keyboardType = if (showKeyboard) {
KeyboardType.Number
} else {
KeyboardType.Text
}
),
shape = RoundedCornerShape(32.dp), shape = RoundedCornerShape(32.dp),
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.height(54.dp) .height(54.dp)
.focusRequester(focusRequester)
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
visualTransformation = VisualTransformation.None, visualTransformation = VisualTransformation.None,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(

View File

@@ -125,7 +125,6 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState -> sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
presentRegisterButton(sharedState) presentRegisterButton(sharedState)
updateEnabledControls(sharedState.inProgress, sharedState.isReRegister) updateEnabledControls(sharedState.inProgress, sharedState.isReRegister)
updateCountrySelection(sharedState.country)
sharedState.networkError?.let { sharedState.networkError?.let {
presentNetworkError(it) presentNetworkError(it)
@@ -185,7 +184,13 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
initializeInputFields() initializeInputFields()
val existingPhoneNumber = sharedViewModel.phoneNumber val existingPhoneNumber = sharedViewModel.phoneNumber
if (existingPhoneNumber != null) { val country = sharedViewModel.country
if (country != null) {
binding.countryEmoji.text = country.emoji
binding.country.text = country.name
spinnerView.setText(country.countryCode)
} else if (existingPhoneNumber != null) {
fragmentViewModel.restoreState(existingPhoneNumber) fragmentViewModel.restoreState(existingPhoneNumber)
spinnerView.setText(existingPhoneNumber.countryCode.toString()) spinnerView.setText(existingPhoneNumber.countryCode.toString())
phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString()) phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString())
@@ -200,9 +205,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
if (country != null) { if (country != null) {
binding.countryEmoji.text = country.emoji binding.countryEmoji.text = country.emoji
binding.country.text = country.name binding.country.text = country.name
binding.countryCode.editText?.setText(country.countryCode)
} }
sharedViewModel.clearCountry()
} }
private fun reformatText(text: Editable?) { private fun reformatText(text: Editable?) {
@@ -248,7 +251,12 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
val sanitized = s.toString().filter { c -> c.isDigit() } val sanitized = s.toString().filter { c -> c.isDigit() }
if (sanitized.isNotNullOrBlank()) { if (sanitized.isNotNullOrBlank()) {
val countryCode: Int = sanitized.toInt() val countryCode: Int = sanitized.toInt()
fragmentViewModel.setCountry(countryCode) if (sharedViewModel.country != null) {
fragmentViewModel.setCountry(countryCode, sharedViewModel.country)
sharedViewModel.clearCountry()
} else {
fragmentViewModel.setCountry(countryCode)
}
} }
} }

View File

@@ -62,22 +62,24 @@ class EnterPhoneNumberViewModel : ViewModel() {
store.update { it.copy(phoneNumber = phoneNumber ?: "") } store.update { it.copy(phoneNumber = phoneNumber ?: "") }
} }
fun setCountry(digits: Int) { fun setCountry(digits: Int, country: Country? = null) {
val matchingIndex = countryCodeToAdapterIndex(digits) val matchingIndex = countryCodeToAdapterIndex(digits)
if (matchingIndex == -1) { if (matchingIndex == -1) {
Log.d(TAG, "Invalid country code specified $digits") Log.d(TAG, "Invalid country code specified $digits")
return return
} }
val matchedCountry = Country(
name = PhoneNumberFormatter.getRegionDisplayName(supportedCountryPrefixes[matchingIndex].regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(supportedCountryPrefixes[matchingIndex].regionCode),
countryCode = digits.toString()
)
store.update { store.update {
it.copy( it.copy(
countryPrefixIndex = matchingIndex, countryPrefixIndex = matchingIndex,
phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode, phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode,
country = Country( country = country ?: matchedCountry
name = PhoneNumberFormatter.getRegionDisplayName(supportedCountryPrefixes[matchingIndex].regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(supportedCountryPrefixes[matchingIndex].regionCode),
countryCode = digits.toString()
)
) )
} }
} }

View File

@@ -129,6 +129,9 @@ class RegistrationViewModel : ViewModel() {
val phoneNumber: Phonenumber.PhoneNumber? val phoneNumber: Phonenumber.PhoneNumber?
get() = store.value.phoneNumber get() = store.value.phoneNumber
val country: Country?
get() = store.value.country
fun maybePrefillE164(context: Context) { fun maybePrefillE164(context: Context) {
Log.v(TAG, "maybePrefillE164()") Log.v(TAG, "maybePrefillE164()")
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) { if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -25,8 +26,13 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
@@ -35,6 +41,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -43,6 +50,7 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.signal.core.ui.Dividers import org.signal.core.ui.Dividers
import org.signal.core.ui.IconButtons.IconButton
import org.signal.core.ui.Previews import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview import org.signal.core.ui.SignalPreview
@@ -237,21 +245,43 @@ fun SearchBar(
hint: String = stringResource(R.string.CountryCodeFragment__search_by), hint: String = stringResource(R.string.CountryCodeFragment__search_by),
onSearch: (String) -> Unit = {} onSearch: (String) -> Unit = {}
) { ) {
val focusRequester = remember { FocusRequester() }
var showKeyboard by remember { mutableStateOf(false) }
TextField( TextField(
value = text, value = text,
onValueChange = { onSearch(it) }, onValueChange = { onSearch(it) },
placeholder = { Text(hint) }, placeholder = { Text(hint) },
trailingIcon = { trailingIcon = {
// TODO(michelle): Add keyboard switch to dialpad IconButton(onClick = {
Icon( showKeyboard = !showKeyboard
imageVector = ImageVector.vectorResource(R.drawable.symbol_number_pad_24), focusRequester.requestFocus()
contentDescription = "Search icon" }) {
) if (showKeyboard) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_keyboard_24),
contentDescription = null
)
} else {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_number_pad_24),
contentDescription = null
)
}
}
}, },
keyboardOptions = KeyboardOptions(
keyboardType = if (showKeyboard) {
KeyboardType.Number
} else {
KeyboardType.Text
}
),
shape = RoundedCornerShape(32.dp), shape = RoundedCornerShape(32.dp),
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.height(54.dp) .height(54.dp)
.focusRequester(focusRequester)
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
visualTransformation = VisualTransformation.None, visualTransformation = VisualTransformation.None,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(

View File

@@ -130,7 +130,6 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState -> sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
presentRegisterButton(sharedState) presentRegisterButton(sharedState)
updateEnabledControls(sharedState.inProgress, sharedState.isReRegister) updateEnabledControls(sharedState.inProgress, sharedState.isReRegister)
updateCountrySelection(sharedState.country)
sharedState.networkError?.let { sharedState.networkError?.let {
presentNetworkError(it) presentNetworkError(it)
@@ -190,7 +189,13 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
initializeInputFields() initializeInputFields()
val existingPhoneNumber = sharedViewModel.phoneNumber val existingPhoneNumber = sharedViewModel.phoneNumber
if (existingPhoneNumber != null) {
val country = sharedViewModel.country
if (country != null) {
binding.countryEmoji.text = country.emoji
binding.country.text = country.name
spinnerView.setText(country.countryCode)
} else if (existingPhoneNumber != null) {
fragmentViewModel.restoreState(existingPhoneNumber) fragmentViewModel.restoreState(existingPhoneNumber)
spinnerView.setText(existingPhoneNumber.countryCode.toString()) spinnerView.setText(existingPhoneNumber.countryCode.toString())
phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString()) phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString())
@@ -210,9 +215,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
if (country != null) { if (country != null) {
binding.countryEmoji.text = country.emoji binding.countryEmoji.text = country.emoji
binding.country.text = country.name binding.country.text = country.name
binding.countryCode.editText?.setText(country.countryCode)
} }
sharedViewModel.clearCountry()
} }
private fun reformatText(text: Editable?) { private fun reformatText(text: Editable?) {
@@ -258,7 +261,12 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
val sanitized = s.toString().filter { c -> c.isDigit() } val sanitized = s.toString().filter { c -> c.isDigit() }
if (sanitized.isNotNullOrBlank()) { if (sanitized.isNotNullOrBlank()) {
val countryCode: Int = sanitized.toInt() val countryCode: Int = sanitized.toInt()
fragmentViewModel.setCountry(countryCode) if (sharedViewModel.country != null) {
fragmentViewModel.setCountry(countryCode, sharedViewModel.country)
sharedViewModel.clearCountry()
} else {
fragmentViewModel.setCountry(countryCode)
}
} }
} }

View File

@@ -60,22 +60,24 @@ class EnterPhoneNumberViewModel : ViewModel() {
store.update { it.copy(phoneNumber = phoneNumber ?: "") } store.update { it.copy(phoneNumber = phoneNumber ?: "") }
} }
fun setCountry(digits: Int) { fun setCountry(digits: Int, country: Country? = null) {
val matchingIndex = countryCodeToAdapterIndex(digits) val matchingIndex = countryCodeToAdapterIndex(digits)
if (matchingIndex == -1) { if (matchingIndex == -1) {
Log.d(TAG, "Invalid country code specified $digits") Log.d(TAG, "Invalid country code specified $digits")
return return
} }
val matchedCountry = Country(
name = PhoneNumberFormatter.getRegionDisplayName(supportedCountryPrefixes[matchingIndex].regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(supportedCountryPrefixes[matchingIndex].regionCode),
countryCode = digits.toString()
)
store.update { store.update {
it.copy( it.copy(
countryPrefixIndex = matchingIndex, countryPrefixIndex = matchingIndex,
phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode, phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode,
country = Country( country = country ?: matchedCountry
name = PhoneNumberFormatter.getRegionDisplayName(supportedCountryPrefixes[matchingIndex].regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(supportedCountryPrefixes[matchingIndex].regionCode),
countryCode = digits.toString()
)
) )
} }
} }