diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java
index 1b30790fc5..ca705f673e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java
@@ -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);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java
deleted file mode 100644
index 307feddc81..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java
+++ /dev/null
@@ -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.
- *
- * 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.
- */
-class UsernameEditViewModel extends ViewModel {
-
- private static final long NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS = 500;
-
- private final PublishSubject events;
- private final UsernameRepository repo;
- private final RxStore uiState;
- private final PublishProcessor 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().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 = 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 = 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 getUiState() {
- return uiState.getStateFlowable().observeOn(AndroidSchedulers.mainThread());
- }
-
- @NonNull Observable 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 create(@NonNull Class modelClass) {
- //noinspection ConstantConditions
- return modelClass.cast(new UsernameEditViewModel(isInRegistration));
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt
new file mode 100644
index 0000000000..c1166cfe7e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt
@@ -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 = PublishSubject.create()
+ private val repo: UsernameRepository = UsernameRepository()
+ private val nicknamePublisher: PublishProcessor = PublishProcessor.create()
+ private val disposables: CompositeDisposable = CompositeDisposable()
+
+ private val uiState: RxStore = RxStore(
+ defaultValue = State(
+ buttonState = ButtonState.SUBMIT_DISABLED,
+ usernameStatus = UsernameStatus.NONE,
+ username = Recipient.self().username.map { 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 {
+ return uiState.stateFlowable.observeOn(AndroidSchedulers.mainThread())
+ }
+
+ fun getEvents(): Observable {
+ 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 ->
+ 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 create(modelClass: Class): 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
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt
index 11c3bf22de..3680c42b63 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt
@@ -33,19 +33,26 @@ object UsernameUtil {
}
@JvmStatic
- fun checkUsername(value: String?): Optional {
- 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
+ }
}
}
diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/UsernameUtilTest.java b/app/src/test/java/org/thoughtcrime/securesms/util/UsernameUtilTest.java
deleted file mode 100644
index 00f0cce462..0000000000
--- a/app/src/test/java/org/thoughtcrime/securesms/util/UsernameUtilTest.java
+++ /dev/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());
- }
-}
diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/UsernameUtilTest.kt b/app/src/test/java/org/thoughtcrime/securesms/util/UsernameUtilTest.kt
new file mode 100644
index 0000000000..2ef519ba3f
--- /dev/null
+++ b/app/src/test/java/org/thoughtcrime/securesms/util/UsernameUtilTest.kt
@@ -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()
+ }
+}