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

@@ -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)
}
}