Add threePid: improve UX (remove dialog)

This commit is contained in:
Benoit Marty 2020-08-31 14:30:47 +02:00
parent 58938a239e
commit e92cf38cde
9 changed files with 153 additions and 54 deletions

View File

@ -17,6 +17,8 @@
package im.vector.app.features.form
import android.text.Editable
import android.view.View
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.textfield.TextInputEditText
@ -35,9 +37,18 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
@EpoxyAttribute
var value: String? = null
@EpoxyAttribute
var showBottomSeparator: Boolean = true
@EpoxyAttribute
var errorMessage: String? = null
@EpoxyAttribute
var enabled: Boolean = true
@EpoxyAttribute
var inputType: Int? = null
@EpoxyAttribute
var onTextChange: ((String) -> Unit)? = null
@ -51,14 +62,17 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
super.bind(holder)
holder.textInputLayout.isEnabled = enabled
holder.textInputLayout.hint = hint
holder.textInputLayout.error = errorMessage
// Update only if text is different
if (holder.textInputEditText.text.toString() != value) {
// Update only if text is different and value is not null
if (value != null && holder.textInputEditText.text.toString() != value) {
holder.textInputEditText.setText(value)
}
holder.textInputEditText.isEnabled = enabled
inputType?.let { holder.textInputEditText.inputType = it }
holder.textInputEditText.addTextChangedListener(onTextChangeListener)
holder.bottomSeparator.isVisible = showBottomSeparator
}
override fun shouldSaveViewState(): Boolean {
@ -73,5 +87,6 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
class Holder : VectorEpoxyHolder() {
val textInputLayout by bind<TextInputLayout>(R.id.formTextInputTextInputLayout)
val textInputEditText by bind<TextInputEditText>(R.id.formTextInputTextInputEditText)
val bottomSeparator by bind<View>(R.id.formTextInputDivider)
}
}

View File

@ -20,6 +20,7 @@ import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.identity.ThreePid
sealed class ThreePidsSettingsAction : VectorViewModelAction {
data class ChangeState(val newState: ThreePidsSettingsState) : ThreePidsSettingsAction()
data class AddThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
data class ContinueThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
data class CancelThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()

View File

@ -16,15 +16,15 @@
package im.vector.app.features.settings.threepids
import android.text.InputType
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import im.vector.app.R
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.getFormattedValue
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
@ -33,6 +33,7 @@ import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.discovery.settingsContinueCancelItem
import im.vector.app.features.discovery.settingsInformationItem
import im.vector.app.features.discovery.settingsSectionTitleItem
import im.vector.app.features.form.formEditTextItem
import org.matrix.android.sdk.api.session.identity.ThreePid
import javax.inject.Inject
@ -44,6 +45,9 @@ class ThreePidsSettingsController @Inject constructor(
interface InteractionListener {
fun addEmail()
fun addMsisdn()
fun cancelAdding()
fun doAddEmail(email: String)
fun doAddMsisdn(msisdn: String)
fun continueThreePid(threePid: ThreePid)
fun cancelThreePid(threePid: ThreePid)
fun deleteThreePid(threePid: ThreePid)
@ -51,8 +55,15 @@ class ThreePidsSettingsController @Inject constructor(
var interactionListener: InteractionListener? = null
private var currentInputValue = ""
override fun buildModels(data: ThreePidsSettingsViewState?) {
if (data == null) return
if (data.state is ThreePidsSettingsState.Idle) {
currentInputValue = ""
}
when (data.threePids) {
is Loading -> {
loadingItem {
@ -68,12 +79,12 @@ class ThreePidsSettingsController @Inject constructor(
}
is Success -> {
val dataList = data.threePids.invoke()
buildThreePids(dataList, data.pendingThreePids)
buildThreePids(dataList, data)
}
}
}
private fun buildThreePids(list: List<ThreePid>, pendingThreePids: Async<List<ThreePid>>) {
private fun buildThreePids(list: List<ThreePid>, data: ThreePidsSettingsViewState) {
val splited = list.groupBy { it is ThreePid.Email }
val emails = splited[true].orEmpty()
val msisdn = splited[false].orEmpty()
@ -86,16 +97,35 @@ class ThreePidsSettingsController @Inject constructor(
emails.forEach { buildThreePid("email ", it) }
// Pending threePids
pendingThreePids.invoke()
data.pendingThreePids.invoke()
?.filterIsInstance(ThreePid.Email::class.java)
?.forEach { buildPendingThreePid("p_email ", it) }
genericButtonItem {
id("addEmail")
text(stringProvider.getString(R.string.settings_add_email_address))
textColor(colorProvider.getColor(R.color.riotx_accent))
buttonClickAction(View.OnClickListener { interactionListener?.addEmail() })
}
when (data.state) {
ThreePidsSettingsState.Idle ->
genericButtonItem {
id("addEmail")
text(stringProvider.getString(R.string.settings_add_email_address))
textColor(colorProvider.getColor(R.color.riotx_accent))
buttonClickAction(View.OnClickListener { interactionListener?.addEmail() })
}
is ThreePidsSettingsState.AddingEmail -> {
formEditTextItem {
id("addingEmail")
inputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS)
hint(stringProvider.getString(R.string.medium_email))
errorMessage(data.state.error)
onTextChange { currentInputValue = it }
showBottomSeparator(false)
}
settingsContinueCancelItem {
id("contAddingEmail")
continueOnClick { interactionListener?.doAddEmail(currentInputValue) }
cancelOnClick { interactionListener?.cancelAdding() }
}
}
is ThreePidsSettingsState.AddingPhoneNumber -> Unit
}.exhaustive
settingsSectionTitleItem {
id("msisdn")
@ -105,26 +135,35 @@ class ThreePidsSettingsController @Inject constructor(
msisdn.forEach { buildThreePid("msisdn ", it) }
// Pending threePids
pendingThreePids.invoke()
data.pendingThreePids.invoke()
?.filterIsInstance(ThreePid.Msisdn::class.java)
?.forEach { buildPendingThreePid("p_msisdn ", it) }
/*
// TODO Support adding MSISDN
genericButtonItem {
id("addMsisdn")
text(stringProvider.getString(R.string.settings_add_phone_number))
textColor(colorProvider.getColor(R.color.riotx_accent))
buttonClickAction(View.OnClickListener { interactionListener?.addMsisdn() })
}
*/
// Avoid empty area
if (msisdn.isEmpty()) {
noResultItem {
id("no_msisdn")
text(stringProvider.getString(R.string.settings_phone_numbers_empty))
when (data.state) {
ThreePidsSettingsState.Idle ->
genericButtonItem {
id("addMsisdn")
text(stringProvider.getString(R.string.settings_add_phone_number))
textColor(colorProvider.getColor(R.color.riotx_accent))
buttonClickAction(View.OnClickListener { interactionListener?.addMsisdn() })
}
is ThreePidsSettingsState.AddingEmail -> Unit
is ThreePidsSettingsState.AddingPhoneNumber -> {
formEditTextItem {
id("addingMsisdn")
inputType(InputType.TYPE_CLASS_PHONE)
hint(stringProvider.getString(R.string.medium_phone_number))
errorMessage(data.state.error)
onTextChange { currentInputValue = it }
showBottomSeparator(false)
}
settingsContinueCancelItem {
id("contAddingMsisdn")
continueOnClick { interactionListener?.doAddMsisdn(currentInputValue) }
cancelOnClick { interactionListener?.cancelAdding() }
}
}
}
}.exhaustive
}
private fun buildThreePid(idPrefix: String, threePid: ThreePid) {

View File

@ -18,9 +18,7 @@ package im.vector.app.features.settings.threepids
import android.content.DialogInterface
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -30,10 +28,11 @@ import im.vector.app.core.dialogs.withColoredButton
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.isEmail
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.toast
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import org.matrix.android.sdk.api.session.identity.ThreePid
import javax.inject.Inject
@ -43,6 +42,7 @@ class ThreePidsSettingsFragment @Inject constructor(
private val epoxyController: ThreePidsSettingsController
) :
VectorBaseFragment(),
OnBackPressed,
ThreePidsSettingsViewModel.Factory by viewModelFactory,
ThreePidsSettingsController.InteractionListener {
@ -90,27 +90,15 @@ class ThreePidsSettingsFragment @Inject constructor(
}
override fun addEmail() {
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val input = layout.findViewById<EditText>(R.id.editText)
input.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_add_email_address)
.setView(layout)
.setPositiveButton(R.string.ok) { _, _ ->
val email = input.text.toString()
doAddEmail(email)
}
.setNegativeButton(R.string.cancel, null)
.show()
viewModel.handle(ThreePidsSettingsAction.ChangeState(ThreePidsSettingsState.AddingEmail(null)))
}
private fun doAddEmail(email: String) {
override fun doAddEmail(email: String) {
viewModel.handle(ThreePidsSettingsAction.ChangeState(ThreePidsSettingsState.AddingEmail(null)))
// Check that email is valid
if (!email.isEmail()) {
requireActivity().toast(R.string.auth_invalid_email)
viewModel.handle(ThreePidsSettingsAction.ChangeState(ThreePidsSettingsState.AddingEmail(getString(R.string.auth_invalid_email))))
return
}
@ -118,9 +106,21 @@ class ThreePidsSettingsFragment @Inject constructor(
}
override fun addMsisdn() {
viewModel.handle(ThreePidsSettingsAction.ChangeState(ThreePidsSettingsState.AddingPhoneNumber(null)))
}
override fun doAddMsisdn(msisdn: String) {
viewModel.handle(ThreePidsSettingsAction.ChangeState(ThreePidsSettingsState.AddingPhoneNumber(null)))
TODO("Not yet implemented")
}
override fun cancelAdding() {
viewModel.handle(ThreePidsSettingsAction.ChangeState(ThreePidsSettingsState.Idle))
// Hide the keyboard
view?.hideKeyboard()
}
override fun continueThreePid(threePid: ThreePid) {
viewModel.handle(ThreePidsSettingsAction.ContinueThreePid(threePid))
}
@ -139,4 +139,15 @@ class ThreePidsSettingsFragment @Inject constructor(
.show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
return withState(viewModel) {
if (it.state is ThreePidsSettingsState.Idle) {
false
} else {
cancelAdding()
true
}
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.threepids
sealed class ThreePidsSettingsState {
object Idle : ThreePidsSettingsState()
data class AddingEmail(val error: String?) : ThreePidsSettingsState()
data class AddingPhoneNumber(val error: String?) : ThreePidsSettingsState()
}

View File

@ -129,14 +129,23 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
override fun handle(action: ThreePidsSettingsAction) {
when (action) {
is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action)
is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action)
is ThreePidsSettingsAction.ContinueThreePid -> handleContinueThreePid(action)
is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action)
is ThreePidsSettingsAction.AccountPassword -> handleAccountPassword(action)
is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action)
is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action)
is ThreePidsSettingsAction.AccountPassword -> handleAccountPassword(action)
is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action)
is ThreePidsSettingsAction.ChangeState -> handleChangeState(action)
}.exhaustive
}
private fun handleChangeState(action: ThreePidsSettingsAction.ChangeState) {
setState {
copy(
state = action.newState
)
}
}
private fun handleAddThreePid(action: ThreePidsSettingsAction.AddThreePid) {
isLoading(true)

View File

@ -22,6 +22,7 @@ import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.identity.ThreePid
data class ThreePidsSettingsViewState(
val state: ThreePidsSettingsState = ThreePidsSettingsState.Idle,
val isLoading: Boolean = false,
val threePids: Async<List<ThreePid>> = Uninitialized,
val pendingThreePids: Async<List<ThreePid>> = Uninitialized

View File

@ -14,6 +14,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:errorEnabled="true"
app:layout_constraintBottom_toTopOf="@+id/formTextInputDivider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -684,7 +684,6 @@
<string name="settings_emails">Email addresses</string>
<string name="settings_phone_numbers">Phone numbers</string>
<string name="settings_phone_numbers_empty">No phone number has been added to your account</string>
<string name="settings_remove_three_pid_confirmation_content">Remove %s?</string>
<string name="error_threepid_auth_failed">Ensure that you have clicked on the link in the email we have sent to you.</string>