support for articles; ui & ux & logic fixes for 2fa and captcha screens; fix mentions;

This commit is contained in:
2024-07-13 01:37:24 +03:00
parent 25acc6505b
commit ce1867c22c
38 changed files with 449 additions and 218 deletions
+1 -1
View File
@@ -151,7 +151,7 @@ dependencies {
// Coil for Compose
implementation(libs.coil.compose)
androidTestImplementation(libs.compose.ui.test.junit4)
// androidTestImplementation(libs.compose.ui.test.junit4)
debugImplementation(libs.compose.ui.test.manifest)
debugImplementation(libs.compose.ui.tooling)
@@ -40,12 +40,15 @@ class OAuthRepositoryImpl(
requireNotNull(result.error)
}
else -> throw IllegalStateException("Unknown result")
is ApiResult.Failure.ApiFailure -> TODO()
// is ApiResult.Failure.ApiFailure -> TODO()
// is ApiResult.Failure.HttpFailure -> TODO()
// is ApiResult.Failure.NetworkFailure -> TODO()
// is ApiResult.Failure.UnknownFailure -> TODO()
is ApiResult.Failure.NetworkFailure -> {
// TODO: 13/07/2024, Danil Nikolaev: implement showing network error
TODO()
}
is ApiResult.Failure.UnknownFailure -> TODO()
else -> throw IllegalStateException("Unknown result")
}
}
}
@@ -104,6 +104,7 @@
<string name="message_attachments_audio_playlist">Playlist</string>
<string name="message_attachments_podcast">Podcast</string>
<string name="message_attachments_narrative">Narrative</string>
<string name="message_attachments_article">Article</string>
<string name="chat_interaction_uploading_file">Uploading file</string>
<string name="chat_interaction_uploading_photo">Uploading photo</string>
+3
View File
@@ -32,6 +32,9 @@ dependencies {
implementation(libs.moshi.kotlin)
ksp(libs.moshi.kotlin.codegen)
implementation(platform(libs.compose.bom))
implementation(libs.bundles.compose)
implementation(libs.room.ktx)
implementation(libs.room.runtime)
ksp(libs.room.compiler)
@@ -1,5 +1,9 @@
package com.meloda.app.fast.model
import androidx.compose.runtime.Immutable
@Immutable
sealed class BaseError {
data object SessionExpired : BaseError()
}
@@ -26,7 +26,8 @@ enum class AttachmentType(var value: String) {
ARTIST("artist"),
AUDIO_PLAYLIST("audio_playlist"),
PODCAST("podcast"),
NARRATIVE("narrative");
NARRATIVE("narrative"),
ARTICLE("article");
fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE)
@@ -0,0 +1,15 @@
package com.meloda.app.fast.model.api.data
import com.meloda.app.fast.model.api.domain.VkArticleDomain
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class VkArticleData(
@Json(name = "id") val id: Int
) : VkAttachmentData {
fun toDomain(): VkArticleDomain = VkArticleDomain(
id = id
)
}
@@ -31,7 +31,8 @@ data class VkAttachmentItemData(
@Json(name = "audios") val audios: List<VkAudioData>?,
@Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?,
@Json(name = "podcast") val podcast: VkPodcastData?,
@Json(name = "narrative") val narrative: VkNarrativeData?
@Json(name = "narrative") val narrative: VkNarrativeData?,
@Json(name = "article") val article: VkArticleData?
) {
fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) {
AttachmentType.UNKNOWN -> VkUnknownAttachment
@@ -58,5 +59,6 @@ data class VkAttachmentItemData(
AttachmentType.AUDIO_PLAYLIST -> audioPlaylist?.toDomain()
AttachmentType.PODCAST -> podcast?.toDomain()
AttachmentType.NARRATIVE -> narrative?.toDomain()
AttachmentType.ARTICLE -> article?.toDomain()
} ?: VkUnknownAttachment
}
@@ -10,11 +10,11 @@ data class VkGroupData(
@Json(name = "id") val id: Int,
@Json(name = "name") val name: String,
@Json(name = "screen_name") val screenName: String,
@Json(name = "is_closed") val isClosed: Int,
@Json(name = "is_closed") val isClosed: Int?,
@Json(name = "type") val type: String,
@Json(name = "is_admin") val isAdmin: Int,
@Json(name = "is_member") val isMember: Int,
@Json(name = "is_advertiser") val isAdvertiser: Int,
@Json(name = "is_admin") val isAdmin: Int?,
@Json(name = "is_member") val isMember: Int?,
@Json(name = "is_advertiser") val isAdvertiser: Int?,
@Json(name = "photo_50") val photo50: String?,
@Json(name = "photo_100") val photo100: String?,
@Json(name = "photo_200") val photo200: String?,
@@ -0,0 +1,10 @@
package com.meloda.app.fast.model.api.domain
import com.meloda.app.fast.model.api.data.AttachmentType
data class VkArticleDomain(
val id: Int
) : VkAttachment {
override val type: AttachmentType = AttachmentType.ARTICLE
}
@@ -75,7 +75,7 @@ data class InvalidCredentialsError(
)
@JsonClass(generateAdapter = true)
data class WrongTwoFaCode(
data class WrongTwoFaCodeError(
@Json(name = "error") override val error: String, // "invalid_request"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "wrong_otp"
@@ -86,7 +86,7 @@ data class WrongTwoFaCode(
)
@JsonClass(generateAdapter = true)
data class WrongTwoFaCodeFormat(
data class WrongTwoFaCodeFormatError(
@Json(name = "error") override val error: String, // "invalid_request"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "otp_format_is_incorrect"
@@ -96,6 +96,17 @@ data class WrongTwoFaCodeFormat(
errorType = errorType
)
@JsonClass(generateAdapter = true)
data class TooManyTriesError(
@Json(name = "error") override val error: String, // "9;Flood control"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "password_bruteforce_attempt"
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = errorType
)
fun OAuthError.toDomain(): OAuthErrorDomain? = when (this) {
is ValidationRequiredError -> {
OAuthErrorDomain.ValidationRequiredError(
@@ -129,13 +140,17 @@ fun OAuthError.toDomain(): OAuthErrorDomain? = when (this) {
OAuthErrorDomain.InvalidCredentialsError
}
is WrongTwoFaCode -> {
is WrongTwoFaCodeError -> {
OAuthErrorDomain.WrongTwoFaCode
}
is WrongTwoFaCodeFormat -> {
is WrongTwoFaCodeFormatError -> {
OAuthErrorDomain.WrongTwoFaCodeFormat
}
is TooManyTriesError -> {
OAuthErrorDomain.TooManyTriesError
}
else -> null
}
@@ -27,5 +27,7 @@ sealed class OAuthErrorDomain {
data object InvalidCredentialsError : OAuthErrorDomain()
data object WrongTwoFaCode : OAuthErrorDomain()
data object WrongTwoFaCodeFormat : OAuthErrorDomain()
data object TooManyTriesError: OAuthErrorDomain()
data object UnknownError : OAuthErrorDomain()
}
@@ -110,6 +110,11 @@ internal class ResultCall<R : Any, E : OAuthError>(
.fromJson(errorBodyString.orEmpty()) ?: return
val error: OAuthError? = when (baseError.error) {
"9;Flood control" -> {
moshi.adapter(TooManyTriesError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"invalid_client" -> {
moshi.adapter(InvalidCredentialsError::class.java)
.fromJson(errorBodyString.orEmpty())
@@ -123,12 +128,12 @@ internal class ResultCall<R : Any, E : OAuthError>(
"invalid_request" -> {
when (val type = baseError.errorType) {
"wrong_otp" -> {
moshi.adapter(WrongTwoFaCode::class.java)
moshi.adapter(WrongTwoFaCodeError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"otp_format_is_incorrect" -> {
moshi.adapter(WrongTwoFaCodeFormat::class.java)
moshi.adapter(WrongTwoFaCodeFormatError::class.java)
.fromJson(errorBodyString.orEmpty())
}
@@ -51,10 +51,12 @@ object VkOAuthErrors {
const val NEED_CAPTCHA = "need_captcha"
const val INVALID_CLIENT = "invalid_client"
const val INVALID_REQUEST = "invalid_request"
const val FLOOD_CONTROL = "9;Flood control"
}
object VkErrorTypes {
const val WRONG_OTP_FORMAT = "otp_format_is_incorrect"
const val WRONG_OTP = "wrong_otp"
const val PASSWORD_BRUTEFORCE_ATTEMPT = "password_bruteforce_attempt"
}
@@ -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 {
@@ -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
)
}
}
@@ -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)
@@ -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
+2
View File
@@ -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)
@@ -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()
}
@@ -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
)
}
}
@@ -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,
@@ -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"]
}
@@ -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)
)
}
}
}
@@ -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)
@@ -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"
@@ -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 = ""
)
}
@@ -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()
}
@@ -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)
}
@@ -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 ->
@@ -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
}
}
@@ -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,
@@ -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)
}
+5
View File
@@ -5,6 +5,7 @@ eithernet = "1.9.0"
haze = "0.7.3"
kotlin = "2.0.0"
ksp = "2.0.0-1.0.22"
vkompose = "0.5.4-k2"
accompanist = "0.34.0"
coil = "2.6.0"
@@ -28,6 +29,7 @@ espressoCore = "3.6.1"
appcompat = "1.7.0"
androidx-navigation = "2.8.0-beta05"
serialization = "1.7.1"
rebugger = "1.0.0-rc03"
[libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
@@ -80,6 +82,8 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
rebugger = { module = "io.github.theapache64:rebugger-android", version.ref = "rebugger" }
[bundles]
compose = [
"compose-material3",
@@ -100,3 +104,4 @@ com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"
org-jetbrains-kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
com-vk-vkompose = { id = "com.vk.vkompose", version.ref = "vkompose" }