From 00d0c34363be185bc34868f58797b2d4563d23b2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Dec 2019 22:58:15 +0100 Subject: [PATCH] SoftLogout: use Epoxy --- .../features/signout/SoftLogoutAction.kt | 2 + .../features/signout/SoftLogoutController.kt | 150 +++++++++++++ .../features/signout/SoftLogoutFragment.kt | 144 ++++--------- .../features/signout/SoftLogoutViewModel.kt | 19 +- .../features/signout/SoftLogoutViewState.kt | 3 +- .../signout/epoxy/LoginCenterButtonItem.kt | 45 ++++ .../signout/epoxy/LoginErrorWithRetryItem.kt | 45 ++++ .../features/signout/epoxy/LoginHeaderItem.kt | 27 +++ .../signout/epoxy/LoginPasswordFormItem.kt | 79 +++++++ .../signout/epoxy/LoginRedButtonItem.kt | 45 ++++ .../features/signout/epoxy/LoginTextItem.kt | 41 ++++ .../features/signout/epoxy/LoginTitleItem.kt | 41 ++++ .../signout/epoxy/LoginTitleSmallItem.kt | 41 ++++ .../main/res/layout/fragment_soft_logout.xml | 200 ------------------ .../res/layout/item_login_centered_button.xml | 10 + .../res/layout/item_login_error_retry.xml | 35 +++ .../src/main/res/layout/item_login_header.xml | 8 + .../res/layout/item_login_password_form.xml | 78 +++++++ .../main/res/layout/item_login_red_button.xml | 19 ++ .../src/main/res/layout/item_login_text.xml | 12 ++ .../src/main/res/layout/item_login_title.xml | 12 ++ .../res/layout/item_login_title_small.xml | 12 ++ 22 files changed, 763 insertions(+), 305 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginCenterButtonItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginErrorWithRetryItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginHeaderItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginPasswordFormItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginRedButtonItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginTextItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginTitleItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginTitleSmallItem.kt delete mode 100644 vector/src/main/res/layout/fragment_soft_logout.xml create mode 100644 vector/src/main/res/layout/item_login_centered_button.xml create mode 100644 vector/src/main/res/layout/item_login_error_retry.xml create mode 100644 vector/src/main/res/layout/item_login_header.xml create mode 100644 vector/src/main/res/layout/item_login_password_form.xml create mode 100644 vector/src/main/res/layout/item_login_red_button.xml create mode 100644 vector/src/main/res/layout/item_login_text.xml create mode 100644 vector/src/main/res/layout/item_login_title.xml create mode 100644 vector/src/main/res/layout/item_login_title_small.xml diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt index a7f25dddba..254f65d622 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt @@ -20,6 +20,8 @@ import im.vector.riotx.core.platform.VectorViewModelAction sealed class SoftLogoutAction : VectorViewModelAction { object RetryLoginFlow : SoftLogoutAction() + object TogglePassword : SoftLogoutAction() + data class SignInAgain(val password: String) : SoftLogoutAction() // TODO Add reset pwd... } diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutController.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutController.kt new file mode 100644 index 0000000000..b95ef91ad9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutController.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2019 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.riotx.features.signout + +import com.airbnb.epoxy.EpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Success +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.login.LoginMode +import im.vector.riotx.features.signout.epoxy.* +import javax.inject.Inject + +class SoftLogoutController @Inject constructor( + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter +) : EpoxyController() { + + var listener: Listener? = null + + private var viewState: SoftLogoutViewState? = null + + init { + // We are requesting a model build directly as the first build of epoxy is on the main thread. + // It avoids to build the whole list of breadcrumbs on the main thread. + requestModelBuild() + } + + fun update(viewState: SoftLogoutViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val safeViewState = viewState ?: return + + buildHeader(safeViewState) + buildForm(safeViewState) + buildClearDataSection() + } + + private fun buildHeader(state: SoftLogoutViewState) { + loginHeaderItem { + id("header") + } + loginTitleItem { + id("title") + text(stringProvider.getString(R.string.soft_logout_title)) + } + loginTitleSmallItem { + id("signTitle") + text(stringProvider.getString(R.string.soft_logout_signin_title)) + } + loginTextItem { + id("signText1") + text(stringProvider.getString(R.string.soft_logout_signin_notice, + state.homeServerUrl, + state.userDisplayName, + state.userId)) + } + if (state.hasUnsavedKeys) { + loginTextItem { + id("signText2") + text(stringProvider.getString(R.string.soft_logout_signin_e2e_warning_notice)) + } + } + } + + private fun buildForm(state: SoftLogoutViewState) { + when (state.asyncHomeServerLoginFlowRequest) { + is Incomplete -> { + loadingItem { + id("loading") + } + } + is Fail -> { + loginErrorWithRetryItem { + id("errorRetry") + text(errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error)) + listener { listener?.retry() } + } + } + is Success -> { + when (state.asyncHomeServerLoginFlowRequest.invoke()) { + LoginMode.Password -> { + loginPasswordFormItem { + id("passwordForm") + stringProvider(stringProvider) + passwordShown(state.passwordShown) + errorText((state.asyncLoginAction as? Fail)?.error?.let { errorFormatter.toHumanReadable(it) }) + passwordRevealClickListener { listener?.revealPasswordClicked() } + forgetPasswordClickListener { listener?.forgetPasswordClicked() } + submitClickListener { password -> listener?.submit(password) } + } + } + LoginMode.Sso -> { + loginCenterButtonItem { + id("sso") + listener { listener?.ssoSubmit() } + } + } + LoginMode.Unknown -> Unit // Should not happen + LoginMode.Unsupported -> Unit // Should not happen + } + } + } + } + + private fun buildClearDataSection() { + loginTitleSmallItem { + id("clearDataTitle") + text(stringProvider.getString(R.string.soft_logout_clear_data_title)) + } + loginTextItem { + id("clearDataText") + text(stringProvider.getString(R.string.soft_logout_clear_data_notice)) + } + loginRedButtonItem { + id("clearDataSubmit") + text(stringProvider.getString(R.string.soft_logout_clear_data_submit)) + listener { listener?.clearData() } + } + } + + interface Listener { + fun retry() + fun submit(password: String) + fun ssoSubmit() + fun clearData() + fun forgetPasswordClicked() + fun revealPasswordClicked() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt index 52ac4bb8e4..4959217aee 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt @@ -17,87 +17,78 @@ package im.vector.riotx.features.signout import android.content.DialogInterface -import android.os.Build import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog -import androidx.autofill.HintConstants -import androidx.core.view.isVisible -import butterknife.OnClick -import com.airbnb.mvrx.* -import com.jakewharton.rxbinding3.widget.textChanges +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.hideKeyboard -import im.vector.riotx.core.extensions.setTextOrHide -import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivityArgs -import im.vector.riotx.features.login.LoginMode -import io.reactivex.rxkotlin.subscribeBy -import kotlinx.android.synthetic.main.fragment_soft_logout.* -import kotlinx.android.synthetic.main.item_error_retry.* +import kotlinx.android.synthetic.main.fragment_generic_recycler.* import javax.inject.Inject /** * In this screen: * - the user is asked to enter a password to sign in again to a homeserver. * - or to cleanup all the data - * TODO: migrate to Epoxy (along with all the login screen?) */ class SoftLogoutFragment @Inject constructor( - private val errorFormatter: ErrorFormatter -) : VectorBaseFragment() { - - private var passwordShown = false + private val errorFormatter: ErrorFormatter, + private val softLogoutController: SoftLogoutController +) : VectorBaseFragment(), SoftLogoutController.Listener { private val softLogoutViewModel: SoftLogoutViewModel by activityViewModel() - override fun getLayoutResId() = R.layout.fragment_soft_logout + override fun getLayoutResId() = R.layout.fragment_generic_recycler override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupSubmitButton() - setupPasswordReveal() - setupAutoFill() + setupRecyclerView() + + // TODO setupSubmitButton() + // TODO setupPasswordReveal() } - private fun setupAutoFill() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - softLogoutPasswordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) - } + private fun setupRecyclerView() { + recyclerView.configureWith(softLogoutController) + softLogoutController.listener = this } - @OnClick(R.id.itemErrorRetryButton) - fun retry() { + override fun onDestroyView() { + recyclerView.cleanup() + softLogoutController.listener = null + super.onDestroyView() + } + + override fun retry() { softLogoutViewModel.handle(SoftLogoutAction.RetryLoginFlow) } - @OnClick(R.id.softLogoutSubmit) - fun submit() { + override fun submit(password: String) { cleanupUi() - - val password = softLogoutPasswordField.text.toString() softLogoutViewModel.handle(SoftLogoutAction.SignInAgain(password)) } - @OnClick(R.id.softLogoutFormSsoSubmit) - fun ssoSubmit() { + override fun ssoSubmit() { // TODO } - @OnClick(R.id.softLogoutClearDataSubmit) - fun clearData() { + override fun clearData() { withState(softLogoutViewModel) { state -> cleanupUi() val messageResId = if (state.hasUnsavedKeys) { - R.string.soft_logout_clear_data_dialog_content - } else { R.string.soft_logout_clear_data_dialog_e2e_warning_content + } else { + R.string.soft_logout_clear_data_dialog_content } AlertDialog.Builder(requireActivity()) @@ -117,83 +108,30 @@ class SoftLogoutFragment @Inject constructor( } private fun cleanupUi() { - softLogoutSubmit.hideKeyboard() - softLogoutPasswordFieldTil.error = null - } - - private fun setupUi(state: SoftLogoutViewState) { - softLogoutNotice.text = getString(R.string.soft_logout_signin_notice, - state.homeServerUrl, - state.userDisplayName, - state.userId) - - softLogoutE2eWarningNotice.isVisible = state.hasUnsavedKeys - } - - private fun setupForm(state: SoftLogoutViewState) { - softLogoutFormLoading.isVisible = state.asyncHomeServerLoginFlowRequest is Loading - softLogoutFormSsoSubmit.isVisible = state.asyncHomeServerLoginFlowRequest.invoke() == LoginMode.Sso - softLogoutFormPassword.isVisible = state.asyncHomeServerLoginFlowRequest.invoke() == LoginMode.Password - softLogoutFormError.isVisible = state.asyncHomeServerLoginFlowRequest is Fail - itemErrorRetryText.setTextOrHide((state.asyncHomeServerLoginFlowRequest as? Fail)?.error?.let { errorFormatter.toHumanReadable(it) }) + recyclerView.hideKeyboard() + // TODO softLogoutPasswordFieldTil.error = null } private fun setupSubmitButton() { - softLogoutPasswordField.textChanges() - .map { it.trim().isNotEmpty() } - .subscribeBy { - softLogoutPasswordFieldTil.error = null - softLogoutSubmit.isEnabled = it - } - .disposeOnDestroyView() +// softLogoutPasswordField.textChanges() +// .map { it.trim().isNotEmpty() } +// .subscribeBy { +// softLogoutPasswordFieldTil.error = null +// softLogoutSubmit.isEnabled = it +// } +// .disposeOnDestroyView() } - @OnClick(R.id.softLogoutForgetPasswordButton) - fun forgetPasswordClicked() { + override fun forgetPasswordClicked() { // TODO // loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) } - private fun setupPasswordReveal() { - passwordShown = false - - softLogoutPasswordReveal.setOnClickListener { - passwordShown = !passwordShown - - renderPasswordField() - } - - renderPasswordField() - } - - private fun renderPasswordField() { - softLogoutPasswordField.showPassword(passwordShown) - - if (passwordShown) { - softLogoutPasswordReveal.setImageResource(R.drawable.ic_eye_closed_black) - softLogoutPasswordReveal.contentDescription = getString(R.string.a11y_hide_password) - } else { - softLogoutPasswordReveal.setImageResource(R.drawable.ic_eye_black) - softLogoutPasswordReveal.contentDescription = getString(R.string.a11y_show_password) - } + override fun revealPasswordClicked() { + softLogoutViewModel.handle(SoftLogoutAction.TogglePassword) } override fun invalidate() = withState(softLogoutViewModel) { state -> - setupUi(state) - setupForm(state) - setupAutoFill() - - when (state.asyncLoginAction) { - is Loading -> { - // Ensure password is hidden - passwordShown = false - renderPasswordField() - } - is Fail -> { - softLogoutPasswordFieldTil.error = errorFormatter.toHumanReadable(state.asyncLoginAction.error) - } - // Success is handled by the SoftLogoutActivity - is Success -> Unit - } + softLogoutController.update(state) } } diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt index 8e73da0416..f2c31fe836 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt @@ -145,11 +145,28 @@ class SoftLogoutViewModel @AssistedInject constructor( when (action) { is SoftLogoutAction.RetryLoginFlow -> getSupportedLoginFlow() is SoftLogoutAction.SignInAgain -> handleSignInAgain(action) + is SoftLogoutAction.TogglePassword -> handleTogglePassword() + } + } + + private fun handleTogglePassword() { + withState { + setState { + copy( + passwordShown = !this.passwordShown + ) + } } } private fun handleSignInAgain(action: SoftLogoutAction.SignInAgain) { - setState { copy(asyncLoginAction = Loading()) } + setState { + copy( + asyncLoginAction = Loading(), + // Ensure password is hidden + passwordShown = false + ) + } currentTask = session.signInAgain(action.password, object : MatrixCallback { override fun onFailure(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt index c58eec821d..efa5565207 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt @@ -28,7 +28,8 @@ data class SoftLogoutViewState( val homeServerUrl: String, val userId: String, val userDisplayName: String, - val hasUnsavedKeys: Boolean + val hasUnsavedKeys: Boolean, + val passwordShown: Boolean = false ) : MvRxState { fun isLoading(): Boolean { diff --git a/vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginCenterButtonItem.kt b/vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginCenterButtonItem.kt new file mode 100644 index 0000000000..dbb6adf405 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/epoxy/LoginCenterButtonItem.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 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.riotx.features.signout.epoxy + +import android.widget.Button +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_login_centered_button) +abstract class LoginCenterButtonItem : VectorEpoxyModel() { + + @EpoxyAttribute var text: String? = null + @EpoxyAttribute var listener: (() -> Unit)? = null + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.button.setTextOrHide(text) + holder.button.setOnClickListener { + listener?.invoke() + } + } + + class Holder : VectorEpoxyHolder() { + val button by bind