Update country picker for findBy and changeNumber.

This commit is contained in:
Michelle Tang
2025-02-07 14:35:45 -05:00
committed by GitHub
parent 20ab362f2c
commit 850c20bcd8
11 changed files with 115 additions and 330 deletions

View File

@@ -1,132 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.changenumber;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.ListFragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.navigation.fragment.NavHostFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.loaders.CountryListLoader;
import java.util.ArrayList;
import java.util.Map;
public final class ChangeNumberCountryPickerFragment extends ListFragment implements LoaderManager.LoaderCallbacks<ArrayList<Map<String, String>>> {
public static final String KEY_COUNTRY = "country";
public static final String KEY_COUNTRY_CODE = "country_code";
private EditText countryFilter;
private ChangeNumberViewModel model;
private String resultKey;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
return inflater.inflate(R.layout.fragment_registration_country_picker, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (getArguments() != null) {
ChangeNumberCountryPickerFragmentArgs arguments = ChangeNumberCountryPickerFragmentArgs.fromBundle(requireArguments());
resultKey = arguments.getResultKey();
}
if (resultKey == null) {
model = new ViewModelProvider(requireActivity()).get(ChangeNumberViewModel.class);
}
countryFilter = view.findViewById(R.id.country_search);
countryFilter.addTextChangedListener(new FilterWatcher());
LoaderManager.getInstance(this).initLoader(0, null, this).forceLoad();
}
@Override
public void onListItemClick(@NonNull ListView listView, @NonNull View view, int position, long id) {
Map<String, String> item = (Map<String, String>) getListAdapter().getItem(position);
int countryCode = Integer.parseInt(item.get("country_code").replace("+", ""));
String countryName = item.get("country_name");
if (resultKey == null) {
model.setNewCountry(countryCode, countryName);
} else {
Bundle result = new Bundle();
result.putString(KEY_COUNTRY, countryName);
result.putInt(KEY_COUNTRY_CODE, countryCode);
getParentFragmentManager().setFragmentResult(resultKey, result);
}
NavHostFragment.findNavController(this).navigateUp();
}
@Override
public @NonNull Loader<ArrayList<Map<String, String>>> onCreateLoader(int id, @Nullable Bundle args) {
return new CountryListLoader(getActivity());
}
@Override
public void onLoadFinished(@NonNull Loader<ArrayList<Map<String, String>>> loader,
@NonNull ArrayList<Map<String, String>> results)
{
((TextView) getListView().getEmptyView()).setText(
R.string.country_selection_fragment__no_matching_countries);
String[] from = { "country_name", "country_code" };
int[] to = { R.id.country_name, R.id.country_code };
setListAdapter(new SimpleAdapter(getActivity(), results, R.layout.country_list_item, from, to));
applyFilter(countryFilter.getText());
}
private void applyFilter(@NonNull CharSequence text) {
SimpleAdapter listAdapter = (SimpleAdapter) getListAdapter();
if (listAdapter != null) {
listAdapter.getFilter().filter(text);
}
}
@Override
public void onLoaderReset(@NonNull Loader<ArrayList<Map<String, String>>> loader) {
setListAdapter(null);
}
private class FilterWatcher implements TextWatcher {
@Override
public void afterTextChanged(Editable s) {
applyFilter(s);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
}
}

View File

@@ -14,12 +14,15 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentChangeNumberEnterPhoneNumberBinding import org.thoughtcrime.securesms.databinding.FragmentChangeNumberEnterPhoneNumberBinding
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.util.ChangeNumberInputController import org.thoughtcrime.securesms.registration.util.ChangeNumberInputController
import org.thoughtcrime.securesms.registrationv3.ui.countrycode.CountryCodeFragment
import org.thoughtcrime.securesms.util.Dialogs import org.thoughtcrime.securesms.util.Dialogs
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -65,15 +68,15 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
override fun onNumberInputDone(view: View) = Unit override fun onNumberInputDone(view: View) = Unit
override fun onPickCountry(view: View) { override fun onPickCountry(view: View) {
findNavController().safeNavigate(ChangeNumberEnterPhoneNumberFragmentDirections.actionEnterPhoneNumberChangeFragmentToCountryPickerFragment(OLD_NUMBER_COUNTRY_SELECT)) findNavController().safeNavigate(ChangeNumberEnterPhoneNumberFragmentDirections.actionEnterPhoneNumberChangeFragmentToCountryPickerFragment(OLD_NUMBER_COUNTRY_SELECT, viewModel.oldCountry))
} }
override fun setNationalNumber(number: String) { override fun setNationalNumber(number: String) {
viewModel.setOldNationalNumber(number) viewModel.setOldNationalNumber(number)
} }
override fun setCountry(countryCode: Int) { override fun setCountry(country: Country) {
viewModel.setOldCountry(countryCode) viewModel.setOldCountry(country)
} }
} }
) )
@@ -96,25 +99,27 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
} }
override fun onPickCountry(view: View) { override fun onPickCountry(view: View) {
findNavController().safeNavigate(ChangeNumberEnterPhoneNumberFragmentDirections.actionEnterPhoneNumberChangeFragmentToCountryPickerFragment(NEW_NUMBER_COUNTRY_SELECT)) findNavController().safeNavigate(ChangeNumberEnterPhoneNumberFragmentDirections.actionEnterPhoneNumberChangeFragmentToCountryPickerFragment(NEW_NUMBER_COUNTRY_SELECT, viewModel.newCountry))
} }
override fun setNationalNumber(number: String) { override fun setNationalNumber(number: String) {
viewModel.setNewNationalNumber(number) viewModel.setNewNationalNumber(number)
} }
override fun setCountry(countryCode: Int) { override fun setCountry(country: Country) {
viewModel.setNewCountry(countryCode) viewModel.setNewCountry(country)
} }
} }
) )
parentFragmentManager.setFragmentResultListener(OLD_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: Bundle -> parentFragmentManager.setFragmentResultListener(OLD_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: Bundle ->
viewModel.setOldCountry(bundle.getInt(ChangeNumberCountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(ChangeNumberCountryPickerFragment.KEY_COUNTRY)) val country = bundle.getParcelableCompat(CountryCodeFragment.RESULT_COUNTRY, Country::class.java)!!
viewModel.setOldCountry(country)
} }
parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: Bundle -> parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: Bundle ->
viewModel.setNewCountry(bundle.getInt(ChangeNumberCountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(ChangeNumberCountryPickerFragment.KEY_COUNTRY)) val country = bundle.getParcelableCompat(CountryCodeFragment.RESULT_COUNTRY, Country::class.java)!!
viewModel.setNewCountry(country)
} }
viewModel.liveOldNumberState.observe(viewLifecycleOwner, oldController::updateNumber) viewModel.liveOldNumberState.observe(viewLifecycleOwner, oldController::updateNumber)

View File

@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.settings.app.changenumber
import org.thoughtcrime.securesms.registration.data.network.Challenge import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.AuthCredentials
@@ -32,7 +33,9 @@ data class ChangeNumberState(
val captchaToken: String? = null, val captchaToken: String? = null,
val challengesRequested: List<Challenge> = emptyList(), val challengesRequested: List<Challenge> = emptyList(),
val challengesPresented: Set<Challenge> = emptySet(), val challengesPresented: Set<Challenge> = emptySet(),
val allowedToRequestCode: Boolean = false val allowedToRequestCode: Boolean = false,
val oldCountry: Country? = null,
val newCountry: Country? = null
) { ) {
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented } val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }
} }

View File

@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.registration.data.network.SessionMetadataResul
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.sms.SmsRetrieverReceiver import org.thoughtcrime.securesms.registration.sms.SmsRetrieverReceiver
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
@@ -100,15 +101,24 @@ class ChangeNumberViewModel : ViewModel() {
val svrTriesRemaining: Int val svrTriesRemaining: Int
get() = store.value.svrTriesRemaining get() = store.value.svrTriesRemaining
val oldCountry: Country?
get() = store.value.oldCountry
val newCountry: Country?
get() = store.value.newCountry
fun setOldNationalNumber(updatedNumber: String) { fun setOldNationalNumber(updatedNumber: String) {
store.update { store.update {
it.copy(oldPhoneNumber = oldNumberState.toBuilder().nationalNumber(updatedNumber).build()) it.copy(oldPhoneNumber = oldNumberState.toBuilder().nationalNumber(updatedNumber).build())
} }
} }
fun setOldCountry(countryCode: Int, country: String? = null) { fun setOldCountry(country: Country) {
store.update { store.update {
it.copy(oldPhoneNumber = oldNumberState.toBuilder().selectedCountryDisplayName(country).countryCode(countryCode).build()) it.copy(
oldPhoneNumber = oldNumberState.toBuilder().selectedCountryDisplayName(country.name).countryCode(country.countryCode).build(),
oldCountry = country
)
} }
} }
@@ -118,9 +128,12 @@ class ChangeNumberViewModel : ViewModel() {
} }
} }
fun setNewCountry(countryCode: Int, country: String? = null) { fun setNewCountry(country: Country) {
store.update { store.update {
it.copy(number = number.toBuilder().selectedCountryDisplayName(country).countryCode(countryCode).build()) it.copy(
number = number.toBuilder().selectedCountryDisplayName(country.name).countryCode(country.countryCode).build(),
newCountry = country
)
} }
} }

View File

@@ -13,7 +13,6 @@ import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -26,8 +25,6 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@@ -49,14 +46,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType 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.intl.Locale
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -83,7 +75,9 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.Use
import org.thoughtcrime.securesms.invites.InviteActions import org.thoughtcrime.securesms.invites.InviteActions
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberVisualTransformation import org.thoughtcrime.securesms.phonenumbers.PhoneNumberVisualTransformation
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.util.CountryPrefix import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeState
import org.thoughtcrime.securesms.registrationv3.ui.countrycode.Screen
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.viewModel import org.thoughtcrime.securesms.util.viewModel
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter import org.whispersystems.signalservice.api.util.PhoneNumberFormatter
@@ -160,7 +154,7 @@ class FindByActivity : PassphraseRequiredActivity() {
} }
} }
}, },
onSelectCountryPrefixClick = { onSelectCountryClick = {
navController.navigate("select-country-prefix") navController.navigate("select-country-prefix")
}, },
onQrCodeScanClicked = { onQrCodeScanClicked = {
@@ -171,24 +165,21 @@ class FindByActivity : PassphraseRequiredActivity() {
} }
composable("select-country-prefix") { composable("select-country-prefix") {
Scaffolds.Settings( Screen(
title = stringResource(id = R.string.FindByActivity__select_country_code), state = CountryCodeState(
onNavigationClick = { navController.popBackStack() }, query = state.query,
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24) filteredList = state.filteredCountries,
) { paddingValues -> countryList = state.supportedCountries
SelectCountryScreen( ),
paddingValues = paddingValues, title = stringResource(R.string.FindByActivity__select_country_code),
searchEntry = state.countryPrefixSearchEntry, onSearch = { search -> viewModel.filterCountries(search) },
onSearchEntryChanged = viewModel::onCountryPrefixSearchEntryChanged, onDismissed = { navController.popBackStack() },
supportedCountryPrefixes = state.supportedCountryPrefixes, onClick = { country ->
onCountryPrefixSelected = { viewModel.onCountrySelected(country)
navController.popBackStack() navController.popBackStack()
viewModel.onCountryPrefixSelected(it)
viewModel.onCountryPrefixSearchEntryChanged("")
} }
) )
} }
}
dialog("invalid-entry") { dialog("invalid-entry") {
val title = if (state.mode == FindByMode.USERNAME) { val title = if (state.mode == FindByMode.USERNAME) {
@@ -201,8 +192,8 @@ class FindByActivity : PassphraseRequiredActivity() {
stringResource(id = R.string.FindByActivity__s_is_not_a_valid_username, state.userEntry) stringResource(id = R.string.FindByActivity__s_is_not_a_valid_username, state.userEntry)
} else { } else {
val formattedNumber = remember(state.userEntry) { val formattedNumber = remember(state.userEntry) {
val cleansed = state.userEntry.removePrefix(state.selectedCountryPrefix.digits.toString()) val cleansed = state.userEntry.removePrefix(state.selectedCountry.countryCode.toString())
PhoneNumberFormatter.formatE164(state.selectedCountryPrefix.digits.toString(), cleansed) PhoneNumberFormatter.formatE164(state.selectedCountry.countryCode.toString(), cleansed)
} }
stringResource(id = R.string.FindByActivity__s_is_not_a_valid_phone_number, formattedNumber) stringResource(id = R.string.FindByActivity__s_is_not_a_valid_phone_number, formattedNumber)
} }
@@ -240,8 +231,8 @@ class FindByActivity : PassphraseRequiredActivity() {
stringResource(id = R.string.FindByActivity__s_is_not_a_signal_user, state.userEntry) stringResource(id = R.string.FindByActivity__s_is_not_a_signal_user, state.userEntry)
} else { } else {
val formattedNumber = remember(state.userEntry) { val formattedNumber = remember(state.userEntry) {
val cleansed = state.userEntry.removePrefix(state.selectedCountryPrefix.digits.toString()) val cleansed = state.userEntry.removePrefix(state.selectedCountry.countryCode.toString())
PhoneNumberFormatter.formatE164(state.selectedCountryPrefix.digits.toString(), cleansed) PhoneNumberFormatter.formatE164(state.selectedCountry.countryCode.toString(), cleansed)
} }
stringResource(id = R.string.FindByActivity__s_is_not_a_signal_user_would, formattedNumber) stringResource(id = R.string.FindByActivity__s_is_not_a_signal_user_would, formattedNumber)
} }
@@ -303,7 +294,7 @@ private fun Content(
state: FindByState, state: FindByState,
onUserEntryChanged: (String) -> Unit, onUserEntryChanged: (String) -> Unit,
onNextClick: () -> Unit, onNextClick: () -> Unit,
onSelectCountryPrefixClick: () -> Unit, onSelectCountryClick: () -> Unit,
onQrCodeScanClicked: () -> Unit onQrCodeScanClicked: () -> Unit
) { ) {
val placeholderLabel = remember(state.mode) { val placeholderLabel = remember(state.mode) {
@@ -338,8 +329,8 @@ private fun Content(
val visualTransformation = if (state.mode == FindByMode.USERNAME) { val visualTransformation = if (state.mode == FindByMode.USERNAME) {
VisualTransformation.None VisualTransformation.None
} else { } else {
remember(state.selectedCountryPrefix) { remember(state.selectedCountry.countryCode) {
PhoneNumberVisualTransformation(state.selectedCountryPrefix.regionCode) PhoneNumberVisualTransformation(state.selectedCountry.regionCode)
} }
} }
@@ -355,8 +346,8 @@ private fun Content(
{ {
PhoneNumberEntryPrefix( PhoneNumberEntryPrefix(
enabled = !state.isLookupInProgress, enabled = !state.isLookupInProgress,
selectedCountryPrefix = state.selectedCountryPrefix, selectedCountry = state.selectedCountry,
onSelectCountryPrefixClick = onSelectCountryPrefixClick onSelectCountryClick = onSelectCountryClick
) )
} }
}, },
@@ -448,8 +439,8 @@ private fun Content(
@Composable @Composable
private fun PhoneNumberEntryPrefix( private fun PhoneNumberEntryPrefix(
enabled: Boolean, enabled: Boolean,
selectedCountryPrefix: CountryPrefix, selectedCountry: Country,
onSelectCountryPrefixClick: () -> Unit onSelectCountryClick: () -> Unit
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -459,10 +450,10 @@ private fun PhoneNumberEntryPrefix(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(1000.dp)) .clip(RoundedCornerShape(1000.dp))
.clickable(onClick = onSelectCountryPrefixClick, enabled = enabled) .clickable(onClick = onSelectCountryClick, enabled = enabled)
) { ) {
Text( Text(
text = selectedCountryPrefix.toString(), text = "+${selectedCountry.countryCode}",
modifier = Modifier modifier = Modifier
.padding(start = 12.dp, top = 6.dp, bottom = 6.dp) .padding(start = 12.dp, top = 6.dp, bottom = 6.dp)
) )
@@ -486,113 +477,6 @@ private fun PhoneNumberEntryPrefix(
} }
} }
@Composable
private fun SelectCountryScreen(
paddingValues: PaddingValues,
searchEntry: String,
onSearchEntryChanged: (String) -> Unit,
onCountryPrefixSelected: (CountryPrefix) -> Unit,
supportedCountryPrefixes: List<CountryPrefix>
) {
val focusRequester = remember {
FocusRequester()
}
Column(
modifier = Modifier.padding(paddingValues)
) {
TextFields.TextField(
value = searchEntry,
onValueChange = onSearchEntryChanged,
placeholder = { Text(text = stringResource(id = R.string.FindByActivity__search)) },
shape = RoundedCornerShape(32.dp),
colors = TextFieldDefaults.colors(
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.onSurface
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp)
.focusRequester(focusRequester)
.heightIn(min = 44.dp),
contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(top = 10.dp, bottom = 10.dp)
)
LazyColumn {
items(
items = supportedCountryPrefixes
) {
CountryPrefixRowItem(
searchTerm = searchEntry,
countryPrefix = it,
onClick = { onCountryPrefixSelected(it) }
)
}
}
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
@Composable
private fun CountryPrefixRowItem(
searchTerm: String,
countryPrefix: CountryPrefix,
onClick: () -> Unit
) {
val regionDisplayName = remember(countryPrefix.regionCode, Locale.current) {
PhoneNumberFormatter.getRegionDisplayName(countryPrefix.regionCode).orElse(countryPrefix.regionCode)
}
if (searchTerm.isNotBlank() && !regionDisplayName.contains(searchTerm, ignoreCase = true)) {
return
}
val highlightedName: AnnotatedString = remember(regionDisplayName, searchTerm) {
if (searchTerm.isBlank()) {
AnnotatedString(regionDisplayName)
} else {
buildAnnotatedString {
append(regionDisplayName)
val startIndex = regionDisplayName.indexOf(searchTerm, ignoreCase = true)
addStyle(
style = SpanStyle(
fontWeight = FontWeight.Bold
),
start = startIndex,
end = startIndex + searchTerm.length
)
}
}
}
Column(
verticalArrangement = spacedBy((-2).dp),
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = dimensionResource(id = CoreUiR.dimen.gutter))
.padding(top = 16.dp, bottom = 14.dp)
) {
Text(
text = highlightedName
)
Text(
text = countryPrefix.toString(),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium
)
}
}
@Preview(name = "Light Theme", group = "content - phone", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Light Theme", group = "content - phone", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "content - phone", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "Dark Theme", group = "content - phone", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable @Composable
@@ -606,7 +490,7 @@ private fun ContentPreviewPhoneNumber() {
), ),
onUserEntryChanged = {}, onUserEntryChanged = {},
onNextClick = {}, onNextClick = {},
onSelectCountryPrefixClick = {}, onSelectCountryClick = {},
onQrCodeScanClicked = {} onQrCodeScanClicked = {}
) )
} }
@@ -625,23 +509,8 @@ private fun ContentPreviewUsername() {
), ),
onUserEntryChanged = {}, onUserEntryChanged = {},
onNextClick = {}, onNextClick = {},
onSelectCountryPrefixClick = {}, onSelectCountryClick = {},
onQrCodeScanClicked = {} onQrCodeScanClicked = {}
) )
} }
} }
@Preview(name = "Light Theme", group = "select country", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "select country", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SelectCountryScreenPreview() {
Previews.Preview {
SelectCountryScreen(
paddingValues = PaddingValues(0.dp),
searchEntry = "",
onSearchEntryChanged = {},
supportedCountryPrefixes = FindByState(mode = FindByMode.PHONE_NUMBER).supportedCountryPrefixes,
onCountryPrefixSelected = {}
)
}
}

View File

@@ -9,7 +9,8 @@ import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.signal.core.util.orNull import org.signal.core.util.orNull
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.util.CountryPrefix import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils
/** /**
* State for driving find by number/username screen. * State for driving find by number/username screen.
@@ -17,12 +18,11 @@ import org.thoughtcrime.securesms.registration.util.CountryPrefix
data class FindByState( data class FindByState(
val mode: FindByMode, val mode: FindByMode,
val userEntry: String = "", val userEntry: String = "",
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes val supportedCountries: List<Country> = CountryUtils.getCountries(),
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) } val filteredCountries: List<Country> = emptyList(),
.sortedBy { it.digits.toString() }, val selectedCountry: Country = supportedCountries.first(),
val selectedCountryPrefix: CountryPrefix = supportedCountryPrefixes.first(), val isLookupInProgress: Boolean = false,
val countryPrefixSearchEntry: String = "", val query: String = ""
val isLookupInProgress: Boolean = false
) { ) {
companion object { companion object {
fun startingState(self: Recipient, mode: FindByMode): FindByState { fun startingState(self: Recipient, mode: FindByMode): FindByState {
@@ -36,7 +36,7 @@ data class FindByState(
val state = FindByState(mode = mode) val state = FindByState(mode = mode)
return state.copy( return state.copy(
selectedCountryPrefix = state.supportedCountryPrefixes.firstOrNull { it.digits == countryCode } ?: state.supportedCountryPrefixes.first() selectedCountry = state.supportedCountries.firstOrNull { it.countryCode == countryCode } ?: state.supportedCountries.first()
) )
} }
} }

View File

@@ -17,7 +17,7 @@ import org.signal.core.util.concurrent.safeBlockingGet
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientRepository import org.thoughtcrime.securesms.recipients.RecipientRepository
import org.thoughtcrime.securesms.registration.util.CountryPrefix import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.util.UsernameUtil import org.thoughtcrime.securesms.util.UsernameUtil
class FindByViewModel( class FindByViewModel(
@@ -40,12 +40,8 @@ class FindByViewModel(
internalState.value = state.value.copy(userEntry = cleansed) internalState.value = state.value.copy(userEntry = cleansed)
} }
fun onCountryPrefixSearchEntryChanged(searchEntry: String) { fun onCountrySelected(country: Country) {
internalState.value = state.value.copy(countryPrefixSearchEntry = searchEntry) internalState.value = state.value.copy(selectedCountry = country)
}
fun onCountryPrefixSelected(countryPrefix: CountryPrefix) {
internalState.value = state.value.copy(selectedCountryPrefix = countryPrefix)
} }
suspend fun onNextClicked(context: Context): FindByResult { suspend fun onNextClicked(context: Context): FindByResult {
@@ -80,7 +76,7 @@ class FindByViewModel(
@WorkerThread @WorkerThread
private fun performPhoneLookup(context: Context): FindByResult { private fun performPhoneLookup(context: Context): FindByResult {
val stateSnapshot = state.value val stateSnapshot = state.value
val countryCode = stateSnapshot.selectedCountryPrefix.digits val countryCode = stateSnapshot.selectedCountry.countryCode
val nationalNumber = stateSnapshot.userEntry.removePrefix(countryCode.toString()) val nationalNumber = stateSnapshot.userEntry.removePrefix(countryCode.toString())
val e164 = "+$countryCode$nationalNumber" val e164 = "+$countryCode$nationalNumber"
@@ -92,4 +88,22 @@ class FindByViewModel(
is RecipientRepository.LookupResult.Success -> FindByResult.Success(result.recipientId) is RecipientRepository.LookupResult.Success -> FindByResult.Success(result.recipientId)
} }
} }
fun filterCountries(filterBy: String) {
if (filterBy.isEmpty()) {
internalState.value = state.value.copy(
query = filterBy,
filteredCountries = emptyList()
)
} else {
internalState.value = state.value.copy(
query = filterBy,
filteredCountries = state.value.supportedCountries.filter { country: Country ->
country.name.contains(filterBy, ignoreCase = true) ||
country.countryCode.toString().contains(filterBy) ||
(filterBy.equals("usa", ignoreCase = true) && country.name.equals("United States", ignoreCase = true))
}
)
}
}
} }

View File

@@ -47,6 +47,7 @@ object CountryUtils {
} }
} }
@JvmStatic
fun countryToEmoji(countryCode: String): String { fun countryToEmoji(countryCode: String): String {
return if (countryCode.isNotEmpty()) { return if (countryCode.isNotEmpty()) {
countryCode countryCode

View File

@@ -21,7 +21,10 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.LabeledEditText; import org.thoughtcrime.securesms.components.LabeledEditText;
import org.thoughtcrime.securesms.registration.ui.countrycode.Country;
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils;
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState; import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
/** /**
* Handle the logic and formatting of phone number input specifically for change number flows. * Handle the logic and formatting of phone number input specifically for change number flows.
@@ -150,7 +153,7 @@ public final class ChangeNumberInputController {
private void setCountryDisplay(@Nullable String regionDisplayName) { private void setCountryDisplay(@Nullable String regionDisplayName) {
countrySpinnerAdapter.clear(); countrySpinnerAdapter.clear();
if (regionDisplayName == null) { if (regionDisplayName == null || regionDisplayName.isEmpty()) {
countrySpinnerAdapter.add(context.getString(R.string.RegistrationActivity_select_your_country)); countrySpinnerAdapter.add(context.getString(R.string.RegistrationActivity_select_your_country));
} else { } else {
countrySpinnerAdapter.add(regionDisplayName); countrySpinnerAdapter.add(regionDisplayName);
@@ -245,7 +248,8 @@ public final class ChangeNumberInputController {
} }
if (!isUpdating) { if (!isUpdating) {
callbacks.setCountry(countryCode); Country country = new Country(CountryUtils.countryToEmoji(regionCode), PhoneNumberFormatter.getRegionDisplayName(regionCode).orElse(""), countryCode, regionCode);
callbacks.setCountry(country);
} }
} }
@@ -269,6 +273,6 @@ public final class ChangeNumberInputController {
void setNationalNumber(@NonNull String number); void setNationalNumber(@NonNull String number);
void setCountry(int countryCode); void setCountry(@NonNull Country country);
} }
} }

View File

@@ -75,6 +75,7 @@ class CountryCodeFragment : ComposeFragment() {
companion object { companion object {
private val TAG = Log.tag(CountryCodeFragment::class.java) private val TAG = Log.tag(CountryCodeFragment::class.java)
const val RESULT_KEY = "result_key"
const val REQUEST_KEY_COUNTRY = "request_key_country" const val REQUEST_KEY_COUNTRY = "request_key_country"
const val REQUEST_COUNTRY = "country" const val REQUEST_COUNTRY = "country"
const val RESULT_COUNTRY = "country" const val RESULT_COUNTRY = "country"
@@ -86,6 +87,8 @@ class CountryCodeFragment : ComposeFragment() {
override fun FragmentContent() { override fun FragmentContent() {
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
val resultKey = arguments?.getString(RESULT_KEY) ?: REQUEST_KEY_COUNTRY
Screen( Screen(
state = state, state = state,
title = stringResource(R.string.CountryCodeFragment__your_country), title = stringResource(R.string.CountryCodeFragment__your_country),
@@ -93,7 +96,7 @@ class CountryCodeFragment : ComposeFragment() {
onDismissed = { findNavController().popBackStack() }, onDismissed = { findNavController().popBackStack() },
onClick = { country -> onClick = { country ->
setFragmentResult( setFragmentResult(
REQUEST_KEY_COUNTRY, resultKey,
bundleOf( bundleOf(
RESULT_COUNTRY to country RESULT_COUNTRY to country
) )

View File

@@ -58,13 +58,18 @@
<fragment <fragment
android:id="@+id/countryPickerFragment" android:id="@+id/countryPickerFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberCountryPickerFragment" android:name="org.thoughtcrime.securesms.registrationv3.ui.countrycode.CountryCodeFragment"
tools:layout="@layout/fragment_registration_country_picker"> tools:layout="@layout/fragment_registration_country_picker">
<argument <argument
android:name="result_key" android:name="result_key"
app:argType="string" app:argType="string"
app:nullable="true" /> app:nullable="true" />
<argument
android:name="country"
app:argType="org.thoughtcrime.securesms.registration.ui.countrycode.Country"
app:nullable="true" />
</fragment> </fragment>
<fragment <fragment