Login: move existing code to a Fragment, MvRx style

This commit is contained in:
Benoit Marty 2019-09-13 10:07:55 +02:00
parent 6249a59203
commit 05b2092ffc
5 changed files with 266 additions and 127 deletions

View File

@ -42,15 +42,12 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag
import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment
import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.login.LoginFragment
import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
@ -65,13 +62,7 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.*
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class]) @Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
@ -134,6 +125,8 @@ interface ScreenComponent {
fun inject(publicRoomsFragment: PublicRoomsFragment) fun inject(publicRoomsFragment: PublicRoomsFragment)
fun inject(loginFragment: LoginFragment)
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment) fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
fun inject(quickReactionFragment: QuickReactionFragment) fun inject(quickReactionFragment: QuickReactionFragment)

View File

@ -18,146 +18,38 @@ package im.vector.riotx.features.login
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.core.view.isVisible
import arrow.core.Try
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.setTextWithColoredPart
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.homeserver.ServerUrlsRepository
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.activity_login.*
import javax.inject.Inject import javax.inject.Inject
class LoginActivity : VectorBaseActivity() { class LoginActivity : VectorBaseActivity() {
@Inject lateinit var authenticator: Authenticator @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
private var passwordShown = false
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun getLayoutRes() = R.layout.activity_simple
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
setupNotice()
setupAuthButton()
setupPasswordReveal()
homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(this))
}
private fun setupNotice() { override fun initUiAndData() {
riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part) if (isFirstCreation()) {
addFragment(LoginFragment(), R.id.simpleFragmentContainer)
riotx_no_registration_notice.setOnClickListener {
openUrlInExternalBrowser(this@LoginActivity, "https://about.riot.im/downloads")
} }
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
showDisclaimerDialog(this) showDisclaimerDialog(this)
} }
private fun authenticate() {
passwordShown = false
renderPasswordField()
val login = loginField.text?.trim().toString()
val password = passwordField.text?.trim().toString()
buildHomeServerConnectionConfig().fold(
{ Toast.makeText(this@LoginActivity, "Authenticate failure: $it", Toast.LENGTH_LONG).show() },
{ authenticateWith(it, login, password) }
)
}
private fun authenticateWith(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String) {
progressBar.isVisible = true
touchArea.isVisible = true
authenticator.authenticate(homeServerConnectionConfig, login, password, object : MatrixCallback<Session> {
override fun onSuccess(data: Session) {
activeSessionHolder.setActiveSession(data)
data.configureAndStart(pushRuleTriggerListener)
goToHome()
}
override fun onFailure(failure: Throwable) {
progressBar.isVisible = false
touchArea.isVisible = false
Toast.makeText(this@LoginActivity, "Authenticate failure: $failure", Toast.LENGTH_LONG).show()
}
})
}
private fun buildHomeServerConnectionConfig(): Try<HomeServerConnectionConfig> {
return Try {
val homeServerUri = homeServerField.text?.trim().toString()
HomeServerConnectionConfig.Builder()
.withHomeServerUri(homeServerUri)
.build()
}
}
private fun setupAuthButton() {
Observable
.combineLatest(
loginField.textChanges().map { it.trim().isNotEmpty() },
passwordField.textChanges().map { it.trim().isNotEmpty() },
homeServerField.textChanges().map { it.trim().isNotEmpty() },
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
}
)
.subscribeBy { authenticateButton.isEnabled = it }
.disposeOnDestroy()
authenticateButton.setOnClickListener { authenticate() }
}
private fun setupPasswordReveal() {
passwordShown = false
passwordReveal.setOnClickListener {
passwordShown = !passwordShown
renderPasswordField()
}
renderPasswordField()
}
private fun renderPasswordField() {
passwordField.showPassword(passwordShown)
passwordReveal.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
}
private fun goToHome() {
val intent = HomeActivity.newIntent(this)
startActivity(intent)
finish()
}
companion object { companion object {
fun newIntent(context: Context): Intent { fun newIntent(context: Context): Intent {

View File

@ -0,0 +1,177 @@
/*
* 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.login
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.view.isVisible
import arrow.core.Try
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.extensions.setTextWithColoredPart
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.homeserver.ServerUrlsRepository
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.activity_login.*
import javax.inject.Inject
/**
* What can be improved:
* - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect
*/
class LoginFragment : VectorBaseFragment() {
private val viewModel: LoginViewModel by activityViewModel()
@Inject lateinit var authenticator: Authenticator
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
private var passwordShown = false
@Inject lateinit var errorFormatter: ErrorFormatter
override fun getLayoutResId() = R.layout.activity_login
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupNotice()
setupAuthButton()
setupPasswordReveal()
homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext()))
// viewModel.joinRoomErrorLiveData.observeEvent(this) { throwable ->
// Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
// .show()
// }
}
private fun setupNotice() {
riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part)
riotx_no_registration_notice.setOnClickListener {
openUrlInExternalBrowser(requireActivity(), "https://about.riot.im/downloads")
}
}
private fun authenticate() {
passwordShown = false
renderPasswordField()
val login = loginField.text?.trim().toString()
val password = passwordField.text?.trim().toString()
buildHomeServerConnectionConfig().fold(
{ Toast.makeText(requireActivity(), "Authenticate failure: $it", Toast.LENGTH_LONG).show() },
{ authenticateWith(it, login, password) }
)
}
private fun authenticateWith(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String) {
progressBar.isVisible = true
touchArea.isVisible = true
authenticator.authenticate(homeServerConnectionConfig, login, password, object : MatrixCallback<Session> {
override fun onSuccess(data: Session) {
activeSessionHolder.setActiveSession(data)
data.configureAndStart(pushRuleTriggerListener)
goToHome()
}
override fun onFailure(failure: Throwable) {
progressBar.isVisible = false
touchArea.isVisible = false
Toast.makeText(requireActivity(), "Authenticate failure: $failure", Toast.LENGTH_LONG).show()
}
})
}
private fun buildHomeServerConnectionConfig(): Try<HomeServerConnectionConfig> {
return Try {
val homeServerUri = homeServerField.text?.trim().toString()
HomeServerConnectionConfig.Builder()
.withHomeServerUri(homeServerUri)
.build()
}
}
private fun setupAuthButton() {
Observable
.combineLatest(
loginField.textChanges().map { it.trim().isNotEmpty() },
passwordField.textChanges().map { it.trim().isNotEmpty() },
homeServerField.textChanges().map { it.trim().isNotEmpty() },
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
}
)
.subscribeBy { authenticateButton.isEnabled = it }
.disposeOnDestroy()
authenticateButton.setOnClickListener { authenticate() }
}
private fun setupPasswordReveal() {
passwordShown = false
passwordReveal.setOnClickListener {
passwordShown = !passwordShown
renderPasswordField()
}
renderPasswordField()
}
private fun renderPasswordField() {
passwordField.showPassword(passwordShown)
passwordReveal.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
}
private fun goToHome() {
val intent = HomeActivity.newIntent(requireActivity())
startActivity(intent)
requireActivity().finish()
}
override fun invalidate() = withState(viewModel) { state ->
}
}

View File

@ -0,0 +1,47 @@
/*
* 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.login
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.riotx.core.platform.VectorViewModel
class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState) : VectorViewModel<LoginViewState>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: LoginViewState): LoginViewModel
}
companion object : MvRxViewModelFactory<LoginViewModel, LoginViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: LoginViewState): LoginViewModel? {
val activity: LoginActivity = (viewModelContext as ActivityViewModelContext).activity()
return activity.loginViewModelFactory.create(state)
}
}
init {
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.login
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
data class LoginViewState(
// Current pagination request
val asyncHomeServerLoginFlowRequest: Async<LoginFlowResult> = Uninitialized
) : MvRxState
// TODO Remove
data class LoginFlowResult(val remover: Boolean)