mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 12:08:34 +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.animation.LayoutTransition;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
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.dialog.MaterialAlertDialogBuilder;
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
import com.google.android.material.textfield.TextInputLayout;
|
||||||
|
|
||||||
import org.signal.core.util.DimensionUnit;
|
|
||||||
import org.thoughtcrime.securesms.LoggingFragment;
|
import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||||
import org.thoughtcrime.securesms.databinding.UsernameEditFragmentBinding;
|
import org.thoughtcrime.securesms.databinding.UsernameEditFragmentBinding;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.util.FragmentResultContract;
|
import org.thoughtcrime.securesms.util.FragmentResultContract;
|
||||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class UsernameEditFragment extends LoggingFragment {
|
public class UsernameEditFragment extends LoggingFragment {
|
||||||
|
|
||||||
private static final float DISABLED_ALPHA = 0.5f;
|
private static final float DISABLED_ALPHA = 0.5f;
|
||||||
|
public static final String IGNORE_TEXT_CHANGE_EVENT = "ignore.text.change.event";
|
||||||
|
|
||||||
private UsernameEditViewModel viewModel;
|
private UsernameEditViewModel viewModel;
|
||||||
private UsernameEditFragmentBinding binding;
|
private UsernameEditFragmentBinding binding;
|
||||||
@@ -99,6 +94,7 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
|
|
||||||
lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged));
|
lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged));
|
||||||
lifecycleDisposable.add(viewModel.getEvents().subscribe(this::onEvent));
|
lifecycleDisposable.add(viewModel.getEvents().subscribe(this::onEvent));
|
||||||
|
lifecycleDisposable.add(viewModel.getUsernameInputState().subscribe(this::presentUsernameInputState));
|
||||||
|
|
||||||
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
|
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
|
||||||
binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
|
binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
|
||||||
@@ -112,10 +108,23 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
binding.usernameText.addTextChangedListener(new SimpleTextWatcher() {
|
binding.usernameText.addTextChangedListener(new SimpleTextWatcher() {
|
||||||
@Override
|
@Override
|
||||||
public void onTextChanged(@NonNull String text) {
|
public void onTextChanged(@NonNull String text) {
|
||||||
|
if (binding.usernameText.getTag() != IGNORE_TEXT_CHANGE_EVENT) {
|
||||||
viewModel.onNicknameUpdated(text);
|
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) {
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
viewModel.onUsernameSubmitted();
|
viewModel.onUsernameSubmitted();
|
||||||
return true;
|
return true;
|
||||||
@@ -127,24 +136,9 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
binding.usernameDescription.setLearnMoreVisible(true);
|
binding.usernameDescription.setLearnMoreVisible(true);
|
||||||
binding.usernameDescription.setOnLinkClickListener(this::onLearnMore);
|
binding.usernameDescription.setOnLinkClickListener(this::onLearnMore);
|
||||||
|
|
||||||
initializeSuffix();
|
|
||||||
ViewUtil.focusAndShowKeyboard(binding.usernameText);
|
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
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
@@ -162,7 +156,7 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
private void onUiStateChanged(@NonNull UsernameEditViewModel.State state) {
|
private void onUiStateChanged(@NonNull UsernameEditViewModel.State state) {
|
||||||
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
|
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
|
||||||
|
|
||||||
presentSuffix(state.username);
|
presentProgressState(state.username);
|
||||||
presentButtonState(state.buttonState);
|
presentButtonState(state.buttonState);
|
||||||
presentSummary(state.username);
|
presentSummary(state.username);
|
||||||
|
|
||||||
@@ -174,7 +168,7 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
break;
|
break;
|
||||||
case TOO_SHORT:
|
case TOO_SHORT:
|
||||||
case TOO_LONG:
|
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)));
|
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -197,6 +191,22 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_taken));
|
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_taken));
|
||||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,9 +312,25 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void presentSuffix(@NonNull UsernameState usernameState) {
|
private void presentUsernameInputState(@NonNull UsernameEditStateMachine.State state) {
|
||||||
binding.usernameTextWrapper.setSuffixText(usernameState.getDiscriminator());
|
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();
|
boolean isInProgress = usernameState.isInProgress();
|
||||||
|
|
||||||
if (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.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
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.schedulers.Schedulers
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
import org.signal.core.util.Result
|
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.UsernameDeleteResult
|
||||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameSetResult
|
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameSetResult
|
||||||
import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason
|
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.UsernameUtil.checkUsername
|
||||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -32,7 +34,6 @@ import java.util.concurrent.TimeUnit
|
|||||||
*/
|
*/
|
||||||
internal class UsernameEditViewModel private constructor(private val isInRegistration: Boolean) : ViewModel() {
|
internal class UsernameEditViewModel private constructor(private val isInRegistration: Boolean) : ViewModel() {
|
||||||
private val events: PublishSubject<Event> = PublishSubject.create()
|
private val events: PublishSubject<Event> = PublishSubject.create()
|
||||||
private val nicknamePublisher: PublishProcessor<String> = PublishProcessor.create()
|
|
||||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
private val disposables: CompositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
private val uiState: RxStore<State> = RxStore(
|
private val uiState: RxStore<State> = RxStore(
|
||||||
@@ -44,10 +45,23 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
|||||||
scheduler = Schedulers.computation()
|
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 {
|
init {
|
||||||
disposables += nicknamePublisher
|
disposables += stateMachineStore
|
||||||
|
.stateFlowable
|
||||||
|
.filter { it.stateModifier == UsernameEditStateMachine.StateModifier.USER }
|
||||||
.debounce(NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
|
.debounce(NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
|
||||||
.subscribe { nickname: String -> onNicknameUpdatedDebounced(nickname) }
|
.subscribeBy(onNext = this::onUsernameStateUpdateDebounced)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
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() {
|
fun onUsernameSkipped() {
|
||||||
@@ -94,26 +132,38 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onUsernameSubmitted() {
|
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
|
val usernameState = uiState.state.username
|
||||||
if (usernameState !is UsernameState.Reserved) {
|
if (usernameState !is T) {
|
||||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usernameState.username == SignalStore.account().username) {
|
if (usernameState.requireUsername() == SignalStore.account().username) {
|
||||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val invalidReason = checkUsername(usernameState.getNickname())
|
val invalidReason = checkUsername(usernameState.getNickname())
|
||||||
if (invalidReason != null) {
|
if (invalidReason != null) {
|
||||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason), it.username) }
|
uiState.update { State(ButtonState.SUBMIT_DISABLED, mapNicknameError(invalidReason), it.username) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uiState.update { State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, it.username) }
|
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()
|
val nickname = usernameState.getNickname()
|
||||||
|
|
||||||
when (result) {
|
when (result) {
|
||||||
@@ -169,19 +219,49 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
|||||||
return events.observeOn(AndroidSchedulers.mainThread())
|
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. */
|
/** 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()) {
|
if (nickname.isBlank()) {
|
||||||
return
|
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)
|
val invalidReason: InvalidReason? = checkUsername(nickname)
|
||||||
if (invalidReason != null) {
|
if (invalidReason != null) {
|
||||||
uiState.update { state ->
|
uiState.update { uiState ->
|
||||||
State(
|
State(
|
||||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||||
usernameStatus = mapUsernameError(invalidReason),
|
usernameStatus = mapNicknameError(invalidReason),
|
||||||
username = state.username
|
username = uiState.username
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -189,26 +269,60 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
|||||||
|
|
||||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading) }
|
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(
|
result.either(
|
||||||
onSuccess = { reserved: UsernameState.Reserved ->
|
onSuccess = { reserved: UsernameState.Reserved ->
|
||||||
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, 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 ->
|
onFailure = { failure: UsernameSetResult ->
|
||||||
when (failure) {
|
when (failure) {
|
||||||
UsernameSetResult.SUCCESS -> {
|
UsernameSetResult.SUCCESS -> {
|
||||||
throw AssertionError()
|
throw AssertionError()
|
||||||
}
|
}
|
||||||
|
|
||||||
UsernameSetResult.USERNAME_INVALID -> {
|
UsernameSetResult.USERNAME_INVALID -> {
|
||||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, UsernameState.NoUsername) }
|
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, UsernameState.NoUsername) }
|
||||||
}
|
}
|
||||||
|
|
||||||
UsernameSetResult.USERNAME_UNAVAILABLE -> {
|
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 -> {
|
UsernameSetResult.NETWORK_ERROR -> {
|
||||||
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername) }
|
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername) }
|
||||||
events.onNext(Event.NETWORK_FAILURE)
|
events.onNext(Event.NETWORK_FAILURE)
|
||||||
}
|
}
|
||||||
|
|
||||||
UsernameSetResult.CANDIDATE_GENERATION_ERROR -> {
|
UsernameSetResult.CANDIDATE_GENERATION_ERROR -> {
|
||||||
// TODO -- Retry
|
// TODO -- Retry
|
||||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername) }
|
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 {
|
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 {
|
enum class ButtonState {
|
||||||
@@ -246,7 +370,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
|||||||
companion object {
|
companion object {
|
||||||
private const val NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS: Long = 1000
|
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) {
|
return when (invalidReason) {
|
||||||
InvalidReason.TOO_SHORT -> UsernameStatus.TOO_SHORT
|
InvalidReason.TOO_SHORT -> UsernameStatus.TOO_SHORT
|
||||||
InvalidReason.TOO_LONG -> UsernameStatus.TOO_LONG
|
InvalidReason.TOO_LONG -> UsernameStatus.TOO_LONG
|
||||||
@@ -255,5 +379,14 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
|||||||
else -> UsernameStatus.INVALID_GENERIC
|
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()
|
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].
|
* 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
|
return Single
|
||||||
.fromCallable { reserveUsernameInternal(nickname) }
|
.fromCallable { reserveUsernameInternal(nickname, discriminator) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,9 +304,13 @@ object UsernameRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun reserveUsernameInternal(nickname: String): Result<UsernameState.Reserved, UsernameSetResult> {
|
private fun reserveUsernameInternal(nickname: String, discriminator: String?): Result<UsernameState.Reserved, UsernameSetResult> {
|
||||||
return try {
|
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
|
val hashes: List<String> = candidates
|
||||||
.map { Base64.encodeUrlSafeWithoutPadding(it.hash) }
|
.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
|
@WorkerThread
|
||||||
private fun confirmUsernameInternal(reserved: UsernameState.Reserved): UsernameSetResult {
|
private fun confirmUsernameInternal(reserved: UsernameState.Reserved): UsernameSetResult {
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ sealed class UsernameState {
|
|||||||
protected open val username: String? = null
|
protected open val username: String? = null
|
||||||
open val isInProgress: Boolean = false
|
open val isInProgress: Boolean = false
|
||||||
|
|
||||||
|
fun requireUsername(): String = username!!
|
||||||
|
|
||||||
object Loading : UsernameState() {
|
object Loading : UsernameState() {
|
||||||
override val isInProgress: Boolean = true
|
override val isInProgress: Boolean = true
|
||||||
}
|
}
|
||||||
@@ -21,6 +23,10 @@ sealed class UsernameState {
|
|||||||
val reserveUsernameResponse: ReserveUsernameResponse
|
val reserveUsernameResponse: ReserveUsernameResponse
|
||||||
) : UsernameState()
|
) : UsernameState()
|
||||||
|
|
||||||
|
data class CaseChange(
|
||||||
|
public override val username: String
|
||||||
|
) : UsernameState()
|
||||||
|
|
||||||
data class Set(
|
data class Set(
|
||||||
override val username: String
|
override val username: String
|
||||||
) : UsernameState()
|
) : UsernameState()
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import java.util.regex.Pattern
|
|||||||
|
|
||||||
object UsernameUtil {
|
object UsernameUtil {
|
||||||
private val TAG = Log.tag(UsernameUtil::class.java)
|
private val TAG = Log.tag(UsernameUtil::class.java)
|
||||||
const val MIN_LENGTH = 3
|
const val MIN_NICKNAME_LENGTH = 3
|
||||||
const val MAX_LENGTH = 32
|
const val MAX_NICKNAME_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_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 val DIGIT_START_PATTERN = Pattern.compile("^[0-9].*$")
|
||||||
private const val BASE_URL_SCHEMELESS = "signal.me/#eu/"
|
private const val BASE_URL_SCHEMELESS = "signal.me/#eu/"
|
||||||
private const val BASE_URL = "https://$BASE_URL_SCHEMELESS"
|
private const val BASE_URL = "https://$BASE_URL_SCHEMELESS"
|
||||||
@@ -17,8 +19,8 @@ object UsernameUtil {
|
|||||||
String.format(
|
String.format(
|
||||||
Locale.US,
|
Locale.US,
|
||||||
"^@?[a-zA-Z_][a-zA-Z0-9_]{%d,%d}(.[0-9]+)?$",
|
"^@?[a-zA-Z_][a-zA-Z0-9_]{%d,%d}(.[0-9]+)?$",
|
||||||
MIN_LENGTH - 1,
|
MIN_NICKNAME_LENGTH - 1,
|
||||||
MAX_LENGTH - 1,
|
MAX_NICKNAME_LENGTH - 1,
|
||||||
Pattern.CASE_INSENSITIVE
|
Pattern.CASE_INSENSITIVE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -39,10 +41,10 @@ object UsernameUtil {
|
|||||||
value == null -> {
|
value == null -> {
|
||||||
InvalidReason.TOO_SHORT
|
InvalidReason.TOO_SHORT
|
||||||
}
|
}
|
||||||
value.length < MIN_LENGTH -> {
|
value.length < MIN_NICKNAME_LENGTH -> {
|
||||||
InvalidReason.TOO_SHORT
|
InvalidReason.TOO_SHORT
|
||||||
}
|
}
|
||||||
value.length > MAX_LENGTH -> {
|
value.length > MAX_NICKNAME_LENGTH -> {
|
||||||
InvalidReason.TOO_LONG
|
InvalidReason.TOO_LONG
|
||||||
}
|
}
|
||||||
DIGIT_START_PATTERN.matcher(value).matches() -> {
|
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 {
|
enum class InvalidReason {
|
||||||
TOO_SHORT,
|
TOO_SHORT,
|
||||||
TOO_LONG,
|
TOO_LONG,
|
||||||
|
|||||||
@@ -35,24 +35,34 @@
|
|||||||
app:srcCompat="@drawable/symbol_at_24"
|
app:srcCompat="@drawable/symbol_at_24"
|
||||||
app:tint="@color/signal_colorOnSurface" />
|
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
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/username_text_wrapper"
|
android:id="@+id/username_text_wrapper"
|
||||||
style="@style/Widget.Signal.TextInputLayout"
|
style="@style/Widget.Signal.TextInputLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
android:layout_marginEnd="16dp"
|
||||||
app:boxStrokeColor="@color/signal_colorPrimary"
|
app:boxStrokeColor="@color/signal_colorPrimary"
|
||||||
app:boxStrokeWidthFocused="2dp"
|
app:boxStrokeWidth="0dp"
|
||||||
|
app:boxStrokeWidthFocused="0dp"
|
||||||
app:errorTextAppearance="@style/Signal.Text.Zero"
|
app:errorTextAppearance="@style/Signal.Text.Zero"
|
||||||
app:expandedHintEnabled="false"
|
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_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/summary"
|
app:layout_constraintTop_toBottomOf="@id/summary"
|
||||||
app:suffixTextColor="@color/signal_colorOnSurface"
|
app:suffixTextColor="@color/signal_colorOnSurface">
|
||||||
tools:errorEnabled="true"
|
|
||||||
tools:suffixText="| .1234">
|
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/username_text"
|
android:id="@+id/username_text"
|
||||||
@@ -60,7 +70,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/UsernameEditFragment_username"
|
android:hint="@string/UsernameEditFragment_username"
|
||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionNext"
|
||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
@@ -71,18 +81,54 @@
|
|||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</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
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
android:id="@+id/suffix_progress"
|
android:id="@+id/suffix_progress"
|
||||||
android:layout_width="16dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="16dp"
|
android:layout_height="16dp"
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="10dp"
|
android:layout_marginBottom="10dp"
|
||||||
app:layout_constraintEnd_toEndOf="@id/username_text_wrapper"
|
app:indicatorColor="@color/signal_colorOnSurfaceVariant"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/username_text_wrapper"
|
|
||||||
app:indicatorSize="16dp"
|
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: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
|
<TextView
|
||||||
android:id="@+id/username_error"
|
android:id="@+id/username_error"
|
||||||
@@ -92,8 +138,8 @@
|
|||||||
android:textAppearance="@style/Signal.Text.BodyMedium"
|
android:textAppearance="@style/Signal.Text.BodyMedium"
|
||||||
android:textColor="@color/signal_colorError"
|
android:textColor="@color/signal_colorError"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintStart_toStartOf="@id/username_description"
|
|
||||||
app:layout_constraintEnd_toEndOf="@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"
|
app:layout_constraintTop_toBottomOf="@id/username_text_wrapper"
|
||||||
tools:text="Error something bad happened. Very super long error message that wraps"
|
tools:text="Error something bad happened. Very super long error message that wraps"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|||||||
@@ -2202,6 +2202,8 @@
|
|||||||
<string name="UnverifiedSendDialog_send">Send</string>
|
<string name="UnverifiedSendDialog_send">Send</string>
|
||||||
|
|
||||||
<!-- UsernameEditFragment -->
|
<!-- UsernameEditFragment -->
|
||||||
|
<!-- Placeholder text for custom discriminator -->
|
||||||
|
<string name="UsernameEditFragment__00">00</string>
|
||||||
<!-- Toolbar title when entering from registration -->
|
<!-- Toolbar title when entering from registration -->
|
||||||
<string name="UsernameEditFragment__add_a_username">Add a username</string>
|
<string name="UsernameEditFragment__add_a_username">Add a username</string>
|
||||||
<!-- Instructional text at the top of the username edit screen -->
|
<!-- Instructional text at the top of the username edit screen -->
|
||||||
@@ -2224,6 +2226,12 @@
|
|||||||
<string name="UsernameEditFragment__skip">Skip</string>
|
<string name="UsernameEditFragment__skip">Skip</string>
|
||||||
<!-- Content description for done button -->
|
<!-- Content description for done button -->
|
||||||
<string name="UsernameEditFragment__done">Done</string>
|
<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">
|
<plurals name="UserNotificationMigrationJob_d_contacts_are_on_signal">
|
||||||
<item quantity="one">%d contact is on Signal!</item>
|
<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);
|
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 {
|
public void deleteUsername() throws IOException {
|
||||||
this.pushServiceSocket.deleteUsername();
|
this.pushServiceSocket.deleteUsername();
|
||||||
}
|
}
|
||||||
@@ -794,7 +800,7 @@ public class SignalServiceAccountManager {
|
|||||||
public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
|
public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
|
||||||
try {
|
try {
|
||||||
UsernameLink link = username.generateLink();
|
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);
|
return new UsernameLinkComponents(link.getEntropy(), serverId);
|
||||||
} catch (BaseUsernameException e) {
|
} catch (BaseUsernameException e) {
|
||||||
|
|||||||
@@ -1147,8 +1147,8 @@ public class PushServiceSocket {
|
|||||||
* @param encryptedUsername URL-safe base64-encoded encrypted username
|
* @param encryptedUsername URL-safe base64-encoded encrypted username
|
||||||
* @return The serverId for the generated link.
|
* @return The serverId for the generated link.
|
||||||
*/
|
*/
|
||||||
public UUID createUsernameLink(String encryptedUsername) throws IOException {
|
public UUID createUsernameLink(String encryptedUsername, boolean keepLinkHandle) throws IOException {
|
||||||
String response = makeServiceRequest(USERNAME_LINK_PATH, "PUT", JsonUtil.toJson(new SetUsernameLinkRequestBody(encryptedUsername)));
|
String response = makeServiceRequest(USERNAME_LINK_PATH, "PUT", JsonUtil.toJson(new SetUsernameLinkRequestBody(encryptedUsername, keepLinkHandle)));
|
||||||
SetUsernameLinkResponseBody parsed = JsonUtil.fromJson(response, SetUsernameLinkResponseBody.class);
|
SetUsernameLinkResponseBody parsed = JsonUtil.fromJson(response, SetUsernameLinkResponseBody.class);
|
||||||
|
|
||||||
return parsed.getUsernameLinkHandle();
|
return parsed.getUsernameLinkHandle();
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ package org.whispersystems.signalservice.internal.push
|
|||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
/** Request body for setting a username link on the service. */
|
/** 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