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

View File

@ -16,15 +16,15 @@
package im.vector.app.features.settings.threepids package im.vector.app.features.settings.threepids
import android.text.InputType
import android.view.View import android.view.View
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.loadingItem 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.extensions.getFormattedValue
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider 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.settingsContinueCancelItem
import im.vector.app.features.discovery.settingsInformationItem import im.vector.app.features.discovery.settingsInformationItem
import im.vector.app.features.discovery.settingsSectionTitleItem import im.vector.app.features.discovery.settingsSectionTitleItem
import im.vector.app.features.form.formEditTextItem
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import javax.inject.Inject import javax.inject.Inject
@ -44,6 +45,9 @@ class ThreePidsSettingsController @Inject constructor(
interface InteractionListener { interface InteractionListener {
fun addEmail() fun addEmail()
fun addMsisdn() fun addMsisdn()
fun cancelAdding()
fun doAddEmail(email: String)
fun doAddMsisdn(msisdn: String)
fun continueThreePid(threePid: ThreePid) fun continueThreePid(threePid: ThreePid)
fun cancelThreePid(threePid: ThreePid) fun cancelThreePid(threePid: ThreePid)
fun deleteThreePid(threePid: ThreePid) fun deleteThreePid(threePid: ThreePid)
@ -51,8 +55,15 @@ class ThreePidsSettingsController @Inject constructor(
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
private var currentInputValue = ""
override fun buildModels(data: ThreePidsSettingsViewState?) { override fun buildModels(data: ThreePidsSettingsViewState?) {
if (data == null) return if (data == null) return
if (data.state is ThreePidsSettingsState.Idle) {
currentInputValue = ""
}
when (data.threePids) { when (data.threePids) {
is Loading -> { is Loading -> {
loadingItem { loadingItem {
@ -68,12 +79,12 @@ class ThreePidsSettingsController @Inject constructor(
} }
is Success -> { is Success -> {
val dataList = data.threePids.invoke() 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 splited = list.groupBy { it is ThreePid.Email }
val emails = splited[true].orEmpty() val emails = splited[true].orEmpty()
val msisdn = splited[false].orEmpty() val msisdn = splited[false].orEmpty()
@ -86,16 +97,35 @@ class ThreePidsSettingsController @Inject constructor(
emails.forEach { buildThreePid("email ", it) } emails.forEach { buildThreePid("email ", it) }
// Pending threePids // Pending threePids
pendingThreePids.invoke() data.pendingThreePids.invoke()
?.filterIsInstance(ThreePid.Email::class.java) ?.filterIsInstance(ThreePid.Email::class.java)
?.forEach { buildPendingThreePid("p_email ", it) } ?.forEach { buildPendingThreePid("p_email ", it) }
when (data.state) {
ThreePidsSettingsState.Idle ->
genericButtonItem { genericButtonItem {
id("addEmail") id("addEmail")
text(stringProvider.getString(R.string.settings_add_email_address)) text(stringProvider.getString(R.string.settings_add_email_address))
textColor(colorProvider.getColor(R.color.riotx_accent)) textColor(colorProvider.getColor(R.color.riotx_accent))
buttonClickAction(View.OnClickListener { interactionListener?.addEmail() }) 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 { settingsSectionTitleItem {
id("msisdn") id("msisdn")
@ -105,26 +135,35 @@ class ThreePidsSettingsController @Inject constructor(
msisdn.forEach { buildThreePid("msisdn ", it) } msisdn.forEach { buildThreePid("msisdn ", it) }
// Pending threePids // Pending threePids
pendingThreePids.invoke() data.pendingThreePids.invoke()
?.filterIsInstance(ThreePid.Msisdn::class.java) ?.filterIsInstance(ThreePid.Msisdn::class.java)
?.forEach { buildPendingThreePid("p_msisdn ", it) } ?.forEach { buildPendingThreePid("p_msisdn ", it) }
/* when (data.state) {
// TODO Support adding MSISDN ThreePidsSettingsState.Idle ->
genericButtonItem { genericButtonItem {
id("addMsisdn") id("addMsisdn")
text(stringProvider.getString(R.string.settings_add_phone_number)) text(stringProvider.getString(R.string.settings_add_phone_number))
textColor(colorProvider.getColor(R.color.riotx_accent)) textColor(colorProvider.getColor(R.color.riotx_accent))
buttonClickAction(View.OnClickListener { interactionListener?.addMsisdn() }) buttonClickAction(View.OnClickListener { interactionListener?.addMsisdn() })
} }
*/ is ThreePidsSettingsState.AddingEmail -> Unit
// Avoid empty area is ThreePidsSettingsState.AddingPhoneNumber -> {
if (msisdn.isEmpty()) { formEditTextItem {
noResultItem { id("addingMsisdn")
id("no_msisdn") inputType(InputType.TYPE_CLASS_PHONE)
text(stringProvider.getString(R.string.settings_phone_numbers_empty)) 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) { 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.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.view.View import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState 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.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive 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.extensions.isEmail
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.toast
import kotlinx.android.synthetic.main.fragment_generic_recycler.* import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import javax.inject.Inject import javax.inject.Inject
@ -43,6 +42,7 @@ class ThreePidsSettingsFragment @Inject constructor(
private val epoxyController: ThreePidsSettingsController private val epoxyController: ThreePidsSettingsController
) : ) :
VectorBaseFragment(), VectorBaseFragment(),
OnBackPressed,
ThreePidsSettingsViewModel.Factory by viewModelFactory, ThreePidsSettingsViewModel.Factory by viewModelFactory,
ThreePidsSettingsController.InteractionListener { ThreePidsSettingsController.InteractionListener {
@ -90,27 +90,15 @@ class ThreePidsSettingsFragment @Inject constructor(
} }
override fun addEmail() { override fun addEmail() {
val inflater = requireActivity().layoutInflater viewModel.handle(ThreePidsSettingsAction.ChangeState(ThreePidsSettingsState.AddingEmail(null)))
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()
} }
private fun doAddEmail(email: String) { override fun doAddEmail(email: String) {
viewModel.handle(ThreePidsSettingsAction.ChangeState(ThreePidsSettingsState.AddingEmail(null)))
// Check that email is valid // Check that email is valid
if (!email.isEmail()) { if (!email.isEmail()) {
requireActivity().toast(R.string.auth_invalid_email) viewModel.handle(ThreePidsSettingsAction.ChangeState(ThreePidsSettingsState.AddingEmail(getString(R.string.auth_invalid_email))))
return return
} }
@ -118,9 +106,21 @@ class ThreePidsSettingsFragment @Inject constructor(
} }
override fun addMsisdn() { 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") TODO("Not yet implemented")
} }
override fun cancelAdding() {
viewModel.handle(ThreePidsSettingsAction.ChangeState(ThreePidsSettingsState.Idle))
// Hide the keyboard
view?.hideKeyboard()
}
override fun continueThreePid(threePid: ThreePid) { override fun continueThreePid(threePid: ThreePid) {
viewModel.handle(ThreePidsSettingsAction.ContinueThreePid(threePid)) viewModel.handle(ThreePidsSettingsAction.ContinueThreePid(threePid))
} }
@ -139,4 +139,15 @@ class ThreePidsSettingsFragment @Inject constructor(
.show() .show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE) .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

@ -134,9 +134,18 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action) is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action)
is ThreePidsSettingsAction.AccountPassword -> handleAccountPassword(action) is ThreePidsSettingsAction.AccountPassword -> handleAccountPassword(action)
is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action) is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action)
is ThreePidsSettingsAction.ChangeState -> handleChangeState(action)
}.exhaustive }.exhaustive
} }
private fun handleChangeState(action: ThreePidsSettingsAction.ChangeState) {
setState {
copy(
state = action.newState
)
}
}
private fun handleAddThreePid(action: ThreePidsSettingsAction.AddThreePid) { private fun handleAddThreePid(action: ThreePidsSettingsAction.AddThreePid) {
isLoading(true) isLoading(true)

View File

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

View File

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

View File

@ -684,7 +684,6 @@
<string name="settings_emails">Email addresses</string> <string name="settings_emails">Email addresses</string>
<string name="settings_phone_numbers">Phone numbers</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="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> <string name="error_threepid_auth_failed">Ensure that you have clicked on the link in the email we have sent to you.</string>