diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 6bfbddbabb..66281aad37 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -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.room.detail.RoomDetailFragment 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.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.detail.timeline.action.* import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView 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.VideoMediaViewerActivity 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.picker.RoomDirectoryPickerFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment -import im.vector.riotx.features.settings.VectorSettingsActivity -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.* import im.vector.riotx.features.settings.push.PushGatewaysFragment @Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class]) @@ -134,6 +125,8 @@ interface ScreenComponent { fun inject(publicRoomsFragment: PublicRoomsFragment) + fun inject(loginFragment: LoginFragment) + fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment) fun inject(quickReactionFragment: QuickReactionFragment) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 41eed536e3..75fcbb7479 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -18,146 +18,38 @@ package im.vector.riotx.features.login import android.content.Context 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.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent -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.extensions.addFragment 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.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 class LoginActivity : VectorBaseActivity() { - @Inject lateinit var authenticator: Authenticator - @Inject lateinit var activeSessionHolder: ActiveSessionHolder - @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener + @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory - private var passwordShown = false override fun injectWith(injector: ScreenComponent) { injector.inject(this) } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_login) - setupNotice() - setupAuthButton() - setupPasswordReveal() - homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(this)) - } + override fun getLayoutRes() = R.layout.activity_simple - 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(this@LoginActivity, "https://about.riot.im/downloads") + override fun initUiAndData() { + if (isFirstCreation()) { + addFragment(LoginFragment(), R.id.simpleFragmentContainer) } } + override fun onResume() { super.onResume() 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 { - 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 { - 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 { 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 { fun newIntent(context: Context): Intent { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt new file mode 100644 index 0000000000..0471ab6a8a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -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 { + 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 { + 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 { 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 -> + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt new file mode 100644 index 0000000000..a34e112e57 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -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(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: LoginViewState): LoginViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: LoginViewState): LoginViewModel? { + val activity: LoginActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.loginViewModelFactory.create(state) + } + } + + init { + + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt new file mode 100644 index 0000000000..ea63f777b7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -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 = Uninitialized +) : MvRxState + + +// TODO Remove +data class LoginFlowResult(val remover: Boolean) \ No newline at end of file