Add capability to request username creation during registration.

This commit is contained in:
Alex Hart 2022-09-08 13:02:56 -03:00 committed by Greyson Parrelli
parent 7e45fc4a3e
commit 977af2c2f3
14 changed files with 328 additions and 75 deletions

View file

@ -570,6 +570,10 @@
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".profiles.username.AddAUsernameActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".profiles.manage.ManageProfileActivity"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateVisible|adjustResize" />

View file

@ -20,17 +20,20 @@ import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.profiles.username.AddAUsernameActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
@ -52,6 +55,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private static final int STATE_TRANSFER_ONGOING = 8;
private static final int STATE_TRANSFER_LOCKED = 9;
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private static final int STATE_CREATE_USERNAME = 11;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@ -156,6 +160,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
case STATE_CREATE_USERNAME: return getCreateUsernameIntent();
default: return null;
}
}
@ -175,6 +180,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_CREATE_SIGNAL_PIN;
} else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME;
} else if (shouldAskUserToCreateUsername()) {
return STATE_CREATE_USERNAME;
} else if (userMustCreateSignalPin()) {
return STATE_CREATE_SIGNAL_PIN;
} else if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null && getClass() != OldDeviceTransferActivity.class) {
@ -200,6 +207,13 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
}
private boolean shouldAskUserToCreateUsername() {
return FeatureFlags.usernames() &&
FeatureFlags.phoneNumberPrivacy() &&
!SignalStore.uiHints().hasSetOrSkippedUsernameCreation() &&
SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode() == PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED;
}
private Intent getCreatePassphraseIntent() {
return getRoutedIntent(PassphraseCreateActivity.class, getIntent());
}
@ -259,6 +273,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return ChangeNumberLockActivity.createIntent(this);
}
private Intent getCreateUsernameIntent() {
return getRoutedIntent(AddAUsernameActivity.class, getIntent());
}
private Intent getRoutedIntent(Intent destination, @Nullable Intent nextIntent) {
if (nextIntent != null) destination.putExtra("next_intent", nextIntent);
return destination;

View file

@ -0,0 +1,22 @@
package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.card.MaterialCardView
import org.thoughtcrime.securesms.R
/**
* A small card with a circular progress indicator in it. Usable in place
* of a ProgressDialog, which is deprecated.
*
* Remember to add this as the last UI element in your XML hierarchy so it'll
* draw over top of other elements.
*/
class ProgressCard @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : MaterialCardView(context, attrs) {
init {
inflate(context, R.layout.progress_card, this)
}
}

View file

@ -9,6 +9,7 @@ public class UiHints extends SignalStoreValues {
private static final String HAS_SEEN_GROUP_SETTINGS_MENU_TOAST = "uihints.has_seen_group_settings_menu_toast";
private static final String HAS_CONFIRMED_DELETE_FOR_EVERYONE_ONCE = "uihints.has_confirmed_delete_for_everyone_once";
private static final String HAS_SET_OR_SKIPPED_USERNAME_CREATION = "uihints.has_set_or_skipped_username_creation";
UiHints(@NonNull KeyValueStore store) {
super(store);
@ -39,4 +40,12 @@ public class UiHints extends SignalStoreValues {
public boolean hasConfirmedDeleteForEveryoneOnce() {
return getBoolean(HAS_CONFIRMED_DELETE_FOR_EVERYONE_ONCE, false);
}
public boolean hasSetOrSkippedUsernameCreation() {
return getBoolean(HAS_SET_OR_SKIPPED_USERNAME_CREATION, false);
}
public void markHasSetOrSkippedUsernameCreation() {
putBoolean(HAS_SET_OR_SKIPPED_USERNAME_CREATION, true);
}
}

View file

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.profiles.edit;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
@ -20,7 +19,6 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
/**
* Shows editing screen for your profile during registration. Also handles group name editing.
*/
@SuppressLint("StaticFieldLeak")
public class EditProfileActivity extends BaseActivity implements EditProfileFragment.Controller {
public static final String NEXT_INTENT = "next_intent";
@ -37,13 +35,6 @@ public class EditProfileActivity extends BaseActivity implements EditProfileFrag
return intent;
}
public static @NonNull Intent getIntentForUserProfileEdit(@NonNull Context context) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
return intent;
}
public static @NonNull Intent getIntentForGroupProfile(@NonNull Context context, @NonNull GroupId groupId) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.SHOW_TOOLBAR, true);

View file

@ -18,6 +18,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
@ -91,7 +92,7 @@ public class EditProfileFragment extends LoggingFragment {
GroupId groupId = GroupId.parseNullableOrThrow(requireArguments().getString(GROUP_ID, null));
initializeViewModel(requireArguments().getBoolean(EXCLUDE_SYSTEM, false), groupId, savedInstanceState != null);
initializeResources(view, groupId);
initializeResources(groupId);
initializeProfileAvatar();
initializeProfileName();
@ -151,11 +152,10 @@ public class EditProfileFragment extends LoggingFragment {
EditProfileViewModel.Factory factory = new EditProfileViewModel.Factory(repository, hasSavedInstanceState, groupId);
viewModel = ViewModelProviders.of(requireActivity(), factory)
.get(EditProfileViewModel.class);
viewModel = new ViewModelProvider(requireActivity(), factory).get(EditProfileViewModel.class);
}
private void initializeResources(@NonNull View view, @Nullable GroupId groupId) {
private void initializeResources(@Nullable GroupId groupId) {
Bundle arguments = requireArguments();
boolean isEditingGroup = groupId != null;

View file

@ -1,7 +1,7 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -17,6 +17,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
@ -29,10 +30,12 @@ import com.google.android.material.textfield.TextInputLayout;
import org.signal.core.util.DimensionUnit;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.databinding.UsernameEditFragmentBinding;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AccessibilityUtil;
import org.thoughtcrime.securesms.util.FragmentResultContract;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.UsernameUtil;
@ -49,6 +52,7 @@ public class UsernameEditFragment extends LoggingFragment {
private UsernameEditFragmentBinding binding;
private ImageView suffixProgress;
private LifecycleDisposable lifecycleDisposable;
private UsernameEditFragmentArgs args;
public static UsernameEditFragment newInstance() {
return new UsernameEditFragment();
@ -62,20 +66,37 @@ public class UsernameEditFragment extends LoggingFragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
binding.toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(view).popBackStack());
Bundle bundle = getArguments();
if (bundle != null) {
args = UsernameEditFragmentArgs.fromBundle(bundle);
} else {
args = new UsernameEditFragmentArgs.Builder().build();
}
if (args.getIsInRegistration()) {
binding.toolbar.setNavigationIcon(null);
binding.toolbar.setTitle(R.string.UsernameEditFragment__add_a_username);
binding.usernameSkipButton.setVisibility(View.VISIBLE);
binding.usernameDoneButton.setVisibility(View.VISIBLE);
} else {
binding.toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(view).popBackStack());
binding.usernameSubmitButton.setVisibility(View.VISIBLE);
}
binding.usernameTextWrapper.setErrorIconDrawable(null);
lifecycleDisposable = new LifecycleDisposable();
lifecycleDisposable.bindTo(getViewLifecycleOwner());
viewModel = new ViewModelProvider(this, new UsernameEditViewModel.Factory()).get(UsernameEditViewModel.class);
viewModel = new ViewModelProvider(this, new UsernameEditViewModel.Factory(args.getIsInRegistration())).get(UsernameEditViewModel.class);
lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged));
viewModel.getEvents().observe(getViewLifecycleOwner(), this::onEvent);
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
binding.usernameDoneButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
binding.usernameSkipButton.setOnClickListener(v -> viewModel.onUsernameSkipped());
UsernameState usernameState = Recipient.self().getUsername().<UsernameState>map(UsernameState.Set::new).orElse(UsernameState.NoUsername.INSTANCE);
binding.usernameText.setText(usernameState.getNickname());
@ -142,15 +163,82 @@ public class UsernameEditFragment extends LoggingFragment {
}
private void onUiStateChanged(@NonNull UsernameEditViewModel.State state) {
EditText usernameInput = binding.usernameText;
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
presentSuffix(state.getUsername());
presentButtonState(state.getButtonState());
switch (state.getUsernameStatus()) {
case NONE:
usernameInputWrapper.setError(null);
break;
case TOO_SHORT:
case TOO_LONG:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_usernames_must_be_between_a_and_b_characters, UsernameUtil.MIN_LENGTH, UsernameUtil.MAX_LENGTH));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
break;
case INVALID_CHARACTERS:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_usernames_can_only_include));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
break;
case CANNOT_START_WITH_NUMBER:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_usernames_cannot_begin_with_a_number));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
break;
case INVALID_GENERIC:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_username_is_invalid));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
break;
case TAKEN:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_taken));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
break;
}
}
private void presentButtonState(@NonNull UsernameEditViewModel.ButtonState buttonState) {
if (args.getIsInRegistration()) {
presentRegistrationButtonState(buttonState);
} else {
presentProfileUpdateButtonState(buttonState);
}
}
private void presentRegistrationButtonState(@NonNull UsernameEditViewModel.ButtonState buttonState) {
binding.usernameText.setEnabled(true);
binding.usernameProgressCard.setVisibility(View.GONE);
switch (buttonState) {
case SUBMIT:
binding.usernameDoneButton.setEnabled(true);
binding.usernameDoneButton.setAlpha(1f);
break;
case SUBMIT_DISABLED:
binding.usernameDoneButton.setEnabled(false);
binding.usernameDoneButton.setAlpha(DISABLED_ALPHA);
break;
case SUBMIT_LOADING:
binding.usernameDoneButton.setEnabled(false);
binding.usernameDoneButton.setAlpha(DISABLED_ALPHA);
binding.usernameProgressCard.setVisibility(View.VISIBLE);
break;
default:
throw new IllegalStateException("Delete functionality is not available during registration.");
}
}
private void presentProfileUpdateButtonState(@NonNull UsernameEditViewModel.ButtonState buttonState) {
CircularProgressMaterialButton submitButton = binding.usernameSubmitButton;
CircularProgressMaterialButton deleteButton = binding.usernameDeleteButton;
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
EditText usernameInput = binding.usernameText;
usernameInput.setEnabled(true);
presentSuffix(state.getUsername());
switch (state.getButtonState()) {
switch (buttonState) {
case SUBMIT:
submitButton.cancelSpinning();
submitButton.setVisibility(View.VISIBLE);
@ -194,38 +282,6 @@ public class UsernameEditFragment extends LoggingFragment {
usernameInput.setEnabled(false);
break;
}
switch (state.getUsernameStatus()) {
case NONE:
usernameInputWrapper.setError(null);
break;
case TOO_SHORT:
case TOO_LONG:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_usernames_must_be_between_a_and_b_characters, UsernameUtil.MIN_LENGTH, UsernameUtil.MAX_LENGTH));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
break;
case INVALID_CHARACTERS:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_usernames_can_only_include));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
break;
case CANNOT_START_WITH_NUMBER:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_usernames_cannot_begin_with_a_number));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
break;
case INVALID_GENERIC:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_username_is_invalid));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
break;
case TAKEN:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_taken));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
break;
}
}
private void presentSuffix(@NonNull UsernameState usernameState) {
@ -257,7 +313,7 @@ public class UsernameEditFragment extends LoggingFragment {
switch (event) {
case SUBMIT_SUCCESS:
ResultContract.setUsernameCreated(getParentFragmentManager());
NavHostFragment.findNavController(this).popBackStack();
closeScreen();
break;
case SUBMIT_FAIL_TAKEN:
Toast.makeText(requireContext(), R.string.UsernameEditFragment_this_username_is_taken, Toast.LENGTH_SHORT).show();
@ -272,6 +328,36 @@ public class UsernameEditFragment extends LoggingFragment {
case NETWORK_FAILURE:
Toast.makeText(requireContext(), R.string.UsernameEditFragment_encountered_a_network_error, Toast.LENGTH_SHORT).show();
break;
case SKIPPED:
closeScreen();
break;
}
}
private void closeScreen() {
if (args.getIsInRegistration()) {
finishAndStartNextIntent();
} else {
NavHostFragment.findNavController(this).popBackStack();
}
}
private void finishAndStartNextIntent() {
FragmentActivity activity = requireActivity();
boolean didLaunch = false;
Intent activityIntent = activity.getIntent();
if (activityIntent != null) {
Intent nextIntent = activityIntent.getParcelableExtra(PassphraseRequiredActivity.NEXT_INTENT_EXTRA);
if (nextIntent != null) {
activity.startActivity(nextIntent);
activity.finish();
didLaunch = true;
}
}
if (!didLaunch) {
activity.finish();
}
}

View file

@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.UsernameUtil;
@ -45,13 +46,15 @@ class UsernameEditViewModel extends ViewModel {
private final RxStore<State> uiState;
private final PublishProcessor<String> nicknamePublisher;
private final CompositeDisposable disposables;
private final boolean isInRegistration;
private UsernameEditViewModel() {
private UsernameEditViewModel(boolean isInRegistration) {
this.repo = new UsernameEditRepository();
this.uiState = new RxStore<>(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, Recipient.self().getUsername().<UsernameState>map(UsernameState.Set::new).orElse(UsernameState.NoUsername.INSTANCE)), Schedulers.computation());
this.events = new SingleLiveEvent<>();
this.nicknamePublisher = PublishProcessor.create();
this.disposables = new CompositeDisposable();
this.isInRegistration = isInRegistration;
Disposable disposable = nicknamePublisher.debounce(NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
.subscribe(this::onNicknameChanged);
@ -67,7 +70,7 @@ class UsernameEditViewModel extends ViewModel {
void onNicknameUpdated(@NonNull String nickname) {
uiState.update(state -> {
if (TextUtils.isEmpty(nickname) && Recipient.self().getUsername().isPresent()) {
return new State(ButtonState.DELETE, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE);
return new State(isInRegistration ? ButtonState.SUBMIT_DISABLED : ButtonState.DELETE, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE);
}
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(nickname);
@ -79,6 +82,11 @@ class UsernameEditViewModel extends ViewModel {
nicknamePublisher.onNext(nickname);
}
void onUsernameSkipped() {
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation();
events.setValue(Event.SKIPPED);
}
void onUsernameSubmitted() {
UsernameState usernameState = uiState.getState().getUsername();
@ -107,6 +115,7 @@ class UsernameEditViewModel extends ViewModel {
switch (result) {
case SUCCESS:
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation();
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
events.postValue(Event.SUBMIT_SUCCESS);
break;
@ -248,14 +257,21 @@ class UsernameEditViewModel extends ViewModel {
}
enum Event {
NETWORK_FAILURE, SUBMIT_SUCCESS, DELETE_SUCCESS, SUBMIT_FAIL_INVALID, SUBMIT_FAIL_TAKEN
NETWORK_FAILURE, SUBMIT_SUCCESS, DELETE_SUCCESS, SUBMIT_FAIL_INVALID, SUBMIT_FAIL_TAKEN, SKIPPED
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final boolean isInRegistration;
Factory(boolean isInRegistration) {
this.isInRegistration = isInRegistration;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new UsernameEditViewModel());
return modelClass.cast(new UsernameEditViewModel(isInRegistration));
}
}
}

View file

@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.profiles.username
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragmentArgs
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
class AddAUsernameActivity : BaseActivity() {
protected open val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
protected open val contentViewId: Int = R.layout.fragment_container
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(contentViewId)
dynamicTheme.onCreate(this)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(
R.id.fragment_container,
NavHostFragment.create(
R.navigation.create_username,
UsernameEditFragmentArgs.Builder().setIsInRegistration(true).build().toBundle()
)
)
.commit()
}
}
override fun onResume() {
super.onResume()
dynamicTheme.onResume(this)
}
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="com.google.android.material.card.MaterialCardView"
tools:visibility="visible">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress_card_child"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="24dp"
android:indeterminate="true"
app:indicatorColor="@color/signal_colorPrimary" />
</merge>

View file

@ -98,14 +98,45 @@
app:layout_constraintTop_toBottomOf="@id/username_text_wrapper"
app:layout_constraintVertical_bias="0" />
<com.google.android.material.button.MaterialButton
android:id="@+id/username_skip_button"
style="@style/Widget.Signal.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginBottom="16dp"
android:minWidth="48dp"
android:text="@string/UsernameEditFragment__skip"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/username_done_button"
style="@style/ThemeOverlay.Material3.FloatingActionButton.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:layout_marginBottom="16dp"
android:contentDescription="@string/UsernameEditFragment__done"
android:visibility="gone"
app:backgroundTint="@color/signal_colorPrimaryContainer"
app:fabCustomSize="48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_arrow_end_24"
app:tint="@color/signal_colorOnPrimaryContainer"
tools:visibility="visible" />
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
android:id="@+id/username_submit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:circularProgressMaterialButton__label="@string/save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
@ -115,7 +146,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone"
@ -123,11 +153,23 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<org.thoughtcrime.securesms.components.ProgressCard
android:id="@+id/username_progress_card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:cardCornerRadius="18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/username_button_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="username_submit_button,username_delete_button" />
app:constraint_referenced_ids="username_submit_button,username_delete_button,username_skip_button,username_done_button" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/create_username"
app:startDestination="@id/createUsernameFragment">
<fragment
android:id="@+id/createUsernameFragment"
android:name="org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment"
tools:layout="@layout/username_edit_fragment">
<argument
android:name="is_in_registration"
android:defaultValue="false"
app:argType="boolean" />
</fragment>
</navigation>

View file

@ -11,14 +11,6 @@
android:label="fragment_create_profile"
tools:layout="@layout/profile_create_fragment">
<action
android:id="@+id/action_editUsername"
app:destination="@id/usernameEditFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_createProfileFragment_to_avatar_picker"
app:destination="@id/avatar_picker"
@ -47,12 +39,6 @@
</fragment>
<fragment
android:id="@+id/usernameEditFragment"
android:name="org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment"
android:label="fragment_edit_username"
tools:layout="@layout/username_edit_fragment" />
<include app:graph="@navigation/avatar_picker" />
<fragment

View file

@ -1873,6 +1873,8 @@
<string name="UnverifiedSendDialog_send">Send</string>
<!-- UsernameEditFragment -->
<!-- Toolbar title when entering from registration -->
<string name="UsernameEditFragment__add_a_username">Add a username</string>
<!-- Instructional text at the top of the username edit screen -->
<string name="UsernameEditFragment__choose_your_username">Choose your username</string>
<string name="UsernameEditFragment_username">Username</string>
@ -1891,6 +1893,10 @@
<!-- Dialog title for explanation about numbers at the end of the username -->
<string name="UsernameEditFragment__what_is_this_number">What is this number?</string>
<string name="UsernameEditFragment__these_digits_help_keep">These digits help keep your username private so you can avoid unwanted messages. Share your username with only the people and groups you\'d like to chat with. If you change usernames you\'ll get a new set of digits.</string>
<!-- Button to allow user to skip -->
<string name="UsernameEditFragment__skip">Skip</string>
<!-- Content description for done button -->
<string name="UsernameEditFragment__done">Done</string>
<plurals name="UserNotificationMigrationJob_d_contacts_are_on_signal">
<item quantity="one">%d contact is on Signal!</item>