forked from melod1n/fast-messenger
support for articles; ui & ux & logic fixes for 2fa and captcha screens; fix mentions;
This commit is contained in:
+4
-14
@@ -2,7 +2,6 @@ package com.meloda.app.fast.auth.captcha
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.meloda.app.fast.auth.captcha.model.CaptchaArguments
|
||||
import com.meloda.app.fast.auth.captcha.model.CaptchaScreenState
|
||||
import com.meloda.app.fast.auth.captcha.navigation.Captcha
|
||||
import com.meloda.app.fast.auth.captcha.validation.CaptchaValidator
|
||||
@@ -13,16 +12,14 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
interface CaptchaViewModel {
|
||||
|
||||
val screenState: StateFlow<CaptchaScreenState>
|
||||
val isNeedToOpenLogin: StateFlow<Boolean>
|
||||
|
||||
fun onCodeInputChanged(newCode: String)
|
||||
|
||||
fun onTextFieldDoneClicked()
|
||||
fun onDoneButtonClicked()
|
||||
|
||||
fun setArguments(arguments: CaptchaArguments)
|
||||
|
||||
fun onNavigatedToLogin()
|
||||
}
|
||||
|
||||
@@ -32,6 +29,7 @@ class CaptchaViewModelImpl(
|
||||
) : CaptchaViewModel, ViewModel() {
|
||||
|
||||
override val screenState = MutableStateFlow(CaptchaScreenState.EMPTY)
|
||||
override val isNeedToOpenLogin = MutableStateFlow(false)
|
||||
|
||||
init {
|
||||
val arguments = Captcha.from(savedStateHandle).arguments
|
||||
@@ -57,20 +55,12 @@ class CaptchaViewModelImpl(
|
||||
override fun onDoneButtonClicked() {
|
||||
if (!processValidation()) return
|
||||
|
||||
screenState.updateValue(screenState.value.copy(isNeedToOpenLogin = true))
|
||||
}
|
||||
|
||||
override fun setArguments(arguments: CaptchaArguments) {
|
||||
// screenState.updateValue(
|
||||
// screenState.value.copy(
|
||||
// captchaSid = arguments.captchaSid,
|
||||
// captchaImage = arguments.captchaImage
|
||||
// )
|
||||
// )
|
||||
isNeedToOpenLogin.update { true }
|
||||
}
|
||||
|
||||
override fun onNavigatedToLogin() {
|
||||
screenState.updateValue(CaptchaScreenState.EMPTY)
|
||||
isNeedToOpenLogin.update { false }
|
||||
}
|
||||
|
||||
private fun processValidation(): Boolean {
|
||||
|
||||
+2
-4
@@ -4,8 +4,7 @@ data class CaptchaScreenState(
|
||||
val captchaSid: String,
|
||||
val captchaImage: String,
|
||||
val captchaCode: String,
|
||||
val codeError: Boolean,
|
||||
val isNeedToOpenLogin: Boolean
|
||||
val codeError: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -13,8 +12,7 @@ data class CaptchaScreenState(
|
||||
captchaSid = "",
|
||||
captchaImage = "",
|
||||
captchaCode = "",
|
||||
codeError = false,
|
||||
isNeedToOpenLogin = false
|
||||
codeError = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ fun NavController.navigateToCaptcha(arguments: CaptchaArguments) {
|
||||
this.navigate(Captcha(arguments))
|
||||
}
|
||||
|
||||
fun NavController.setCaptchaResult(code: String) {
|
||||
fun NavController.setCaptchaResult(code: String?) {
|
||||
this.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("captchacode", code)
|
||||
|
||||
+11
-5
@@ -27,6 +27,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -61,6 +62,7 @@ fun CaptchaScreen(
|
||||
viewModel: CaptchaViewModel = koinViewModel<CaptchaViewModelImpl>()
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
|
||||
|
||||
var confirmedExit by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
@@ -70,8 +72,10 @@ fun CaptchaScreen(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (confirmedExit) {
|
||||
onBack()
|
||||
LaunchedEffect(confirmedExit) {
|
||||
if (confirmedExit) {
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = !confirmedExit) {
|
||||
@@ -93,9 +97,11 @@ fun CaptchaScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (screenState.isNeedToOpenLogin) {
|
||||
viewModel.onNavigatedToLogin()
|
||||
onResult(screenState.captchaCode)
|
||||
LaunchedEffect(isNeedToOpenLogin) {
|
||||
if (isNeedToOpenLogin) {
|
||||
viewModel.onNavigatedToLogin()
|
||||
onResult(screenState.captchaCode)
|
||||
}
|
||||
}
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
@@ -89,4 +89,6 @@ dependencies {
|
||||
implementation(libs.eithernet)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.kotlin.serialization)
|
||||
|
||||
implementation(libs.rebugger)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@ import com.meloda.app.fast.data.db.AccountsRepository
|
||||
import com.meloda.app.fast.data.processState
|
||||
import com.meloda.app.fast.model.database.AccountEntity
|
||||
import com.meloda.app.fast.network.OAuthErrorDomain
|
||||
import com.meloda.fast.auth.login.model.CaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginCaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginError
|
||||
import com.meloda.fast.auth.login.model.LoginScreenState
|
||||
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||
import com.meloda.fast.auth.login.model.LoginValidationResult
|
||||
import com.meloda.fast.auth.login.validation.LoginValidator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -27,10 +29,19 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface LoginViewModel {
|
||||
val screenState: StateFlow<LoginScreenState>
|
||||
val loginError: StateFlow<LoginError?>
|
||||
|
||||
val twoFaCode: StateFlow<String?>
|
||||
val twoFaArguments: StateFlow<LoginTwoFaArguments?>
|
||||
val captchaCode: StateFlow<String?>
|
||||
val captchaArguments: StateFlow<LoginCaptchaArguments?>
|
||||
val userBannedArguments: StateFlow<LoginUserBannedArguments?>
|
||||
val isNeedToOpenMain: StateFlow<Boolean>
|
||||
|
||||
fun onPasswordVisibilityButtonClicked()
|
||||
|
||||
@@ -56,10 +67,18 @@ class LoginViewModelImpl(
|
||||
private val oAuthUseCase: OAuthUseCase,
|
||||
private val usersUseCase: UsersUseCase,
|
||||
private val accountsRepository: AccountsRepository,
|
||||
private val loginValidator: LoginValidator,
|
||||
private val loginValidator: LoginValidator
|
||||
) : ViewModel(), LoginViewModel {
|
||||
|
||||
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||
override val loginError = MutableStateFlow<LoginError?>(null)
|
||||
|
||||
override val twoFaCode = MutableStateFlow<String?>(null)
|
||||
override val twoFaArguments = MutableStateFlow<LoginTwoFaArguments?>(null)
|
||||
override val captchaCode = MutableStateFlow<String?>(null)
|
||||
override val captchaArguments = MutableStateFlow<LoginCaptchaArguments?>(null)
|
||||
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
||||
override val isNeedToOpenMain = MutableStateFlow(false)
|
||||
|
||||
private val validationState: StateFlow<List<LoginValidationResult>> =
|
||||
screenState.map(loginValidator::validate)
|
||||
@@ -86,37 +105,38 @@ class LoginViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onSignInButtonClicked() {
|
||||
if (screenState.value.isLoading) return
|
||||
login()
|
||||
}
|
||||
|
||||
override fun onErrorDialogDismissed() {
|
||||
screenState.setValue { old -> old.copy(error = null) }
|
||||
loginError.update { null }
|
||||
}
|
||||
|
||||
override fun onNavigatedToMain() {
|
||||
screenState.setValue { old -> old.copy(isNeedToNavigateToMain = false) }
|
||||
isNeedToOpenMain.update { false }
|
||||
}
|
||||
|
||||
override fun onNavigatedToUserBanned() {
|
||||
screenState.setValue { old -> old.copy(userBannedArguments = null) }
|
||||
userBannedArguments.update { null }
|
||||
}
|
||||
|
||||
override fun onNavigatedToCaptcha() {
|
||||
screenState.setValue { old -> old.copy(captchaArguments = null) }
|
||||
captchaArguments.update { null }
|
||||
}
|
||||
|
||||
override fun onNavigatedToTwoFa() {
|
||||
screenState.setValue { old -> old.copy(twoFaArguments = null) }
|
||||
twoFaArguments.update { null }
|
||||
}
|
||||
|
||||
override fun onTwoFaCodeReceived(code: String) {
|
||||
screenState.setValue { old -> old.copy(validationCode = code) }
|
||||
twoFaCode.update { code }
|
||||
|
||||
login()
|
||||
}
|
||||
|
||||
override fun onCaptchaCodeReceived(code: String) {
|
||||
screenState.setValue { old -> old.copy(captchaCode = code) }
|
||||
captchaCode.update { code }
|
||||
|
||||
login()
|
||||
}
|
||||
@@ -149,7 +169,8 @@ class LoginViewModelImpl(
|
||||
accountsRepository.storeAccounts(listOf(currentAccount))
|
||||
|
||||
delay(350)
|
||||
screenState.setValue { old -> old.copy(isNeedToNavigateToMain = true) }
|
||||
|
||||
isNeedToOpenMain.update { true }
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -163,7 +184,10 @@ class LoginViewModelImpl(
|
||||
|
||||
Log.d(
|
||||
"LoginViewModel",
|
||||
"auth: login: ${currentState.login}; password: ${currentState.password}; code: ${currentState.validationCode}"
|
||||
"auth: login: ${currentState.login}; " +
|
||||
"password: ${currentState.password}; " +
|
||||
"2fa code: ${twoFaCode.value}; " +
|
||||
"captcha code: ${captchaCode.value}"
|
||||
)
|
||||
|
||||
processValidation()
|
||||
@@ -173,14 +197,17 @@ class LoginViewModelImpl(
|
||||
login = currentState.login,
|
||||
password = currentState.password,
|
||||
forceSms = forceSms,
|
||||
twoFaCode = currentState.validationCode,
|
||||
captchaSid = currentState.captchaArguments?.captchaSid,
|
||||
captchaKey = currentState.captchaCode
|
||||
twoFaCode = twoFaCode.value,
|
||||
captchaSid = captchaArguments.value?.captchaSid,
|
||||
captchaKey = captchaCode.value
|
||||
).listenValue { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
Log.d("LoginViewModelImpl", "login: error: $error")
|
||||
|
||||
twoFaCode.update { null }
|
||||
captchaCode.update { null }
|
||||
|
||||
parseError(error)
|
||||
},
|
||||
success = { response ->
|
||||
@@ -213,20 +240,20 @@ class LoginViewModelImpl(
|
||||
|
||||
accountsRepository.storeAccounts(listOf(currentAccount))
|
||||
|
||||
captchaArguments.update { null }
|
||||
captchaCode.update { null }
|
||||
|
||||
twoFaArguments.update { null }
|
||||
twoFaCode.update { null }
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
captchaArguments = null,
|
||||
captchaCode = null,
|
||||
validationSid = null,
|
||||
validationCode = null,
|
||||
twoFaArguments = null,
|
||||
|
||||
login = "",
|
||||
password = "",
|
||||
|
||||
isNeedToNavigateToMain = true
|
||||
)
|
||||
}
|
||||
|
||||
isNeedToOpenMain.update { true }
|
||||
}
|
||||
)
|
||||
screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
|
||||
@@ -238,7 +265,7 @@ class LoginViewModelImpl(
|
||||
is State.Error.OAuthError -> {
|
||||
when (val error = stateError.error) {
|
||||
is OAuthErrorDomain.ValidationRequiredError -> {
|
||||
val twoFaArguments = LoginTwoFaArguments(
|
||||
val arguments = LoginTwoFaArguments(
|
||||
validationSid = error.validationSid,
|
||||
redirectUri = error.redirectUri,
|
||||
phoneMask = error.phoneMask,
|
||||
@@ -246,25 +273,49 @@ class LoginViewModelImpl(
|
||||
canResendSms = error.validationResend == "sms",
|
||||
wrongCodeError = null
|
||||
)
|
||||
screenState.setValue { old -> old.copy(twoFaArguments = twoFaArguments) }
|
||||
true
|
||||
twoFaArguments.update { arguments }
|
||||
}
|
||||
|
||||
is OAuthErrorDomain.CaptchaRequiredError -> {
|
||||
val captchaArguments = CaptchaArguments(
|
||||
val arguments = LoginCaptchaArguments(
|
||||
captchaSid = error.captchaSid,
|
||||
captchaImage = error.captchaImageUrl
|
||||
)
|
||||
screenState.setValue { old -> old.copy(captchaArguments = captchaArguments) }
|
||||
true
|
||||
captchaArguments.update { arguments }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.InvalidCredentialsError -> TODO()
|
||||
is OAuthErrorDomain.UserBannedError -> TODO()
|
||||
OAuthErrorDomain.WrongTwoFaCode -> TODO()
|
||||
OAuthErrorDomain.WrongTwoFaCodeFormat -> TODO()
|
||||
OAuthErrorDomain.UnknownError -> TODO()
|
||||
OAuthErrorDomain.InvalidCredentialsError -> {
|
||||
loginError.update { LoginError.WrongCredentials }
|
||||
}
|
||||
|
||||
is OAuthErrorDomain.UserBannedError -> {
|
||||
val arguments = LoginUserBannedArguments(
|
||||
name = error.memberName,
|
||||
message = error.message,
|
||||
restoreUrl = error.restoreUrl,
|
||||
accessToken = error.accessToken
|
||||
)
|
||||
userBannedArguments.update { arguments }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.WrongTwoFaCode -> {
|
||||
loginError.update { LoginError.WrongTwoFaCode }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.WrongTwoFaCodeFormat -> {
|
||||
loginError.update { LoginError.WrongTwoFaCodeFormat }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.TooManyTriesError -> {
|
||||
loginError.update { LoginError.TooManyTries }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.UnknownError -> {
|
||||
loginError.update { LoginError.Unknown }
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
|
||||
@@ -44,6 +44,10 @@ class OAuthUseCaseImpl(
|
||||
)
|
||||
}
|
||||
|
||||
VkOAuthErrors.FLOOD_CONTROL -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
|
||||
}
|
||||
|
||||
VkOAuthErrors.NEED_VALIDATION -> {
|
||||
if (response.banInfo != null) {
|
||||
val info = requireNotNull(response.banInfo)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class CaptchaArguments(
|
||||
data class LoginCaptchaArguments(
|
||||
val captchaSid: String,
|
||||
val captchaImage: String
|
||||
) : Parcelable
|
||||
@@ -1,6 +1,12 @@
|
||||
package com.meloda.fast.auth.login.model
|
||||
|
||||
sealed interface LoginError {
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
data object WrongCredentials : LoginError
|
||||
@Immutable
|
||||
sealed class LoginError {
|
||||
data object Unknown : LoginError()
|
||||
data object WrongCredentials : LoginError()
|
||||
data object TooManyTries : LoginError()
|
||||
data object WrongTwoFaCode : LoginError()
|
||||
data object WrongTwoFaCodeFormat : LoginError()
|
||||
}
|
||||
|
||||
+1
-20
@@ -2,43 +2,24 @@ package com.meloda.fast.auth.login.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
// TODO: 04/05/2024, Danil Nikolaev: simplify
|
||||
@Immutable
|
||||
data class LoginScreenState(
|
||||
val login: String,
|
||||
val password: String,
|
||||
val captchaCode: String?,
|
||||
val validationSid: String?,
|
||||
val validationCode: String?,
|
||||
val isLoading: Boolean,
|
||||
val loginError: Boolean,
|
||||
val passwordError: Boolean,
|
||||
val passwordVisible: Boolean,
|
||||
val copiedCode: String?,
|
||||
val isNeedToNavigateToMain: Boolean,
|
||||
val twoFaArguments: LoginTwoFaArguments?,
|
||||
val captchaArguments: CaptchaArguments?,
|
||||
val userBannedArguments: UserBannedArguments?,
|
||||
val error: LoginError?,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = LoginScreenState(
|
||||
login = "",
|
||||
password = "",
|
||||
captchaCode = null,
|
||||
validationSid = null,
|
||||
validationCode = null,
|
||||
isLoading = false,
|
||||
loginError = false,
|
||||
passwordError = false,
|
||||
passwordVisible = false,
|
||||
copiedCode = null,
|
||||
isNeedToNavigateToMain = false,
|
||||
twoFaArguments = null,
|
||||
captchaArguments = null,
|
||||
userBannedArguments = null,
|
||||
error = null,
|
||||
passwordVisible = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class UserBannedArguments(
|
||||
data class LoginUserBannedArguments(
|
||||
val name: String,
|
||||
val message: String,
|
||||
val restoreUrl: String,
|
||||
+20
-6
@@ -1,5 +1,6 @@
|
||||
package com.meloda.fast.auth.login.navigation
|
||||
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
@@ -7,9 +8,9 @@ import com.meloda.app.fast.common.extensions.navigation.sharedViewModel
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import com.meloda.fast.auth.login.LoginViewModel
|
||||
import com.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import com.meloda.fast.auth.login.model.CaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginCaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
|
||||
import com.meloda.fast.auth.login.model.UserBannedArguments
|
||||
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||
import com.meloda.fast.auth.login.presentation.LoginScreen
|
||||
import com.meloda.fast.auth.login.presentation.LogoScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -22,16 +23,19 @@ object Logo
|
||||
|
||||
fun NavGraphBuilder.loginRoute(
|
||||
onError: (BaseError) -> Unit,
|
||||
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
||||
onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit,
|
||||
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
|
||||
onNavigateToMain: () -> Unit,
|
||||
onNavigateToUserBanned: (UserBannedArguments) -> Unit,
|
||||
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||
onNavigateToCredentials: () -> Unit,
|
||||
navController: NavController
|
||||
) {
|
||||
composable<Login> {
|
||||
composable<Login> { backStackEntry ->
|
||||
val viewModel: LoginViewModel =
|
||||
it.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
||||
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
||||
|
||||
val twoFaCode = backStackEntry.getTwoFaResult()
|
||||
val captchaCode = backStackEntry.getCaptchaResult()
|
||||
|
||||
LoginScreen(
|
||||
onError = onError,
|
||||
@@ -39,6 +43,8 @@ fun NavGraphBuilder.loginRoute(
|
||||
onNavigateToMain = onNavigateToMain,
|
||||
onNavigateToCaptcha = onNavigateToCaptcha,
|
||||
onNavigateToTwoFa = onNavigateToTwoFa,
|
||||
twoFaCode = twoFaCode,
|
||||
captchaCode = captchaCode,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
@@ -54,3 +60,11 @@ fun NavGraphBuilder.loginRoute(
|
||||
fun NavController.navigateToLogin() {
|
||||
this.navigate(route = Login)
|
||||
}
|
||||
|
||||
fun NavBackStackEntry.getTwoFaResult(): String? {
|
||||
return savedStateHandle["twofacode"]
|
||||
}
|
||||
|
||||
fun NavBackStackEntry.getCaptchaResult(): String? {
|
||||
return savedStateHandle["captchacode"]
|
||||
}
|
||||
|
||||
+128
-37
@@ -23,6 +23,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -57,10 +58,11 @@ import com.meloda.app.fast.designsystem.handleTabKey
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import com.meloda.fast.auth.login.LoginViewModel
|
||||
import com.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import com.meloda.fast.auth.login.model.CaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginCaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginError
|
||||
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
|
||||
import com.meloda.fast.auth.login.model.UserBannedArguments
|
||||
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||
import com.theapache64.rebugger.Rebugger
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@@ -68,49 +70,64 @@ import com.meloda.app.fast.designsystem.R as UiR
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
onNavigateToUserBanned: (UserBannedArguments) -> Unit,
|
||||
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||
onNavigateToMain: () -> Unit,
|
||||
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
||||
onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit,
|
||||
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
|
||||
twoFaCode: String?,
|
||||
captchaCode: String?,
|
||||
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
||||
val userBannedArguments by viewModel.userBannedArguments.collectAsStateWithLifecycle()
|
||||
val captchaArguments by viewModel.captchaArguments.collectAsStateWithLifecycle()
|
||||
val twoFaArguments by viewModel.twoFaArguments.collectAsStateWithLifecycle()
|
||||
val loginError by viewModel.loginError.collectAsStateWithLifecycle()
|
||||
|
||||
if (screenState.isNeedToNavigateToMain) {
|
||||
viewModel.onNavigatedToMain()
|
||||
onNavigateToMain()
|
||||
LaunchedEffect(isNeedToOpenMain) {
|
||||
if (isNeedToOpenMain) {
|
||||
viewModel.onNavigatedToMain()
|
||||
onNavigateToMain()
|
||||
}
|
||||
}
|
||||
|
||||
screenState.userBannedArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToUserBanned()
|
||||
onNavigateToUserBanned(arguments)
|
||||
LaunchedEffect(userBannedArguments) {
|
||||
userBannedArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToUserBanned()
|
||||
onNavigateToUserBanned(arguments)
|
||||
}
|
||||
}
|
||||
|
||||
screenState.captchaArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToCaptcha()
|
||||
onNavigateToCaptcha(arguments)
|
||||
LaunchedEffect(captchaArguments) {
|
||||
captchaArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToCaptcha()
|
||||
onNavigateToCaptcha(arguments)
|
||||
}
|
||||
}
|
||||
|
||||
screenState.twoFaArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToTwoFa()
|
||||
onNavigateToTwoFa(arguments)
|
||||
LaunchedEffect(twoFaArguments) {
|
||||
twoFaArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToTwoFa()
|
||||
onNavigateToTwoFa(arguments)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(twoFaCode) {
|
||||
if (twoFaCode != null) {
|
||||
viewModel.onTwoFaCodeReceived(twoFaCode)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(captchaCode) {
|
||||
if (captchaCode != null) {
|
||||
viewModel.onCaptchaCodeReceived(captchaCode)
|
||||
}
|
||||
}
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
val (loginFocusable, passwordFocusable) = FocusRequester.createRefs()
|
||||
|
||||
// TODO: 29/06/2024, Danil Nikolaev: remove lambda
|
||||
val goButtonClickAction = {
|
||||
if (!screenState.isLoading) {
|
||||
focusManager.clearFocus()
|
||||
viewModel.onSignInButtonClicked()
|
||||
}
|
||||
}
|
||||
val loginFieldTabClick = {
|
||||
passwordFocusable.requestFocus()
|
||||
true
|
||||
}
|
||||
|
||||
var loginText by remember { mutableStateOf(TextFieldValue(screenState.login)) }
|
||||
val showLoginError = screenState.loginError
|
||||
|
||||
@@ -165,8 +182,14 @@ fun LoginScreen(
|
||||
.height(58.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.handleEnterKey(loginFieldTabClick::invoke)
|
||||
.handleTabKey(loginFieldTabClick::invoke)
|
||||
.handleEnterKey {
|
||||
passwordFocusable.requestFocus()
|
||||
true
|
||||
}
|
||||
.handleTabKey {
|
||||
passwordFocusable.requestFocus()
|
||||
true
|
||||
}
|
||||
.focusRequester(loginFocusable)
|
||||
.connectNode(handler = autoFillEmailHandler)
|
||||
.defaultFocusChangeAutoFill(handler = autoFillEmailHandler),
|
||||
@@ -213,7 +236,8 @@ fun LoginScreen(
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.handleEnterKey {
|
||||
goButtonClickAction.invoke()
|
||||
focusManager.clearFocus()
|
||||
viewModel.onSignInButtonClicked()
|
||||
true
|
||||
}
|
||||
.focusRequester(passwordFocusable)
|
||||
@@ -261,7 +285,10 @@ fun LoginScreen(
|
||||
keyboardType = KeyboardType.Password
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onGo = { goButtonClickAction.invoke() }
|
||||
onGo = {
|
||||
focusManager.clearFocus()
|
||||
viewModel.onSignInButtonClicked()
|
||||
}
|
||||
),
|
||||
isError = showPasswordError,
|
||||
visualTransformation = if (screenState.passwordVisible) {
|
||||
@@ -282,7 +309,10 @@ fun LoginScreen(
|
||||
) {
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = goButtonClickAction::invoke,
|
||||
onClick = {
|
||||
focusManager.clearFocus()
|
||||
viewModel.onSignInButtonClicked()
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
modifier = Modifier.testTag("Sign in button")
|
||||
) {
|
||||
@@ -306,7 +336,31 @@ fun LoginScreen(
|
||||
|
||||
HandleError(
|
||||
onDismiss = viewModel::onErrorDialogDismissed,
|
||||
error = screenState.error
|
||||
error = loginError
|
||||
)
|
||||
|
||||
Rebugger(
|
||||
trackMap = mapOf(
|
||||
"onError" to onError,
|
||||
"onNavigateToUserBanned" to onNavigateToUserBanned,
|
||||
"onNavigateToMain" to onNavigateToMain,
|
||||
"onNavigateToCaptcha" to onNavigateToCaptcha,
|
||||
"onNavigateToTwoFa" to onNavigateToTwoFa,
|
||||
"viewModel" to viewModel,
|
||||
"screenState" to screenState,
|
||||
"isNeedToOpenMain" to isNeedToOpenMain,
|
||||
"userBannedArguments" to userBannedArguments,
|
||||
"captchaArguments" to captchaArguments,
|
||||
"twoFaArguments" to twoFaArguments,
|
||||
"loginError" to loginError,
|
||||
"focusManager" to focusManager,
|
||||
"loginText" to loginText,
|
||||
"showLoginError" to showLoginError,
|
||||
"autoFillEmailHandler" to autoFillEmailHandler,
|
||||
"passwordText" to passwordText,
|
||||
"showPasswordError" to showPasswordError,
|
||||
"autoFillPasswordHandler" to autoFillPasswordHandler,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -316,15 +370,52 @@ fun HandleError(
|
||||
error: LoginError?,
|
||||
) {
|
||||
when (error) {
|
||||
LoginError.WrongCredentials -> {
|
||||
null -> Unit
|
||||
|
||||
LoginError.Unknown -> {
|
||||
MaterialDialog(
|
||||
onDismissAction = onDismiss,
|
||||
title = UiText.Simple("Error"),
|
||||
text = UiText.Simple("Wrong login or password"),
|
||||
text = UiText.Simple("Unknown error."),
|
||||
confirmText = UiText.Resource(UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
LoginError.WrongCredentials -> {
|
||||
MaterialDialog(
|
||||
onDismissAction = onDismiss,
|
||||
title = UiText.Simple("Error"),
|
||||
text = UiText.Simple("Wrong login or password."),
|
||||
confirmText = UiText.Resource(UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
LoginError.TooManyTries -> {
|
||||
MaterialDialog(
|
||||
onDismissAction = onDismiss,
|
||||
title = UiText.Simple("Error"),
|
||||
text = UiText.Simple("Too many tries. Try in another hour or later."),
|
||||
confirmText = UiText.Resource(UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
LoginError.WrongTwoFaCode -> {
|
||||
MaterialDialog(
|
||||
onDismissAction = onDismiss,
|
||||
title = UiText.Simple("Error"),
|
||||
text = UiText.Simple("Wrong validation code."),
|
||||
confirmText = UiText.Resource(UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
LoginError.WrongTwoFaCodeFormat -> {
|
||||
MaterialDialog(
|
||||
onDismissAction = onDismiss,
|
||||
title = UiText.Simple("Error"),
|
||||
text = UiText.Simple("Wrong validation code format."),
|
||||
confirmText = UiText.Resource(UiR.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-4
@@ -19,6 +19,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -40,11 +41,13 @@ fun LogoScreen(
|
||||
onShowCredentials: () -> Unit,
|
||||
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
||||
|
||||
if (screenState.isNeedToNavigateToMain) {
|
||||
viewModel.onNavigatedToMain()
|
||||
onNavigateToMain()
|
||||
LaunchedEffect(isNeedToOpenMain) {
|
||||
if (isNeedToOpenMain) {
|
||||
viewModel.onNavigatedToMain()
|
||||
onNavigateToMain()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold { padding ->
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.meloda.app.fast.auth
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.navigation
|
||||
import com.meloda.app.fast.auth.captcha.model.CaptchaArguments
|
||||
import com.meloda.app.fast.auth.captcha.navigation.captchaRoute
|
||||
@@ -20,6 +19,7 @@ import com.meloda.fast.auth.login.navigation.Logo
|
||||
import com.meloda.fast.auth.login.navigation.loginRoute
|
||||
import com.meloda.fast.auth.login.navigation.navigateToLogin
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.net.URLEncoder
|
||||
|
||||
@Serializable
|
||||
object AuthGraph
|
||||
@@ -27,7 +27,7 @@ object AuthGraph
|
||||
fun NavGraphBuilder.authNavGraph(
|
||||
onError: (BaseError) -> Unit,
|
||||
onNavigateToMain: () -> Unit,
|
||||
navController: NavHostController
|
||||
navController: NavController
|
||||
) {
|
||||
navigation<AuthGraph>(
|
||||
startDestination = Logo
|
||||
@@ -46,7 +46,7 @@ fun NavGraphBuilder.authNavGraph(
|
||||
navController.navigateToTwoFa(
|
||||
TwoFaArguments(
|
||||
validationSid = arguments.validationSid,
|
||||
redirectUri = arguments.redirectUri,
|
||||
redirectUri = URLEncoder.encode(arguments.redirectUri, "utf-8"),
|
||||
phoneMask = arguments.phoneMask,
|
||||
validationType = arguments.validationType,
|
||||
canResendSms = arguments.canResendSms,
|
||||
@@ -70,7 +70,10 @@ fun NavGraphBuilder.authNavGraph(
|
||||
)
|
||||
|
||||
twoFaRoute(
|
||||
onBack = navController::navigateUp,
|
||||
onBack = {
|
||||
navController.navigateUp()
|
||||
navController.setTwoFaResult(null)
|
||||
},
|
||||
onResult = { code ->
|
||||
navController.popBackStack()
|
||||
navController.setTwoFaResult(code)
|
||||
@@ -78,7 +81,10 @@ fun NavGraphBuilder.authNavGraph(
|
||||
)
|
||||
|
||||
captchaRoute(
|
||||
onBack = navController::navigateUp,
|
||||
onBack = {
|
||||
navController.navigateUp()
|
||||
navController.setCaptchaResult(null)
|
||||
},
|
||||
onResult = { code ->
|
||||
navController.popBackStack()
|
||||
navController.setCaptchaResult(code)
|
||||
|
||||
+12
-26
@@ -1,10 +1,8 @@
|
||||
package com.meloda.app.fast.auth.twofa
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaArguments
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaScreenState
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaValidationType
|
||||
import com.meloda.app.fast.auth.twofa.navigation.TwoFa
|
||||
@@ -20,6 +18,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@@ -27,6 +26,8 @@ interface TwoFaViewModel {
|
||||
|
||||
val screenState: StateFlow<TwoFaScreenState>
|
||||
|
||||
val isNeedToOpenLogin: StateFlow<Boolean>
|
||||
|
||||
fun onCodeInputChanged(newCode: String)
|
||||
|
||||
fun onBackButtonClicked()
|
||||
@@ -36,8 +37,6 @@ interface TwoFaViewModel {
|
||||
fun onDoneButtonClicked()
|
||||
|
||||
fun onNavigatedToLogin()
|
||||
|
||||
fun setArguments(arguments: TwoFaArguments)
|
||||
}
|
||||
|
||||
class TwoFaViewModelImpl(
|
||||
@@ -48,6 +47,8 @@ class TwoFaViewModelImpl(
|
||||
|
||||
override val screenState = MutableStateFlow(TwoFaScreenState.EMPTY)
|
||||
|
||||
override val isNeedToOpenLogin = MutableStateFlow(false)
|
||||
|
||||
private var delayJob: Job? = null
|
||||
|
||||
init {
|
||||
@@ -88,12 +89,8 @@ class TwoFaViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onCancelButtonClicked() {
|
||||
screenState.updateValue(
|
||||
screenState.value.copy(
|
||||
twoFaCode = null,
|
||||
isNeedToOpenLogin = true
|
||||
)
|
||||
)
|
||||
screenState.setValue { old -> old.copy(twoFaCode = null) }
|
||||
isNeedToOpenLogin.update { true }
|
||||
}
|
||||
|
||||
override fun onRequestSmsButtonClicked() {
|
||||
@@ -107,25 +104,12 @@ class TwoFaViewModelImpl(
|
||||
override fun onDoneButtonClicked() {
|
||||
if (!processValidation()) return
|
||||
|
||||
screenState.updateValue(screenState.value.copy(isNeedToOpenLogin = true))
|
||||
isNeedToOpenLogin.update { true }
|
||||
}
|
||||
|
||||
override fun onNavigatedToLogin() {
|
||||
screenState.updateValue(TwoFaScreenState.EMPTY)
|
||||
}
|
||||
|
||||
override fun setArguments(arguments: TwoFaArguments) {
|
||||
Log.d("TwoFaViewModel", "TwoFaArguments: $arguments")
|
||||
|
||||
// screenState.updateValue(
|
||||
// screenState.value.copy(
|
||||
// twoFaSid = arguments.validationSid,
|
||||
// canResendSms = arguments.canResendSms,
|
||||
// codeError = arguments.wrongCodeError,
|
||||
// twoFaText = getTwoFaText(TwoFaValidationType.parse(arguments.validationType)),
|
||||
// phoneMask = arguments.phoneMask
|
||||
// )
|
||||
// )
|
||||
isNeedToOpenLogin.update { false }
|
||||
}
|
||||
|
||||
private fun processValidation(): Boolean {
|
||||
@@ -147,7 +131,9 @@ class TwoFaViewModelImpl(
|
||||
authUseCase.sendSms(validationSid)
|
||||
.listenValue { state ->
|
||||
state.processState(
|
||||
error = { error -> },
|
||||
error = { error ->
|
||||
|
||||
},
|
||||
success = { response ->
|
||||
val newValidationType = response.validationType
|
||||
val newCanResendSms = response.validationResend == "sms"
|
||||
|
||||
-2
@@ -9,7 +9,6 @@ data class TwoFaScreenState(
|
||||
val canResendSms: Boolean,
|
||||
val codeError: String?,
|
||||
val delayTime: Int,
|
||||
val isNeedToOpenLogin: Boolean,
|
||||
val phoneMask: String
|
||||
) {
|
||||
|
||||
@@ -21,7 +20,6 @@ data class TwoFaScreenState(
|
||||
canResendSms = false,
|
||||
codeError = null,
|
||||
delayTime = 0,
|
||||
isNeedToOpenLogin = false,
|
||||
phoneMask = ""
|
||||
)
|
||||
}
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
package com.meloda.app.fast.auth.twofa.model
|
||||
|
||||
sealed class TwoFaUiAction {
|
||||
data class CodeResult(val code: String) : TwoFaUiAction()
|
||||
data object BackClicked : TwoFaUiAction()
|
||||
}
|
||||
+5
-8
@@ -6,7 +6,6 @@ import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.toRoute
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaArguments
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaUiAction
|
||||
import com.meloda.app.fast.auth.twofa.presentation.TwoFaScreen
|
||||
import com.meloda.app.fast.common.customNavType
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -28,12 +27,8 @@ fun NavGraphBuilder.twoFaRoute(
|
||||
) {
|
||||
composable<TwoFa>(typeMap = TwoFa.typeMap) {
|
||||
TwoFaScreen(
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
TwoFaUiAction.BackClicked -> onBack()
|
||||
is TwoFaUiAction.CodeResult -> onResult(action.code)
|
||||
}
|
||||
}
|
||||
onBack = onBack,
|
||||
onCodeResult = onResult
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -42,8 +37,10 @@ fun NavController.navigateToTwoFa(arguments: TwoFaArguments) {
|
||||
this.navigate(TwoFa(arguments))
|
||||
}
|
||||
|
||||
fun NavController.setTwoFaResult(code: String) {
|
||||
fun NavController.setTwoFaResult(code: String?) {
|
||||
this.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("twofacode", code)
|
||||
}
|
||||
|
||||
|
||||
|
||||
+22
-17
@@ -26,6 +26,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -43,7 +44,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.meloda.app.fast.auth.twofa.TwoFaViewModel
|
||||
import com.meloda.app.fast.auth.twofa.TwoFaViewModelImpl
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaUiAction
|
||||
import com.meloda.app.fast.common.UiText
|
||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||
import com.meloda.app.fast.designsystem.TextFieldErrorText
|
||||
@@ -51,17 +51,18 @@ import com.meloda.app.fast.designsystem.getString
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
private typealias OnAction = (TwoFaUiAction) -> Unit
|
||||
|
||||
@Composable
|
||||
fun TwoFaScreen(
|
||||
onAction: OnAction,
|
||||
onBack: () -> Unit,
|
||||
onCodeResult: (code: String) -> Unit,
|
||||
viewModel: TwoFaViewModel = koinViewModel<TwoFaViewModelImpl>(),
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
|
||||
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
|
||||
|
||||
var confirmedExit by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
@@ -70,8 +71,10 @@ fun TwoFaScreen(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (confirmedExit) {
|
||||
onAction(TwoFaUiAction.BackClicked)
|
||||
LaunchedEffect(confirmedExit) {
|
||||
if (confirmedExit) {
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = !confirmedExit) {
|
||||
@@ -93,17 +96,22 @@ fun TwoFaScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (screenState.isNeedToOpenLogin) {
|
||||
viewModel.onNavigatedToLogin()
|
||||
LaunchedEffect(isNeedToOpenLogin) {
|
||||
if (isNeedToOpenLogin) {
|
||||
viewModel.onNavigatedToLogin()
|
||||
|
||||
val code = screenState.twoFaCode
|
||||
if (code == null) {
|
||||
onAction(TwoFaUiAction.BackClicked)
|
||||
} else {
|
||||
onAction(TwoFaUiAction.CodeResult(code = code))
|
||||
val code = screenState.twoFaCode
|
||||
if (code == null) {
|
||||
onBack()
|
||||
} else {
|
||||
onCodeResult(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var code by remember { mutableStateOf(TextFieldValue(screenState.twoFaCode.orEmpty())) }
|
||||
val codeError = screenState.codeError
|
||||
|
||||
Scaffold { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -113,7 +121,7 @@ fun TwoFaScreen(
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { onAction(TwoFaUiAction.BackClicked) },
|
||||
onClick = onBack,
|
||||
text = {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
@@ -155,9 +163,6 @@ fun TwoFaScreen(
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
var code by remember { mutableStateOf(TextFieldValue(screenState.twoFaCode.orEmpty())) }
|
||||
val codeError = screenState.codeError
|
||||
|
||||
TextField(
|
||||
value = code,
|
||||
onValueChange = { newText ->
|
||||
|
||||
+6
-2
@@ -222,7 +222,7 @@ class ConversationsViewModelImpl(
|
||||
private fun loadConversations(
|
||||
offset: Int = currentOffset.value
|
||||
) {
|
||||
conversationsUseCase.getConversations(count = 30, offset = offset).listenValue { state ->
|
||||
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset).listenValue { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
when (error) {
|
||||
@@ -247,7 +247,7 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
},
|
||||
success = { response ->
|
||||
val itemsCountSufficient = response.size == 30
|
||||
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||
canPaginate.setValue { itemsCountSufficient }
|
||||
|
||||
val paginationExhausted = !itemsCountSufficient &&
|
||||
@@ -621,5 +621,9 @@ class ConversationsViewModelImpl(
|
||||
old.copy(conversations = uiConversations)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOAD_COUNT = 30
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+30
-1
@@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -299,7 +300,35 @@ fun ConversationItem(
|
||||
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = conversation.message,
|
||||
text = kotlin.run {
|
||||
val builder =
|
||||
AnnotatedString.Builder(conversation.message.text)
|
||||
|
||||
conversation.message.spanStyles.map { spanStyleRange ->
|
||||
val updatedSpanStyle =
|
||||
if (spanStyleRange.item.color == Color.Red) {
|
||||
spanStyleRange.item.copy(color = MaterialTheme.colorScheme.primary)
|
||||
} else {
|
||||
spanStyleRange.item
|
||||
}
|
||||
|
||||
builder.addStyle(
|
||||
style = updatedSpanStyle,
|
||||
start = spanStyleRange.start,
|
||||
end = spanStyleRange.end
|
||||
)
|
||||
}
|
||||
|
||||
conversation.message.paragraphStyles.forEach { style ->
|
||||
builder.addStyle(
|
||||
style = style.item,
|
||||
start = style.start,
|
||||
end = style.end
|
||||
)
|
||||
}
|
||||
|
||||
builder.toAnnotatedString()
|
||||
},
|
||||
minLines = 1,
|
||||
maxLines = maxLines,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
|
||||
+3
-5
@@ -611,6 +611,7 @@ private fun getAttachmentIconByType(attachmentType: AttachmentType): UiImage? {
|
||||
AttachmentType.AUDIO_PLAYLIST -> null
|
||||
AttachmentType.PODCAST -> null
|
||||
AttachmentType.NARRATIVE -> null
|
||||
AttachmentType.ARTICLE -> null
|
||||
}?.let(UiImage::Resource)
|
||||
}
|
||||
|
||||
@@ -660,18 +661,14 @@ private fun getTextWithVisualizedMentions(
|
||||
var currentIndex = 0
|
||||
val replacements = mutableListOf<Pair<IntRange, String>>()
|
||||
|
||||
// TODO: 25/04/2024, Danil Nikolaev: check why not working ([id279494346|@iworld2rist] да убери ты Елену Шлипс от меня)
|
||||
val result = regex.replace(originalText) { matchResult ->
|
||||
val idPrefix = matchResult.groups[1]?.value.orEmpty()
|
||||
val startIndex = matchResult.range.first
|
||||
val endIndex = matchResult.range.last
|
||||
|
||||
val id = matchResult.groups[2]?.value ?: ""
|
||||
val text = matchResult.groups[3]?.value ?: ""
|
||||
|
||||
val replaced =
|
||||
text.substring(startIndex, endIndex + 1)
|
||||
.replace("[$idPrefix$id|$text]", text)
|
||||
val replaced = matchResult.groups[3]?.value.orEmpty()
|
||||
|
||||
val indexRange =
|
||||
(startIndex + currentIndex)..startIndex + currentIndex + replaced.length
|
||||
@@ -757,6 +754,7 @@ private fun getAttachmentUiText(
|
||||
AttachmentType.AUDIO_PLAYLIST -> UiR.string.message_attachments_audio_playlist
|
||||
AttachmentType.PODCAST -> UiR.string.message_attachments_podcast
|
||||
AttachmentType.NARRATIVE -> UiR.string.message_attachments_narrative
|
||||
AttachmentType.ARTICLE -> UiR.string.message_attachments_article
|
||||
}.let(UiText::Resource)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user