mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 03:58:48 +00:00
Add ability to set custom username discriminators.
This commit is contained in:
@@ -3,14 +3,12 @@ package org.thoughtcrime.securesms.profiles.manage;
|
||||
import android.animation.LayoutTransition;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -25,25 +23,22 @@ import androidx.navigation.fragment.NavHostFragment;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
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.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FragmentResultContract;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class UsernameEditFragment extends LoggingFragment {
|
||||
|
||||
private static final float DISABLED_ALPHA = 0.5f;
|
||||
public static final String IGNORE_TEXT_CHANGE_EVENT = "ignore.text.change.event";
|
||||
|
||||
private UsernameEditViewModel viewModel;
|
||||
private UsernameEditFragmentBinding binding;
|
||||
@@ -99,6 +94,7 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
|
||||
lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged));
|
||||
lifecycleDisposable.add(viewModel.getEvents().subscribe(this::onEvent));
|
||||
lifecycleDisposable.add(viewModel.getUsernameInputState().subscribe(this::presentUsernameInputState));
|
||||
|
||||
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
|
||||
binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
|
||||
@@ -112,10 +108,23 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
binding.usernameText.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(@NonNull String text) {
|
||||
if (binding.usernameText.getTag() != IGNORE_TEXT_CHANGE_EVENT) {
|
||||
viewModel.onNicknameUpdated(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
binding.usernameText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
|
||||
binding.discriminatorText.setText(usernameState.getDiscriminator());
|
||||
binding.discriminatorText.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(@NonNull String text) {
|
||||
if (binding.discriminatorText.getTag() != IGNORE_TEXT_CHANGE_EVENT) {
|
||||
viewModel.onDiscriminatorUpdated(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
binding.discriminatorText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
viewModel.onUsernameSubmitted();
|
||||
return true;
|
||||
@@ -127,24 +136,9 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
binding.usernameDescription.setLearnMoreVisible(true);
|
||||
binding.usernameDescription.setOnLinkClickListener(this::onLearnMore);
|
||||
|
||||
initializeSuffix();
|
||||
ViewUtil.focusAndShowKeyboard(binding.usernameText);
|
||||
}
|
||||
|
||||
private void initializeSuffix() {
|
||||
TextView suffixTextView = binding.usernameTextWrapper.getSuffixTextView();
|
||||
Drawable pipe = Objects.requireNonNull(ContextCompat.getDrawable(requireContext(), R.drawable.pipe_divider));
|
||||
|
||||
pipe.setBounds(0, 0, (int) DimensionUnit.DP.toPixels(1f), (int) DimensionUnit.DP.toPixels(20f));
|
||||
suffixTextView.setCompoundDrawablesRelative(pipe, null, null, null);
|
||||
|
||||
ViewUtil.setLeftMargin(suffixTextView, (int) DimensionUnit.DP.toPixels(16f));
|
||||
|
||||
binding.usernameTextWrapper.getSuffixTextView().setCompoundDrawablePadding((int) DimensionUnit.DP.toPixels(16f));
|
||||
|
||||
suffixTextView.setOnClickListener(this::onLearnMore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
@@ -162,7 +156,7 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
private void onUiStateChanged(@NonNull UsernameEditViewModel.State state) {
|
||||
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
|
||||
|
||||
presentSuffix(state.username);
|
||||
presentProgressState(state.username);
|
||||
presentButtonState(state.buttonState);
|
||||
presentSummary(state.username);
|
||||
|
||||
@@ -174,7 +168,7 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
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.setError(getResources().getString(R.string.UsernameEditFragment_usernames_must_be_between_a_and_b_characters, UsernameUtil.MIN_NICKNAME_LENGTH, UsernameUtil.MAX_NICKNAME_LENGTH));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||
|
||||
break;
|
||||
@@ -197,6 +191,22 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_taken));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||
|
||||
break;
|
||||
case DISCRIMINATOR_HAS_INVALID_CHARACTERS:
|
||||
case DISCRIMINATOR_NOT_AVAILABLE:
|
||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment__this_username_is_not_available_try_another_number));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||
|
||||
break;
|
||||
case DISCRIMINATOR_TOO_LONG:
|
||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment__invalid_username_enter_a_maximum_of_d_digits, UsernameUtil.MAX_DISCRIMINATOR_LENGTH));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||
|
||||
break;
|
||||
case DISCRIMINATOR_TOO_SHORT:
|
||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment__invalid_username_enter_a_minimum_of_d_digits, UsernameUtil.MIN_DISCRIMINATOR_LENGTH));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -302,9 +312,25 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void presentSuffix(@NonNull UsernameState usernameState) {
|
||||
binding.usernameTextWrapper.setSuffixText(usernameState.getDiscriminator());
|
||||
private void presentUsernameInputState(@NonNull UsernameEditStateMachine.State state) {
|
||||
binding.usernameText.setTag(IGNORE_TEXT_CHANGE_EVENT);
|
||||
String nickname = state.getNickname();
|
||||
if (!binding.usernameText.getText().toString().equals(nickname)) {
|
||||
binding.usernameText.setText(state.getNickname());
|
||||
binding.usernameText.setSelection(binding.usernameText.length());
|
||||
}
|
||||
binding.usernameText.setTag(null);
|
||||
|
||||
binding.discriminatorText.setTag(IGNORE_TEXT_CHANGE_EVENT);
|
||||
String discriminator = state.getDiscriminator();
|
||||
if (!binding.discriminatorText.getText().toString().equals(discriminator)) {
|
||||
binding.discriminatorText.setText(state.getDiscriminator());
|
||||
binding.discriminatorText.setSelection(binding.discriminatorText.length());
|
||||
}
|
||||
binding.discriminatorText.setTag(null);
|
||||
}
|
||||
|
||||
private void presentProgressState(@NonNull UsernameState usernameState) {
|
||||
boolean isInProgress = usernameState.isInProgress();
|
||||
|
||||
if (isInProgress) {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.profiles.manage
|
||||
|
||||
object UsernameEditStateMachine {
|
||||
|
||||
enum class StateModifier {
|
||||
USER,
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
sealed class State {
|
||||
abstract val nickname: String
|
||||
abstract val discriminator: String
|
||||
abstract val stateModifier: StateModifier
|
||||
|
||||
abstract fun onUserChangedNickname(nickname: String): State
|
||||
abstract fun onUserChangedDiscriminator(discriminator: String): State
|
||||
abstract fun onSystemChangedNickname(nickname: String): State
|
||||
abstract fun onSystemChangedDiscriminator(discriminator: String): State
|
||||
}
|
||||
|
||||
/**
|
||||
* This state is representative of when the user has not manually changed either field in
|
||||
* the form, and it is assumed that both values are either blank or system provided.
|
||||
*
|
||||
* This can be thought of as our "initial state" and can be pre-populated with username information
|
||||
* for the local user.
|
||||
*/
|
||||
data class NoUserEntry(
|
||||
override val nickname: String,
|
||||
override val discriminator: String,
|
||||
override val stateModifier: StateModifier
|
||||
) : State() {
|
||||
override fun onUserChangedNickname(nickname: String): State {
|
||||
return if (nickname.isBlank()) {
|
||||
NoUserEntry(
|
||||
nickname = "",
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.USER
|
||||
)
|
||||
} else {
|
||||
UserEnteredNickname(
|
||||
nickname = nickname,
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.USER
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserChangedDiscriminator(discriminator: String): State {
|
||||
return if (discriminator.isBlank()) {
|
||||
NoUserEntry(
|
||||
nickname = nickname,
|
||||
discriminator = "",
|
||||
stateModifier = StateModifier.USER
|
||||
)
|
||||
} else {
|
||||
UserEnteredDiscriminator(
|
||||
nickname = nickname,
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.USER
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSystemChangedNickname(nickname: String): State {
|
||||
return copy(nickname = nickname, stateModifier = StateModifier.SYSTEM)
|
||||
}
|
||||
|
||||
override fun onSystemChangedDiscriminator(discriminator: String): State {
|
||||
return copy(discriminator = discriminator, stateModifier = StateModifier.SYSTEM)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has altered the nickname field with something that is non-empty.
|
||||
* The user has not altered the discriminator field.
|
||||
*/
|
||||
data class UserEnteredNickname(
|
||||
override val nickname: String,
|
||||
override val discriminator: String,
|
||||
override val stateModifier: StateModifier
|
||||
) : State() {
|
||||
override fun onUserChangedNickname(nickname: String): State {
|
||||
return if (nickname.isBlank()) {
|
||||
NoUserEntry(
|
||||
nickname = "",
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.USER
|
||||
)
|
||||
} else {
|
||||
copy(nickname = nickname, stateModifier = StateModifier.USER)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserChangedDiscriminator(discriminator: String): State {
|
||||
return if (discriminator.isBlank()) {
|
||||
copy(discriminator = "", stateModifier = StateModifier.USER)
|
||||
} else {
|
||||
UserEnteredNicknameAndDiscriminator(
|
||||
nickname = nickname,
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.USER
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSystemChangedNickname(nickname: String): State {
|
||||
return NoUserEntry(
|
||||
nickname = nickname,
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.SYSTEM
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSystemChangedDiscriminator(discriminator: String): State {
|
||||
return copy(discriminator = discriminator, stateModifier = StateModifier.SYSTEM)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has altered the discriminator field with something that is non-empty.
|
||||
* The user has not altered the nickname field.
|
||||
*/
|
||||
data class UserEnteredDiscriminator(
|
||||
override val nickname: String,
|
||||
override val discriminator: String,
|
||||
override val stateModifier: StateModifier
|
||||
) : State() {
|
||||
override fun onUserChangedNickname(nickname: String): State {
|
||||
return if (nickname.isBlank()) {
|
||||
copy(nickname = nickname, stateModifier = StateModifier.USER)
|
||||
} else if (discriminator.isBlank()) {
|
||||
UserEnteredNickname(
|
||||
nickname = nickname,
|
||||
discriminator = "",
|
||||
stateModifier = StateModifier.USER
|
||||
)
|
||||
} else {
|
||||
UserEnteredNicknameAndDiscriminator(
|
||||
nickname = nickname,
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.USER
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserChangedDiscriminator(discriminator: String): State {
|
||||
return copy(discriminator = discriminator, stateModifier = StateModifier.USER)
|
||||
}
|
||||
|
||||
override fun onSystemChangedNickname(nickname: String): State {
|
||||
return copy(nickname = nickname, stateModifier = StateModifier.SYSTEM)
|
||||
}
|
||||
|
||||
override fun onSystemChangedDiscriminator(discriminator: String): State {
|
||||
return NoUserEntry(
|
||||
nickname = nickname,
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.SYSTEM
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has altered the nickname field with something that is non-empty.
|
||||
* The user has altered the discriminator field with something that is non-empty.
|
||||
*/
|
||||
data class UserEnteredNicknameAndDiscriminator(
|
||||
override val nickname: String,
|
||||
override val discriminator: String,
|
||||
override val stateModifier: StateModifier
|
||||
) : State() {
|
||||
override fun onUserChangedNickname(nickname: String): State {
|
||||
return if (nickname.isBlank()) {
|
||||
UserEnteredDiscriminator(
|
||||
nickname = "",
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.USER
|
||||
)
|
||||
} else if (discriminator.isBlank()) {
|
||||
UserEnteredNickname(
|
||||
nickname = nickname,
|
||||
discriminator = "",
|
||||
stateModifier = StateModifier.USER
|
||||
)
|
||||
} else {
|
||||
copy(nickname = nickname, stateModifier = StateModifier.USER)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserChangedDiscriminator(discriminator: String): State {
|
||||
return copy(discriminator = discriminator, stateModifier = StateModifier.USER)
|
||||
}
|
||||
|
||||
override fun onSystemChangedNickname(nickname: String): State {
|
||||
return UserEnteredDiscriminator(
|
||||
nickname = nickname,
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.SYSTEM
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSystemChangedDiscriminator(discriminator: String): State {
|
||||
return UserEnteredNickname(
|
||||
nickname = nickname,
|
||||
discriminator = discriminator,
|
||||
stateModifier = StateModifier.SYSTEM
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,10 @@ 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.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.Result
|
||||
@@ -15,6 +16,7 @@ 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.util.UsernameUtil.InvalidReason
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil.checkDiscriminator
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil.checkUsername
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -32,7 +34,6 @@ import java.util.concurrent.TimeUnit
|
||||
*/
|
||||
internal class UsernameEditViewModel private constructor(private val isInRegistration: Boolean) : ViewModel() {
|
||||
private val events: PublishSubject<Event> = PublishSubject.create()
|
||||
private val nicknamePublisher: PublishProcessor<String> = PublishProcessor.create()
|
||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private val uiState: RxStore<State> = RxStore(
|
||||
@@ -44,10 +45,23 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
scheduler = Schedulers.computation()
|
||||
)
|
||||
|
||||
private val stateMachineStore = RxStore<UsernameEditStateMachine.State>(
|
||||
defaultValue = UsernameEditStateMachine.NoUserEntry(
|
||||
nickname = SignalStore.account().username?.split(UsernameState.DELIMITER)?.first() ?: "",
|
||||
discriminator = SignalStore.account().username?.split(UsernameState.DELIMITER)?.last() ?: "",
|
||||
stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM
|
||||
),
|
||||
scheduler = Schedulers.computation()
|
||||
)
|
||||
|
||||
val usernameInputState: Flowable<UsernameEditStateMachine.State> = stateMachineStore.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
init {
|
||||
disposables += nicknamePublisher
|
||||
disposables += stateMachineStore
|
||||
.stateFlowable
|
||||
.filter { it.stateModifier == UsernameEditStateMachine.StateModifier.USER }
|
||||
.debounce(NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.subscribe { nickname: String -> onNicknameUpdatedDebounced(nickname) }
|
||||
.subscribeBy(onNext = this::onUsernameStateUpdateDebounced)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -85,7 +99,31 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
}
|
||||
}
|
||||
|
||||
nicknamePublisher.onNext(nickname)
|
||||
stateMachineStore.update {
|
||||
it.onUserChangedNickname(nickname)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDiscriminatorUpdated(discriminator: String) {
|
||||
uiState.update { state: State ->
|
||||
if (discriminator.isBlank() && SignalStore.account().username != null) {
|
||||
return@update State(
|
||||
buttonState = if (isInRegistration) ButtonState.SUBMIT_DISABLED else ButtonState.DELETE,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = UsernameState.NoUsername
|
||||
)
|
||||
}
|
||||
|
||||
State(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = state.username
|
||||
)
|
||||
}
|
||||
|
||||
stateMachineStore.update {
|
||||
it.onUserChangedDiscriminator(discriminator)
|
||||
}
|
||||
}
|
||||
|
||||
fun onUsernameSkipped() {
|
||||
@@ -94,26 +132,38 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
}
|
||||
|
||||
fun onUsernameSubmitted() {
|
||||
val state = stateMachineStore.state
|
||||
|
||||
if (isCaseChange(state)) {
|
||||
handleUserConfirmation(UsernameRepository::updateUsername)
|
||||
} else {
|
||||
handleUserConfirmation(UsernameRepository::confirmUsername)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T : UsernameState> handleUserConfirmation(
|
||||
repositoryWorkerMethod: (T) -> Single<UsernameSetResult>
|
||||
) {
|
||||
val usernameState = uiState.state.username
|
||||
if (usernameState !is UsernameState.Reserved) {
|
||||
if (usernameState !is T) {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
||||
return
|
||||
}
|
||||
|
||||
if (usernameState.username == SignalStore.account().username) {
|
||||
if (usernameState.requireUsername() == SignalStore.account().username) {
|
||||
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) }
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, mapNicknameError(invalidReason), it.username) }
|
||||
return
|
||||
}
|
||||
|
||||
uiState.update { State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, it.username) }
|
||||
|
||||
disposables += UsernameRepository.confirmUsername(usernameState).subscribe { result: UsernameSetResult ->
|
||||
disposables += repositoryWorkerMethod(usernameState).subscribe { result: UsernameSetResult ->
|
||||
val nickname = usernameState.getNickname()
|
||||
|
||||
when (result) {
|
||||
@@ -169,19 +219,49 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
return events.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
private fun isCaseChange(state: UsernameEditStateMachine.State): Boolean {
|
||||
if (state is UsernameEditStateMachine.UserEnteredDiscriminator || state is UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator) {
|
||||
return false
|
||||
}
|
||||
|
||||
val newLower = state.nickname.lowercase()
|
||||
val oldLower = SignalStore.account().username?.split(UsernameState.DELIMITER)?.firstOrNull()?.lowercase()
|
||||
|
||||
return newLower == oldLower
|
||||
}
|
||||
|
||||
/** Triggered when the debounced nickname event stream fires. */
|
||||
private fun onNicknameUpdatedDebounced(nickname: String) {
|
||||
private fun onUsernameStateUpdateDebounced(state: UsernameEditStateMachine.State) {
|
||||
val nickname = state.nickname
|
||||
if (nickname.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (state is UsernameEditStateMachine.NoUserEntry || state.stateModifier == UsernameEditStateMachine.StateModifier.SYSTEM) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isCaseChange(state)) {
|
||||
val discriminator = SignalStore.account().username?.split(UsernameState.DELIMITER)?.lastOrNull() ?: error("Unexpected case change, no discriminator!")
|
||||
uiState.update {
|
||||
State(
|
||||
buttonState = ButtonState.SUBMIT,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = UsernameState.CaseChange("${state.nickname}${UsernameState.DELIMITER}$discriminator")
|
||||
)
|
||||
}
|
||||
|
||||
stateMachineStore.update { s -> s.onSystemChangedDiscriminator(discriminator) }
|
||||
return
|
||||
}
|
||||
|
||||
val invalidReason: InvalidReason? = checkUsername(nickname)
|
||||
if (invalidReason != null) {
|
||||
uiState.update { state ->
|
||||
uiState.update { uiState ->
|
||||
State(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = mapUsernameError(invalidReason),
|
||||
username = state.username
|
||||
usernameStatus = mapNicknameError(invalidReason),
|
||||
username = uiState.username
|
||||
)
|
||||
}
|
||||
return
|
||||
@@ -189,26 +269,60 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading) }
|
||||
|
||||
disposables += UsernameRepository.reserveUsername(nickname).subscribe { result: Result<UsernameState.Reserved, UsernameSetResult> ->
|
||||
val isDiscriminatorSetByUser = state is UsernameEditStateMachine.UserEnteredDiscriminator || state is UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator
|
||||
val discriminator = if (isDiscriminatorSetByUser) {
|
||||
state.discriminator
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val discriminatorInvalidReason = checkDiscriminator(discriminator)
|
||||
if (isDiscriminatorSetByUser && discriminatorInvalidReason != null) {
|
||||
uiState.update { s ->
|
||||
State(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = mapDiscriminatorError(discriminatorInvalidReason),
|
||||
username = s.username
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
disposables += UsernameRepository.reserveUsername(nickname, discriminator).subscribe { result: Result<UsernameState.Reserved, UsernameSetResult> ->
|
||||
result.either(
|
||||
onSuccess = { reserved: UsernameState.Reserved ->
|
||||
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, reserved) }
|
||||
|
||||
val d = reserved.getDiscriminator()
|
||||
if (!isDiscriminatorSetByUser && d != null) {
|
||||
stateMachineStore.update { s -> s.onSystemChangedDiscriminator(d) }
|
||||
}
|
||||
},
|
||||
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) }
|
||||
val status = if (isDiscriminatorSetByUser) {
|
||||
UsernameStatus.DISCRIMINATOR_NOT_AVAILABLE
|
||||
} else {
|
||||
UsernameStatus.TAKEN
|
||||
}
|
||||
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, status, 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) }
|
||||
@@ -226,7 +340,17 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
)
|
||||
|
||||
enum class UsernameStatus {
|
||||
NONE, TAKEN, TOO_SHORT, TOO_LONG, CANNOT_START_WITH_NUMBER, INVALID_CHARACTERS, INVALID_GENERIC
|
||||
NONE,
|
||||
TAKEN,
|
||||
TOO_SHORT,
|
||||
TOO_LONG,
|
||||
CANNOT_START_WITH_NUMBER,
|
||||
INVALID_CHARACTERS,
|
||||
INVALID_GENERIC,
|
||||
DISCRIMINATOR_NOT_AVAILABLE,
|
||||
DISCRIMINATOR_TOO_SHORT,
|
||||
DISCRIMINATOR_TOO_LONG,
|
||||
DISCRIMINATOR_HAS_INVALID_CHARACTERS
|
||||
}
|
||||
|
||||
enum class ButtonState {
|
||||
@@ -246,7 +370,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
companion object {
|
||||
private const val NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS: Long = 1000
|
||||
|
||||
private fun mapUsernameError(invalidReason: InvalidReason): UsernameStatus {
|
||||
private fun mapNicknameError(invalidReason: InvalidReason): UsernameStatus {
|
||||
return when (invalidReason) {
|
||||
InvalidReason.TOO_SHORT -> UsernameStatus.TOO_SHORT
|
||||
InvalidReason.TOO_LONG -> UsernameStatus.TOO_LONG
|
||||
@@ -255,5 +379,14 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
else -> UsernameStatus.INVALID_GENERIC
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapDiscriminatorError(invalidReason: InvalidReason): UsernameStatus {
|
||||
return when (invalidReason) {
|
||||
InvalidReason.TOO_SHORT -> UsernameStatus.DISCRIMINATOR_TOO_SHORT
|
||||
InvalidReason.TOO_LONG -> UsernameStatus.DISCRIMINATOR_TOO_LONG
|
||||
InvalidReason.INVALID_CHARACTERS -> UsernameStatus.DISCRIMINATOR_HAS_INVALID_CHARACTERS
|
||||
else -> UsernameStatus.INVALID_GENERIC
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,12 +88,21 @@ object UsernameRepository {
|
||||
|
||||
private val accountManager: SignalServiceAccountManager get() = ApplicationDependencies.getSignalServiceAccountManager()
|
||||
|
||||
/**
|
||||
* Given a username, update the username link, given that all was changed was the casing of the nickname.
|
||||
*/
|
||||
fun updateUsername(caseChange: UsernameState.CaseChange): Single<UsernameSetResult> {
|
||||
return Single
|
||||
.fromCallable { updateUsernameInternal(caseChange) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a nickname, this will temporarily reserve a matching discriminator that can later be confirmed via [confirmUsername].
|
||||
*/
|
||||
fun reserveUsername(nickname: String): Single<Result<UsernameState.Reserved, UsernameSetResult>> {
|
||||
fun reserveUsername(nickname: String, discriminator: String?): Single<Result<UsernameState.Reserved, UsernameSetResult>> {
|
||||
return Single
|
||||
.fromCallable { reserveUsernameInternal(nickname) }
|
||||
.fromCallable { reserveUsernameInternal(nickname, discriminator) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@@ -295,9 +304,13 @@ object UsernameRepository {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun reserveUsernameInternal(nickname: String): Result<UsernameState.Reserved, UsernameSetResult> {
|
||||
private fun reserveUsernameInternal(nickname: String, discriminator: String?): Result<UsernameState.Reserved, UsernameSetResult> {
|
||||
return try {
|
||||
val candidates: List<Username> = Username.candidatesFrom(nickname, UsernameUtil.MIN_LENGTH, UsernameUtil.MAX_LENGTH)
|
||||
val candidates: List<Username> = if (discriminator == null) {
|
||||
Username.candidatesFrom(nickname, UsernameUtil.MIN_NICKNAME_LENGTH, UsernameUtil.MAX_NICKNAME_LENGTH)
|
||||
} else {
|
||||
listOf(Username("$nickname${UsernameState.DELIMITER}$discriminator"))
|
||||
}
|
||||
|
||||
val hashes: List<String> = candidates
|
||||
.map { Base64.encodeUrlSafeWithoutPadding(it.hash) }
|
||||
@@ -327,6 +340,31 @@ object UsernameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun updateUsernameInternal(caseChange: UsernameState.CaseChange): UsernameSetResult {
|
||||
return try {
|
||||
val oldUsernameLink = SignalStore.account().usernameLink ?: return UsernameSetResult.USERNAME_INVALID
|
||||
val username = Username(caseChange.username)
|
||||
val newUsernameLink = username.generateLink(oldUsernameLink.entropy)
|
||||
val usernameLinkComponents = accountManager.updateUsernameLink(newUsernameLink)
|
||||
|
||||
SignalStore.account().username = username.username
|
||||
SignalStore.account().usernameLink = usernameLinkComponents
|
||||
SignalDatabase.recipients.setUsername(Recipient.self().id, username.username)
|
||||
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
|
||||
SignalStore.account().usernameSyncErrorCount = 0
|
||||
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
Log.i(TAG, "[updateUsername] Successfully updated username.")
|
||||
|
||||
UsernameSetResult.SUCCESS
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "[updateUsername] Generic network exception.", e)
|
||||
UsernameSetResult.NETWORK_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun confirmUsernameInternal(reserved: UsernameState.Reserved): UsernameSetResult {
|
||||
return try {
|
||||
|
||||
@@ -10,6 +10,8 @@ sealed class UsernameState {
|
||||
protected open val username: String? = null
|
||||
open val isInProgress: Boolean = false
|
||||
|
||||
fun requireUsername(): String = username!!
|
||||
|
||||
object Loading : UsernameState() {
|
||||
override val isInProgress: Boolean = true
|
||||
}
|
||||
@@ -21,6 +23,10 @@ sealed class UsernameState {
|
||||
val reserveUsernameResponse: ReserveUsernameResponse
|
||||
) : UsernameState()
|
||||
|
||||
data class CaseChange(
|
||||
public override val username: String
|
||||
) : UsernameState()
|
||||
|
||||
data class Set(
|
||||
override val username: String
|
||||
) : UsernameState()
|
||||
|
||||
@@ -6,9 +6,11 @@ import java.util.regex.Pattern
|
||||
|
||||
object UsernameUtil {
|
||||
private val TAG = Log.tag(UsernameUtil::class.java)
|
||||
const val MIN_LENGTH = 3
|
||||
const val MAX_LENGTH = 32
|
||||
private val FULL_PATTERN = Pattern.compile(String.format(Locale.US, "^[a-zA-Z_][a-zA-Z0-9_]{%d,%d}$", MIN_LENGTH - 1, MAX_LENGTH - 1), Pattern.CASE_INSENSITIVE)
|
||||
const val MIN_NICKNAME_LENGTH = 3
|
||||
const val MAX_NICKNAME_LENGTH = 32
|
||||
const val MIN_DISCRIMINATOR_LENGTH = 2
|
||||
const val MAX_DISCRIMINATOR_LENGTH = 10
|
||||
private val FULL_PATTERN = Pattern.compile(String.format(Locale.US, "^[a-zA-Z_][a-zA-Z0-9_]{%d,%d}$", MIN_NICKNAME_LENGTH - 1, MAX_NICKNAME_LENGTH - 1), Pattern.CASE_INSENSITIVE)
|
||||
private val DIGIT_START_PATTERN = Pattern.compile("^[0-9].*$")
|
||||
private const val BASE_URL_SCHEMELESS = "signal.me/#eu/"
|
||||
private const val BASE_URL = "https://$BASE_URL_SCHEMELESS"
|
||||
@@ -17,8 +19,8 @@ object UsernameUtil {
|
||||
String.format(
|
||||
Locale.US,
|
||||
"^@?[a-zA-Z_][a-zA-Z0-9_]{%d,%d}(.[0-9]+)?$",
|
||||
MIN_LENGTH - 1,
|
||||
MAX_LENGTH - 1,
|
||||
MIN_NICKNAME_LENGTH - 1,
|
||||
MAX_NICKNAME_LENGTH - 1,
|
||||
Pattern.CASE_INSENSITIVE
|
||||
)
|
||||
)
|
||||
@@ -39,10 +41,10 @@ object UsernameUtil {
|
||||
value == null -> {
|
||||
InvalidReason.TOO_SHORT
|
||||
}
|
||||
value.length < MIN_LENGTH -> {
|
||||
value.length < MIN_NICKNAME_LENGTH -> {
|
||||
InvalidReason.TOO_SHORT
|
||||
}
|
||||
value.length > MAX_LENGTH -> {
|
||||
value.length > MAX_NICKNAME_LENGTH -> {
|
||||
InvalidReason.TOO_LONG
|
||||
}
|
||||
DIGIT_START_PATTERN.matcher(value).matches() -> {
|
||||
@@ -57,6 +59,26 @@ object UsernameUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun checkDiscriminator(value: String?): InvalidReason? {
|
||||
return when {
|
||||
value == null -> {
|
||||
null
|
||||
}
|
||||
value.length < MIN_DISCRIMINATOR_LENGTH -> {
|
||||
InvalidReason.TOO_SHORT
|
||||
}
|
||||
value.length > MAX_DISCRIMINATOR_LENGTH -> {
|
||||
InvalidReason.TOO_LONG
|
||||
}
|
||||
value.toIntOrNull() == null -> {
|
||||
InvalidReason.INVALID_CHARACTERS
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class InvalidReason {
|
||||
TOO_SHORT,
|
||||
TOO_LONG,
|
||||
|
||||
@@ -35,24 +35,34 @@
|
||||
app:srcCompat="@drawable/symbol_at_24"
|
||||
app:tint="@color/signal_colorOnSurface" />
|
||||
|
||||
<View
|
||||
android:id="@+id/username_box_fill"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/signal_colorSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="@id/username_text_wrapper"
|
||||
app:layout_constraintEnd_toEndOf="@id/discriminator_text"
|
||||
app:layout_constraintStart_toStartOf="@id/username_text_wrapper"
|
||||
app:layout_constraintTop_toTopOf="@id/username_text_wrapper" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/username_text_wrapper"
|
||||
style="@style/Widget.Signal.TextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:boxStrokeColor="@color/signal_colorPrimary"
|
||||
app:boxStrokeWidthFocused="2dp"
|
||||
app:boxStrokeWidth="0dp"
|
||||
app:boxStrokeWidthFocused="0dp"
|
||||
app:errorTextAppearance="@style/Signal.Text.Zero"
|
||||
app:expandedHintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toStartOf="@id/suffix_progress"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/summary"
|
||||
app:suffixTextColor="@color/signal_colorOnSurface"
|
||||
tools:errorEnabled="true"
|
||||
tools:suffixText="| .1234">
|
||||
app:suffixTextColor="@color/signal_colorOnSurface">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/username_text"
|
||||
@@ -60,7 +70,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/UsernameEditFragment_username"
|
||||
android:imeOptions="actionDone"
|
||||
android:imeOptions="actionNext"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
@@ -71,18 +81,54 @@
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/discriminator_text"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:background="@null"
|
||||
android:hint="@string/UsernameEditFragment__00"
|
||||
android:imeOptions="actionDone"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:minHeight="48dp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="@id/username_text_wrapper"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="21" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/suffix_progress"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/username_text_wrapper"
|
||||
app:layout_constraintBottom_toBottomOf="@id/username_text_wrapper"
|
||||
app:indicatorColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:indicatorSize="16dp"
|
||||
app:trackThickness="1dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/username_text_wrapper"
|
||||
app:layout_constraintEnd_toStartOf="@id/discriminator_text"
|
||||
app:trackColor="@color/transparent"
|
||||
app:indicatorColor="@color/signal_colorOnSurfaceVariant"/>
|
||||
app:trackThickness="1dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="20dp"
|
||||
android:background="@color/signal_colorOutline"
|
||||
app:layout_constraintBottom_toBottomOf="@id/discriminator_text"
|
||||
app:layout_constraintStart_toStartOf="@id/discriminator_text"
|
||||
app:layout_constraintTop_toTopOf="@id/discriminator_text" />
|
||||
|
||||
<View
|
||||
android:id="@+id/username_text_focused_stroke"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="2dp"
|
||||
android:background="@color/signal_colorPrimary"
|
||||
app:layout_constraintBottom_toBottomOf="@id/username_text_wrapper"
|
||||
app:layout_constraintEnd_toEndOf="@id/discriminator_text"
|
||||
app:layout_constraintStart_toStartOf="@id/username_text_wrapper" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/username_error"
|
||||
@@ -92,8 +138,8 @@
|
||||
android:textAppearance="@style/Signal.Text.BodyMedium"
|
||||
android:textColor="@color/signal_colorError"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="@id/username_description"
|
||||
app:layout_constraintEnd_toEndOf="@id/username_description"
|
||||
app:layout_constraintStart_toStartOf="@id/username_description"
|
||||
app:layout_constraintTop_toBottomOf="@id/username_text_wrapper"
|
||||
tools:text="Error something bad happened. Very super long error message that wraps"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -2202,6 +2202,8 @@
|
||||
<string name="UnverifiedSendDialog_send">Send</string>
|
||||
|
||||
<!-- UsernameEditFragment -->
|
||||
<!-- Placeholder text for custom discriminator -->
|
||||
<string name="UsernameEditFragment__00">00</string>
|
||||
<!-- 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 -->
|
||||
@@ -2224,6 +2226,12 @@
|
||||
<string name="UsernameEditFragment__skip">Skip</string>
|
||||
<!-- Content description for done button -->
|
||||
<string name="UsernameEditFragment__done">Done</string>
|
||||
<!-- Displayed when the chosen discriminator is not available for the given nickname -->
|
||||
<string name="UsernameEditFragment__this_username_is_not_available_try_another_number">This username is not available, try another number.</string>
|
||||
<!-- Displayed when the chosen discriminator is too short -->'
|
||||
<string name="UsernameEditFragment__invalid_username_enter_a_minimum_of_d_digits">Invalid username, enter a minimum of %1$d digits.</string>
|
||||
<!-- Displayed when the chosen discriminator is too long -->'
|
||||
<string name="UsernameEditFragment__invalid_username_enter_a_maximum_of_d_digits">Invalid username, enter a maximum of %1$d digits.</string>
|
||||
|
||||
<plurals name="UserNotificationMigrationJob_d_contacts_are_on_signal">
|
||||
<item quantity="one">%d contact is on Signal!</item>
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage
|
||||
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.assertIs
|
||||
|
||||
class UsernameEditStateMachineTest {
|
||||
@Test
|
||||
fun `Given NoUserEntry, when user clears the username field, then I expect NoUserEntry with empty username and copied discriminator`() {
|
||||
val given = UsernameEditStateMachine.NoUserEntry(nickname = "MilesMorales", discriminator = "07", stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = "", discriminator = "07", stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedNickname("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given NoUserEntry, when user enters text into the username field, then I expect UserEnteredNickname with given username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.NoUserEntry(nickname = "MilesMorales", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedNickname(nickname)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given NoUserEntry, when user clears the discriminator field, then I expect NoUserEntry with given username and empty discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedDiscriminator("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given NoUserEntry, when user enters text into the discriminator field, then I expect UserEnteredDiscriminator with given discriminator and copied nickname`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedDiscriminator(discriminator)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given NoUserEntry, when system clears the username field, then I expect NoUserEntry with empty username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = "", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedNickname("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given NoUserEntry, when system enters text into the username field, then I expect NoUserEntry with given username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.NoUserEntry(nickname = "MilesMorales", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedNickname(nickname)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given NoUserEntry, when system clears the discriminator field, then I expect NoUserEntry with given username and empty discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedDiscriminator("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given NoUserEntry, when system enters text into the discriminator field, then I expect UserEnteredDiscriminator with given discriminator and copied nickname`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedDiscriminator(discriminator)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNickname, when user clears the username field, then I expect NoUserEntry with empty username and copied discriminator`() {
|
||||
val given = UsernameEditStateMachine.UserEnteredNickname(nickname = "MilesMorales", discriminator = "07", stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = "", discriminator = "07", stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedNickname("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNickname, when user enters text into the username field, then I expect UserEnteredNickname with given username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNickname(nickname = "MilesMorales", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedNickname(nickname)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNickname, when user clears the discriminator field, then I expect UserEnteredNickname with given username and empty discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedDiscriminator("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNickname, when user enters text into the discriminator field, then I expect UserEnteredNicknameAndDiscriminator with given discriminator and copied nickname`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedDiscriminator(discriminator)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNickname, when system clears the username field, then I expect NoUserEntry with empty username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = "", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedNickname("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNickname, when system enters text into the username field, then I expect NoUserEntry with given username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNickname(nickname = "MilesMorales", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedNickname(nickname)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNickname, when system clears the discriminator field, then I expect UserEnteredNickname with given username and empty discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedDiscriminator("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNickname, when system enters text into the discriminator field, then I expect UserEnteredDiscriminator with given discriminator and copied nickname`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedDiscriminator(discriminator)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredDiscriminator, when user clears the username field, then I expect NoUserEntry with empty username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = "", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedNickname("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredDiscriminator, when user enters text into the username field, then I expect UserEnteredNicknameAndDiscriminator with given username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = "MilesMorales", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedNickname(nickname)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredDiscriminator with an empty discriminator, when user enters text into the username field, then I expect UserEnteredNickname with given username and empty discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = ""
|
||||
val given = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = "MilesMorales", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedNickname(nickname)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredDiscriminator, when user clears the discriminator field, then I expect UserEnteredDiscriminator with given username and empty discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedDiscriminator("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredDiscriminator, when user enters text into the discriminator field, then I expect UserEnteredDiscriminator with given discriminator and copied nickname`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedDiscriminator(discriminator)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredDiscriminator, when system clears the username field, then I expect UserEnteredDiscriminator with empty username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = "", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedNickname("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredDiscriminator, when system enters text into the username field, then I expect UserEnteredDiscriminator with given username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = "MilesMorales", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedNickname(nickname)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredDiscriminator, when system clears the discriminator field, then I expect NoUserEntry with given username and empty discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedDiscriminator("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredDiscriminator, when system enters text into the discriminator field, then I expect NoUserEntry with given discriminator and copied nickname`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.NoUserEntry(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedDiscriminator(discriminator)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNicknameAndDiscriminator, when user clears the username field, then I expect UserEnteredDiscriminator with empty username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = "", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedNickname("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNicknameAndDiscriminator, when user enters text into the username field, then I expect UserEnteredNicknameAndDiscriminator with given username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = "MilesMorales", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedNickname(nickname)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNicknameAndDiscriminator with empty discriminator, when user enters text into the username field, then I expect UserEnteredNickname with given username and empty discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = ""
|
||||
val given = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = "MilesMorales", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedNickname(nickname)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNicknameAndDiscriminator, when user clears the discriminator field, then I expect UserEnteredNicknameAndDiscriminator with given username and empty discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedDiscriminator("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNicknameAndDiscriminator, when user enters text into the discriminator field, then I expect UserEnteredNicknameAndDiscriminator with given discriminator and copied nickname`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val actual = given.onUserChangedDiscriminator(discriminator)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNicknameAndDiscriminator, when system clears the username field, then I expect UserEnteredDiscriminator with empty username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = "", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedNickname("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNicknameAndDiscriminator, when system enters text into the username field, then I expect NoUserEntry with given username and copied discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = "MilesMorales", discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.UserEnteredDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedNickname(nickname)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNicknameAndDiscriminator, when system clears the discriminator field, then I expect UserEnteredNickname with given username and empty discriminator`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = "", stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedDiscriminator("")
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given UserEnteredNicknameAndDiscriminator, when system enters text into the discriminator field, then I expect NoUserEntry with given discriminator and copied nickname`() {
|
||||
val nickname = "Nick"
|
||||
val discriminator = "07"
|
||||
val given = UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.USER)
|
||||
val expected = UsernameEditStateMachine.UserEnteredNickname(nickname = nickname, discriminator = discriminator, stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM)
|
||||
val actual = given.onSystemChangedDiscriminator(discriminator)
|
||||
|
||||
actual assertIs expected
|
||||
}
|
||||
}
|
||||
@@ -787,6 +787,12 @@ public class SignalServiceAccountManager {
|
||||
this.pushServiceSocket.confirmUsername(username, reserveUsernameResponse);
|
||||
}
|
||||
|
||||
public UsernameLinkComponents updateUsernameLink(UsernameLink newUsernameLink) throws IOException {
|
||||
UUID serverId = this.pushServiceSocket.createUsernameLink(Base64.encodeUrlSafeWithoutPadding(newUsernameLink.getEncryptedUsername()), true);
|
||||
|
||||
return new UsernameLinkComponents(newUsernameLink.getEntropy(), serverId);
|
||||
}
|
||||
|
||||
public void deleteUsername() throws IOException {
|
||||
this.pushServiceSocket.deleteUsername();
|
||||
}
|
||||
@@ -794,7 +800,7 @@ public class SignalServiceAccountManager {
|
||||
public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
|
||||
try {
|
||||
UsernameLink link = username.generateLink();
|
||||
UUID serverId = this.pushServiceSocket.createUsernameLink(Base64.encodeUrlSafeWithPadding(link.getEncryptedUsername()));
|
||||
UUID serverId = this.pushServiceSocket.createUsernameLink(Base64.encodeUrlSafeWithPadding(link.getEncryptedUsername()), false);
|
||||
|
||||
return new UsernameLinkComponents(link.getEntropy(), serverId);
|
||||
} catch (BaseUsernameException e) {
|
||||
|
||||
@@ -1147,8 +1147,8 @@ public class PushServiceSocket {
|
||||
* @param encryptedUsername URL-safe base64-encoded encrypted username
|
||||
* @return The serverId for the generated link.
|
||||
*/
|
||||
public UUID createUsernameLink(String encryptedUsername) throws IOException {
|
||||
String response = makeServiceRequest(USERNAME_LINK_PATH, "PUT", JsonUtil.toJson(new SetUsernameLinkRequestBody(encryptedUsername)));
|
||||
public UUID createUsernameLink(String encryptedUsername, boolean keepLinkHandle) throws IOException {
|
||||
String response = makeServiceRequest(USERNAME_LINK_PATH, "PUT", JsonUtil.toJson(new SetUsernameLinkRequestBody(encryptedUsername, keepLinkHandle)));
|
||||
SetUsernameLinkResponseBody parsed = JsonUtil.fromJson(response, SetUsernameLinkResponseBody.class);
|
||||
|
||||
return parsed.getUsernameLinkHandle();
|
||||
|
||||
@@ -3,4 +3,4 @@ package org.whispersystems.signalservice.internal.push
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
/** Request body for setting a username link on the service. */
|
||||
data class SetUsernameLinkRequestBody(@JsonProperty val usernameLinkEncryptedValue: String)
|
||||
data class SetUsernameLinkRequestBody(@JsonProperty val usernameLinkEncryptedValue: String, @JsonProperty val keepLinkHandle: Boolean)
|
||||
|
||||
Reference in New Issue
Block a user