Login UX flow: set avatar and display name after account creation

This commit is contained in:
Benoit Marty 2021-04-14 17:38:51 +02:00 committed by Benoit Marty
parent 408a0fc010
commit c141b26212
15 changed files with 555 additions and 22 deletions

View File

@ -76,6 +76,7 @@ import im.vector.app.features.login2.LoginFragment2SigninPassword
import im.vector.app.features.login2.LoginFragment2SigninUsername import im.vector.app.features.login2.LoginFragment2SigninUsername
import im.vector.app.features.login2.LoginFragment2SignupPassword import im.vector.app.features.login2.LoginFragment2SignupPassword
import im.vector.app.features.login2.LoginFragment2SignupUsername import im.vector.app.features.login2.LoginFragment2SignupUsername
import im.vector.app.features.login2.created.AccountCreatedFragment
import im.vector.app.features.login2.LoginFragmentToAny2 import im.vector.app.features.login2.LoginFragmentToAny2
import im.vector.app.features.login2.LoginGenericTextInputFormFragment2 import im.vector.app.features.login2.LoginGenericTextInputFormFragment2
import im.vector.app.features.login2.LoginResetPasswordFragment2 import im.vector.app.features.login2.LoginResetPasswordFragment2
@ -286,6 +287,11 @@ interface FragmentModule {
@FragmentKey(LoginFragment2SigninUsername::class) @FragmentKey(LoginFragment2SigninUsername::class)
fun bindLoginFragment2SigninUsername(fragment: LoginFragment2SigninUsername): Fragment fun bindLoginFragment2SigninUsername(fragment: LoginFragment2SigninUsername): Fragment
@Binds
@IntoMap
@FragmentKey(AccountCreatedFragment::class)
fun bindAccountCreatedFragment(fragment: AccountCreatedFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(LoginFragment2SignupUsername::class) @FragmentKey(LoginFragment2SignupUsername::class)

View File

@ -41,6 +41,7 @@ import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import jp.wasabeef.glide.transformations.BlurTransformation import jp.wasabeef.glide.transformations.BlurTransformation
import jp.wasabeef.glide.transformations.ColorFilterTransformation import jp.wasabeef.glide.transformations.ColorFilterTransformation
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
@ -113,6 +114,23 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
.into(imageView) .into(imageView)
} }
@UiThread
fun render(profileInfo: LoginProfileInfo, imageView: ImageView) {
// Create a Fake MatrixItem, for the placeholder
val matrixItem = MatrixItem.UserItem(
// Need an id starting with @
id = profileInfo.matrixId,
displayName = profileInfo.displayName
)
val placeholder = getPlaceholderDrawable(matrixItem)
GlideApp.with(imageView)
.load(profileInfo.fullAvatarUrl)
.apply(RequestOptions.circleCropTransform())
.placeholder(placeholder)
.into(imageView)
}
@UiThread @UiThread
fun render(glideRequests: GlideRequests, fun render(glideRequests: GlideRequests,
matrixItem: MatrixItem, matrixItem: MatrixItem,

View File

@ -34,7 +34,7 @@ import org.matrix.android.sdk.api.failure.Failure
/** /**
* Parent Fragment for all the login/registration screens * Parent Fragment for all the login/registration screens
*/ */
abstract class AbstractLoginFragment2<VB: ViewBinding> : VectorBaseFragment<VB>(), OnBackPressed { abstract class AbstractLoginFragment2<VB : ViewBinding> : VectorBaseFragment<VB>(), OnBackPressed {
protected val loginViewModel: LoginViewModel2 by activityViewModel() protected val loginViewModel: LoginViewModel2 by activityViewModel()
@ -147,11 +147,19 @@ abstract class AbstractLoginFragment2<VB: ViewBinding> : VectorBaseFragment<VB>(
} }
} }
final override fun invalidate() = withState(loginViewModel) { state -> final override fun invalidate() {
// True when email is sent with success to the homeserver withState(loginViewModel) { state ->
isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not() // True when email is sent with success to the homeserver
isResetPasswordStarted = state.resetPasswordEmail.isNullOrBlank().not()
updateWithState(state) updateWithState(state)
}
invalidateMore()
}
protected open fun invalidateMore() {
// No op by default
} }
open fun updateWithState(state: LoginViewState2) { open fun updateWithState(state: LoginViewState2) {

View File

@ -41,6 +41,7 @@ sealed class LoginAction2 : VectorViewModelAction {
// Username to Login or Register, depending on the signMode // Username to Login or Register, depending on the signMode
data class SetUserName(val username: String) : LoginAction2() data class SetUserName(val username: String) : LoginAction2()
// Password to Login or Register, depending on the signMode // Password to Login or Register, depending on the signMode
data class SetUserPassword(val password: String) : LoginAction2() data class SetUserPassword(val password: String) : LoginAction2()
@ -82,4 +83,7 @@ sealed class LoginAction2 : VectorViewModelAction {
data class PostViewEvent(val viewEvent: LoginViewEvents2) : LoginAction2() data class PostViewEvent(val viewEvent: LoginViewEvents2) : LoginAction2()
data class UserAcceptCertificate(val fingerprint: Fingerprint) : LoginAction2() data class UserAcceptCertificate(val fingerprint: Fingerprint) : LoginAction2()
// Account customization is over
object Finish : LoginAction2()
} }

View File

@ -46,6 +46,7 @@ import im.vector.app.features.login.LoginWaitForEmailFragmentArgument
import im.vector.app.features.login.isSupported import im.vector.app.features.login.isSupported
import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.login.terms.LoginTermsFragmentArgument
import im.vector.app.features.login.terms.toLocalizedLoginTerms import im.vector.app.features.login.terms.toLocalizedLoginTerms
import im.vector.app.features.login2.created.AccountCreatedFragment
import im.vector.app.features.login2.terms.LoginTermsFragment2 import im.vector.app.features.login2.terms.LoginTermsFragment2
import im.vector.app.features.pin.UnlockedActivity import im.vector.app.features.pin.UnlockedActivity
@ -245,14 +246,26 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
is LoginViewEvents2.OnLoginModeNotSupported -> is LoginViewEvents2.OnLoginModeNotSupported ->
onLoginModeNotSupported(event.supportedTypes) onLoginModeNotSupported(event.supportedTypes)
is LoginViewEvents2.OnSessionCreated -> handleOnSessionCreated(event) is LoginViewEvents2.OnSessionCreated -> handleOnSessionCreated(event)
is LoginViewEvents2.Finish -> terminate(true)
}.exhaustive }.exhaustive
} }
private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) { private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) {
// TODO Propose to set avatar and display name if (event.newAccount) {
// Propose to set avatar and display name
// Back on this Fragment will finish the Activity
addFragmentToBackstack(R.id.loginFragmentContainer,
AccountCreatedFragment::class.java,
option = commonOption)
} else {
terminate(false)
}
}
private fun terminate(newAccount: Boolean) {
val intent = HomeActivity.newIntent( val intent = HomeActivity.newIntent(
this, this,
accountCreation = event.newAccount accountCreation = newAccount
) )
startActivity(intent) startActivity(intent)
finish() finish()
@ -260,7 +273,12 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
private fun updateWithState(LoginViewState2: LoginViewState2) { private fun updateWithState(LoginViewState2: LoginViewState2) {
// Loading // Loading
views.loginLoading.isVisible = LoginViewState2.isLoading setIsLoading(LoginViewState2.isLoading)
}
// Hack for AccountCreatedFragment
fun setIsLoading(isLoading: Boolean) {
views.loginLoading.isVisible = isLoading
} }
private fun onWebLoginError(onWebLoginError: LoginViewEvents2.OnWebLoginError) { private fun onWebLoginError(onWebLoginError: LoginViewEvents2.OnWebLoginError) {

View File

@ -23,16 +23,14 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.autofill.HintConstants import androidx.autofill.HintConstants
import androidx.core.view.isVisible
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.extensions.showPassword import im.vector.app.core.extensions.showPassword
import im.vector.app.databinding.FragmentLogin2SigninPasswordBinding import im.vector.app.databinding.FragmentLogin2SigninPasswordBinding
import im.vector.app.features.home.AvatarRenderer
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidPassword
import javax.inject.Inject import javax.inject.Inject
@ -42,7 +40,9 @@ import javax.inject.Inject
* - the user is asked for password to sign in to a homeserver. * - the user is asked for password to sign in to a homeserver.
* - He also can reset his password * - He also can reset his password
*/ */
class LoginFragment2SigninPassword @Inject constructor() : AbstractSSOLoginFragment2<FragmentLogin2SigninPasswordBinding>() { class LoginFragment2SigninPassword @Inject constructor(
private val avatarRenderer: AvatarRenderer
) : AbstractSSOLoginFragment2<FragmentLogin2SigninPasswordBinding>() {
private var passwordShown = false private var passwordShown = false
@ -106,15 +106,10 @@ class LoginFragment2SigninPassword @Inject constructor() : AbstractSSOLoginFragm
state.loginProfileInfo?.displayName?.takeIf { it.isNotBlank() } ?: state.userIdentifier() state.loginProfileInfo?.displayName?.takeIf { it.isNotBlank() } ?: state.userIdentifier()
) )
if (state.loginProfileInfo != null) { avatarRenderer.render(
views.loginUserIcon.isVisible = true profileInfo = state.loginProfileInfo ?: LoginProfileInfo(state.userIdentifier(), null, null),
Glide.with(requireContext()) imageView = views.loginUserIcon
.load(state.loginProfileInfo.fullAvatarUrl) )
.apply(RequestOptions.circleCropTransform())
.into(views.loginUserIcon)
} else {
views.loginUserIcon.isVisible = false
}
} }
private fun setupSubmitButton() { private fun setupSubmitButton() {

View File

@ -56,4 +56,6 @@ sealed class LoginViewEvents2 : VectorViewEvents {
data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginViewEvents2() data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginViewEvents2()
data class OnSessionCreated(val newAccount: Boolean): LoginViewEvents2() data class OnSessionCreated(val newAccount: Boolean): LoginViewEvents2()
object Finish : LoginViewEvents2()
} }

View File

@ -145,9 +145,15 @@ class LoginViewModel2 @AssistedInject constructor(
is LoginAction2.UserAcceptCertificate -> handleUserAcceptCertificate(action) is LoginAction2.UserAcceptCertificate -> handleUserAcceptCertificate(action)
LoginAction2.ClearHomeServerHistory -> handleClearHomeServerHistory() LoginAction2.ClearHomeServerHistory -> handleClearHomeServerHistory()
is LoginAction2.PostViewEvent -> _viewEvents.post(action.viewEvent) is LoginAction2.PostViewEvent -> _viewEvents.post(action.viewEvent)
is LoginAction2.Finish -> handleFinish()
}.exhaustive }.exhaustive
} }
private fun handleFinish() {
// Just post a view Event
_viewEvents.post(LoginViewEvents2.Finish)
}
private fun handleChooseAServerForSignin() { private fun handleChooseAServerForSignin() {
// Just post a view Event // Just post a view Event
_viewEvents.post(LoginViewEvents2.OpenServerSelection) _viewEvents.post(LoginViewEvents2.OpenServerSelection)

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2021 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.login2.created
import android.net.Uri
import im.vector.app.core.platform.VectorViewModelAction
sealed class AccountCreatedAction : VectorViewModelAction {
data class SetDisplayName(val displayName: String) : AccountCreatedAction()
data class SetAvatar(val avatarUri: Uri, val filename: String) : AccountCreatedAction()
}

View File

@ -0,0 +1,160 @@
/*
* Copyright (c) 2021 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.login2.created
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.resources.ColorProvider
import im.vector.app.databinding.DialogBaseEditTextBinding
import im.vector.app.databinding.FragmentLoginAccountCreatedBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.login2.AbstractLoginFragment2
import im.vector.app.features.login2.LoginAction2
import im.vector.app.features.login2.LoginActivity2
import im.vector.app.features.login2.LoginViewState2
import org.matrix.android.sdk.api.util.MatrixItem
import java.util.UUID
import javax.inject.Inject
/**
* In this screen:
* - the account has been created and we propose the user to set an avatar and a display name
*/
class AccountCreatedFragment @Inject constructor(
val accountCreatedViewModelFactory: AccountCreatedViewModel.Factory,
private val avatarRenderer: AvatarRenderer,
private val matrixItemColorProvider: MatrixItemColorProvider,
colorProvider: ColorProvider
) : AbstractLoginFragment2<FragmentLoginAccountCreatedBinding>(),
GalleryOrCameraDialogHelper.Listener {
private val viewModel: AccountCreatedViewModel by fragmentViewModel()
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginAccountCreatedBinding {
return FragmentLoginAccountCreatedBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupClickListener()
setupSubmitButton()
observeViewEvents()
}
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is AccountCreatedViewEvents.Failure -> displayErrorDialog(it.throwable)
}
}
}
private fun setupClickListener() {
views.loginAccountCreatedMessage.setOnClickListener {
// Update display name
displayDialog()
}
views.loginAccountCreatedAvatar.setOnClickListener {
galleryOrCameraDialogHelper.show()
}
}
private fun displayDialog() = withState(viewModel) { state ->
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val views = DialogBaseEditTextBinding.bind(layout)
views.editText.setText(state.currentUser()?.getBestName().orEmpty())
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_display_name)
.setView(layout)
.setPositiveButton(R.string.ok) { _, _ ->
val newName = views.editText.text.toString()
viewModel.handle(AccountCreatedAction.SetDisplayName(newName))
}
.setNegativeButton(R.string.cancel, null)
.show()
}
override fun onImageReady(uri: Uri?) {
uri ?: return
viewModel.handle(AccountCreatedAction.SetAvatar(
avatarUri = uri,
filename = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString())
)
}
private fun setupSubmitButton() {
views.loginAccountCreatedLater.setOnClickListener { terminate() }
views.loginAccountCreatedDone.setOnClickListener { terminate() }
}
private fun terminate() {
loginViewModel.handle(LoginAction2.Finish)
}
override fun invalidateMore() = withState(viewModel) { state ->
// Ugly hack...
(activity as? LoginActivity2)?.setIsLoading(state.isLoading)
views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId)
val user = state.currentUser()
if (user != null) {
avatarRenderer.render(user, views.loginAccountCreatedAvatar)
views.loginAccountCreatedMemberName.text = user.getBestName()
} else {
// Should not happen
views.loginAccountCreatedMemberName.text = state.userId
}
// User color
views.loginAccountCreatedMemberName
.setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(state.userId)))
views.loginAccountCreatedLater.isVisible = state.hasBeenModified.not()
views.loginAccountCreatedDone.isVisible = state.hasBeenModified
}
override fun updateWithState(state: LoginViewState2) {
// No op
}
override fun resetViewModel() {
// No op
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
// Just start the next Activity
terminate()
return false
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2021 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.login2.created
import im.vector.app.core.platform.VectorViewEvents
/**
* Transient events for Account Created
*/
sealed class AccountCreatedViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : AccountCreatedViewEvents()
}

View File

@ -0,0 +1,105 @@
/*
* Copyright (c) 2021 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.login2.created
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
class AccountCreatedViewModel @AssistedInject constructor(
@Assisted initialState: AccountCreatedViewState,
private val session: Session
) : VectorViewModel<AccountCreatedViewState, AccountCreatedAction, AccountCreatedViewEvents>(initialState) {
@AssistedFactory
interface Factory {
fun create(initialState: AccountCreatedViewState): AccountCreatedViewModel
}
companion object : MvRxViewModelFactory<AccountCreatedViewModel, AccountCreatedViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: AccountCreatedViewState): AccountCreatedViewModel? {
val fragment: AccountCreatedFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.accountCreatedViewModelFactory.create(state)
}
}
init {
setState {
copy(
userId = session.myUserId
)
}
observeUser()
}
private fun observeUser() {
session.rx()
.liveUser(session.myUserId)
.unwrap()
.map { it.toMatrixItem() }
.execute {
copy(currentUser = it)
}
}
override fun handle(action: AccountCreatedAction) {
when (action) {
is AccountCreatedAction.SetAvatar -> handleSetAvatar(action)
is AccountCreatedAction.SetDisplayName -> handleSetDisplayName(action)
}
}
private fun handleSetAvatar(action: AccountCreatedAction.SetAvatar) {
setState { copy(isLoading = true) }
viewModelScope.launch {
val result = runCatching { session.updateAvatar(session.myUserId, action.avatarUri, action.filename) }
.onFailure { _viewEvents.post(AccountCreatedViewEvents.Failure(it)) }
setState {
copy(
isLoading = false,
hasBeenModified = hasBeenModified || result.isSuccess
)
}
}
}
private fun handleSetDisplayName(action: AccountCreatedAction.SetDisplayName) {
setState { copy(isLoading = true) }
viewModelScope.launch {
val result = runCatching { session.setDisplayName(session.myUserId, action.displayName) }
.onFailure { _viewEvents.post(AccountCreatedViewEvents.Failure(it)) }
setState {
copy(
isLoading = false,
hasBeenModified = hasBeenModified || result.isSuccess
)
}
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2021 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.login2.created
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.util.MatrixItem
data class AccountCreatedViewState(
val userId: String = "",
val isLoading: Boolean = false,
val currentUser: Async<MatrixItem.UserItem> = Uninitialized,
val hasBeenModified: Boolean = false
) : MvRxState

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<androidx.core.widget.NestedScrollView style="@style/LoginFormScrollView">
<LinearLayout style="@style/LoginFormContainer">
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/login_account_created_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:id="@+id/loginAccountCreatedSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small"
tools:text="@string/login_account_created_subtitle" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/login_account_created_notice"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_account_created_notice_2"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<RelativeLayout
android:id="@+id/loginAccountCreatedMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin">
<ImageView
android:id="@+id/loginAccountCreatedAvatar"
android:layout_width="44dp"
android:layout_height="44dp"
android:contentDescription="@string/avatar"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/loginAccountCreatedMemberName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:layout_toEndOf="@+id/loginAccountCreatedAvatar"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
tools:text="\@user:domain.org" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/loginAccountCreatedMemberName"
android:layout_marginStart="8dp"
android:layout_toEndOf="@+id/loginAccountCreatedAvatar"
android:text="@string/login_account_created_message"
android:textColor="?riotx_text_primary"
android:textSize="14sp" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/login_account_created_instruction"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/loginAccountCreatedLater"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/later" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginAccountCreatedDone"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/done"
android:visibility="gone"
tools:layout_marginEnd="120dp"
tools:visibility="visible" />
</FrameLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -27,5 +27,11 @@
<string name="login_wait_for_email_notice_2">We just sent an email to %1$s.</string> <string name="login_wait_for_email_notice_2">We just sent an email to %1$s.</string>
<string name="login_wait_for_email_help">Click on the link it contains to continue the account creation.</string> <string name="login_wait_for_email_help">Click on the link it contains to continue the account creation.</string>
<string name="login_account_created_title">Congratulations!</string>
<string name="login_account_created_subtitle">You account %s has been successfully created.</string>
<string name="login_account_created_notice">To complete your profile, you can set a profile image and/or a display name. This can also be done later from the settings.</string>
<string name="login_account_created_notice_2">This is how your messages will appear:</string>
<string name="login_account_created_message">Hello Matrix world!</string>
<string name="login_account_created_instruction">Click on the image and on your name to configure them.</string>
</resources> </resources>