mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Refactor username state to use Username models.
This commit is contained in:
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsNull
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.api.util.Usernames
|
||||
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -96,7 +97,7 @@ class UsernameEditFragmentTest {
|
||||
fun testNicknameUpdateHappyPath() {
|
||||
val nickname = "Spiderman"
|
||||
val discriminator = "4578"
|
||||
val username = "$nickname${UsernameState.DELIMITER}$discriminator"
|
||||
val username = "$nickname${Usernames.DELIMITER}$discriminator"
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Put("/v1/accounts/username/reserved") {
|
||||
|
||||
@@ -104,7 +104,7 @@ class EditProfileViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public Single<UsernameRepository.UsernameDeleteResult> deleteUsername() {
|
||||
return UsernameRepository.deleteUsername().observeOn(AndroidSchedulers.mainThread());
|
||||
return UsernameRepository.deleteUsernameAndLink().observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public boolean shouldShowUsername() {
|
||||
|
||||
@@ -101,10 +101,6 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
binding.usernameDoneButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
|
||||
binding.usernameSkipButton.setOnClickListener(v -> viewModel.onUsernameSkipped());
|
||||
|
||||
String username = SignalStore.account().getUsername();
|
||||
UsernameState usernameState = username != null ? new UsernameState.Set(username) : UsernameState.NoUsername.INSTANCE;
|
||||
|
||||
binding.usernameText.setText(usernameState.getNickname());
|
||||
binding.usernameText.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(@NonNull String text) {
|
||||
@@ -114,7 +110,6 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
}
|
||||
});
|
||||
|
||||
binding.discriminatorText.setText(usernameState.getDiscriminator());
|
||||
binding.discriminatorText.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(@NonNull String text) {
|
||||
@@ -156,9 +151,9 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
private void onUiStateChanged(@NonNull UsernameEditViewModel.State state) {
|
||||
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
|
||||
|
||||
presentProgressState(state.username);
|
||||
presentProgressState(state.usernameState);
|
||||
presentButtonState(state.buttonState);
|
||||
presentSummary(state.username);
|
||||
presentSummary(state.usernameState);
|
||||
|
||||
binding.root.setLayoutTransition(ANIMATED_LAYOUT);
|
||||
|
||||
@@ -227,7 +222,7 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
|
||||
private void presentSummary(@NonNull UsernameState usernameState) {
|
||||
if (usernameState.getUsername() != null) {
|
||||
binding.summary.setText(usernameState.getUsername());
|
||||
binding.summary.setText(usernameState.getUsername().getUsername());
|
||||
binding.summary.setAlpha(1f);
|
||||
} else if (usernameState instanceof UsernameState.Loading) {
|
||||
binding.summary.setAlpha(0.5f);
|
||||
|
||||
@@ -12,6 +12,8 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.usernames.Username
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameDeleteResult
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameSetResult
|
||||
@@ -19,6 +21,7 @@ import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil.checkDiscriminator
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil.checkUsername
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.util.Usernames
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@@ -40,15 +43,15 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
defaultValue = State(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = SignalStore.account().username?.let { UsernameState.Set(it) } ?: UsernameState.NoUsername
|
||||
usernameState = SignalStore.account().username?.let { UsernameState.Set(Username(it)) } ?: UsernameState.NoUsername
|
||||
),
|
||||
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() ?: "",
|
||||
nickname = SignalStore.account().username?.split(Usernames.DELIMITER)?.first() ?: "",
|
||||
discriminator = SignalStore.account().username?.split(Usernames.DELIMITER)?.last() ?: "",
|
||||
stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM
|
||||
),
|
||||
scheduler = Schedulers.computation()
|
||||
@@ -76,7 +79,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
return@update State(
|
||||
buttonState = if (isInRegistration) ButtonState.SUBMIT_DISABLED else ButtonState.DELETE,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = UsernameState.NoUsername
|
||||
usernameState = UsernameState.NoUsername
|
||||
)
|
||||
}
|
||||
|
||||
@@ -88,13 +91,13 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
State(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = state.username
|
||||
usernameState = state.usernameState
|
||||
)
|
||||
} else {
|
||||
State(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = state.username
|
||||
usernameState = state.usernameState
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -110,14 +113,14 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
return@update State(
|
||||
buttonState = if (isInRegistration) ButtonState.SUBMIT_DISABLED else ButtonState.DELETE,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = UsernameState.NoUsername
|
||||
usernameState = UsernameState.NoUsername
|
||||
)
|
||||
}
|
||||
|
||||
State(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = UsernameStatus.NONE,
|
||||
username = state.username
|
||||
usernameState = state.usernameState
|
||||
)
|
||||
}
|
||||
|
||||
@@ -132,61 +135,62 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
}
|
||||
|
||||
fun onUsernameSubmitted() {
|
||||
val state = stateMachineStore.state
|
||||
val editState = stateMachineStore.state
|
||||
val usernameState = uiState.state.usernameState
|
||||
val isCaseChange = isCaseChange(editState)
|
||||
|
||||
if (isCaseChange(state)) {
|
||||
handleUserConfirmation(UsernameRepository::updateUsername)
|
||||
} else {
|
||||
handleUserConfirmation(UsernameRepository::confirmUsername)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T : UsernameState> handleUserConfirmation(
|
||||
repositoryWorkerMethod: (T) -> Single<UsernameSetResult>
|
||||
) {
|
||||
val usernameState = uiState.state.username
|
||||
if (usernameState !is T) {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
||||
if (usernameState !is UsernameState.Reserved && usernameState !is UsernameState.CaseChange) {
|
||||
Log.w(TAG, "Username was submitted, current state is invalid! State: ${usernameState.javaClass.simpleName}")
|
||||
uiState.update { it.copy(buttonState = ButtonState.SUBMIT_DISABLED, usernameStatus = UsernameStatus.NONE) }
|
||||
return
|
||||
}
|
||||
|
||||
if (usernameState.requireUsername() == SignalStore.account().username) {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
||||
if (usernameState.requireUsername().username == SignalStore.account().username) {
|
||||
Log.d(TAG, "Username was submitted, but was identical to the current username. Ignoring.")
|
||||
uiState.update { it.copy(buttonState = ButtonState.SUBMIT_DISABLED, usernameStatus = UsernameStatus.NONE) }
|
||||
return
|
||||
}
|
||||
|
||||
val invalidReason = checkUsername(usernameState.getNickname())
|
||||
val invalidReason: InvalidReason? = checkUsername(usernameState.getNickname())
|
||||
if (invalidReason != null) {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, mapNicknameError(invalidReason), it.username) }
|
||||
Log.w(TAG, "Username was submitted, but did not pass validity checks. Reason: $invalidReason")
|
||||
uiState.update { it.copy(buttonState = ButtonState.SUBMIT_DISABLED, usernameStatus = mapNicknameError(invalidReason)) }
|
||||
return
|
||||
}
|
||||
|
||||
uiState.update { State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, it.username) }
|
||||
uiState.update { it.copy(buttonState = ButtonState.SUBMIT_LOADING, usernameStatus = UsernameStatus.NONE) }
|
||||
|
||||
disposables += repositoryWorkerMethod(usernameState).subscribe { result: UsernameSetResult ->
|
||||
val usernameConfirmOperation: Single<UsernameSetResult> = if (isCaseChange) {
|
||||
UsernameRepository.updateUsernameDisplayForCurrentLink(usernameState.requireUsername())
|
||||
} else {
|
||||
val reservation = usernameState as UsernameState.Reserved
|
||||
UsernameRepository.confirmUsernameAndCreateNewLink(reservation.requireUsername())
|
||||
}
|
||||
|
||||
disposables += usernameConfirmOperation.subscribe { result: UsernameSetResult ->
|
||||
val nickname = usernameState.getNickname()
|
||||
|
||||
when (result) {
|
||||
UsernameSetResult.SUCCESS -> {
|
||||
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation()
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.usernameState) }
|
||||
events.onNext(Event.SUBMIT_SUCCESS)
|
||||
}
|
||||
|
||||
UsernameSetResult.USERNAME_INVALID -> {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, it.username) }
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, it.usernameState) }
|
||||
events.onNext(Event.SUBMIT_FAIL_INVALID)
|
||||
nickname?.let { onNicknameUpdated(it) }
|
||||
}
|
||||
|
||||
UsernameSetResult.CANDIDATE_GENERATION_ERROR, UsernameSetResult.USERNAME_UNAVAILABLE -> {
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, it.username) }
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, it.usernameState) }
|
||||
events.onNext(Event.SUBMIT_FAIL_TAKEN)
|
||||
nickname?.let { onNicknameUpdated(it) }
|
||||
}
|
||||
|
||||
UsernameSetResult.NETWORK_ERROR -> {
|
||||
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, it.username) }
|
||||
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, it.usernameState) }
|
||||
events.onNext(Event.NETWORK_FAILURE)
|
||||
}
|
||||
}
|
||||
@@ -194,17 +198,17 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
}
|
||||
|
||||
fun onUsernameDeleted() {
|
||||
uiState.update { state: State -> State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.username) }
|
||||
uiState.update { state: State -> State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.usernameState) }
|
||||
|
||||
disposables += UsernameRepository.deleteUsername().subscribe { result: UsernameDeleteResult ->
|
||||
disposables += UsernameRepository.deleteUsernameAndLink().subscribe { result: UsernameDeleteResult ->
|
||||
when (result) {
|
||||
UsernameDeleteResult.SUCCESS -> {
|
||||
uiState.update { state: State -> State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.username) }
|
||||
uiState.update { state: State -> State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameState) }
|
||||
events.onNext(Event.DELETE_SUCCESS)
|
||||
}
|
||||
|
||||
UsernameDeleteResult.NETWORK_ERROR -> {
|
||||
uiState.update { state: State -> State(ButtonState.DELETE, UsernameStatus.NONE, state.username) }
|
||||
uiState.update { state: State -> State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameState) }
|
||||
events.onNext(Event.NETWORK_FAILURE)
|
||||
}
|
||||
}
|
||||
@@ -225,7 +229,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
}
|
||||
|
||||
val newLower = state.nickname.lowercase()
|
||||
val oldLower = SignalStore.account().username?.split(UsernameState.DELIMITER)?.firstOrNull()?.lowercase()
|
||||
val oldLower = SignalStore.account().username?.split(Usernames.DELIMITER)?.firstOrNull()?.lowercase()
|
||||
|
||||
return newLower == oldLower
|
||||
}
|
||||
@@ -241,13 +245,24 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
return
|
||||
}
|
||||
|
||||
val invalidReason: InvalidReason? = checkUsername(nickname)
|
||||
if (invalidReason != null) {
|
||||
uiState.update { uiState ->
|
||||
uiState.copy(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = mapNicknameError(invalidReason)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (isCaseChange(state)) {
|
||||
val discriminator = SignalStore.account().username?.split(UsernameState.DELIMITER)?.lastOrNull() ?: error("Unexpected case change, no discriminator!")
|
||||
val discriminator = SignalStore.account().username?.split(Usernames.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")
|
||||
usernameState = UsernameState.CaseChange(Username("${state.nickname}${Usernames.DELIMITER}$discriminator"))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -255,18 +270,6 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
return
|
||||
}
|
||||
|
||||
val invalidReason: InvalidReason? = checkUsername(nickname)
|
||||
if (invalidReason != null) {
|
||||
uiState.update { uiState ->
|
||||
State(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = mapNicknameError(invalidReason),
|
||||
username = uiState.username
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading) }
|
||||
|
||||
val isDiscriminatorSetByUser = state is UsernameEditStateMachine.UserEnteredDiscriminator || state is UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator
|
||||
@@ -282,7 +285,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
State(
|
||||
buttonState = ButtonState.SUBMIT_DISABLED,
|
||||
usernameStatus = mapDiscriminatorError(discriminatorInvalidReason),
|
||||
username = s.username
|
||||
usernameState = s.usernameState
|
||||
)
|
||||
}
|
||||
return
|
||||
@@ -333,10 +336,10 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
}
|
||||
}
|
||||
|
||||
class State(
|
||||
data class State(
|
||||
@JvmField val buttonState: ButtonState,
|
||||
@JvmField val usernameStatus: UsernameStatus,
|
||||
@JvmField val username: UsernameState
|
||||
@JvmField val usernameState: UsernameState
|
||||
)
|
||||
|
||||
enum class UsernameStatus {
|
||||
@@ -368,6 +371,8 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(UsernameEditViewModel::class.java)
|
||||
|
||||
private const val NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS: Long = 1000
|
||||
|
||||
private fun mapNicknameError(invalidReason: InvalidReason): UsernameStatus {
|
||||
@@ -376,7 +381,6 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
|
||||
InvalidReason.TOO_LONG -> UsernameStatus.TOO_LONG
|
||||
InvalidReason.STARTS_WITH_NUMBER -> UsernameStatus.CANNOT_START_WITH_NUMBER
|
||||
InvalidReason.INVALID_CHARACTERS -> UsernameStatus.INVALID_CHARACTERS
|
||||
else -> UsernameStatus.INVALID_GENERIC
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotAssocia
|
||||
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.api.util.Usernames
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.api.util.toByteArray
|
||||
import java.io.IOException
|
||||
@@ -89,16 +90,7 @@ object UsernameRepository {
|
||||
private val accountManager: SignalServiceAccountManager get() = ApplicationDependencies.getSignalServiceAccountManager()
|
||||
|
||||
/**
|
||||
* Given a username, update the username link, given that all was changed was the casing of the nickname.
|
||||
*/
|
||||
fun updateUsername(caseChange: UsernameState.CaseChange): Single<UsernameSetResult> {
|
||||
return Single
|
||||
.fromCallable { updateUsernameInternal(caseChange) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a nickname, this will temporarily reserve a matching discriminator that can later be confirmed via [confirmUsername].
|
||||
* Given a nickname, this will temporarily reserve a matching discriminator that can later be confirmed via [confirmUsernameAndCreateNewLink].
|
||||
*/
|
||||
fun reserveUsername(nickname: String, discriminator: String?): Single<Result<UsernameState.Reserved, UsernameSetResult>> {
|
||||
return Single
|
||||
@@ -107,19 +99,32 @@ object UsernameRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a reserved username (obtained via [reserveUsername]), this will confirm that reservation, assigning the user that username.
|
||||
* This changes the encrypted username associated with your current username link.
|
||||
* The intent of this is to allow users to change the casing of their username without changing the link,
|
||||
* since usernames are case-insensitive.
|
||||
*/
|
||||
fun confirmUsername(reserved: UsernameState.Reserved): Single<UsernameSetResult> {
|
||||
fun updateUsernameDisplayForCurrentLink(updatedUsername: Username): Single<UsernameSetResult> {
|
||||
return Single
|
||||
.fromCallable { confirmUsernameInternal(reserved) }
|
||||
.fromCallable { updateUsernameDisplayForCurrentLinkInternal(updatedUsername) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the username from the local user's account
|
||||
* Given a reserved username (obtained via [reserveUsername]), this will confirm that reservation, assigning the user that username.
|
||||
* It will also create a new username link. Therefore, be sure to call [updateUsernameDisplayForCurrentLink] instead if all that has changed is the
|
||||
* casing, and you want to keep the link the same.
|
||||
*/
|
||||
fun confirmUsernameAndCreateNewLink(username: Username): Single<UsernameSetResult> {
|
||||
return Single
|
||||
.fromCallable { confirmUsernameAndCreateNewLinkInternal(username) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the username and username link from the local user's account
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deleteUsername(): Single<UsernameDeleteResult> {
|
||||
fun deleteUsernameAndLink(): Single<UsernameDeleteResult> {
|
||||
return Single
|
||||
.fromCallable { deleteUsernameInternal() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -309,7 +314,7 @@ object UsernameRepository {
|
||||
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"))
|
||||
listOf(Username("$nickname${Usernames.DELIMITER}$discriminator"))
|
||||
}
|
||||
|
||||
val hashes: List<String> = candidates
|
||||
@@ -324,7 +329,7 @@ object UsernameRepository {
|
||||
}
|
||||
|
||||
Log.i(TAG, "[reserveUsername] Successfully reserved username.")
|
||||
success(UsernameState.Reserved(candidates[hashIndex].username, response))
|
||||
success(UsernameState.Reserved(candidates[hashIndex]))
|
||||
} catch (e: BaseUsernameException) {
|
||||
Log.w(TAG, "[reserveUsername] An error occurred while generating candidates.")
|
||||
failure(UsernameSetResult.CANDIDATE_GENERATION_ERROR)
|
||||
@@ -341,63 +346,65 @@ object UsernameRepository {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun updateUsernameInternal(caseChange: UsernameState.CaseChange): UsernameSetResult {
|
||||
private fun updateUsernameDisplayForCurrentLinkInternal(updatedUsername: Username): UsernameSetResult {
|
||||
Log.i(TAG, "[updateUsernameDisplayForCurrentLink] Beginning username update...")
|
||||
|
||||
return try {
|
||||
val oldUsernameLink = SignalStore.account().usernameLink ?: return UsernameSetResult.USERNAME_INVALID
|
||||
val username = Username(caseChange.username)
|
||||
val newUsernameLink = username.generateLink(oldUsernameLink.entropy)
|
||||
val newUsernameLink = updatedUsername.generateLink(oldUsernameLink.entropy)
|
||||
val usernameLinkComponents = accountManager.updateUsernameLink(newUsernameLink)
|
||||
|
||||
SignalStore.account().username = username.username
|
||||
SignalStore.account().username = updatedUsername.username
|
||||
SignalStore.account().usernameLink = usernameLinkComponents
|
||||
SignalDatabase.recipients.setUsername(Recipient.self().id, updatedUsername.username)
|
||||
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
|
||||
SignalStore.account().usernameSyncErrorCount = 0
|
||||
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
Log.i(TAG, "[updateUsernameDisplayForCurrentLink] Successfully updated username.")
|
||||
|
||||
UsernameSetResult.SUCCESS
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "[updateUsernameDisplayForCurrentLink] Generic network exception.", e)
|
||||
UsernameSetResult.NETWORK_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun confirmUsernameAndCreateNewLinkInternal(username: Username): UsernameSetResult {
|
||||
Log.i(TAG, "[confirmUsernameAndCreateNewLink] Beginning username confirmation...")
|
||||
|
||||
return try {
|
||||
accountManager.confirmUsername(username)
|
||||
SignalStore.account().username = username.username
|
||||
SignalStore.account().usernameLink = null
|
||||
SignalDatabase.recipients.setUsername(Recipient.self().id, username.username)
|
||||
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
|
||||
SignalStore.account().usernameSyncErrorCount = 0
|
||||
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
Log.i(TAG, "[updateUsername] Successfully updated username.")
|
||||
|
||||
UsernameSetResult.SUCCESS
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "[updateUsername] Generic network exception.", e)
|
||||
UsernameSetResult.NETWORK_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun confirmUsernameInternal(reserved: UsernameState.Reserved): UsernameSetResult {
|
||||
return try {
|
||||
val username = Username(reserved.username)
|
||||
accountManager.confirmUsername(reserved.username, reserved.reserveUsernameResponse)
|
||||
SignalStore.account().username = username.username
|
||||
SignalStore.account().usernameLink = null
|
||||
SignalDatabase.recipients.setUsername(Recipient.self().id, reserved.username)
|
||||
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
|
||||
SignalStore.account().usernameSyncErrorCount = 0
|
||||
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
Log.i(TAG, "[confirmUsername] Successfully confirmed username.")
|
||||
Log.i(TAG, "[confirmUsernameAndCreateNewLink] Successfully confirmed username.")
|
||||
|
||||
if (tryToSetUsernameLink(username)) {
|
||||
Log.i(TAG, "[confirmUsername] Successfully confirmed username link.")
|
||||
Log.i(TAG, "[confirmUsernameAndCreateNewLink] Successfully confirmed username link.")
|
||||
} else {
|
||||
Log.w(TAG, "[confirmUsername] Failed to confirm a username link. We'll try again when the user goes to view their link.")
|
||||
Log.w(TAG, "[confirmUsernameAndCreateNewLink] Failed to confirm a username link. We'll try again when the user goes to view their link.")
|
||||
}
|
||||
|
||||
UsernameSetResult.SUCCESS
|
||||
} catch (e: UsernameTakenException) {
|
||||
Log.w(TAG, "[confirmUsername] Username gone.")
|
||||
Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username gone.")
|
||||
UsernameSetResult.USERNAME_UNAVAILABLE
|
||||
} catch (e: UsernameIsNotReservedException) {
|
||||
Log.w(TAG, "[confirmUsername] Username was not reserved.")
|
||||
Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username was not reserved.")
|
||||
UsernameSetResult.USERNAME_INVALID
|
||||
} catch (e: BaseUsernameException) {
|
||||
Log.w(TAG, "[confirmUsername] Username was not reserved.")
|
||||
Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username was not reserved.")
|
||||
UsernameSetResult.USERNAME_INVALID
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "[confirmUsername] Generic network exception.", e)
|
||||
Log.w(TAG, "[confirmUsernameAndCreateNewLink] Generic network exception.", e)
|
||||
UsernameSetResult.NETWORK_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage
|
||||
|
||||
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
||||
import org.signal.libsignal.usernames.Username
|
||||
import org.whispersystems.signalservice.api.util.discriminator
|
||||
import org.whispersystems.signalservice.api.util.nickname
|
||||
|
||||
/**
|
||||
* Describes the state of the username suffix, which is a spanned CharSequence.
|
||||
*/
|
||||
sealed class UsernameState {
|
||||
|
||||
protected open val username: String? = null
|
||||
protected open val username: Username? = null
|
||||
open val isInProgress: Boolean = false
|
||||
|
||||
fun requireUsername(): String = username!!
|
||||
fun requireUsername(): Username = username!!
|
||||
|
||||
object Loading : UsernameState() {
|
||||
override val isInProgress: Boolean = true
|
||||
@@ -19,27 +21,22 @@ sealed class UsernameState {
|
||||
object NoUsername : UsernameState()
|
||||
|
||||
data class Reserved(
|
||||
public override val username: String,
|
||||
val reserveUsernameResponse: ReserveUsernameResponse
|
||||
public override val username: Username
|
||||
) : UsernameState()
|
||||
|
||||
data class CaseChange(
|
||||
public override val username: String
|
||||
public override val username: Username
|
||||
) : UsernameState()
|
||||
|
||||
data class Set(
|
||||
override val username: String
|
||||
override val username: Username
|
||||
) : UsernameState()
|
||||
|
||||
fun getNickname(): String? {
|
||||
return username?.split(DELIMITER)?.firstOrNull()
|
||||
return username?.nickname
|
||||
}
|
||||
|
||||
fun getDiscriminator(): String? {
|
||||
return username?.split(DELIMITER)?.lastOrNull()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DELIMITER = "."
|
||||
return username?.discriminator
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,8 +783,8 @@ public class SignalServiceAccountManager {
|
||||
return this.pushServiceSocket.reserveUsername(usernameHashes);
|
||||
}
|
||||
|
||||
public void confirmUsername(String username, ReserveUsernameResponse reserveUsernameResponse) throws IOException {
|
||||
this.pushServiceSocket.confirmUsername(username, reserveUsernameResponse);
|
||||
public void confirmUsername(Username username) throws IOException {
|
||||
this.pushServiceSocket.confirmUsername(username);
|
||||
}
|
||||
|
||||
public UsernameLinkComponents updateUsernameLink(UsernameLink newUsernameLink) throws IOException {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.util
|
||||
|
||||
import org.signal.libsignal.usernames.Username
|
||||
|
||||
val Username.nickname: String get() = username.split(Usernames.DELIMITER)[0]
|
||||
val Username.discriminator: String get() = username.split(Usernames.DELIMITER)[1]
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.util
|
||||
|
||||
object Usernames {
|
||||
const val DELIMITER = "."
|
||||
}
|
||||
@@ -1109,18 +1109,16 @@ public class PushServiceSocket {
|
||||
* PUT /v1/accounts/username_hash/confirm
|
||||
* Set a previously reserved username for the account.
|
||||
*
|
||||
* @param username The username the user wishes to confirm. For example, myusername.27
|
||||
* @param reserveUsernameResponse The response object from the reservation
|
||||
* @throws IOException Thrown when the username is invalid or taken, or when another network error occurs.
|
||||
* @param username The username the user wishes to confirm.
|
||||
* @throws IOException Thrown when the username is invalid or taken, or when another network error occurs.
|
||||
*/
|
||||
public void confirmUsername(String username, ReserveUsernameResponse reserveUsernameResponse) throws IOException {
|
||||
public void confirmUsername(Username username) throws IOException {
|
||||
try {
|
||||
byte[] randomness = new byte[32];
|
||||
random.nextBytes(randomness);
|
||||
|
||||
byte[] proof = new Username(username).generateProofWithRandomness(randomness);
|
||||
ConfirmUsernameRequest confirmUsernameRequest = new ConfirmUsernameRequest(reserveUsernameResponse.getUsernameHash(),
|
||||
Base64.encodeUrlSafeWithoutPadding(proof));
|
||||
byte[] proof = username.generateProofWithRandomness(randomness);
|
||||
ConfirmUsernameRequest confirmUsernameRequest = new ConfirmUsernameRequest(Base64.encodeUrlSafeWithoutPadding(username.getHash()), Base64.encodeUrlSafeWithoutPadding(proof));
|
||||
|
||||
makeServiceRequest(CONFIRM_USERNAME_PATH, "PUT", JsonUtil.toJson(confirmUsernameRequest), NO_HEADERS, (responseCode, body) -> {
|
||||
switch (responseCode) {
|
||||
@@ -1159,7 +1157,7 @@ public class PushServiceSocket {
|
||||
makeServiceRequest(USERNAME_LINK_PATH, "DELETE", null);
|
||||
}
|
||||
|
||||
/** Given a link serverId (see {@link #createUsernameLink(String)}), this will return the encrypted username associate with the link. */
|
||||
/** Given a link serverId (see {@link #createUsernameLink(String, boolean)}}), this will return the encrypted username associate with the link. */
|
||||
public byte[] getEncryptedUsernameFromLinkServerId(UUID serverId) throws IOException {
|
||||
String response = makeServiceRequestWithoutAuthentication(String.format(USERNAME_FROM_LINK_PATH, serverId.toString()), "GET", null);
|
||||
GetUsernameFromLinkResponseBody parsed = JsonUtil.fromJson(response, GetUsernameFromLinkResponseBody.class);
|
||||
|
||||
Reference in New Issue
Block a user