mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 02:08:40 +00:00
Fix error display when entering invalid username characters.
Also convert UsernameEditViewModel to kotlin.
This commit is contained in:
@@ -166,11 +166,11 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
private void onUiStateChanged(@NonNull UsernameEditViewModel.State state) {
|
||||
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
|
||||
|
||||
presentSuffix(state.getUsername());
|
||||
presentButtonState(state.getButtonState());
|
||||
presentSummary(state.getUsername());
|
||||
presentSuffix(state.username);
|
||||
presentButtonState(state.buttonState);
|
||||
presentSummary(state.username);
|
||||
|
||||
switch (state.getUsernameStatus()) {
|
||||
switch (state.usernameStatus) {
|
||||
case NONE:
|
||||
usernameInputWrapper.setError(null);
|
||||
break;
|
||||
@@ -216,7 +216,7 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
private void presentSummary(@NonNull UsernameState usernameState) {
|
||||
if (usernameState.getUsername() != null) {
|
||||
if (usernameState.getUsername() != null || usernameState instanceof UsernameState.Loading) {
|
||||
binding.summary.setText(usernameState.getUsername());
|
||||
} else {
|
||||
binding.summary.setText(R.string.UsernameEditFragment__choose_your_username);
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason;
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
/**
|
||||
* Manages the state around username updates.
|
||||
* <p>
|
||||
* A note on naming conventions:
|
||||
* <p>
|
||||
* Usernames are made up of two discrete components, a nickname and a discriminator. They are formatted thusly:
|
||||
* <p>
|
||||
* [nickname]#[discriminator]
|
||||
* <p>
|
||||
* The nickname is user-controlled, whereas the discriminator is controlled by the server.
|
||||
*/
|
||||
class UsernameEditViewModel extends ViewModel {
|
||||
|
||||
private static final long NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS = 500;
|
||||
|
||||
private final PublishSubject<Event> events;
|
||||
private final UsernameRepository repo;
|
||||
private final RxStore<State> uiState;
|
||||
private final PublishProcessor<String> nicknamePublisher;
|
||||
private final CompositeDisposable disposables;
|
||||
private final boolean isInRegistration;
|
||||
|
||||
private UsernameEditViewModel(boolean isInRegistration) {
|
||||
this.repo = new UsernameRepository();
|
||||
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 = PublishSubject.create();
|
||||
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);
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
disposables.clear();
|
||||
uiState.dispose();
|
||||
}
|
||||
|
||||
void onNicknameUpdated(@NonNull String nickname) {
|
||||
uiState.update(state -> {
|
||||
if (TextUtils.isEmpty(nickname) && Recipient.self().getUsername().isPresent()) {
|
||||
return new State(isInRegistration ? ButtonState.SUBMIT_DISABLED : ButtonState.DELETE, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE);
|
||||
}
|
||||
|
||||
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(nickname);
|
||||
|
||||
return invalidReason.map(reason -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(reason), state.usernameState))
|
||||
.orElseGet(() -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
|
||||
});
|
||||
|
||||
nicknamePublisher.onNext(nickname);
|
||||
}
|
||||
|
||||
void onUsernameSkipped() {
|
||||
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation();
|
||||
events.onNext(Event.SKIPPED);
|
||||
}
|
||||
|
||||
void onUsernameSubmitted() {
|
||||
UsernameState usernameState = uiState.getState().getUsername();
|
||||
|
||||
if (!(usernameState instanceof UsernameState.Reserved)) {
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Objects.equals(usernameState.getUsername(), Recipient.self().getUsername().orElse(null))) {
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(usernameState.getNickname());
|
||||
|
||||
if (invalidReason.isPresent()) {
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason.get()), state.usernameState));
|
||||
return;
|
||||
}
|
||||
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, state.usernameState));
|
||||
|
||||
Disposable confirmUsernameDisposable = repo.confirmUsername((UsernameState.Reserved) usernameState)
|
||||
.subscribe(result -> {
|
||||
String nickname = usernameState.getNickname();
|
||||
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation();
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
|
||||
events.onNext(Event.SUBMIT_SUCCESS);
|
||||
break;
|
||||
case USERNAME_INVALID:
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, state.usernameState));
|
||||
events.onNext(Event.SUBMIT_FAIL_INVALID);
|
||||
|
||||
if (nickname != null) {
|
||||
onNicknameUpdated(nickname);
|
||||
}
|
||||
break;
|
||||
case CANDIDATE_GENERATION_ERROR:
|
||||
case USERNAME_UNAVAILABLE:
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, state.usernameState));
|
||||
events.onNext(Event.SUBMIT_FAIL_TAKEN);
|
||||
|
||||
if (nickname != null) {
|
||||
onNicknameUpdated(nickname);
|
||||
}
|
||||
break;
|
||||
case NETWORK_ERROR:
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameState));
|
||||
events.onNext(Event.NETWORK_FAILURE);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(confirmUsernameDisposable);
|
||||
}
|
||||
|
||||
void onUsernameDeleted() {
|
||||
uiState.update(state -> new State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.usernameState));
|
||||
|
||||
Disposable deletionDisposable = repo.deleteUsername().subscribe(result -> {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
uiState.update(state -> new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameState));
|
||||
events.onNext(Event.DELETE_SUCCESS);
|
||||
break;
|
||||
case NETWORK_ERROR:
|
||||
uiState.update(state -> new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameState));
|
||||
events.onNext(Event.NETWORK_FAILURE);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(deletionDisposable);
|
||||
}
|
||||
|
||||
@NonNull Flowable<State> getUiState() {
|
||||
return uiState.getStateFlowable().observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull Observable<Event> getEvents() {
|
||||
return events.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
private void onNicknameChanged(@NonNull String nickname) {
|
||||
if (TextUtils.isEmpty(nickname)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading.INSTANCE));
|
||||
Disposable reserveDisposable = repo.reserveUsername(nickname).subscribe(result -> {
|
||||
result.either(
|
||||
reserved -> {
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, reserved));
|
||||
return null;
|
||||
},
|
||||
failure -> {
|
||||
switch (failure) {
|
||||
case SUCCESS:
|
||||
throw new AssertionError();
|
||||
case USERNAME_INVALID:
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, UsernameState.NoUsername.INSTANCE));
|
||||
break;
|
||||
case USERNAME_UNAVAILABLE:
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername.INSTANCE));
|
||||
break;
|
||||
case NETWORK_ERROR:
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE));
|
||||
events.onNext(Event.NETWORK_FAILURE);
|
||||
break;
|
||||
case CANDIDATE_GENERATION_ERROR:
|
||||
// TODO -- Retry
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername.INSTANCE));
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
disposables.add(reserveDisposable);
|
||||
}
|
||||
|
||||
private static UsernameStatus mapUsernameError(@NonNull InvalidReason invalidReason) {
|
||||
switch (invalidReason) {
|
||||
case TOO_SHORT:
|
||||
return UsernameStatus.TOO_SHORT;
|
||||
case TOO_LONG:
|
||||
return UsernameStatus.TOO_LONG;
|
||||
case STARTS_WITH_NUMBER:
|
||||
return UsernameStatus.CANNOT_START_WITH_NUMBER;
|
||||
case INVALID_CHARACTERS:
|
||||
return UsernameStatus.INVALID_CHARACTERS;
|
||||
default:
|
||||
return UsernameStatus.INVALID_GENERIC;
|
||||
}
|
||||
}
|
||||
|
||||
static class State {
|
||||
private final ButtonState buttonState;
|
||||
private final UsernameStatus usernameStatus;
|
||||
private final UsernameState usernameState;
|
||||
|
||||
private State(@NonNull ButtonState buttonState,
|
||||
@NonNull UsernameStatus usernameStatus,
|
||||
@NonNull UsernameState usernameState)
|
||||
{
|
||||
this.buttonState = buttonState;
|
||||
this.usernameStatus = usernameStatus;
|
||||
this.usernameState = usernameState;
|
||||
}
|
||||
|
||||
@NonNull ButtonState getButtonState() {
|
||||
return buttonState;
|
||||
}
|
||||
|
||||
@NonNull UsernameStatus getUsernameStatus() {
|
||||
return usernameStatus;
|
||||
}
|
||||
|
||||
@NonNull UsernameState getUsername() {
|
||||
return usernameState;
|
||||
}
|
||||
}
|
||||
|
||||
enum UsernameStatus {
|
||||
NONE, TAKEN, TOO_SHORT, TOO_LONG, CANNOT_START_WITH_NUMBER, INVALID_CHARACTERS, INVALID_GENERIC
|
||||
}
|
||||
|
||||
enum ButtonState {
|
||||
SUBMIT, SUBMIT_DISABLED, SUBMIT_LOADING, DELETE, DELETE_LOADING, DELETE_DISABLED
|
||||
}
|
||||
|
||||
enum Event {
|
||||
NETWORK_FAILURE, SUBMIT_SUCCESS, DELETE_SUCCESS, SUBMIT_FAIL_INVALID, SUBMIT_FAIL_TAKEN, SKIPPED
|
||||
}
|
||||
|
||||
static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
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(isInRegistration));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.Result
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameDeleteResult
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameSetResult
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil.checkUsername
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Manages the state around username updates.
|
||||
*
|
||||
*
|
||||
* A note on naming conventions:
|
||||
* Usernames are made up of two discrete components, a nickname and a discriminator. They are formatted thusly:
|
||||
*
|
||||
* [nickname].[discriminator]
|
||||
*
|
||||
* The nickname is user-controlled, whereas the discriminator is controlled by the server.
|
||||
*/
|
||||
internal class UsernameEditViewModel private constructor(private val isInRegistration: Boolean) : ViewModel() {
|
||||
private val events: PublishSubject<Event> = PublishSubject.create()
|
||||
private val repo: UsernameRepository = UsernameRepository()
|
||||
private val nicknamePublisher: PublishProcessor<String> = PublishProcessor.create()
|
||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private val uiState: RxStore<State> = RxStore(
|
||||
defaultValue = State(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = Recipient.self().username.map<UsernameState> { UsernameState.Set(it) }.orElse(UsernameState.NoUsername)
|
||||
),
|
||||
scheduler = Schedulers.computation()
|
||||
)
|
||||
|
||||
init {
|
||||
disposables += nicknamePublisher
|
||||
.debounce(NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.subscribe { nickname: String -> onNicknameUpdatedDebounced(nickname) }
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
disposables.clear()
|
||||
uiState.dispose()
|
||||
}
|
||||
|
||||
fun onNicknameUpdated(nickname: String) {
|
||||
uiState.update { state: State ->
|
||||
if (nickname.isBlank() && Recipient.self().username.isPresent) {
|
||||
return@update State(
|
||||
buttonState = if (isInRegistration) ButtonState.SUBMIT_DISABLED else ButtonState.DELETE,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = UsernameState.NoUsername
|
||||
)
|
||||
}
|
||||
|
||||
val invalidReason: InvalidReason? = checkUsername(nickname)
|
||||
|
||||
if (invalidReason != null) {
|
||||
State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason), state.username)
|
||||
} else {
|
||||
State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.username)
|
||||
}
|
||||
}
|
||||
|
||||
nicknamePublisher.onNext(nickname)
|
||||
}
|
||||
|
||||
fun onUsernameSkipped() {
|
||||
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation()
|
||||
events.onNext(Event.SKIPPED)
|
||||
}
|
||||
|
||||
fun onUsernameSubmitted() {
|
||||
val usernameState = uiState.state.username
|
||||
if (usernameState !is UsernameState.Reserved) {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
||||
return
|
||||
}
|
||||
|
||||
if (usernameState.username == Recipient.self().username.orElse(null)) {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
||||
return
|
||||
}
|
||||
|
||||
val invalidReason = checkUsername(usernameState.getNickname())
|
||||
if (invalidReason != null) {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason), it.username) }
|
||||
return
|
||||
}
|
||||
|
||||
uiState.update { State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, it.username) }
|
||||
|
||||
disposables += repo.confirmUsername(usernameState).subscribe { result: UsernameSetResult ->
|
||||
val nickname = usernameState.getNickname()
|
||||
|
||||
when (result) {
|
||||
UsernameSetResult.SUCCESS -> {
|
||||
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation()
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
||||
events.onNext(Event.SUBMIT_SUCCESS)
|
||||
}
|
||||
|
||||
UsernameSetResult.USERNAME_INVALID -> {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, it.username) }
|
||||
events.onNext(Event.SUBMIT_FAIL_INVALID)
|
||||
nickname?.let { onNicknameUpdated(it) }
|
||||
}
|
||||
|
||||
UsernameSetResult.CANDIDATE_GENERATION_ERROR, UsernameSetResult.USERNAME_UNAVAILABLE -> {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, it.username) }
|
||||
events.onNext(Event.SUBMIT_FAIL_TAKEN)
|
||||
nickname?.let { onNicknameUpdated(it) }
|
||||
}
|
||||
|
||||
UsernameSetResult.NETWORK_ERROR -> {
|
||||
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, it.username) }
|
||||
events.onNext(Event.NETWORK_FAILURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onUsernameDeleted() {
|
||||
uiState.update { state: State -> State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.username) }
|
||||
|
||||
disposables += repo.deleteUsername().subscribe { result: UsernameDeleteResult ->
|
||||
when (result) {
|
||||
UsernameDeleteResult.SUCCESS -> {
|
||||
uiState.update { state: State -> State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.username) }
|
||||
events.onNext(Event.DELETE_SUCCESS)
|
||||
}
|
||||
|
||||
UsernameDeleteResult.NETWORK_ERROR -> {
|
||||
uiState.update { state: State -> State(ButtonState.DELETE, UsernameStatus.NONE, state.username) }
|
||||
events.onNext(Event.NETWORK_FAILURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getUiState(): Flowable<State> {
|
||||
return uiState.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun getEvents(): Observable<Event> {
|
||||
return events.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
/** Triggered when the debounced nickname event stream fires. */
|
||||
private fun onNicknameUpdatedDebounced(nickname: String) {
|
||||
if (nickname.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
val invalidReason: InvalidReason? = checkUsername(nickname)
|
||||
if (invalidReason != null) {
|
||||
return
|
||||
}
|
||||
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading) }
|
||||
|
||||
disposables += repo.reserveUsername(nickname).subscribe { result: Result<UsernameState.Reserved, UsernameSetResult> ->
|
||||
result.either(
|
||||
onSuccess = { reserved: UsernameState.Reserved ->
|
||||
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, reserved) }
|
||||
},
|
||||
onFailure = { failure: UsernameSetResult ->
|
||||
when (failure) {
|
||||
UsernameSetResult.SUCCESS -> {
|
||||
throw AssertionError()
|
||||
}
|
||||
UsernameSetResult.USERNAME_INVALID -> {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, UsernameState.NoUsername) }
|
||||
}
|
||||
UsernameSetResult.USERNAME_UNAVAILABLE -> {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername) }
|
||||
}
|
||||
UsernameSetResult.NETWORK_ERROR -> {
|
||||
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername) }
|
||||
events.onNext(Event.NETWORK_FAILURE)
|
||||
}
|
||||
UsernameSetResult.CANDIDATE_GENERATION_ERROR -> {
|
||||
// TODO -- Retry
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername) }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class State(
|
||||
@JvmField val buttonState: ButtonState,
|
||||
@JvmField val usernameStatus: UsernameStatus,
|
||||
@JvmField val username: UsernameState
|
||||
)
|
||||
|
||||
enum class UsernameStatus {
|
||||
NONE, TAKEN, TOO_SHORT, TOO_LONG, CANNOT_START_WITH_NUMBER, INVALID_CHARACTERS, INVALID_GENERIC
|
||||
}
|
||||
|
||||
enum class ButtonState {
|
||||
SUBMIT, SUBMIT_DISABLED, SUBMIT_LOADING, DELETE, DELETE_LOADING, DELETE_DISABLED
|
||||
}
|
||||
|
||||
enum class Event {
|
||||
NETWORK_FAILURE, SUBMIT_SUCCESS, DELETE_SUCCESS, SUBMIT_FAIL_INVALID, SUBMIT_FAIL_TAKEN, SKIPPED
|
||||
}
|
||||
|
||||
class Factory(private val isInRegistration: Boolean) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(UsernameEditViewModel(isInRegistration))!!
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS: Long = 500
|
||||
private fun mapUsernameError(invalidReason: InvalidReason): UsernameStatus {
|
||||
return when (invalidReason) {
|
||||
InvalidReason.TOO_SHORT -> UsernameStatus.TOO_SHORT
|
||||
InvalidReason.TOO_LONG -> UsernameStatus.TOO_LONG
|
||||
InvalidReason.STARTS_WITH_NUMBER -> UsernameStatus.CANNOT_START_WITH_NUMBER
|
||||
InvalidReason.INVALID_CHARACTERS -> UsernameStatus.INVALID_CHARACTERS
|
||||
else -> UsernameStatus.INVALID_GENERIC
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,19 +33,26 @@ object UsernameUtil {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun checkUsername(value: String?): Optional<InvalidReason> {
|
||||
return if (value == null) {
|
||||
Optional.of(InvalidReason.TOO_SHORT)
|
||||
} else if (value.length < MIN_LENGTH) {
|
||||
Optional.of(InvalidReason.TOO_SHORT)
|
||||
} else if (value.length > MAX_LENGTH) {
|
||||
Optional.of(InvalidReason.TOO_LONG)
|
||||
} else if (DIGIT_START_PATTERN.matcher(value).matches()) {
|
||||
Optional.of(InvalidReason.STARTS_WITH_NUMBER)
|
||||
} else if (!FULL_PATTERN.matcher(value).matches()) {
|
||||
Optional.of(InvalidReason.INVALID_CHARACTERS)
|
||||
} else {
|
||||
Optional.empty()
|
||||
fun checkUsername(value: String?): InvalidReason? {
|
||||
return when {
|
||||
value == null -> {
|
||||
InvalidReason.TOO_SHORT
|
||||
}
|
||||
value.length < MIN_LENGTH -> {
|
||||
InvalidReason.TOO_SHORT
|
||||
}
|
||||
value.length > MAX_LENGTH -> {
|
||||
InvalidReason.TOO_LONG
|
||||
}
|
||||
DIGIT_START_PATTERN.matcher(value).matches() -> {
|
||||
InvalidReason.STARTS_WITH_NUMBER
|
||||
}
|
||||
!FULL_PATTERN.matcher(value).matches() -> {
|
||||
InvalidReason.INVALID_CHARACTERS
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class UsernameUtilTest {
|
||||
|
||||
@Test
|
||||
public void checkUsername_tooShort() {
|
||||
assertEquals(UsernameUtil.InvalidReason.TOO_SHORT, UsernameUtil.checkUsername(null).get());
|
||||
assertEquals(UsernameUtil.InvalidReason.TOO_SHORT, UsernameUtil.checkUsername("").get());
|
||||
assertEquals(UsernameUtil.InvalidReason.TOO_SHORT, UsernameUtil.checkUsername("ab").get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkUsername_tooLong() {
|
||||
assertEquals(UsernameUtil.InvalidReason.TOO_LONG, UsernameUtil.checkUsername("abcdefghijklmnopqrstuvwxyz1234567").get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkUsername_startsWithNumber() {
|
||||
assertEquals(UsernameUtil.InvalidReason.STARTS_WITH_NUMBER, UsernameUtil.checkUsername("0abcdefg").get());
|
||||
assertEquals(UsernameUtil.InvalidReason.STARTS_WITH_NUMBER, UsernameUtil.checkUsername("9abcdefg").get());
|
||||
assertEquals(UsernameUtil.InvalidReason.STARTS_WITH_NUMBER, UsernameUtil.checkUsername("8675309").get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkUsername_invalidCharacters() {
|
||||
assertEquals(UsernameUtil.InvalidReason.INVALID_CHARACTERS, UsernameUtil.checkUsername("$abcd").get());
|
||||
assertEquals(UsernameUtil.InvalidReason.INVALID_CHARACTERS, UsernameUtil.checkUsername(" abcd").get());
|
||||
assertEquals(UsernameUtil.InvalidReason.INVALID_CHARACTERS, UsernameUtil.checkUsername("ab cde").get());
|
||||
assertEquals(UsernameUtil.InvalidReason.INVALID_CHARACTERS, UsernameUtil.checkUsername("%%%%%").get());
|
||||
assertEquals(UsernameUtil.InvalidReason.INVALID_CHARACTERS, UsernameUtil.checkUsername("-----").get());
|
||||
assertEquals(UsernameUtil.InvalidReason.INVALID_CHARACTERS, UsernameUtil.checkUsername("asĸ_me").get());
|
||||
assertEquals(UsernameUtil.InvalidReason.INVALID_CHARACTERS, UsernameUtil.checkUsername("+18675309").get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkUsername_validUsernames() {
|
||||
assertFalse(UsernameUtil.checkUsername("abcd").isPresent());
|
||||
assertFalse(UsernameUtil.checkUsername("abcdefghijklmnopqrstuvwxyz").isPresent());
|
||||
assertFalse(UsernameUtil.checkUsername("ABCDEFGHIJKLMNOPQRSTUVWXYZ").isPresent());
|
||||
assertFalse(UsernameUtil.checkUsername("web_head").isPresent());
|
||||
assertFalse(UsernameUtil.checkUsername("Spider_Fan_1991").isPresent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.assertIs
|
||||
import org.thoughtcrime.securesms.assertIsNotNull
|
||||
import org.thoughtcrime.securesms.assertIsNull
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil.checkUsername
|
||||
|
||||
class UsernameUtilTest {
|
||||
@Test
|
||||
fun checkUsername_tooShort() {
|
||||
checkUsername(null) assertIs UsernameUtil.InvalidReason.TOO_SHORT
|
||||
checkUsername("") assertIs UsernameUtil.InvalidReason.TOO_SHORT
|
||||
checkUsername("ab") assertIs UsernameUtil.InvalidReason.TOO_SHORT
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkUsername_tooLong() {
|
||||
checkUsername("abcdefghijklmnopqrstuvwxyz1234567") assertIs UsernameUtil.InvalidReason.TOO_LONG
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkUsername_startsWithNumber() {
|
||||
checkUsername("0abcdefg") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER
|
||||
checkUsername("9abcdefg") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER
|
||||
checkUsername("8675309") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkUsername_invalidCharacters() {
|
||||
checkUsername("\$abcd") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
|
||||
checkUsername(" abcd") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
|
||||
checkUsername("ab cde") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
|
||||
checkUsername("%%%%%") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
|
||||
checkUsername("-----") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
|
||||
checkUsername("asĸ_me") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
|
||||
checkUsername("+18675309") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkUsername_validUsernames() {
|
||||
checkUsername("abcd").assertIsNull()
|
||||
checkUsername("abcdefghijklmnopqrstuvwxyz").assertIsNull()
|
||||
checkUsername("ABCDEFGHIJKLMNOPQRSTUVWXYZ").assertIsNull()
|
||||
checkUsername("web_head").assertIsNull()
|
||||
checkUsername("Spider_Fan_1991").assertIsNull()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user