Update username recovery flow.

This commit is contained in:
Greyson Parrelli
2024-01-19 16:59:06 -05:00
parent 5e97a6b192
commit 16b78f0843
11 changed files with 119 additions and 25 deletions

View File

@@ -59,7 +59,7 @@ class UsernameEditFragmentTest {
@Test @Test
fun testUsernameCreationInRegistration() { fun testUsernameCreationInRegistration() {
val scenario = createScenario(true) val scenario = createScenario(UsernameEditMode.REGISTRATION)
scenario.moveToState(Lifecycle.State.RESUMED) scenario.moveToState(Lifecycle.State.RESUMED)
@@ -77,7 +77,7 @@ class UsernameEditFragmentTest {
@Ignore("Flakey espresso test.") @Ignore("Flakey espresso test.")
@Test @Test
fun testUsernameCreationOutsideOfRegistration() { fun testUsernameCreationOutsideOfRegistration() {
val scenario = createScenario() val scenario = createScenario(UsernameEditMode.NORMAL)
scenario.moveToState(Lifecycle.State.RESUMED) scenario.moveToState(Lifecycle.State.RESUMED)
@@ -108,7 +108,7 @@ class UsernameEditFragmentTest {
} }
) )
val scenario = createScenario(isInRegistration = true) val scenario = createScenario(UsernameEditMode.REGISTRATION)
scenario.moveToState(Lifecycle.State.RESUMED) scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.username_text)).perform(typeText(nickname)) onView(withId(R.id.username_text)).perform(typeText(nickname))
@@ -132,8 +132,8 @@ class UsernameEditFragmentTest {
onView(withId(R.id.username_done_button)).check(matches(isNotEnabled())) onView(withId(R.id.username_done_button)).check(matches(isNotEnabled()))
} }
private fun createScenario(isInRegistration: Boolean = false): FragmentScenario<UsernameEditFragment> { private fun createScenario(mode: UsernameEditMode = UsernameEditMode.NORMAL): FragmentScenario<UsernameEditFragment> {
val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle() val fragmentArgs = UsernameEditFragmentArgs.Builder().setMode(mode).build().toBundle()
return launchFragmentInContainer( return launchFragmentInContainer(
fragmentArgs = fragmentArgs, fragmentArgs = fragmentArgs,
themeResId = R.style.Signal_DayNight_NoActionBar themeResId = R.style.Signal_DayNight_NoActionBar

View File

@@ -66,6 +66,7 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
StartLocation.PRIVACY -> AppSettingsFragmentDirections.actionDirectToPrivacy() StartLocation.PRIVACY -> AppSettingsFragmentDirections.actionDirectToPrivacy()
StartLocation.LINKED_DEVICES -> AppSettingsFragmentDirections.actionDirectToDevices() StartLocation.LINKED_DEVICES -> AppSettingsFragmentDirections.actionDirectToDevices()
StartLocation.USERNAME_LINK -> AppSettingsFragmentDirections.actionDirectToUsernameLinkSettings() StartLocation.USERNAME_LINK -> AppSettingsFragmentDirections.actionDirectToUsernameLinkSettings()
StartLocation.RECOVER_USERNAME -> AppSettingsFragmentDirections.actionDirectToUsernameRecovery()
} }
} }
@@ -192,6 +193,9 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
@JvmStatic @JvmStatic
fun usernameLinkSettings(context: Context): Intent = getIntentForStartLocation(context, StartLocation.USERNAME_LINK) fun usernameLinkSettings(context: Context): Intent = getIntentForStartLocation(context, StartLocation.USERNAME_LINK)
@JvmStatic
fun usernameRecovery(context: Context): Intent = getIntentForStartLocation(context, StartLocation.RECOVER_USERNAME)
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent { private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
return Intent(context, AppSettingsActivity::class.java) return Intent(context, AppSettingsActivity::class.java)
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings) .putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
@@ -214,7 +218,8 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
NOTIFICATION_PROFILE_DETAILS(11), NOTIFICATION_PROFILE_DETAILS(11),
PRIVACY(12), PRIVACY(12),
LINKED_DEVICES(13), LINKED_DEVICES(13),
USERNAME_LINK(14); USERNAME_LINK(14),
RECOVER_USERNAME(15);
companion object { companion object {
fun fromCode(code: Int?): StartLocation { fun fromCode(code: Int?): StartLocation {

View File

@@ -731,7 +731,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
.setTitle("Corrupt your username?") .setTitle("Corrupt your username?")
.setMessage("Are you sure? You might not be able to get your original username back.") .setMessage("Are you sure? You might not be able to get your original username back.")
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
val random = "${(1..5).map { ('a'..'z').random() }.joinToString(separator = "") }.${Random.nextInt(1, 100)}" val random = "${(1..5).map { ('a'..'z').random() }.joinToString(separator = "") }.${Random.nextInt(10, 100)}"
SignalStore.account().username = random SignalStore.account().username = random
SignalDatabase.recipients.setUsername(Recipient.self().id, random) SignalDatabase.recipients.setUsername(Recipient.self().id, random)

View File

@@ -44,6 +44,9 @@ import android.widget.FrameLayout;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.OnBackPressedCallback; import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.IdRes; import androidx.annotation.IdRes;
@@ -155,6 +158,7 @@ import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile; import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity; import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity;
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment;
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment; import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -700,6 +704,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
Snackbar.make(fab, R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show(); Snackbar.make(fab, R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show();
viewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL); viewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL);
} }
if (resultCode == RESULT_OK && requestCode == UsernameEditFragment.REQUEST_CODE) {
String snackbarString = getString(R.string.ConversationListFragment_username_recovered_toast, SignalStore.account().getUsername());
Snackbar.make(fab, snackbarString, Snackbar.LENGTH_LONG).show();
}
} }
private void onConversationClicked(@NonNull ThreadRecord threadRecord) { private void onConversationClicked(@NonNull ThreadRecord threadRecord) {
@@ -791,7 +800,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
} else if (reminderActionId == R.id.reminder_action_cds_permanent_error_learn_more) { } else if (reminderActionId == R.id.reminder_action_cds_permanent_error_learn_more) {
CdsPermanentErrorBottomSheet.show(getChildFragmentManager()); CdsPermanentErrorBottomSheet.show(getChildFragmentManager());
} else if (reminderActionId == R.id.reminder_action_fix_username_and_link) { } else if (reminderActionId == R.id.reminder_action_fix_username_and_link) {
startActivity(EditProfileActivity.getIntent(requireContext())); startActivityForResult(AppSettingsActivity.usernameRecovery(requireContext()), UsernameEditFragment.REQUEST_CODE);
} else if (reminderActionId == R.id.reminder_action_fix_username_link) { } else if (reminderActionId == R.id.reminder_action_fix_username_link) {
startActivity(AppSettingsActivity.usernameLinkSettings(requireContext())); startActivity(AppSettingsActivity.usernameLinkSettings(requireContext()));
} else if (reminderActionId == R.id.reminder_action_re_register) { } else if (reminderActionId == R.id.reminder_action_re_register) {

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.profiles.manage; package org.thoughtcrime.securesms.profiles.manage;
import android.animation.LayoutTransition; import android.animation.LayoutTransition;
import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.os.Bundle; import android.os.Bundle;
@@ -30,6 +31,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.databinding.UsernameEditFragmentBinding; import org.thoughtcrime.securesms.databinding.UsernameEditFragmentBinding;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.FragmentResultContract; import org.thoughtcrime.securesms.util.FragmentResultContract;
import org.thoughtcrime.securesms.util.UsernameUtil; import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
@@ -40,6 +42,9 @@ public class UsernameEditFragment extends LoggingFragment {
private static final float DISABLED_ALPHA = 0.5f; private static final float DISABLED_ALPHA = 0.5f;
public static final String IGNORE_TEXT_CHANGE_EVENT = "ignore.text.change.event"; public static final String IGNORE_TEXT_CHANGE_EVENT = "ignore.text.change.event";
public static final int REQUEST_CODE = 4242;
public static final String EXTRA_USERNAME = "username";
private UsernameEditViewModel viewModel; private UsernameEditViewModel viewModel;
private UsernameEditFragmentBinding binding; private UsernameEditFragmentBinding binding;
private LifecycleDisposable lifecycleDisposable; private LifecycleDisposable lifecycleDisposable;
@@ -75,13 +80,19 @@ public class UsernameEditFragment extends LoggingFragment {
args = new UsernameEditFragmentArgs.Builder().build(); args = new UsernameEditFragmentArgs.Builder().build();
} }
if (args.getIsInRegistration()) { if (args.getMode() == UsernameEditMode.REGISTRATION) {
binding.toolbar.setNavigationIcon(null); binding.toolbar.setNavigationIcon(null);
binding.toolbar.setTitle(R.string.UsernameEditFragment__add_a_username); binding.toolbar.setTitle(R.string.UsernameEditFragment__add_a_username);
binding.usernameSkipButton.setVisibility(View.VISIBLE); binding.usernameSkipButton.setVisibility(View.VISIBLE);
binding.usernameDoneButton.setVisibility(View.VISIBLE); binding.usernameDoneButton.setVisibility(View.VISIBLE);
} else { } else {
binding.toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(view).popBackStack()); binding.toolbar.setNavigationOnClickListener(v -> {
if (args.getMode() == UsernameEditMode.RECOVERY) {
getActivity().finish();
} else {
Navigation.findNavController(view).popBackStack();
}
});
binding.usernameSubmitButton.setVisibility(View.VISIBLE); binding.usernameSubmitButton.setVisibility(View.VISIBLE);
} }
@@ -90,13 +101,13 @@ public class UsernameEditFragment extends LoggingFragment {
lifecycleDisposable = new LifecycleDisposable(); lifecycleDisposable = new LifecycleDisposable();
lifecycleDisposable.bindTo(getViewLifecycleOwner()); lifecycleDisposable.bindTo(getViewLifecycleOwner());
viewModel = new ViewModelProvider(this, new UsernameEditViewModel.Factory(args.getIsInRegistration())).get(UsernameEditViewModel.class); viewModel = new ViewModelProvider(this, new UsernameEditViewModel.Factory(args.getMode())).get(UsernameEditViewModel.class);
lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged)); lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged));
lifecycleDisposable.add(viewModel.getEvents().subscribe(this::onEvent)); lifecycleDisposable.add(viewModel.getEvents().subscribe(this::onEvent));
lifecycleDisposable.add(viewModel.getUsernameInputState().subscribe(this::presentUsernameInputState)); lifecycleDisposable.add(viewModel.getUsernameInputState().subscribe(this::presentUsernameInputState));
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted()); binding.usernameSubmitButton.setOnClickListener(v -> promptOrSubmitUsername());
binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted()); binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
binding.usernameDoneButton.setOnClickListener(v -> viewModel.onUsernameSubmitted()); binding.usernameDoneButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
binding.usernameSkipButton.setOnClickListener(v -> viewModel.onUsernameSkipped()); binding.usernameSkipButton.setOnClickListener(v -> viewModel.onUsernameSkipped());
@@ -121,7 +132,7 @@ public class UsernameEditFragment extends LoggingFragment {
binding.discriminatorText.setOnEditorActionListener((v, actionId, event) -> { binding.discriminatorText.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) { if (actionId == EditorInfo.IME_ACTION_DONE) {
viewModel.onUsernameSubmitted(); promptOrSubmitUsername();
return true; return true;
} }
return false; return false;
@@ -140,6 +151,22 @@ public class UsernameEditFragment extends LoggingFragment {
binding = null; binding = null;
} }
private void promptOrSubmitUsername() {
if (args.getMode() == UsernameEditMode.RECOVERY) {
new MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.UsernameEditFragment_recovery_dialog_confirmation)
.setPositiveButton(android.R.string.ok, ((dialog, which) -> {
viewModel.onUsernameSubmitted();
dialog.dismiss();
}))
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
.show();
} else {
viewModel.onUsernameSubmitted();
}
}
private void onLearnMore(@Nullable View unused) { private void onLearnMore(@Nullable View unused) {
new MaterialAlertDialogBuilder(requireContext()) new MaterialAlertDialogBuilder(requireContext())
.setTitle(new StringBuilder("#\n").append(getString(R.string.UsernameEditFragment__what_is_this_number))) .setTitle(new StringBuilder("#\n").append(getString(R.string.UsernameEditFragment__what_is_this_number)))
@@ -182,7 +209,7 @@ public class UsernameEditFragment extends LoggingFragment {
} }
private void presentButtonState(@NonNull UsernameEditViewModel.ButtonState buttonState) { private void presentButtonState(@NonNull UsernameEditViewModel.ButtonState buttonState) {
if (args.getIsInRegistration()) { if (args.getMode() == UsernameEditMode.REGISTRATION) {
presentRegistrationButtonState(buttonState); presentRegistrationButtonState(buttonState);
} else { } else {
presentProfileUpdateButtonState(buttonState); presentProfileUpdateButtonState(buttonState);
@@ -306,6 +333,9 @@ public class UsernameEditFragment extends LoggingFragment {
switch (event) { switch (event) {
case SUBMIT_SUCCESS: case SUBMIT_SUCCESS:
ResultContract.setUsernameCreated(getParentFragmentManager()); ResultContract.setUsernameCreated(getParentFragmentManager());
if (getActivity() != null) {
getActivity().setResult(Activity.RESULT_OK);
}
closeScreen(); closeScreen();
break; break;
case SUBMIT_FAIL_TAKEN: case SUBMIT_FAIL_TAKEN:
@@ -328,8 +358,10 @@ public class UsernameEditFragment extends LoggingFragment {
} }
private void closeScreen() { private void closeScreen() {
if (args.getIsInRegistration()) { if (args.getMode() == UsernameEditMode.REGISTRATION) {
finishAndStartNextIntent(); finishAndStartNextIntent();
} else if (args.getMode() == UsernameEditMode.RECOVERY) {
getActivity().finish();
} else { } else {
NavHostFragment.findNavController(this).popBackStack(); NavHostFragment.findNavController(this).popBackStack();
} }

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.profiles.manage
enum class UsernameEditMode {
/** A typical launch, no special conditions. */
NORMAL,
/** Screen is launched during registration, includes special first-time flows. */
REGISTRATION,
/** Screen was launched because the username was in a bad state and needs to be recovered. Shows a special dialog. */
RECOVERY;
val allowsDelete get() = this == NORMAL || this == RECOVERY
}

View File

@@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit
* *
* The nickname is user-controlled, whereas the discriminator is controlled by the server. * The nickname is user-controlled, whereas the discriminator is controlled by the server.
*/ */
internal class UsernameEditViewModel private constructor(private val isInRegistration: Boolean) : ViewModel() { internal class UsernameEditViewModel private constructor(private val mode: UsernameEditMode) : ViewModel() {
private val events: PublishSubject<Event> = PublishSubject.create() private val events: PublishSubject<Event> = PublishSubject.create()
private val disposables: CompositeDisposable = CompositeDisposable() private val disposables: CompositeDisposable = CompositeDisposable()
@@ -67,6 +67,10 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
.filter { it.stateModifier == UsernameEditStateMachine.StateModifier.USER } .filter { it.stateModifier == UsernameEditStateMachine.StateModifier.USER }
.debounce(NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) .debounce(NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
.subscribeBy(onNext = this::onUsernameStateUpdateDebounced) .subscribeBy(onNext = this::onUsernameStateUpdateDebounced)
if (mode == UsernameEditMode.RECOVERY) {
onNicknameUpdated(SignalStore.account().username?.split(Usernames.DELIMITER)?.first() ?: "")
}
} }
override fun onCleared() { override fun onCleared() {
@@ -79,7 +83,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
uiState.update { state: State -> uiState.update { state: State ->
if (nickname.isBlank() && SignalStore.account().username != null) { if (nickname.isBlank() && SignalStore.account().username != null) {
return@update State( return@update State(
buttonState = if (isInRegistration) ButtonState.SUBMIT_DISABLED else ButtonState.DELETE, buttonState = if (mode.allowsDelete) ButtonState.DELETE else ButtonState.SUBMIT_DISABLED,
usernameStatus = UsernameStatus.NONE, usernameStatus = UsernameStatus.NONE,
usernameState = UsernameState.NoUsername usernameState = UsernameState.NoUsername
) )
@@ -101,7 +105,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
uiState.update { state: State -> uiState.update { state: State ->
if (discriminator.isBlank() && SignalStore.account().username != null) { if (discriminator.isBlank() && SignalStore.account().username != null) {
return@update State( return@update State(
buttonState = if (isInRegistration) ButtonState.SUBMIT_DISABLED else ButtonState.DELETE, buttonState = if (mode.allowsDelete) ButtonState.DELETE else ButtonState.SUBMIT_DISABLED,
usernameStatus = UsernameStatus.NONE, usernameStatus = UsernameStatus.NONE,
usernameState = UsernameState.NoUsername usernameState = UsernameState.NoUsername
) )
@@ -140,7 +144,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
return return
} }
if (usernameState.requireUsername().username == SignalStore.account().username) { if (usernameState.requireUsername().username == SignalStore.account().username && mode != UsernameEditMode.RECOVERY) {
Log.d(TAG, "Username was submitted, but was identical to the current username. Ignoring.") Log.d(TAG, "Username was submitted, but was identical to the current username. Ignoring.")
uiState.update { it.copy(buttonState = ButtonState.SUBMIT_DISABLED, usernameStatus = UsernameStatus.NONE) } uiState.update { it.copy(buttonState = ButtonState.SUBMIT_DISABLED, usernameStatus = UsernameStatus.NONE) }
return return
@@ -219,6 +223,10 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
} }
private fun isCaseChange(state: UsernameEditStateMachine.State): Boolean { private fun isCaseChange(state: UsernameEditStateMachine.State): Boolean {
if (mode == UsernameEditMode.RECOVERY) {
return false
}
if (state is UsernameEditStateMachine.UserEnteredDiscriminator || state is UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator) { if (state is UsernameEditStateMachine.UserEnteredDiscriminator || state is UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator) {
return false return false
} }
@@ -358,9 +366,9 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
NETWORK_FAILURE, SUBMIT_SUCCESS, DELETE_SUCCESS, SUBMIT_FAIL_INVALID, SUBMIT_FAIL_TAKEN, SKIPPED NETWORK_FAILURE, SUBMIT_SUCCESS, DELETE_SUCCESS, SUBMIT_FAIL_INVALID, SUBMIT_FAIL_TAKEN, SKIPPED
} }
class Factory(private val isInRegistration: Boolean) : ViewModelProvider.Factory { class Factory(private val mode: UsernameEditMode) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(UsernameEditViewModel(isInRegistration))!! return modelClass.cast(UsernameEditViewModel(mode))!!
} }
} }

View File

@@ -5,6 +5,7 @@ import androidx.navigation.fragment.NavHostFragment
import org.thoughtcrime.securesms.BaseActivity import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragmentArgs import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragmentArgs
import org.thoughtcrime.securesms.profiles.manage.UsernameEditMode
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme import org.thoughtcrime.securesms.util.DynamicTheme
@@ -23,7 +24,7 @@ class AddAUsernameActivity : BaseActivity() {
R.id.fragment_container, R.id.fragment_container,
NavHostFragment.create( NavHostFragment.create(
R.navigation.create_username, R.navigation.create_username,
UsernameEditFragmentArgs.Builder().setIsInRegistration(true).build().toBundle() UsernameEditFragmentArgs.Builder().setMode(UsernameEditMode.REGISTRATION).build().toBundle()
) )
) )
.commit() .commit()

View File

@@ -583,6 +583,21 @@
app:popUpTo="@id/app_settings" app:popUpTo="@id/app_settings"
app:popUpToInclusive="true" /> app:popUpToInclusive="true" />
<action
android:id="@+id/action_direct_to_usernameRecovery"
app:destination="@id/create_username"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit"
app:popUpTo="@id/app_settings"
app:popUpToInclusive="true">
<argument
android:name="mode"
android:defaultValue="RECOVERY"
app:argType="org.thoughtcrime.securesms.profiles.manage.UsernameEditMode" />
</action>
<!-- endregion --> <!-- endregion -->
<!-- Internal Settings --> <!-- Internal Settings -->
@@ -923,6 +938,7 @@
<include app:graph="@navigation/username_link_settings" /> <include app:graph="@navigation/username_link_settings" />
<include app:graph="@navigation/create_username" />
<include app:graph="@navigation/story_privacy_settings" /> <include app:graph="@navigation/story_privacy_settings" />
</navigation> </navigation>

View File

@@ -11,9 +11,9 @@
tools:layout="@layout/username_edit_fragment"> tools:layout="@layout/username_edit_fragment">
<argument <argument
android:name="is_in_registration" android:name="mode"
android:defaultValue="false" android:defaultValue="NORMAL"
app:argType="boolean" /> app:argType="org.thoughtcrime.securesms.profiles.manage.UsernameEditMode" />
</fragment> </fragment>

View File

@@ -562,6 +562,8 @@
<string name="ConversationListFragment__turn_your_notification_profile_on_or_off_here">Turn your notification profile on or off here.</string> <string name="ConversationListFragment__turn_your_notification_profile_on_or_off_here">Turn your notification profile on or off here.</string>
<!-- Message shown in top toast to indicate the named profile is on --> <!-- Message shown in top toast to indicate the named profile is on -->
<string name="ConversationListFragment__s_on">%1$s on</string> <string name="ConversationListFragment__s_on">%1$s on</string>
<!-- -->
<string name="ConversationListFragment_username_recovered_toast">Your QR code and link have been reset and your username is %1$s</string>
<!-- ConversationListItem --> <!-- ConversationListItem -->
<string name="ConversationListItem_key_exchange_message">Key exchange message</string> <string name="ConversationListItem_key_exchange_message">Key exchange message</string>
@@ -2241,6 +2243,8 @@
<string name="UsernameEditFragment__invalid_username_enter_a_minimum_of_d_digits">Invalid username, enter a minimum of %1$d digits.</string> <string name="UsernameEditFragment__invalid_username_enter_a_minimum_of_d_digits">Invalid username, enter a minimum of %1$d digits.</string>
<!-- Displayed when the chosen discriminator is too long --> <!-- Displayed when the chosen discriminator is too long -->
<string name="UsernameEditFragment__invalid_username_enter_a_maximum_of_d_digits">Invalid username, enter a maximum of %1$d digits.</string> <string name="UsernameEditFragment__invalid_username_enter_a_maximum_of_d_digits">Invalid username, enter a maximum of %1$d digits.</string>
<!-- The body of an alert dialog asking the user to confirm that they want to recover their username -->
<string name="UsernameEditFragment_recovery_dialog_confirmation">Recovering your username will reset your existing QR code and link. Are you sure?</string>
<plurals name="UserNotificationMigrationJob_d_contacts_are_on_signal"> <plurals name="UserNotificationMigrationJob_d_contacts_are_on_signal">
<item quantity="one">%d contact is on Signal!</item> <item quantity="one">%d contact is on Signal!</item>