Identity: Improve identity choice screen after review

This commit is contained in:
Benoit Marty 2020-05-13 23:33:51 +02:00
parent 789bcc8d77
commit 6b09a78ece
8 changed files with 165 additions and 148 deletions

View file

@ -39,6 +39,7 @@ import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util.assertMainThread
import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding3.view.clicks
import im.vector.riotx.R
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.HasScreenInjector
@ -49,6 +50,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import timber.log.Timber
import java.util.concurrent.TimeUnit
abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
@ -249,6 +251,18 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
.disposeOnDestroyView()
}
/* ==========================================================================================
* Views
* ========================================================================================== */
protected fun View.debouncedClicks(onClicked: () -> Unit) {
clicks()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onClicked() }
.disposeOnDestroyView()
}
/* ==========================================================================================
* MENU MANAGEMENT
* ========================================================================================== */

View file

@ -19,6 +19,7 @@ package im.vector.riotx.features.discovery.change
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class SetIdentityServerAction : VectorViewModelAction {
data class UpdateIdentityServerUrl(val url: String) : SetIdentityServerAction()
object DoChangeIdentityServerUrl : SetIdentityServerAction()
object UseDefaultIdentityServer : SetIdentityServerAction()
data class UseCustomIdentityServer(val url: String) : SetIdentityServerAction()
}

View file

@ -18,25 +18,22 @@ package im.vector.riotx.features.discovery.change
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ProgressBar
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import butterknife.BindView
import butterknife.OnTextChanged
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.textfield.TextInputLayout
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.toReducedUrl
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.discovery.DiscoverySharedViewModel
import im.vector.riotx.features.terms.ReviewTermsActivity
import kotlinx.android.synthetic.main.fragment_set_identity_server.*
import javax.inject.Inject
class SetIdentityServerFragment @Inject constructor(
@ -45,47 +42,26 @@ class SetIdentityServerFragment @Inject constructor(
override fun getLayoutResId() = R.layout.fragment_set_identity_server
override fun getMenuRes() = R.menu.menu_submit
@BindView(R.id.discovery_identity_server_enter_til)
lateinit var mKeyInputLayout: TextInputLayout
@BindView(R.id.discovery_identity_server_enter_edittext)
lateinit var mKeyTextEdit: EditText
@BindView(R.id.discovery_identity_server_loading)
lateinit var mProgressBar: ProgressBar
private val viewModel by fragmentViewModel(SetIdentityServerViewModel::class)
lateinit var sharedViewModel: DiscoverySharedViewModel
override fun invalidate() = withState(viewModel) { state ->
if (state.isVerifyingServer) {
mKeyTextEdit.isEnabled = false
mProgressBar.isVisible = true
if (state.defaultIdentityServerUrl.isNullOrEmpty()) {
// No default
identityServerSetDefaultNotice.isVisible = false
identityServerSetDefaultSubmit.isVisible = false
identityServerSetDefaultAlternative.setText(R.string.identity_server_set_alternative_notice_no_default)
} else {
mKeyTextEdit.isEnabled = true
mProgressBar.isVisible = false
}
val newText = state.newIdentityServerUrl ?: ""
if (newText != mKeyTextEdit.text.toString()) {
mKeyTextEdit.setText(newText)
}
mKeyInputLayout.error = state.errorMessageId?.let { getString(it) }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_submit -> {
withState(viewModel) { state ->
if (!state.isVerifyingServer) {
viewModel.handle(SetIdentityServerAction.DoChangeIdentityServerUrl)
}
}
true
}
else -> super.onOptionsItemSelected(item)
identityServerSetDefaultNotice.text = getString(
R.string.identity_server_set_default_notice,
state.homeServerUrl.toReducedUrl(),
state.defaultIdentityServerUrl.toReducedUrl()
)
identityServerSetDefaultNotice.isVisible = true
identityServerSetDefaultSubmit.isVisible = true
identityServerSetDefaultSubmit.text = getString(R.string.identity_server_set_default_submit, state.defaultIdentityServerUrl.toReducedUrl())
identityServerSetDefaultAlternative.setText(R.string.identity_server_set_alternative_notice)
}
}
@ -94,20 +70,35 @@ class SetIdentityServerFragment @Inject constructor(
sharedViewModel = activityViewModelProvider.get(DiscoverySharedViewModel::class.java)
mKeyTextEdit.setOnEditorActionListener { _, actionId, _ ->
identityServerSetDefaultAlternativeTextInput.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
withState(viewModel) { state ->
if (!state.isVerifyingServer) {
viewModel.handle(SetIdentityServerAction.DoChangeIdentityServerUrl)
}
}
viewModel.handle(SetIdentityServerAction.UseCustomIdentityServer(identityServerSetDefaultAlternativeTextInput.text.toString()))
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
identityServerSetDefaultAlternativeTextInput
.textChanges()
.subscribe {
identityServerSetDefaultAlternativeTil.error = null
identityServerSetDefaultAlternativeSubmit.isEnabled = it.isNotEmpty()
}
.disposeOnDestroyView()
identityServerSetDefaultSubmit.debouncedClicks {
viewModel.handle(SetIdentityServerAction.UseDefaultIdentityServer)
}
identityServerSetDefaultAlternativeSubmit.debouncedClicks {
viewModel.handle(SetIdentityServerAction.UseCustomIdentityServer(identityServerSetDefaultAlternativeTextInput.text.toString()))
}
viewModel.observeViewEvents {
when (it) {
is SetIdentityServerViewEvents.Loading -> showLoading(it.message)
is SetIdentityServerViewEvents.Failure -> identityServerSetDefaultAlternativeTil.error = getString(it.errorMessageId)
is SetIdentityServerViewEvents.OtherFailure -> showFailure(it.failure)
is SetIdentityServerViewEvents.NoTerms -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_discovery_no_terms_title)
@ -117,23 +108,25 @@ class SetIdentityServerFragment @Inject constructor(
}
.setNegativeButton(R.string.cancel, null)
.show()
Unit
}
is SetIdentityServerViewEvents.TermsAccepted -> {
processIdentityServerChange()
}
is SetIdentityServerViewEvents.TermsAccepted -> processIdentityServerChange()
is SetIdentityServerViewEvents.ShowTerms -> {
navigator.openTerms(
this,
TermsService.ServiceType.IdentityService,
SetIdentityServerViewModel.sanitatizeBaseURL(it.newIdentityServer),
SetIdentityServerViewModel.sanitatizeBaseURL(it.identityServerUrl),
null)
}
}
}.exhaustive
}
}
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.identity_server)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) {
if (Activity.RESULT_OK == resultCode) {
@ -146,21 +139,9 @@ class SetIdentityServerFragment @Inject constructor(
}
private fun processIdentityServerChange() {
withState(viewModel) { state ->
if (state.newIdentityServerUrl != null) {
sharedViewModel.requestChangeToIdentityServer(state.newIdentityServerUrl)
parentFragmentManager.popBackStack()
}
viewModel.currentWantedUrl?.let {
sharedViewModel.requestChangeToIdentityServer(it)
parentFragmentManager.popBackStack()
}
}
@OnTextChanged(R.id.discovery_identity_server_enter_edittext)
fun onTextEditChange(s: Editable?) {
s?.toString()?.let { viewModel.handle(SetIdentityServerAction.UpdateIdentityServerUrl(it)) }
}
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.identity_server)
}
}

View file

@ -16,12 +16,10 @@
package im.vector.riotx.features.discovery.change
import androidx.annotation.StringRes
import com.airbnb.mvrx.MvRxState
data class SetIdentityServerState(
// At first, will contain the default identity server url if any
val newIdentityServerUrl: String? = null,
@StringRes val errorMessageId: Int? = null,
val isVerifyingServer: Boolean = false
val homeServerUrl: String = "",
// Will contain the default identity server url if any
val defaultIdentityServerUrl: String? = null
) : MvRxState

View file

@ -16,10 +16,16 @@
package im.vector.riotx.features.discovery.change
import androidx.annotation.StringRes
import im.vector.riotx.core.platform.VectorViewEvents
sealed class SetIdentityServerViewEvents : VectorViewEvents {
data class ShowTerms(val newIdentityServer: String) : SetIdentityServerViewEvents()
data class Loading(val message: CharSequence? = null) : SetIdentityServerViewEvents()
data class Failure(@StringRes val errorMessageId: Int) : SetIdentityServerViewEvents()
data class OtherFailure(val failure: Throwable) : SetIdentityServerViewEvents()
data class ShowTerms(val identityServerUrl: String) : SetIdentityServerViewEvents()
object NoTerms : SetIdentityServerViewEvents()
object TermsAccepted : SetIdentityServerViewEvents()
}

View file

@ -49,7 +49,8 @@ class SetIdentityServerViewModel @AssistedInject constructor(
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return SetIdentityServerState(
newIdentityServerUrl = session.identityService().getDefaultIdentityServer()
homeServerUrl = session.sessionParams.homeServerUrl,
defaultIdentityServerUrl = session.identityService().getDefaultIdentityServer()
)
}
@ -68,36 +69,37 @@ class SetIdentityServerViewModel @AssistedInject constructor(
}
}
val userLanguage = stringProvider.getString(R.string.resources_language)
var currentWantedUrl: String? = null
private set
private val userLanguage = stringProvider.getString(R.string.resources_language)
override fun handle(action: SetIdentityServerAction) {
when (action) {
is SetIdentityServerAction.UpdateIdentityServerUrl -> updateIdentityServerUrl(action)
SetIdentityServerAction.DoChangeIdentityServerUrl -> doChangeIdentityServerUrl()
SetIdentityServerAction.UseDefaultIdentityServer -> useDefault()
is SetIdentityServerAction.UseCustomIdentityServer -> usedCustomIdentityServerUrl(action)
}.exhaustive
}
private fun updateIdentityServerUrl(action: SetIdentityServerAction.UpdateIdentityServerUrl) {
setState {
copy(
newIdentityServerUrl = action.url,
errorMessageId = null
)
}
private fun useDefault() = withState { state ->
state.defaultIdentityServerUrl?.let { doChangeIdentityServerUrl(it) }
}
private fun doChangeIdentityServerUrl() = withState {
var baseUrl: String? = it.newIdentityServerUrl
if (baseUrl.isNullOrBlank()) {
setState {
copy(errorMessageId = R.string.settings_discovery_please_enter_server)
}
return@withState
private fun usedCustomIdentityServerUrl(action: SetIdentityServerAction.UseCustomIdentityServer) {
doChangeIdentityServerUrl(action.url)
}
private fun doChangeIdentityServerUrl(url: String) {
var baseUrl = url
if (baseUrl.isEmpty()) {
_viewEvents.post(SetIdentityServerViewEvents.Failure(R.string.settings_discovery_please_enter_server))
return
}
baseUrl = sanitatizeBaseURL(baseUrl)
setState {
copy(isVerifyingServer = true)
}
currentWantedUrl = baseUrl
_viewEvents.post(SetIdentityServerViewEvents.Loading())
// First ping the identity server v2 API
mxSession.identityService().isValidIdentityServer(baseUrl, object : MatrixCallback<Unit> {
@ -107,15 +109,11 @@ class SetIdentityServerViewModel @AssistedInject constructor(
}
override fun onFailure(failure: Throwable) {
setState {
copy(
isVerifyingServer = false,
errorMessageId = if (failure is IdentityServiceError.OutdatedIdentityServer) {
R.string.identity_server_error_outdated_identity_server
} else {
R.string.settings_discovery_bad_identity_server
}
)
if (failure is IdentityServiceError.OutdatedIdentityServer) {
_viewEvents.post(SetIdentityServerViewEvents.Failure(R.string.identity_server_error_outdated_identity_server))
} else {
_viewEvents.post(SetIdentityServerViewEvents.Failure(R.string.settings_discovery_bad_identity_server))
_viewEvents.post(SetIdentityServerViewEvents.OtherFailure(failure))
}
}
})
@ -127,9 +125,6 @@ class SetIdentityServerViewModel @AssistedInject constructor(
object : MatrixCallback<GetTermsResponse> {
override fun onSuccess(data: GetTermsResponse) {
// has all been accepted?
setState {
copy(isVerifyingServer = false)
}
val resp = data.serverResponse
val tos = resp.getLocalizedTerms(userLanguage)
if (tos.isEmpty()) {
@ -147,19 +142,11 @@ class SetIdentityServerViewModel @AssistedInject constructor(
override fun onFailure(failure: Throwable) {
if (failure is Failure.OtherServerError && failure.httpCode == 404) {
setState {
copy(isVerifyingServer = false)
}
// 404: Same as NoTerms
// TODO Handle the case where identity
_viewEvents.post(SetIdentityServerViewEvents.NoTerms)
} else {
setState {
copy(
isVerifyingServer = false,
errorMessageId = R.string.settings_discovery_bad_identity_server
)
}
_viewEvents.post(SetIdentityServerViewEvents.Failure(R.string.settings_discovery_bad_identity_server))
_viewEvents.post(SetIdentityServerViewEvents.OtherFailure(failure))
}
}
})

View file

@ -3,51 +3,75 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/identityServerSetDefaultNotice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
android:visibility="gone"
tools:text="@string/identity_server_set_default_notice"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/identityServerSetDefaultSubmit"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp"
android:text="@string/identity_server_set_default_submit"
android:textAllCaps="false"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/identityServerSetDefaultAlternative"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
tools:text="@string/identity_server_set_alternative_notice" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/discovery_identity_server_enter_til"
android:id="@+id/identityServerSetDefaultAlternativeTil"
style="@style/VectorTextInputLayout"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:errorEnabled="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/discovery_identity_server_enter_edittext"
android:id="@+id/identityServerSetDefaultAlternativeTextInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings_discovery_enter_identity_server"
android:imeOptions="actionDone"
android:inputType="textUri"
android:maxLines="3"
android:textColor="?android:textColorPrimary"
tools:text="vector.im" />
android:maxLines="1"
android:textColor="?riotx_text_primary" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/discovery_identity_server_loading"
<com.google.android.material.button.MaterialButton
android:id="@+id/identityServerSetDefaultAlternativeSubmit"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/discovery_identity_server_enter_til"
tools:visibility="visible" />
android:layout_gravity="end"
android:text="@string/identity_server_set_alternative_submit"
android:textAllCaps="false" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</ScrollView>

View file

@ -1736,7 +1736,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<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>
<string name="settings_discovery_enter_identity_server">Enter an identity server URL</string>
<string name="settings_discovery_bad_identity_server">Could not connect to identity server</string>
<string name="settings_discovery_please_enter_server">Please enter the identity server url</string>
<string name="settings_discovery_no_terms_title">Identity server has no terms of services</string>
@ -2412,4 +2412,10 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="identity_server_error_binding_error">The association has failed.</string>
<string name="identity_server_error_no_current_binding_error">The is no current association with this identifier.</string>
<string name="identity_server_set_default_notice">Your homeserver (%1$s) proposes to use %2$s for your identity server</string>
<string name="identity_server_set_default_submit">Use %1$s</string>
<string name="identity_server_set_alternative_notice">Alternatively, you can enter any other identity server URL</string>
<string name="identity_server_set_alternative_notice_no_default">Enter the URL of an identity server</string>
<string name="identity_server_set_alternative_submit">Submit</string>
</resources>