Identity: cancel binding WIP

This commit is contained in:
Benoit Marty 2020-05-10 15:53:54 +02:00
parent 69759b7415
commit ae0d09a049
12 changed files with 240 additions and 103 deletions

View File

@ -40,15 +40,19 @@ interface IdentityService {
fun setNewIdentityServer(url: String?, callback: MatrixCallback<String?>): Cancelable
/**
* This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid,
* and then the threePid will be associated with the matrix account
* This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid
*/
fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* This will perform the actual association of ThreePid and Matrix account
* This will cancel a pending binding of threePid.
*/
fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* This will ask the identity server to send an new email or a new SMS to let the user confirm he owns the ThreePid
*/
fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* Submit the code that the identity server has sent to the user (in email or SMS)
@ -58,6 +62,12 @@ interface IdentityService {
fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback<Unit>): Cancelable
/**
* This will perform the actual association of ThreePid and Matrix account
*/
fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable
/**
* Unbind a threePid
* The request will actually be done on the homeserver
*/
fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable

View File

@ -123,7 +123,19 @@ internal class DefaultIdentityService @Inject constructor(
override fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid))
identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, false))
}
}
override fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
identityServiceStore.deletePendingBinding(threePid)
}
}
override fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, true))
}
}

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.session.identity
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.identity.getCountryCode
import im.vector.matrix.android.internal.di.UserId
@ -30,7 +31,9 @@ import javax.inject.Inject
internal interface IdentityRequestTokenForBindingTask : Task<IdentityRequestTokenForBindingTask.Params, Unit> {
data class Params(
val threePid: ThreePid
val threePid: ThreePid,
// True to request the identity server to send again the email or the SMS
val sendAgain: Boolean
)
}
@ -45,6 +48,10 @@ internal class DefaultIdentityRequestTokenForBindingTask @Inject constructor(
val pendingBindingEntity = identityServiceStore.getPendingBinding(params.threePid)
if (params.sendAgain && pendingBindingEntity == null) {
throw IdentityServiceError.NoCurrentBindingError
}
val clientSecret = pendingBindingEntity?.clientSecret ?: UUID.randomUUID().toString()
val sendAttempt = pendingBindingEntity?.sendAttempt?.inc() ?: 1

View File

@ -32,10 +32,7 @@ internal fun IdentityPendingBindingEntity.Companion.getOrCreate(realm: Realm, th
}
internal fun IdentityPendingBindingEntity.Companion.delete(realm: Realm, threePid: ThreePid) {
realm.where<IdentityPendingBindingEntity>()
.equalTo(IdentityPendingBindingEntityFields.THREE_PID, threePid.toPrimaryKey())
.findAll()
.deleteAllFromRealm()
get(realm, threePid)?.deleteFromRealm()
}
internal fun IdentityPendingBindingEntity.Companion.deleteAll(realm: Realm) {

View File

@ -21,6 +21,7 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.google.i18n.phonenumbers.PhoneNumberUtil
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.identity.SharedState
@ -28,6 +29,7 @@ import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import timber.log.Timber
@ -111,15 +113,15 @@ class DiscoverySettingsController @Inject constructor(
colorProvider(colorProvider)
stringProvider(stringProvider)
when (piState.isShared) {
is Loading -> buttonIndeterminate(true)
is Loading -> {
buttonIndeterminate(true)
}
is Fail -> {
buttonType(SettingsTextButtonItem.ButtonType.NORMAL)
buttonStyle(SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE)
buttonTitle(stringProvider.getString(R.string.global_retry))
infoMessage(piState.isShared.error.message)
buttonClickListener(View.OnClickListener {
listener?.onTapRetryToRetrieveBindings()
})
buttonClickListener { listener?.onTapRetryToRetrieveBindings() }
}
is Success -> when (piState.isShared()) {
SharedState.SHARED,
@ -143,8 +145,8 @@ class DiscoverySettingsController @Inject constructor(
}
when (piState.isShared()) {
SharedState.BINDING_IN_PROGRESS -> {
val errorText = if (piState.isTokenSubmitted is Fail) {
val error = piState.isTokenSubmitted.error
val errorText = if (piState.finalRequest is Fail) {
val error = piState.finalRequest.error
// Deal with error 500
//Ref: https://github.com/matrix-org/sydent/issues/292
if (error is Failure.ServerError
@ -160,18 +162,22 @@ class DiscoverySettingsController @Inject constructor(
id("tverif" + piState.threePid.value)
descriptionText(stringProvider.getString(R.string.settings_text_message_sent, phoneNumber))
errorText(errorText)
inProgress(piState.isTokenSubmitted is Loading)
inProgress(piState.finalRequest is Loading)
interactionListener(object : SettingsItemEditText.Listener {
override fun onValidate(code: String) {
if (piState.threePid is ThreePid.Msisdn) {
listener?.sendMsisdnVerificationCode(piState.threePid, code)
}
}
override fun onCancel() {
listener?.cancelBinding(piState.threePid)
}
})
}
}
else -> Unit
}
}.exhaustive
}
}
}
@ -210,15 +216,17 @@ class DiscoverySettingsController @Inject constructor(
colorProvider(colorProvider)
stringProvider(stringProvider)
when (piState.isShared) {
is Loading -> buttonIndeterminate(true)
is Loading -> {
buttonIndeterminate(true)
showBottomButtons(false)
}
is Fail -> {
buttonType(SettingsTextButtonItem.ButtonType.NORMAL)
buttonStyle(SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE)
buttonTitle(stringProvider.getString(R.string.global_retry))
infoMessage(piState.isShared.error.message)
buttonClickListener(View.OnClickListener {
listener?.onTapRetryToRetrieveBindings()
})
buttonClickListener { listener?.onTapRetryToRetrieveBindings() }
showBottomButtons(false)
}
is Success -> when (piState.isShared()) {
SharedState.SHARED,
@ -235,14 +243,32 @@ class DiscoverySettingsController @Inject constructor(
}
SharedState.BINDING_IN_PROGRESS -> {
buttonType(SettingsTextButtonItem.ButtonType.NORMAL)
buttonTitleId(R.string._continue)
infoMessageTintColorId(R.color.vector_info_color)
buttonTitle(null)
showBottomButtons(true)
when (piState.finalRequest) {
is Uninitialized -> {
infoMessage(stringProvider.getString(R.string.settings_discovery_confirm_mail, piState.threePid.value))
buttonClickListener(View.OnClickListener {
infoMessageTintColorId(R.color.vector_info_color)
showBottomLoading(false)
}
is Loading -> {
infoMessage(stringProvider.getString(R.string.settings_discovery_confirm_mail, piState.threePid.value))
infoMessageTintColorId(R.color.vector_info_color)
showBottomLoading(true)
}
is Fail -> {
infoMessage(stringProvider.getString(R.string.settings_discovery_confirm_mail_not_clicked, piState.threePid.value))
infoMessageTintColorId(R.color.riotx_destructive_accent)
showBottomLoading(false)
}
is Success -> Unit /* Cannot happen */
}
cancelClickListener { listener?.cancelBinding(piState.threePid) }
continueClickListener {
if (piState.threePid is ThreePid.Email) {
listener?.checkEmailVerification(piState.threePid)
}
})
}
}
}
}
@ -318,6 +344,7 @@ class DiscoverySettingsController @Inject constructor(
fun onTapRevoke(threePid: ThreePid)
fun onTapShare(threePid: ThreePid)
fun checkEmailVerification(threePid: ThreePid.Email)
fun cancelBinding(threePid: ThreePid)
fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String)
fun onTapChangeIdentityServer()
fun onTapDisconnectIdentityServer()

View File

@ -130,6 +130,10 @@ class DiscoverySettingsFragment @Inject constructor(
viewModel.handle(DiscoverySettingsAction.SubmitMsisdnToken(threePid, code))
}
override fun cancelBinding(threePid: ThreePid) {
viewModel.handle(DiscoverySettingsAction.CancelBinding(threePid))
}
override fun onTapChangeIdentityServer() = withState(viewModel) { state ->
//we should prompt if there are bound items with current is
val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty()

View File

@ -44,7 +44,8 @@ data class PidInfo(
// Retrieved from IdentityServer, or transient state
val isShared: Async<SharedState>,
// Contains information about a current request to submit the token (for instance SMS code received by SMS)
val isTokenSubmitted: Async<Unit> = Uninitialized
// Or a current binding finalization, for email
val finalRequest: Async<Unit> = Uninitialized
)
data class DiscoverySettingsState(
@ -64,6 +65,7 @@ sealed class DiscoverySettingsAction : VectorViewModelAction {
data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction()
data class SubmitMsisdnToken(val threePid: ThreePid.Msisdn, val code: String) : DiscoverySettingsAction()
data class CancelBinding(val threePid: ThreePid) : DiscoverySettingsAction()
}
sealed class DiscoverySettingsViewEvents : VectorViewEvents {
@ -133,6 +135,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action)
is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action)
is DiscoverySettingsAction.SubmitMsisdnToken -> submitMsisdnToken(action)
is DiscoverySettingsAction.CancelBinding -> cancelBinding(action)
}.exhaustive
}
@ -214,7 +217,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
emailList = Success(
currentMails.map {
if (it.threePid == threePid) {
it.copy(isTokenSubmitted = submitState)
it.copy(finalRequest = submitState)
} else {
it
}
@ -223,7 +226,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
phoneNumbersList = Success(
phones.map {
if (it.threePid == threePid) {
it.copy(isTokenSubmitted = submitState)
it.copy(finalRequest = submitState)
} else {
it
}
@ -274,6 +277,18 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
})
}
private fun cancelBinding(action: DiscoverySettingsAction.CancelBinding) {
identityService.cancelBindThreePid(action.threePid, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
changeThreePidState(action.threePid, Success(SharedState.NOT_SHARED))
}
override fun onFailure(failure: Throwable) {
// This could never fail
}
})
}
private fun startListenToIdentityManager() {
identityService.addListener(identityServerManagerListener)
}
@ -362,25 +377,23 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
private fun finalizeBind3pid(action: DiscoverySettingsAction.FinalizeBind3pid) = withState { state ->
val threePid = when (action.threePid) {
is ThreePid.Email -> {
changeThreePidState(action.threePid, Loading())
state.emailList()?.find { it.threePid.value == action.threePid.email }?.threePid ?: return@withState
}
is ThreePid.Msisdn -> {
changeThreePidState(action.threePid, Loading())
state.phoneNumbersList()?.find { it.threePid.value == action.threePid.msisdn }?.threePid ?: return@withState
}
}
changeThreePidSubmitState(action.threePid, Loading())
identityService.finalizeBindThreePid(threePid, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
changeThreePidSubmitState(action.threePid, Uninitialized)
changeThreePidState(action.threePid, Success(SharedState.SHARED))
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(DiscoverySettingsViewEvents.Failure(failure))
// Restore previous state after an error
changeThreePidState(action.threePid, Success(SharedState.BINDING_IN_PROGRESS))
changeThreePidSubmitState(action.threePid, Fail(failure))
}
})

View File

@ -49,6 +49,10 @@ abstract class SettingsItemEditText : EpoxyModelWithHolder<SettingsItemEditText.
interactionListener?.onValidate(code)
}
holder.cancelButton.setOnClickListener {
interactionListener?.onCancel()
}
holder.editText.isEnabled = !inProgress
if (errorText.isNullOrBlank()) {
@ -75,10 +79,12 @@ abstract class SettingsItemEditText : EpoxyModelWithHolder<SettingsItemEditText.
val editText by bind<EditText>(R.id.settings_item_edittext)
val textInputLayout by bind<TextInputLayout>(R.id.settings_item_enter_til)
val validateButton by bind<Button>(R.id.settings_item_enter_button)
val cancelButton by bind<Button>(R.id.settings_item_enter_cancel_button)
val progress by bind<View>(R.id.settings_item_enter_progress)
}
interface Listener {
fun onValidate(code: String)
fun onCancel()
}
}

View File

@ -15,7 +15,6 @@
*/
package im.vector.riotx.features.discovery
import android.view.View
import android.widget.Button
import android.widget.CompoundButton
import android.widget.ProgressBar
@ -30,7 +29,9 @@ import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.ClickListener
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.onClick
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
@ -82,7 +83,19 @@ abstract class SettingsTextButtonItem : EpoxyModelWithHolder<SettingsTextButtonI
var checked: Boolean? = null
@EpoxyAttribute
var buttonClickListener: View.OnClickListener? = null
var showBottomLoading: Boolean = false
@EpoxyAttribute
var showBottomButtons: Boolean = false
@EpoxyAttribute
var buttonClickListener: ClickListener? = null
@EpoxyAttribute
var continueClickListener: ClickListener? = null
@EpoxyAttribute
var cancelClickListener: ClickListener? = null
@EpoxyAttribute
var switchChangeListener: CompoundButton.OnCheckedChangeListener? = null
@ -108,35 +121,41 @@ abstract class SettingsTextButtonItem : EpoxyModelWithHolder<SettingsTextButtonI
}
if (buttonTitleId != null) {
holder.button.setText(buttonTitleId!!)
holder.mainButton.setText(buttonTitleId!!)
} else {
holder.button.setTextOrHide(buttonTitle)
holder.mainButton.setTextOrHide(buttonTitle)
}
holder.bottomLoading.isVisible = showBottomLoading
holder.continueButton.isInvisible = showBottomLoading || !showBottomButtons
holder.cancelButton.isVisible = !showBottomLoading && showBottomButtons
holder.continueButton.onClick(continueClickListener)
holder.cancelButton.onClick(cancelClickListener)
if (buttonIndeterminate) {
holder.spinner.isVisible = true
holder.button.isInvisible = true
holder.mainButton.isInvisible = true
holder.switchButton.isInvisible = true
holder.switchButton.setOnCheckedChangeListener(null)
holder.button.setOnClickListener(null)
holder.mainButton.setOnClickListener(null)
} else {
holder.spinner.isVisible = false
when (buttonType) {
ButtonType.NORMAL -> {
holder.button.isVisible = true
holder.mainButton.isVisible = true
holder.switchButton.isVisible = false
when (buttonStyle) {
ButtonStyle.POSITIVE -> {
holder.button.setTextColor(colorProvider.getColorFromAttribute(R.attr.colorAccent))
holder.mainButton.setTextColor(colorProvider.getColorFromAttribute(R.attr.colorAccent))
}
ButtonStyle.DESTRUCTIVE -> {
holder.button.setTextColor(colorProvider.getColor(R.color.vector_error_color))
holder.mainButton.setTextColor(colorProvider.getColor(R.color.vector_error_color))
}
}
holder.button.setOnClickListener(buttonClickListener)
holder.mainButton.onClick(buttonClickListener)
}
ButtonType.SWITCH -> {
holder.button.isVisible = false
holder.mainButton.isInvisible = true
holder.switchButton.isVisible = true
//set to null before changing the state
holder.switchButton.setOnCheckedChangeListener(null)
@ -165,9 +184,12 @@ abstract class SettingsTextButtonItem : EpoxyModelWithHolder<SettingsTextButtonI
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.settings_item_text)
val button by bind<Button>(R.id.settings_item_button)
val mainButton by bind<Button>(R.id.settings_item_button)
val switchButton by bind<Switch>(R.id.settings_item_switch)
val spinner by bind<ProgressBar>(R.id.settings_item_button_spinner)
val errorTextView by bind<TextView>(R.id.settings_item_error_message)
val continueButton by bind<Button>(R.id.settings_item_continue_button)
val cancelButton by bind<Button>(R.id.settings_item_cancel_button)
val bottomLoading by bind<ProgressBar>(R.id.settings_item_bottom_loading)
}
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -11,12 +12,6 @@
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="@dimen/layout_vertical_margin">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/settings_item_text"
android:layout_width="0dp"
@ -28,22 +23,23 @@
android:orientation="vertical"
android:textColor="?android:textColorPrimary"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@+id/settings_item_button"
app:layout_constraintEnd_toStartOf="@+id/settings_item_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/settings_item_button"
tools:drawableLeft="@drawable/ic_notification_privacy_warning"
tools:drawableStart="@drawable/ic_notification_privacy_warning"
tools:drawableTint="@color/vector_error_color"
tools:text="foo@bar.test" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="40dp"
android:minWidth="70dp">
<Button
android:id="@+id/settings_item_button"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/share"
tools:visibility="visible" />
@ -52,8 +48,11 @@
style="?android:attr/progressBarStyle"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:visibility="invisible"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/settings_item_button"
app:layout_constraintEnd_toEndOf="@+id/settings_item_button"
app:layout_constraintStart_toStartOf="@+id/settings_item_button"
app:layout_constraintTop_toTopOf="@+id/settings_item_button"
tools:visibility="invisible" />
<Switch
@ -62,13 +61,11 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/settings_item_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/settings_item_button"
tools:visibility="visible" />
</FrameLayout>
</LinearLayout>
<TextView
android:id="@+id/settings_item_error_message"
android:layout_width="match_parent"
@ -77,10 +74,42 @@
android:gravity="center_vertical"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/settings_item_button"
tools:drawableStart="@drawable/ic_notification_privacy_warning"
tools:text="Error Message"
tools:textColor="@color/vector_info_color"
tools:visibility="visible">
</TextView>
</LinearLayout>
<Button
android:id="@+id/settings_item_continue_button"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/_continue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/settings_item_error_message" />
<ProgressBar
android:id="@+id/settings_item_bottom_loading"
style="?android:attr/progressBarStyle"
android:layout_width="20dp"
android:layout_height="20dp"
app:layout_constraintBottom_toBottomOf="@+id/settings_item_continue_button"
app:layout_constraintEnd_toEndOf="@+id/settings_item_continue_button"
app:layout_constraintStart_toStartOf="@+id/settings_item_continue_button"
app:layout_constraintTop_toTopOf="@+id/settings_item_continue_button" />
<Button
android:id="@+id/settings_item_cancel_button"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:textColor="@color/riotx_destructive_accent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/settings_item_error_message" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -54,6 +54,15 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/settings_item_enter_til" />
<Button
android:id="@+id/settings_item_enter_cancel_button"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/settings_item_enter_til" />
<ProgressBar
android:id="@+id/settings_item_enter_progress"
style="?android:attr/progressBarStyle"

View File

@ -1733,6 +1733,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="settings_discovery_disconnect_identity_server_info">Disconnecting from your identity server will mean you wont be discoverable by other users and you wont be able to invite others by email or phone.</string>
<string name="settings_discovery_msisdn_title">Discoverable phone numbers</string>
<string name="settings_discovery_confirm_mail">We sent you a confirm email to %s, check your email and click on the confirmation link</string>
<string name="settings_discovery_confirm_mail_not_clicked">We sent you a confirm email to %s, please first check your email and click on the confirmation link</string>
<string name="settings_discovery_mail_pending">Pending</string>
<string name="settings_discovery_enter_identity_server">Enter a new identity server</string>