Implement new API endpoints for Usernames.

This commit is contained in:
Alex Hart
2022-09-08 10:59:02 -03:00
committed by Greyson Parrelli
parent ca0e52e141
commit 9b9453734c
20 changed files with 516 additions and 199 deletions

View File

@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
@@ -341,11 +342,11 @@ public class RetrieveProfileJob extends BaseJob {
SignalServiceProfile profile = profileAndCredential.getProfile();
ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
setProfileName(recipient, profile.getName());
boolean wroteNewProfileName = setProfileName(recipient, profile.getName());
setProfileAbout(recipient, profile.getAbout(), profile.getAboutEmoji());
setProfileAvatar(recipient, profile.getAvatar());
setProfileBadges(recipient, profile.getBadges());
clearUsername(recipient);
setProfileCapabilities(recipient, profile.getCapabilities());
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
@@ -353,6 +354,10 @@ public class RetrieveProfileJob extends BaseJob {
profileAndCredential.getExpiringProfileKeyCredential()
.ifPresent(profileKeyCredential -> setExpiringProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential));
}
if (recipient.hasNonUsernameDisplayName(context) || wroteNewProfileName) {
clearUsername(recipient);
}
}
private void setProfileBadges(@NonNull Recipient recipient, @Nullable List<SignalServiceProfile.Badge> serviceBadges) {
@@ -436,16 +441,16 @@ public class RetrieveProfileJob extends BaseJob {
}
}
private void setProfileName(Recipient recipient, String profileName) {
private boolean setProfileName(Recipient recipient, String profileName) {
try {
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
if (profileKey == null) return;
if (profileKey == null) return false;
String plaintextProfileName = Util.emptyIfNull(ProfileUtil.decryptString(profileKey, profileName));
if (TextUtils.isEmpty(plaintextProfileName)) {
Log.w(TAG, "No name set on the profile for " + recipient.getId() + " -- Leaving it alone");
return;
return false;
}
ProfileName remoteProfileName = ProfileName.fromSerialized(plaintextProfileName);
@@ -470,12 +475,16 @@ public class RetrieveProfileJob extends BaseJob {
Log.i(TAG, String.format(Locale.US, "Name changed, but wasn't relevant to write an event. blocked: %s, group: %s, self: %s, firstSet: %s, displayChange: %s",
recipient.isBlocked(), recipient.isGroup(), recipient.isSelf(), localDisplayName.isEmpty(), !remoteDisplayName.equals(localDisplayName)));
}
return true;
}
} catch (InvalidCiphertextException e) {
Log.w(TAG, "Bad profile key for " + recipient.getId());
} catch (IOException e) {
Log.w(TAG, e);
}
return false;
}
private void setProfileAbout(@NonNull Recipient recipient, @Nullable String encryptedAbout, @Nullable String encryptedEmoji) {

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -15,16 +16,15 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.CircularProgressIndicatorSpec;
import com.google.android.material.progressindicator.IndeterminateDrawable;
import com.google.android.material.textfield.TextInputLayout;
import org.signal.core.util.DimensionUnit;
@@ -38,10 +38,8 @@ import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
import java.util.Objects;
import java.util.function.Consumer;
public class UsernameEditFragment extends LoggingFragment {
@@ -76,19 +74,20 @@ public class UsernameEditFragment extends LoggingFragment {
lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged));
viewModel.getEvents().observe(getViewLifecycleOwner(), this::onEvent);
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted(binding.usernameText.getText().toString()));
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
binding.usernameText.setText(Recipient.self().getUsername().orElse(null));
UsernameState usernameState = Recipient.self().getUsername().<UsernameState>map(UsernameState.Set::new).orElse(UsernameState.NoUsername.INSTANCE);
binding.usernameText.setText(usernameState.getNickname());
binding.usernameText.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(String text) {
viewModel.onUsernameUpdated(text);
public void onTextChanged(@NonNull String text) {
viewModel.onNicknameUpdated(text);
}
});
binding.usernameText.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
viewModel.onUsernameSubmitted(binding.usernameText.getText().toString());
viewModel.onUsernameSubmitted();
return true;
}
return false;
@@ -118,9 +117,10 @@ public class UsernameEditFragment extends LoggingFragment {
layoutParams.topMargin = suffixTextView.getPaddingTop();
layoutParams.bottomMargin = suffixTextView.getPaddingBottom();
layoutParams.setMarginEnd(suffixTextView.getPaddingEnd());
suffixProgress = new ImageView(requireContext());
suffixProgress.setImageDrawable(UsernameSuffix.getInProgressDrawable(requireContext()));
suffixProgress.setImageDrawable(getInProgressDrawable());
suffixParent.addView(suffixProgress, 0, layoutParams);
suffixTextView.setOnClickListener(this::onLearnMore);
@@ -148,7 +148,7 @@ public class UsernameEditFragment extends LoggingFragment {
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
usernameInput.setEnabled(true);
presentSuffix(state.getUsernameSuffix());
presentSuffix(state.getUsername());
switch (state.getButtonState()) {
case SUBMIT:
@@ -224,18 +224,14 @@ 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 AVAILABLE:
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_available));
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_accent_green)));
break;
}
}
private void presentSuffix(@NonNull UsernameSuffix usernameSuffix) {
binding.usernameTextWrapper.setSuffixText(usernameSuffix.getCharSequence());
private void presentSuffix(@NonNull UsernameState usernameState) {
binding.usernameTextWrapper.setSuffixText(usernameState.getDiscriminator());
boolean isInProgress = usernameSuffix.isInProgress();
boolean isInProgress = usernameState.isInProgress();
if (isInProgress) {
suffixProgress.setVisibility(View.VISIBLE);
@@ -244,11 +240,23 @@ public class UsernameEditFragment extends LoggingFragment {
}
}
private IndeterminateDrawable<CircularProgressIndicatorSpec> getInProgressDrawable() {
CircularProgressIndicatorSpec spec = new CircularProgressIndicatorSpec(requireContext(), null);
spec.indicatorInset = 0;
spec.indicatorSize = (int) DimensionUnit.DP.toPixels(16f);
spec.trackColor = ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant);
spec.trackThickness = (int) DimensionUnit.DP.toPixels(1f);
IndeterminateDrawable<CircularProgressIndicatorSpec> drawable = IndeterminateDrawable.createCircularDrawable(requireContext(), spec);
drawable.setBounds(0, 0, spec.indicatorSize, spec.indicatorSize);
return drawable;
}
private void onEvent(@NonNull UsernameEditViewModel.Event event) {
switch (event) {
case SUBMIT_SUCCESS:
ResultContract.setUsernameCreated(getParentFragmentManager());
Toast.makeText(requireContext(), R.string.UsernameEditFragment_successfully_set_username, Toast.LENGTH_SHORT).show();
NavHostFragment.findNavController(this).popBackStack();
break;
case SUBMIT_FAIL_TAKEN:

View File

@@ -1,18 +1,19 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.signal.core.util.Result;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException;
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse;
import java.io.IOException;
import java.util.concurrent.Executor;
@@ -21,18 +22,20 @@ class UsernameEditRepository {
private static final String TAG = Log.tag(UsernameEditRepository.class);
private final Application application;
private final SignalServiceAccountManager accountManager;
private final Executor executor;
UsernameEditRepository() {
this.application = ApplicationDependencies.getApplication();
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
this.executor = SignalExecutors.UNBOUNDED;
}
void setUsername(@NonNull String username, @NonNull Callback<UsernameSetResult> callback) {
executor.execute(() -> callback.onComplete(setUsernameInternal(username)));
void reserveUsername(@NonNull String nickname, @NonNull Callback<Result<ReserveUsernameResponse, UsernameSetResult>> callback) {
executor.execute(() -> callback.onComplete(reserveUsernameInternal(nickname)));
}
void confirmUsername(@NonNull ReserveUsernameResponse reserveUsernameResponse, @NonNull Callback<UsernameSetResult> callback) {
executor.execute(() -> callback.onComplete(confirmUsernameInternal(reserveUsernameResponse)));
}
void deleteUsername(@NonNull Callback<UsernameDeleteResult> callback) {
@@ -40,20 +43,38 @@ class UsernameEditRepository {
}
@WorkerThread
private @NonNull UsernameSetResult setUsernameInternal(@NonNull String username) {
private @NonNull Result<ReserveUsernameResponse, UsernameSetResult> reserveUsernameInternal(@NonNull String nickname) {
try {
accountManager.setUsername(username);
SignalDatabase.recipients().setUsername(Recipient.self().getId(), username);
Log.i(TAG, "[setUsername] Successfully set username.");
ReserveUsernameResponse username = accountManager.reserveUsername(nickname);
Log.i(TAG, "[reserveUsername] Successfully reserved username.");
return Result.success(username);
} catch (UsernameTakenException e) {
Log.w(TAG, "[reserveUsername] Username taken.");
return Result.failure(UsernameSetResult.USERNAME_UNAVAILABLE);
} catch (UsernameMalformedException e) {
Log.w(TAG, "[reserveUsername] Username malformed.");
return Result.failure(UsernameSetResult.USERNAME_INVALID);
} catch (IOException e) {
Log.w(TAG, "[reserveUsername] Generic network exception.", e);
return Result.failure(UsernameSetResult.NETWORK_ERROR);
}
}
@WorkerThread
private @NonNull UsernameSetResult confirmUsernameInternal(@NonNull ReserveUsernameResponse reserveUsernameResponse) {
try {
accountManager.confirmUsername(reserveUsernameResponse);
SignalDatabase.recipients().setUsername(Recipient.self().getId(), reserveUsernameResponse.getUsername());
Log.i(TAG, "[confirmUsername] Successfully reserved username.");
return UsernameSetResult.SUCCESS;
} catch (UsernameTakenException e) {
Log.w(TAG, "[setUsername] Username taken.");
Log.w(TAG, "[confirmUsername] Username gone.");
return UsernameSetResult.USERNAME_UNAVAILABLE;
} catch (UsernameMalformedException e) {
Log.w(TAG, "[setUsername] Username malformed.");
} catch (UsernameIsNotReservedException e) {
Log.w(TAG, "[confirmUsername] Username was not reserved.");
return UsernameSetResult.USERNAME_INVALID;
} catch (IOException e) {
Log.w(TAG, "[setUsername] Generic network exception.", e);
Log.w(TAG, "[confirmUsername] Generic network exception.", e);
return UsernameSetResult.NETWORK_ERROR;
}
}
@@ -79,10 +100,6 @@ class UsernameEditRepository {
SUCCESS, NETWORK_ERROR
}
enum UsernameAvailableResult {
TRUE, FALSE, NETWORK_ERROR
}
interface Callback<E> {
void onComplete(E result);
}

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.app.Application;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -9,86 +8,126 @@ import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason;
import org.thoughtcrime.securesms.util.rx.RxStore;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.processors.PublishProcessor;
import io.reactivex.rxjava3.schedulers.Schedulers;
/**
* Manages the state around username updates.
*
* A note on naming conventions:
*
* Usernames are made up of two discrete components, a nickname and a discriminator. They are formatted thusly:
*
* [nickname]#[discriminator]
*
* The nickname is user-controlled, whereas the discriminator is controlled by the server.
*/
class UsernameEditViewModel extends ViewModel {
private static final String TAG = Log.tag(UsernameEditViewModel.class);
private static final long NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS = 500;
private final Application application;
private final SingleLiveEvent<Event> events;
private final UsernameEditRepository repo;
private final RxStore<State> uiState;
private final SingleLiveEvent<Event> events;
private final UsernameEditRepository repo;
private final RxStore<State> uiState;
private final PublishProcessor<String> nicknamePublisher;
private final CompositeDisposable disposables;
private UsernameEditViewModel() {
this.application = ApplicationDependencies.getApplication();
this.repo = new UsernameEditRepository();
this.uiState = new RxStore<>(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameSuffix.NONE), Schedulers.computation());
this.events = new SingleLiveEvent<>();
this.repo = new UsernameEditRepository();
this.uiState = new RxStore<>(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, Recipient.self().getUsername().<UsernameState>map(UsernameState.Set::new).orElse(UsernameState.NoUsername.INSTANCE)), Schedulers.computation());
this.events = new SingleLiveEvent<>();
this.nicknamePublisher = PublishProcessor.create();
this.disposables = new CompositeDisposable();
Disposable disposable = nicknamePublisher.debounce(NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
.subscribe(this::onNicknameChanged);
disposables.add(disposable);
}
void onUsernameUpdated(@NonNull String username) {
@Override
protected void onCleared() {
super.onCleared();
disposables.clear();
}
void onNicknameUpdated(@NonNull String nickname) {
uiState.update(state -> {
if (TextUtils.isEmpty(username) && Recipient.self().getUsername().isPresent()) {
return new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameSuffix);
if (TextUtils.isEmpty(nickname) && Recipient.self().getUsername().isPresent()) {
return new State(ButtonState.DELETE, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE);
}
if (username.equals(Recipient.self().getUsername().orElse(null))) {
return new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameSuffix);
}
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(nickname);
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(username);
return invalidReason.map(reason -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(reason), state.usernameSuffix))
.orElseGet(() -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameSuffix));
return invalidReason.map(reason -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(reason), state.usernameState))
.orElseGet(() -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
});
nicknamePublisher.onNext(nickname);
}
void onUsernameSubmitted(@NonNull String username) {
if (username.equals(Recipient.self().getUsername().orElse(null))) {
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameSuffix));
void onUsernameSubmitted() {
UsernameState usernameState = uiState.getState().getUsername();
if (!(usernameState instanceof UsernameState.Reserved)) {
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
return;
}
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(username);
if (Objects.equals(usernameState.getUsername(), Recipient.self().getUsername().orElse(null))) {
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
return;
}
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(usernameState.getNickname());
if (invalidReason.isPresent()) {
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason.get()), state.usernameSuffix));
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason.get()), state.usernameState));
return;
}
uiState.update(state -> new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, state.usernameSuffix));
uiState.update(state -> new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, state.usernameState));
repo.setUsername(username, (result) -> {
repo.confirmUsername(((UsernameState.Reserved) usernameState).getReserveUsernameResponse(), (result) -> {
ThreadUtil.runOnMain(() -> {
String nickname = usernameState.getNickname();
switch (result) {
case SUCCESS:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameSuffix));
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
events.postValue(Event.SUBMIT_SUCCESS);
break;
case USERNAME_INVALID:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, state.usernameSuffix));
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, state.usernameState));
events.postValue(Event.SUBMIT_FAIL_INVALID);
if (nickname != null) {
onNicknameUpdated(nickname);
}
break;
case USERNAME_UNAVAILABLE:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, state.usernameSuffix));
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, state.usernameState));
events.postValue(Event.SUBMIT_FAIL_TAKEN);
if (nickname != null) {
onNicknameUpdated(nickname);
}
break;
case NETWORK_ERROR:
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameSuffix));
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameState));
events.postValue(Event.NETWORK_FAILURE);
break;
}
@@ -97,17 +136,17 @@ class UsernameEditViewModel extends ViewModel {
}
void onUsernameDeleted() {
uiState.update(state -> new State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.usernameSuffix));
uiState.update(state -> new State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.usernameState));
repo.deleteUsername((result) -> {
ThreadUtil.runOnMain(() -> {
switch (result) {
case SUCCESS:
uiState.update(state -> new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameSuffix));
uiState.update(state -> new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameState));
events.postValue(Event.DELETE_SUCCESS);
break;
case NETWORK_ERROR:
uiState.update(state -> new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameSuffix));
uiState.update(state -> new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameState));
events.postValue(Event.NETWORK_FAILURE);
break;
}
@@ -123,28 +162,68 @@ class UsernameEditViewModel extends ViewModel {
return events;
}
private void onNicknameChanged(@NonNull String nickname) {
if (TextUtils.isEmpty(nickname)) {
return;
}
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading.INSTANCE));
repo.reserveUsername(nickname, result -> {
ThreadUtil.runOnMain(() -> {
result.either(
reserveUsernameJsonResponse -> {
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, new UsernameState.Reserved(reserveUsernameJsonResponse)));
return null;
},
failure -> {
switch (failure) {
case SUCCESS:
throw new AssertionError();
case USERNAME_INVALID:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, UsernameState.NoUsername.INSTANCE));
break;
case USERNAME_UNAVAILABLE:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername.INSTANCE));
break;
case NETWORK_ERROR:
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE));
events.postValue(Event.NETWORK_FAILURE);
break;
}
return null;
});
});
});
}
private static UsernameStatus mapUsernameError(@NonNull InvalidReason invalidReason) {
switch (invalidReason) {
case TOO_SHORT: return UsernameStatus.TOO_SHORT;
case TOO_LONG: return UsernameStatus.TOO_LONG;
case STARTS_WITH_NUMBER: return UsernameStatus.CANNOT_START_WITH_NUMBER;
case INVALID_CHARACTERS: return UsernameStatus.INVALID_CHARACTERS;
default: return UsernameStatus.INVALID_GENERIC;
case TOO_SHORT:
return UsernameStatus.TOO_SHORT;
case TOO_LONG:
return UsernameStatus.TOO_LONG;
case STARTS_WITH_NUMBER:
return UsernameStatus.CANNOT_START_WITH_NUMBER;
case INVALID_CHARACTERS:
return UsernameStatus.INVALID_CHARACTERS;
default:
return UsernameStatus.INVALID_GENERIC;
}
}
static class State {
private final ButtonState buttonState;
private final UsernameStatus usernameStatus;
private final UsernameSuffix usernameSuffix;
private final UsernameState usernameState;
private State(@NonNull ButtonState buttonState,
@NonNull UsernameStatus usernameStatus,
@NonNull UsernameSuffix usernameSuffix)
@NonNull UsernameState usernameState)
{
this.buttonState = buttonState;
this.usernameStatus = usernameStatus;
this.usernameSuffix = usernameSuffix;
this.usernameState = usernameState;
}
@NonNull ButtonState getButtonState() {
@@ -155,13 +234,13 @@ class UsernameEditViewModel extends ViewModel {
return usernameStatus;
}
@NonNull UsernameSuffix getUsernameSuffix() {
return usernameSuffix;
@NonNull UsernameState getUsername() {
return usernameState;
}
}
enum UsernameStatus {
NONE, AVAILABLE, 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
}
enum ButtonState {
@@ -174,7 +253,7 @@ class UsernameEditViewModel extends ViewModel {
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new UsernameEditViewModel());
}

View File

@@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.profiles.manage
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
/**
* Describes the state of the username suffix, which is a spanned CharSequence.
*/
sealed class UsernameState {
protected open val username: String? = null
open val isInProgress: Boolean = false
object Loading : UsernameState() {
override val isInProgress: Boolean = true
}
object NoUsername : UsernameState()
data class Reserved(
val reserveUsernameResponse: ReserveUsernameResponse
) : UsernameState() {
override val username: String? = reserveUsernameResponse.username
}
data class Set(
override val username: String
) : UsernameState()
fun getNickname(): String? {
return username?.split('#')?.firstOrNull()
}
fun getDiscriminator(): String? {
return username?.split('#')?.lastOrNull()
}
}

View File

@@ -1,43 +0,0 @@
package org.thoughtcrime.securesms.profiles.manage
import android.content.Context
import androidx.core.content.ContextCompat
import com.google.android.material.progressindicator.CircularProgressIndicatorSpec
import com.google.android.material.progressindicator.IndeterminateDrawable
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
/**
* Describes the state of the username suffix, which is a spanned CharSequence.
*/
data class UsernameSuffix(
val charSequence: CharSequence?
) {
val isInProgress = charSequence == null
companion object {
@JvmField
val LOADING = UsernameSuffix(null)
@JvmField
val NONE = UsernameSuffix("")
@JvmStatic
fun fromCode(code: Int) = UsernameSuffix("#$code")
@JvmStatic
fun getInProgressDrawable(context: Context): IndeterminateDrawable<CircularProgressIndicatorSpec> {
val progressIndicatorSpec = CircularProgressIndicatorSpec(context, null).apply {
indicatorInset = 0
indicatorSize = DimensionUnit.DP.toPixels(16f).toInt()
trackColor = ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant)
trackThickness = DimensionUnit.DP.toPixels(1f).toInt()
}
return IndeterminateDrawable.createCircularDrawable(context, progressIndicatorSpec).apply {
setBounds(0, 0, DimensionUnit.DP.toPixels(16f).toInt(), DimensionUnit.DP.toPixels(16f).toInt())
}
}
}
}

View File

@@ -570,6 +570,37 @@ public class Recipient {
}
public @NonNull String getDisplayName(@NonNull Context context) {
String name = getNameFromLocalData(context);
if (Util.isEmpty(name)) {
name = context.getString(R.string.Recipient_unknown);
}
return StringUtil.isolateBidi(name);
}
public @NonNull String getDisplayNameOrUsername(@NonNull Context context) {
String name = getNameFromLocalData(context);
if (Util.isEmpty(name)) {
name = StringUtil.isolateBidi(username);
}
if (Util.isEmpty(name)) {
name = StringUtil.isolateBidi(context.getString(R.string.Recipient_unknown));
}
return StringUtil.isolateBidi(name);
}
public boolean hasNonUsernameDisplayName(@NonNull Context context) {
return getNameFromLocalData(context) != null;
}
/**
* @return local name for user ignoring the username.
*/
private @Nullable String getNameFromLocalData(@NonNull Context context) {
String name = getGroupName(context);
if (Util.isEmpty(name)) {
@@ -588,40 +619,6 @@ public class Recipient {
name = email;
}
if (Util.isEmpty(name)) {
name = context.getString(R.string.Recipient_unknown);
}
return StringUtil.isolateBidi(name);
}
public @NonNull String getDisplayNameOrUsername(@NonNull Context context) {
String name = getGroupName(context);
if (Util.isEmpty(name)) {
name = systemContactName;
}
if (Util.isEmpty(name)) {
name = StringUtil.isolateBidi(getProfileName().toString());
}
if (Util.isEmpty(name) && !Util.isEmpty(e164)) {
name = PhoneNumberFormatter.prettyPrint(e164);
}
if (Util.isEmpty(name)) {
name = StringUtil.isolateBidi(email);
}
if (Util.isEmpty(name)) {
name = StringUtil.isolateBidi(username);
}
if (Util.isEmpty(name)) {
name = StringUtil.isolateBidi(context.getString(R.string.Recipient_unknown));
}
return name;
}

View File

@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.IOException;
@@ -67,8 +68,8 @@ public class UsernameUtil {
try {
Log.d(TAG, "No local user with this username. Searching remotely.");
SignalServiceProfile profile = ApplicationDependencies.getSignalServiceMessageReceiver().retrieveProfileByUsername(username, Optional.empty(), Locale.getDefault());
return Optional.ofNullable(profile.getServiceId());
ACI aci = ApplicationDependencies.getSignalServiceAccountManager().getAciByUsername(username);
return Optional.ofNullable(aci);
} catch (IOException e) {
return Optional.empty();
}