diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b85b1136..0e132007 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -112,11 +112,11 @@ android { useLiveLiterals = true } -// packaging { -// resources { -// excludes += "/META-INF/{AL2.0,LGPL2.1}" -// } -// } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } } dependencies { diff --git a/app/src/androidTest/kotlin/com/meloda/fast/tests/LoginSignInTest.kt b/app/src/androidTest/kotlin/com/meloda/fast/tests/LoginSignInTest.kt deleted file mode 100644 index f436216c..00000000 --- a/app/src/androidTest/kotlin/com/meloda/fast/tests/LoginSignInTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.meloda.app.fast.tests - -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import org.junit.Rule -import org.junit.Test - -class LoginSignInTest { - - @get:Rule - val composeTestRule = createComposeRule() - - @Test - fun signInButtonIsClickable() { - composeTestRule.setContent { -// LogoScreen(onAction = {}) - } - - composeTestRule.onNodeWithTag(testTag = "Sign in button").assertHasClickAction() - } - - @Test - fun signInButtonTriggersSignInAction() { - var signInClicked = true - - composeTestRule.setContent { -// com.meloda.fast.auth.login.presentation.LogoScreen( -// onAction = { action -> -// when (action) { -// UiAction.NextClicked -> { -// signInClicked = true -// } -// -// else -> Unit -// } -// } -// ) - } - - composeTestRule.onNodeWithTag("Sign in button").performClick() - - assert(signInClicked) - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2cf96e8f..4a0fcea5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,7 +20,7 @@ android:theme="@style/AppTheme"> diff --git a/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt b/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt deleted file mode 100644 index 09e45559..00000000 --- a/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt +++ /dev/null @@ -1,197 +0,0 @@ -package com.meloda.app.fast - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideIn -import androidx.compose.animation.slideOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarDefaults -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.navigation -import androidx.navigation.compose.rememberNavController -import com.meloda.app.fast.conversations.navigation.Conversations -import com.meloda.app.fast.conversations.navigation.conversationsRoute -import com.meloda.app.fast.designsystem.LocalBottomPadding -import com.meloda.app.fast.designsystem.LocalHazeState -import com.meloda.app.fast.designsystem.LocalTheme -import com.meloda.app.fast.friends.navigation.Friends -import com.meloda.app.fast.friends.navigation.friendsRoute -import com.meloda.app.fast.model.BaseError -import com.meloda.app.fast.profile.navigation.Profile -import com.meloda.app.fast.profile.navigation.profileRoute -import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.hazeChild -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import dev.chrisbanes.haze.materials.HazeMaterials -import kotlinx.serialization.Serializable -import com.meloda.app.fast.designsystem.R as UiR - -@Serializable -object MainGraph - -@Serializable -object Main - -data class BottomNavigationItem( - val titleResId: Int, - val selectedIconResId: Int, - val unselectedIconResId: Int, - val route: Any, -) - -@OptIn(ExperimentalHazeMaterialsApi::class) -fun NavGraphBuilder.mainScreen( - onError: (BaseError) -> Unit, - onNavigateToSettings: () -> Unit, - onNavigateToMessagesHistory: (conversationId: Int) -> Unit, -) { - val items = listOf( - BottomNavigationItem( - titleResId = UiR.string.title_friends, - selectedIconResId = UiR.drawable.baseline_people_alt_24, - unselectedIconResId = UiR.drawable.outline_people_alt_24, - route = Friends, - ), - BottomNavigationItem( - titleResId = UiR.string.title_conversations, - selectedIconResId = UiR.drawable.baseline_chat_24, - unselectedIconResId = UiR.drawable.outline_chat_24, - route = Conversations - ), - BottomNavigationItem( - titleResId = UiR.string.title_profile, - selectedIconResId = UiR.drawable.baseline_account_circle_24, - unselectedIconResId = UiR.drawable.outline_account_circle_24, - route = Profile - ) - ) - val routes = items.map(BottomNavigationItem::route) - - composable
{ - val currentTheme = LocalTheme.current - val hazeState = remember { HazeState() } - val navController = rememberNavController() - - var selectedItemIndex by rememberSaveable { - mutableIntStateOf(1) - } - - var isBottomBarVisible by rememberSaveable { - mutableStateOf(true) - } - - Scaffold( - bottomBar = { - AnimatedVisibility( - visible = isBottomBarVisible, - enter = slideIn { IntOffset(0, 400) }, - exit = slideOut { IntOffset(0, 400) } - ) { - NavigationBar( - modifier = Modifier - .then( - if (currentTheme.usingBlur) { - Modifier.hazeChild( - state = hazeState, - style = HazeMaterials.thick() - ) - } else Modifier - ) - .fillMaxWidth(), - containerColor = NavigationBarDefaults.containerColor.copy( - alpha = if (currentTheme.usingBlur) 0f else 1f - ) - ) { - items.forEachIndexed { index, item -> - NavigationBarItem( - selected = selectedItemIndex == index, - onClick = { - if (selectedItemIndex != index) { - val currentRoute = routes[selectedItemIndex] - - selectedItemIndex = index - navController.navigate(item.route) { - popUpTo(route = currentRoute) { - inclusive = true - } - } - } - }, - icon = { - Icon( - painter = painterResource( - id = if (selectedItemIndex == index) item.selectedIconResId - else item.unselectedIconResId - ), - contentDescription = null - ) - }, - alwaysShowLabel = false - ) - } - } - } - } - ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = if (currentTheme.usingBlur) 0.dp else padding.calculateBottomPadding()) - ) { - CompositionLocalProvider( - LocalHazeState provides hazeState, - LocalBottomPadding provides if (currentTheme.usingBlur) padding.calculateBottomPadding() else 0.dp - ) { - NavHost( - navController = navController, - startDestination = MainGraph, - enterTransition = { fadeIn(animationSpec = tween(200)) }, - exitTransition = { fadeOut(animationSpec = tween(200)) } - ) { - navigation(startDestination = Conversations) { - friendsRoute( - onError = onError, - navController = navController - ) - conversationsRoute( - onError = onError, - onNavigateToMessagesHistory = onNavigateToMessagesHistory, - navController = navController, - onListScrollingUp = { isScrolling -> -// isBottomBarVisible = isScrolling - } - ) - profileRoute( - onError = onError, - onNavigateToSettings = onNavigateToSettings, - navController = navController - ) - } - } - } - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt b/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt index 1b31d419..64b43407 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt @@ -16,11 +16,13 @@ import com.meloda.app.fast.model.MainScreenState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch interface MainViewModel { val screenState: StateFlow + val isNeedToOpenAuth: StateFlow val longPollState: StateFlow val startOnlineService: StateFlow @@ -38,7 +40,7 @@ interface MainViewModel { fun onError(error: BaseError) - fun onAuthOpened() + fun onNavigatedToAuth() } class MainViewModelImpl( @@ -51,6 +53,7 @@ class MainViewModelImpl( } override val screenState = MutableStateFlow(MainScreenState.EMPTY) + override val isNeedToOpenAuth = MutableStateFlow(false) override val longPollState = MutableStateFlow( if (SettingsController.getBoolean( @@ -109,13 +112,13 @@ class MainViewModelImpl( override fun onError(error: BaseError) { when (error) { BaseError.SessionExpired -> { - screenState.setValue { old -> old.copy(isNeedToOpenAuth = true) } + isNeedToOpenAuth.update { true } } } } - override fun onAuthOpened() { - screenState.setValue { old -> old.copy(isNeedToOpenAuth = false) } + override fun onNavigatedToAuth() { + isNeedToOpenAuth.update { false } } private fun loadAccounts() { diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/BottomNavigationItem.kt b/app/src/main/kotlin/com/meloda/app/fast/model/BottomNavigationItem.kt new file mode 100644 index 00000000..90fc279c --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/model/BottomNavigationItem.kt @@ -0,0 +1,8 @@ +package com.meloda.app.fast.model + +data class BottomNavigationItem( + val titleResId: Int, + val selectedIconResId: Int, + val unselectedIconResId: Int, + val route: Any, +) diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt b/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt index 302a100d..93bb33e6 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt @@ -10,21 +10,17 @@ data class MainScreenState( val useDarkTheme: Boolean, val useDynamicColors: Boolean, val isNeedToRequestNotifications: Boolean, - val isNeedToOpenAppPermissions: Boolean, - val isNeedToOpenAuth: Boolean, + val isNeedToOpenAppPermissions: Boolean ) { companion object { val EMPTY: MainScreenState = MainScreenState( accounts = emptyList(), accountsLoaded = false, - - // TODO: 05/05/2024, Danil Nikolaev: implement useDarkTheme = false, useDynamicColors = false, isNeedToRequestNotifications = false, - isNeedToOpenAppPermissions = false, - isNeedToOpenAuth = false, + isNeedToOpenAppPermissions = false ) } } diff --git a/app/src/main/kotlin/com/meloda/app/fast/navigation/MainGraph.kt b/app/src/main/kotlin/com/meloda/app/fast/navigation/MainGraph.kt new file mode 100644 index 00000000..6584dcc5 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/navigation/MainGraph.kt @@ -0,0 +1,54 @@ +package com.meloda.app.fast.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.meloda.app.fast.conversations.navigation.Conversations +import com.meloda.app.fast.friends.navigation.Friends +import com.meloda.app.fast.model.BaseError +import com.meloda.app.fast.model.BottomNavigationItem +import com.meloda.app.fast.presentation.MainScreen +import com.meloda.app.fast.profile.navigation.Profile +import kotlinx.serialization.Serializable +import com.meloda.app.fast.designsystem.R as UiR + +@Serializable +object MainGraph + +@Serializable +object Main + +fun NavGraphBuilder.mainScreen( + onError: (BaseError) -> Unit, + onSettingsButtonClicked: () -> Unit, + onConversationClicked: (conversationId: Int) -> Unit, +) { + val navigationItems = listOf( + BottomNavigationItem( + titleResId = UiR.string.title_friends, + selectedIconResId = UiR.drawable.baseline_people_alt_24, + unselectedIconResId = UiR.drawable.outline_people_alt_24, + route = Friends, + ), + BottomNavigationItem( + titleResId = UiR.string.title_conversations, + selectedIconResId = UiR.drawable.baseline_chat_24, + unselectedIconResId = UiR.drawable.outline_chat_24, + route = Conversations + ), + BottomNavigationItem( + titleResId = UiR.string.title_profile, + selectedIconResId = UiR.drawable.baseline_account_circle_24, + unselectedIconResId = UiR.drawable.outline_account_circle_24, + route = Profile + ) + ) + + composable
{ + MainScreen( + navigationItems = navigationItems, + onError = onError, + onSettingsButtonClicked = onSettingsButtonClicked, + onConversationItemClicked = onConversationClicked, + ) + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/MainActivity.kt b/app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt similarity index 95% rename from app/src/main/kotlin/com/meloda/app/fast/MainActivity.kt rename to app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt index ab704389..d65787bc 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/MainActivity.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt @@ -1,4 +1,4 @@ -package com.meloda.app.fast +package com.meloda.app.fast.presentation import android.Manifest import android.app.NotificationChannel @@ -26,6 +26,7 @@ import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState +import com.meloda.app.fast.MainViewModel import com.meloda.app.fast.common.UiText import com.meloda.app.fast.common.extensions.ifEmpty import com.meloda.app.fast.common.extensions.isSdkAtLeast @@ -100,13 +101,15 @@ class MainActivity : AppCompatActivity() { multiline = theme.multiline ) ) { + val currentTheme = LocalTheme.current + AppTheme( - useDarkTheme = LocalTheme.current.usingDarkStyle, - useDynamicColors = LocalTheme.current.usingDynamicColors, - selectedColorScheme = LocalTheme.current.selectedColorScheme, - useAmoledBackground = LocalTheme.current.usingAmoledBackground, + useDarkTheme = currentTheme.usingDarkStyle, + useDynamicColors = currentTheme.usingDynamicColors, + selectedColorScheme = currentTheme.selectedColorScheme, + useAmoledBackground = currentTheme.usingAmoledBackground, ) { - RootGraph() + RootScreen() } } } diff --git a/app/src/main/kotlin/com/meloda/app/fast/presentation/MainScreen.kt b/app/src/main/kotlin/com/meloda/app/fast/presentation/MainScreen.kt new file mode 100644 index 00000000..95296be7 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/presentation/MainScreen.kt @@ -0,0 +1,141 @@ +package com.meloda.app.fast.presentation + +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.navigation +import androidx.navigation.compose.rememberNavController +import com.meloda.app.fast.navigation.MainGraph +import com.meloda.app.fast.conversations.navigation.Conversations +import com.meloda.app.fast.conversations.navigation.conversationsScreen +import com.meloda.app.fast.designsystem.LocalBottomPadding +import com.meloda.app.fast.designsystem.LocalHazeState +import com.meloda.app.fast.designsystem.LocalTheme +import com.meloda.app.fast.friends.navigation.friendsScreen +import com.meloda.app.fast.model.BaseError +import com.meloda.app.fast.model.BottomNavigationItem +import com.meloda.app.fast.profile.navigation.profileScreen +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials + +@OptIn(ExperimentalHazeMaterialsApi::class) +@Composable +fun MainScreen( + navigationItems: List, + onError: (BaseError) -> Unit = {}, + onSettingsButtonClicked: () -> Unit = {}, + onConversationItemClicked: (conversationId: Int) -> Unit = {} +) { + val currentTheme = LocalTheme.current + val hazeState = remember { HazeState() } + val navController = rememberNavController() + + var selectedItemIndex by rememberSaveable { + mutableIntStateOf(1) + } + + Scaffold( + bottomBar = { + NavigationBar( + modifier = Modifier + .then( + if (currentTheme.usingBlur) { + Modifier.hazeChild( + state = hazeState, + style = HazeMaterials.thick() + ) + } else Modifier + ) + .fillMaxWidth(), + containerColor = NavigationBarDefaults.containerColor.copy( + alpha = if (currentTheme.usingBlur) 0f else 1f + ) + ) { + navigationItems.forEachIndexed { index, item -> + NavigationBarItem( + selected = selectedItemIndex == index, + onClick = { + if (selectedItemIndex != index) { + val currentRoute = navigationItems[selectedItemIndex].route + + selectedItemIndex = index + navController.navigate(item.route) { + popUpTo(route = currentRoute) { + inclusive = true + } + } + } + }, + icon = { + Icon( + painter = painterResource( + id = if (selectedItemIndex == index) item.selectedIconResId + else item.unselectedIconResId + ), + contentDescription = null + ) + }, + alwaysShowLabel = false + ) + } + } + } + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = if (currentTheme.usingBlur) 0.dp else padding.calculateBottomPadding()) + ) { + CompositionLocalProvider( + LocalHazeState provides hazeState, + LocalBottomPadding provides if (currentTheme.usingBlur) padding.calculateBottomPadding() else 0.dp + ) { + NavHost( + navController = navController, + startDestination = MainGraph, + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) } + ) { + navigation(startDestination = Conversations) { + friendsScreen( + onError = onError, + navController = navController + ) + conversationsScreen( + onError = onError, + onConversationItemClicked = onConversationItemClicked, + navController = navController + ) + profileScreen( + onError = onError, + onSettingsButtonClicked = onSettingsButtonClicked, + navController = navController + ) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/RootGraph.kt b/app/src/main/kotlin/com/meloda/app/fast/presentation/RootScreen.kt similarity index 62% rename from app/src/main/kotlin/com/meloda/app/fast/RootGraph.kt rename to app/src/main/kotlin/com/meloda/app/fast/presentation/RootScreen.kt index 806ae91b..fac53d8a 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/RootGraph.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/presentation/RootScreen.kt @@ -1,10 +1,10 @@ -package com.meloda.app.fast +package com.meloda.app.fast.presentation import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -12,33 +12,40 @@ import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController +import com.meloda.app.fast.MainViewModel +import com.meloda.app.fast.MainViewModelImpl import com.meloda.app.fast.auth.AuthGraph import com.meloda.app.fast.auth.authNavGraph import com.meloda.app.fast.auth.navigateToAuth -import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsRoute +import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsScreen import com.meloda.app.fast.chatmaterials.navigation.navigateToChatMaterials import com.meloda.app.fast.common.UserConfig -import com.meloda.app.fast.languagepicker.navigation.languagePickerRoute +import com.meloda.app.fast.languagepicker.navigation.languagePickerScreen import com.meloda.app.fast.languagepicker.navigation.navigateToLanguagePicker -import com.meloda.app.fast.messageshistory.navigation.messagesHistoryRoute +import com.meloda.app.fast.messageshistory.navigation.messagesHistoryScreen import com.meloda.app.fast.messageshistory.navigation.navigateToMessagesHistory -import com.meloda.app.fast.settings.presentation.navigateToSettings -import com.meloda.app.fast.settings.presentation.settingsRoute +import com.meloda.app.fast.navigation.Main +import com.meloda.app.fast.navigation.mainScreen +import com.meloda.app.fast.settings.navigation.navigateToSettings +import com.meloda.app.fast.settings.navigation.settingsScreen import org.koin.androidx.compose.koinViewModel @Composable -fun RootGraph(navController: NavHostController = rememberNavController()) { +fun RootScreen(navController: NavHostController = rememberNavController()) { val viewModel: MainViewModel = koinViewModel() val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val isNeedToOpenAuth by viewModel.isNeedToOpenAuth.collectAsStateWithLifecycle() - if (screenState.isNeedToOpenAuth) { - viewModel.onAuthOpened() - navController.navigateToAuth(clearBackStack = true) + LaunchedEffect(isNeedToOpenAuth) { + if (isNeedToOpenAuth) { + viewModel.onNavigatedToAuth() + navController.navigateToAuth(clearBackStack = true) + } } if (screenState.accountsLoaded) { - val isNeedToShowConversations by remember { - derivedStateOf { screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn() } + val isNeedToShowConversations = remember { + screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn() } NavHost( @@ -48,32 +55,30 @@ fun RootGraph(navController: NavHostController = rememberNavController()) { exitTransition = { fadeOut(animationSpec = tween(200)) } ) { authNavGraph( - onError = viewModel::onError, onNavigateToMain = navController::navigateToMain, navController = navController ) mainScreen( onError = viewModel::onError, - onNavigateToSettings = navController::navigateToSettings, - onNavigateToMessagesHistory = navController::navigateToMessagesHistory + onSettingsButtonClicked = navController::navigateToSettings, + onConversationClicked = navController::navigateToMessagesHistory ) - messagesHistoryRoute( + messagesHistoryScreen( onError = viewModel::onError, onBack = navController::navigateUp, - onNavigateToChatAttachments = navController::navigateToChatMaterials + onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials ) - chatMaterialsRoute( + chatMaterialsScreen( onBack = navController::navigateUp ) - settingsRoute( - onError = viewModel::onError, + settingsScreen( onBack = navController::navigateUp, - onNavigateToAuth = { navController.navigateToAuth(true) }, - onNavigateToLanguagePicker = navController::navigateToLanguagePicker + onLogOutButtonClicked = { navController.navigateToAuth(true) }, + onLanguageItemClicked = navController::navigateToLanguagePicker ) - languagePickerRoute(onBack = navController::navigateUp) + languagePickerScreen(onBack = navController::navigateUp) } } diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt index bb461775..98389087 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt @@ -8,7 +8,7 @@ interface OAuthRepository { login: String, password: String, forceSms: Boolean, - twoFaCode: String?, + validationCode: String?, captchaSid: String?, captchaKey: String? ): AuthDirectResponse diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt index 874f8ab3..4c59a017 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt @@ -16,7 +16,7 @@ class OAuthRepositoryImpl( login: String, password: String, forceSms: Boolean, - twoFaCode: String?, + validationCode: String?, captchaSid: String?, captchaKey: String? ): AuthDirectResponse = withContext(Dispatchers.IO) { @@ -27,8 +27,8 @@ class OAuthRepositoryImpl( username = login, password = password, scope = VkConstants.Auth.SCOPE, - twoFaForceSms = forceSms, - twoFaCode = twoFaCode, + validationForceSms = forceSms, + validationCode = validationCode, captchaSid = captchaSid, captchaKey = captchaKey, ) diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt index 023ce664..dc56193b 100644 --- a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt @@ -38,8 +38,6 @@ object SettingsKeys { const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash" const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert" const val KEY_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list" - const val KEY_SHOW_NAME_IN_BUBBLES = "debug_show_title_in_bubbles" - const val KEY_SHOW_DATE_UNDER_BUBBLES = "debug_show_date_under_bubbles" const val KEY_ENABLE_ANIMATIONS_IN_MESSAGES = "debug_enable_animations_in_messages" const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category" diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt index a45208f5..9e730f51 100644 --- a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt @@ -20,10 +20,7 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat -import com.meloda.app.fast.datastore.isUsingAmoledBackground -import com.meloda.app.fast.datastore.isUsingDynamicColors import com.meloda.app.fast.datastore.model.ThemeConfig -import com.meloda.app.fast.datastore.selectedColorScheme import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme import dev.chrisbanes.haze.HazeState @@ -128,10 +125,10 @@ val LocalBottomPadding = compositionLocalOf { @Composable fun AppTheme( predefinedColorScheme: ColorScheme? = null, - useDarkTheme: Boolean = isUsingDarkTheme(), - useDynamicColors: Boolean = isUsingDynamicColors(), - selectedColorScheme: Int = selectedColorScheme(), - useAmoledBackground: Boolean = isUsingAmoledBackground(), + useDarkTheme: Boolean = false, + useDynamicColors: Boolean = false, + useAmoledBackground: Boolean = false, + selectedColorScheme: Int = 0, content: @Composable () -> Unit ) { val colorScheme: ColorScheme = when { diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt index 853b3545..ab92eacc 100644 --- a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt @@ -81,167 +81,166 @@ fun MaterialDialog( ) } - AppTheme { - if (isVisible) { -// AlertAnimation(visible = isVisible) { - BasicAlertDialog( - onDismissRequest = onDismissRequest - ) { - val scrollState = rememberScrollState() - val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } } - val canScrollForward by remember { derivedStateOf { scrollState.value < scrollState.maxValue } } - Surface( - modifier = Modifier.fillMaxWidth(), - color = AlertDialogDefaults.containerColor, - shape = AlertDialogDefaults.shape, - tonalElevation = AlertDialogDefaults.TonalElevation - ) { - Column(modifier = Modifier.padding(bottom = 10.dp)) { - val stringTitle = title?.getString() - if (stringTitle != null) { + if (isVisible) { +// AlertAnimation(visible = isVisible) { + BasicAlertDialog( + onDismissRequest = onDismissRequest + ) { + val scrollState = rememberScrollState() + val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } } + val canScrollForward by remember { derivedStateOf { scrollState.value < scrollState.maxValue } } + + Surface( + modifier = Modifier.fillMaxWidth(), + color = AlertDialogDefaults.containerColor, + shape = AlertDialogDefaults.shape, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column(modifier = Modifier.padding(bottom = 10.dp)) { + val stringTitle = title?.getString() + if (stringTitle != null) { + Spacer(modifier = Modifier.height(20.dp)) + } + + Row { + stringTitle?.let { title -> + Spacer(modifier = Modifier.width(24.dp)) + Text( + modifier = Modifier.weight(1f), + text = title, + style = MaterialTheme.typography.headlineSmall + ) + Spacer(modifier = Modifier.width(20.dp)) + } + } + + if (canScrollBackward) { + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = false) + .verticalScroll(scrollState) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + val stringMessage = text?.getString() + if (stringMessage != null && stringTitle == null) { Spacer(modifier = Modifier.height(20.dp)) } Row { - stringTitle?.let { title -> + stringMessage?.let { message -> Spacer(modifier = Modifier.width(24.dp)) Text( modifier = Modifier.weight(1f), - text = title, - style = MaterialTheme.typography.headlineSmall + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.width(20.dp)) } } - if (canScrollBackward) { - HorizontalDivider(modifier = Modifier.fillMaxWidth()) - } + Spacer(modifier = Modifier.height(8.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f, fill = false) - .verticalScroll(scrollState) - ) { - Spacer(modifier = Modifier.height(8.dp)) + if (alertItems.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + AlertItems( + selectionType = itemsSelectionType, + items = alertItems, + onItemClick = { index -> + onItemClick?.invoke(index) - val stringMessage = text?.getString() - if (stringMessage != null && stringTitle == null) { - Spacer(modifier = Modifier.height(20.dp)) - } + if (itemsSelectionType == ItemsSelectionType.None) { + onDismissRequest.invoke() + } else { + val newItems = + alertItems.mapIndexed { itemIndex, item -> + item.copy(isSelected = itemIndex == index) + } - Row { - stringMessage?.let { message -> - Spacer(modifier = Modifier.width(24.dp)) - Text( - modifier = Modifier.weight(1f), - text = message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.width(20.dp)) + alertItems = newItems + } + }, + onItemCheckedChanged = { index -> + val newItems = alertItems.toMutableList() + val oldItem = newItems[index] + newItems[index] = + oldItem.copy(isSelected = !oldItem.isSelected) + + alertItems = newItems.toImmutableList() } - } - - Spacer(modifier = Modifier.height(8.dp)) - - if (alertItems.isNotEmpty()) { + ) + Spacer(modifier = Modifier.height(10.dp)) + } else { + customContent?.let { content -> Spacer(modifier = Modifier.height(4.dp)) - AlertItems( - selectionType = itemsSelectionType, - items = alertItems, - onItemClick = { index -> - onItemClick?.invoke(index) - - if (itemsSelectionType == ItemsSelectionType.None) { - onDismissRequest.invoke() - } else { - val newItems = - alertItems.mapIndexed { itemIndex, item -> - item.copy(isSelected = itemIndex == index) - } - - alertItems = newItems - } - }, - onItemCheckedChanged = { index -> - val newItems = alertItems.toMutableList() - val oldItem = newItems[index] - newItems[index] = - oldItem.copy(isSelected = !oldItem.isSelected) - - alertItems = newItems.toImmutableList() - } - ) + content.invoke(this) Spacer(modifier = Modifier.height(10.dp)) - } else { - customContent?.let { content -> - Spacer(modifier = Modifier.height(4.dp)) - content.invoke(this) - Spacer(modifier = Modifier.height(10.dp)) + } + } + } + + if (canScrollForward) { + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + } + + Row { + Spacer(modifier = Modifier.width(20.dp)) + neutralText?.getString()?.let { text -> + TextButton( + onClick = { + if (buttonsInvokeDismiss) { + onDismissRequest.invoke() + } else { + isVisible = false + } + neutralAction?.invoke() } + ) { + Text(text = text) } } - if (canScrollForward) { - HorizontalDivider(modifier = Modifier.fillMaxWidth()) + Spacer(modifier = Modifier.weight(1f)) + + cancelText?.getString()?.let { text -> + TextButton( + onClick = { + if (buttonsInvokeDismiss) { + onDismissRequest.invoke() + } else { + isVisible = false + } + cancelAction?.invoke() + } + ) { + Text(text = text) + } } - Row { - Spacer(modifier = Modifier.width(20.dp)) - neutralText?.getString()?.let { text -> - TextButton( - onClick = { - if (buttonsInvokeDismiss) { - onDismissRequest.invoke() - } else { - isVisible = false - } - neutralAction?.invoke() + Spacer(modifier = Modifier.width(2.dp)) + + confirmText?.getString()?.let { text -> + TextButton( + onClick = { + if (buttonsInvokeDismiss) { + onDismissRequest.invoke() + } else { + isVisible = false } - ) { - Text(text = text) + confirmAction?.invoke() } + ) { + Text(text = text) } - - Spacer(modifier = Modifier.weight(1f)) - - cancelText?.getString()?.let { text -> - TextButton( - onClick = { - if (buttonsInvokeDismiss) { - onDismissRequest.invoke() - } else { - isVisible = false - } - cancelAction?.invoke() - } - ) { - Text(text = text) - } - } - - Spacer(modifier = Modifier.width(2.dp)) - - confirmText?.getString()?.let { text -> - TextButton( - onClick = { - if (buttonsInvokeDismiss) { - onDismissRequest.invoke() - } else { - isVisible = false - } - confirmAction?.invoke() - } - ) { - Text(text = text) - } - } - - Spacer(modifier = Modifier.width(20.dp)) } + + Spacer(modifier = Modifier.width(20.dp)) } } } @@ -263,23 +262,6 @@ fun AlertAnimation( ) } -@Preview -@Composable -fun AlertItemsPreview() { - AppTheme { - AlertItems( - selectionType = ItemsSelectionType.None, - items = ImmutableList(5) { index -> - DialogItem( - title = "Item #${index + 1}", - isSelected = index % 2 == 0 - ) - }, - onItemClick = {} - ) - } -} - @Composable private fun AlertItems( selectionType: ItemsSelectionType, diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/OAuthRequest.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/OAuthRequest.kt index efcd9d66..7a408b52 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/OAuthRequest.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/OAuthRequest.kt @@ -7,9 +7,9 @@ data class AuthDirectRequest( val username: String, val password: String, val scope: String, - val twoFaSupported: Boolean = true, - val twoFaForceSms: Boolean = false, - val twoFaCode: String? = null, + val validationSupported: Boolean = true, + val validationForceSms: Boolean = false, + val validationCode: String? = null, val captchaSid: String? = null, val captchaKey: String? = null, val trustedHash: String? = null @@ -23,11 +23,11 @@ data class AuthDirectRequest( "username" to username, "password" to password, "scope" to scope, - "2fa_supported" to if (twoFaSupported) "1" else "0", - "force_sms" to if (twoFaForceSms) "1" else "0" + "2fa_supported" to if (validationSupported) "1" else "0", + "force_sms" to if (validationForceSms) "1" else "0" ) .apply { - twoFaCode?.let { this["code"] = it } + validationCode?.let { this["code"] = it } captchaSid?.let { this["captcha_sid"] = it } captchaKey?.let { this["captcha_key"] = it } trustedHash?.let { this["trusted_hash"] = it } diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/OAuthResponse.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/OAuthResponse.kt index c27a8073..04f53e1c 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/OAuthResponse.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/OAuthResponse.kt @@ -7,7 +7,7 @@ import com.squareup.moshi.JsonClass data class AuthDirectResponse( @Json(name = "access_token") val accessToken: String?, @Json(name = "user_id") val userId: Int?, - @Json(name = "trusted_hash") val twoFaHash: String?, + @Json(name = "trusted_hash") val validationHash: String?, @Json(name = "validation_sid") val validationSid: String?, @Json(name = "validation_type") val validationType: String?, @Json(name = "phone_mask") val phoneMask: String?, diff --git a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthError.kt b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthError.kt index 90fefb1c..ec3ea073 100644 --- a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthError.kt +++ b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthError.kt @@ -75,7 +75,7 @@ data class InvalidCredentialsError( ) @JsonClass(generateAdapter = true) -data class WrongTwoFaCodeError( +data class WrongValidationCodeError( @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 WrongTwoFaCodeError( ) @JsonClass(generateAdapter = true) -data class WrongTwoFaCodeFormatError( +data class WrongValidationCodeFormatError( @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" @@ -140,12 +140,12 @@ fun OAuthError.toDomain(): OAuthErrorDomain? = when (this) { OAuthErrorDomain.InvalidCredentialsError } - is WrongTwoFaCodeError -> { - OAuthErrorDomain.WrongTwoFaCode + is WrongValidationCodeError -> { + OAuthErrorDomain.WrongValidationCode } - is WrongTwoFaCodeFormatError -> { - OAuthErrorDomain.WrongTwoFaCodeFormat + is WrongValidationCodeFormatError -> { + OAuthErrorDomain.WrongValidationCodeFormat } is TooManyTriesError -> { diff --git a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthErrorDomain.kt b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthErrorDomain.kt index b33039f0..f1c526f0 100644 --- a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthErrorDomain.kt +++ b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthErrorDomain.kt @@ -25,8 +25,8 @@ sealed class OAuthErrorDomain { ) : OAuthErrorDomain() data object InvalidCredentialsError : OAuthErrorDomain() - data object WrongTwoFaCode : OAuthErrorDomain() - data object WrongTwoFaCodeFormat : OAuthErrorDomain() + data object WrongValidationCode : OAuthErrorDomain() + data object WrongValidationCodeFormat : OAuthErrorDomain() data object TooManyTriesError: OAuthErrorDomain() data object UnknownError : OAuthErrorDomain() diff --git a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthResultCallFactory.kt b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthResultCallFactory.kt index 4ec71364..91f6b2a8 100644 --- a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthResultCallFactory.kt +++ b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthResultCallFactory.kt @@ -128,12 +128,12 @@ internal class ResultCall( "invalid_request" -> { when (val type = baseError.errorType) { "wrong_otp" -> { - moshi.adapter(WrongTwoFaCodeError::class.java) + moshi.adapter(WrongValidationCodeError::class.java) .fromJson(errorBodyString.orEmpty()) } "otp_format_is_incorrect" -> { - moshi.adapter(WrongTwoFaCodeFormatError::class.java) + moshi.adapter(WrongValidationCodeFormatError::class.java) .fromJson(errorBodyString.orEmpty()) } diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index 1f89be7d..b564fcaa 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -81,7 +81,7 @@ dependencies { implementation(projects.feature.auth.login) implementation(projects.feature.auth.captcha) - implementation(projects.feature.auth.twofa) + implementation(projects.feature.auth.validation) implementation(projects.feature.auth.userbanned) implementation(libs.koin.androidx.compose) diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/CaptchaViewModel.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/CaptchaViewModel.kt index 5bb6a48b..23a38b4b 100644 --- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/CaptchaViewModel.kt +++ b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/CaptchaViewModel.kt @@ -18,7 +18,7 @@ interface CaptchaViewModel { fun onCodeInputChanged(newCode: String) - fun onTextFieldDoneClicked() + fun onTextFieldDoneAction() fun onDoneButtonClicked() fun onNavigatedToLogin() @@ -32,24 +32,22 @@ class CaptchaViewModelImpl( override val screenState = MutableStateFlow(CaptchaScreenState.EMPTY) override val isNeedToOpenLogin = MutableStateFlow(false) + init { - val arguments = Captcha.from(savedStateHandle).arguments + val captchaImage = Captcha.from(savedStateHandle).captchaImageUrl screenState.setValue { old -> - old.copy( - captchaSid = arguments.captchaSid, - captchaImage = URLDecoder.decode(arguments.captchaImage, "utf-8") - ) + old.copy(captchaImageUrl = URLDecoder.decode(captchaImage, "utf-8")) } } override fun onCodeInputChanged(newCode: String) { - val newState = screenState.value.copy(captchaCode = newCode.trim()) + val newState = screenState.value.copy(code = newCode.trim()) screenState.update { newState } processValidation() } - override fun onTextFieldDoneClicked() { + override fun onTextFieldDoneAction() { onDoneButtonClicked() } diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaArguments.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaArguments.kt deleted file mode 100644 index fe8ea8cf..00000000 --- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaArguments.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.app.fast.auth.captcha.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable - -@Serializable -@Parcelize -data class CaptchaArguments( - val captchaSid: String, - val captchaImage: String -) : Parcelable diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaScreenState.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaScreenState.kt index b794364c..2dd53721 100644 --- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaScreenState.kt +++ b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaScreenState.kt @@ -1,17 +1,15 @@ package com.meloda.app.fast.auth.captcha.model data class CaptchaScreenState( - val captchaSid: String, - val captchaImage: String, - val captchaCode: String, + val captchaImageUrl: String, + val code: String, val codeError: Boolean ) { companion object { val EMPTY = CaptchaScreenState( - captchaSid = "", - captchaImage = "", - captchaCode = "", + captchaImageUrl = "", + code = "", codeError = false ) } diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaNavigation.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaNavigation.kt new file mode 100644 index 00000000..b2e345c7 --- /dev/null +++ b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaNavigation.kt @@ -0,0 +1,40 @@ +package com.meloda.app.fast.auth.captcha.navigation + +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.meloda.app.fast.auth.captcha.presentation.CaptchaRoute +import kotlinx.serialization.Serializable + +@Serializable +data class Captcha(val captchaImageUrl: String) { + + companion object { + fun from(savedStateHandle: SavedStateHandle) = savedStateHandle.toRoute() + } +} + + +fun NavGraphBuilder.captchaScreen( + onBack: () -> Unit, + onResult: (String) -> Unit +) { + composable { + CaptchaRoute( + onBack = onBack, + onResult = onResult + ) + } +} + +fun NavController.navigateToCaptcha(captchaImageUrl: String) { + this.navigate(Captcha(captchaImageUrl)) +} + +fun NavController.setCaptchaResult(code: String?) { + this.currentBackStackEntry + ?.savedStateHandle + ?.set("captcha_code", code) +} diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaRoute.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaRoute.kt deleted file mode 100644 index 308d74cb..00000000 --- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaRoute.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.meloda.app.fast.auth.captcha.navigation - -import androidx.lifecycle.SavedStateHandle -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import androidx.navigation.toRoute -import com.meloda.app.fast.auth.captcha.model.CaptchaArguments -import com.meloda.app.fast.auth.captcha.presentation.CaptchaScreen -import com.meloda.app.fast.common.customNavType -import kotlinx.serialization.Serializable -import kotlin.reflect.typeOf - -@Serializable -data class Captcha(val arguments: CaptchaArguments) { - - companion object { - val typeMap = mapOf(typeOf() to customNavType()) - - fun from(savedStateHandle: SavedStateHandle) = - savedStateHandle.toRoute(typeMap) - } -} - - -fun NavGraphBuilder.captchaRoute( - onBack: () -> Unit, - onResult: (String) -> Unit -) { - composable( - typeMap = Captcha.typeMap - ) { - CaptchaScreen( - onBack = onBack, - onResult = onResult - ) - } -} - -fun NavController.navigateToCaptcha(arguments: CaptchaArguments) { - this.navigate(Captcha(arguments)) -} - -fun NavController.setCaptchaResult(code: String?) { - this.currentBackStackEntry - ?.savedStateHandle - ?.set("captchacode", code) -} diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt index 35078b71..076cbd52 100644 --- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt +++ b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt @@ -49,6 +49,7 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import com.meloda.app.fast.auth.captcha.CaptchaViewModel import com.meloda.app.fast.auth.captcha.CaptchaViewModelImpl +import com.meloda.app.fast.auth.captcha.model.CaptchaScreenState import com.meloda.app.fast.common.UiText import com.meloda.app.fast.designsystem.MaterialDialog import com.meloda.app.fast.designsystem.TextFieldErrorText @@ -56,7 +57,7 @@ import org.koin.androidx.compose.koinViewModel import com.meloda.app.fast.designsystem.R as UiR @Composable -fun CaptchaScreen( +fun CaptchaRoute( onBack: () -> Unit, onResult: (String) -> Unit, viewModel: CaptchaViewModel = koinViewModel() @@ -64,6 +65,30 @@ fun CaptchaScreen( val screenState by viewModel.screenState.collectAsStateWithLifecycle() val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle() + LaunchedEffect(isNeedToOpenLogin) { + if (isNeedToOpenLogin) { + viewModel.onNavigatedToLogin() + onResult(screenState.code) + } + } + + CaptchaScreen( + screenState = screenState, + onBack = onBack, + onCodeInputChanged = viewModel::onCodeInputChanged, + onTextFieldDoneAction = viewModel::onTextFieldDoneAction, + onDoneButtonClicked = viewModel::onDoneButtonClicked + ) +} + +@Composable +fun CaptchaScreen( + screenState: CaptchaScreenState = CaptchaScreenState.EMPTY, + onBack: () -> Unit = {}, + onCodeInputChanged: (String) -> Unit = {}, + onTextFieldDoneAction: () -> Unit = {}, + onDoneButtonClicked: () -> Unit = {} +) { var confirmedExit by rememberSaveable { mutableStateOf(false) } @@ -97,13 +122,6 @@ fun CaptchaScreen( ) } - LaunchedEffect(isNeedToOpenLogin) { - if (isNeedToOpenLogin) { - viewModel.onNavigatedToLogin() - onResult(screenState.captchaCode) - } - } - val focusManager = LocalFocusManager.current Scaffold { padding -> @@ -171,7 +189,7 @@ fun CaptchaScreen( } else { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(screenState.captchaImage) + .data(screenState.captchaImageUrl) .crossfade(true) .build(), contentDescription = "Captcha image", @@ -183,14 +201,14 @@ fun CaptchaScreen( Spacer(modifier = Modifier.height(30.dp)) - var code by remember { mutableStateOf(TextFieldValue(screenState.captchaCode)) } + var code by remember { mutableStateOf(TextFieldValue(screenState.code)) } val showError = screenState.codeError TextField( value = code, onValueChange = { newText -> code = newText - viewModel.onCodeInputChanged(newText.text) + onCodeInputChanged(newText.text) }, label = { Text(text = "Code") }, placeholder = { Text(text = "Code") }, @@ -213,7 +231,7 @@ fun CaptchaScreen( keyboardActions = KeyboardActions( onDone = { focusManager.clearFocus() - viewModel.onTextFieldDoneClicked() + onTextFieldDoneAction() } ), isError = showError @@ -225,7 +243,7 @@ fun CaptchaScreen( } FloatingActionButton( - onClick = viewModel::onDoneButtonClicked, + onClick = onDoneButtonClicked, containerColor = MaterialTheme.colorScheme.secondaryContainer, modifier = Modifier.align(Alignment.CenterHorizontally) ) { diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/validation/CaptchaValidator.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/validation/CaptchaValidator.kt index 4f185a6b..555a84a7 100644 --- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/validation/CaptchaValidator.kt +++ b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/validation/CaptchaValidator.kt @@ -7,7 +7,7 @@ class CaptchaValidator { fun validate(screenState: CaptchaScreenState): CaptchaValidationResult { return when { - screenState.captchaCode.isEmpty() -> CaptchaValidationResult.Empty + screenState.code.trim().isEmpty() -> CaptchaValidationResult.Empty else -> CaptchaValidationResult.Valid } } diff --git a/feature/auth/login/build.gradle.kts b/feature/auth/login/build.gradle.kts index 90be05ef..a2eab37f 100644 --- a/feature/auth/login/build.gradle.kts +++ b/feature/auth/login/build.gradle.kts @@ -91,4 +91,7 @@ dependencies { implementation(libs.kotlin.serialization) implementation(libs.rebugger) + + androidTestImplementation(libs.compose.ui.test.junit4) + debugImplementation(libs.compose.ui.test.manifest) } diff --git a/feature/auth/login/src/androidTest/kotlin/com/meloda/fast/auth/login/LoginSignInTests.kt b/feature/auth/login/src/androidTest/kotlin/com/meloda/fast/auth/login/LoginSignInTests.kt new file mode 100644 index 00000000..d6ec3568 --- /dev/null +++ b/feature/auth/login/src/androidTest/kotlin/com/meloda/fast/auth/login/LoginSignInTests.kt @@ -0,0 +1,23 @@ +package com.meloda.fast.auth.login + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import com.meloda.fast.auth.login.presentation.LoginScreen +import org.junit.Rule +import org.junit.Test + +class LoginSignInTests { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun singInButton_isClickable() { + composeTestRule.setContent { + LoginScreen() + } + + composeTestRule.onNodeWithTag(testTag = "sing_in_fab").assertHasClickAction() + } +} diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt index f270ad56..32cda2e3 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt @@ -15,10 +15,10 @@ 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.LoginCaptchaArguments +import com.meloda.fast.auth.login.model.CaptchaArguments 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.LoginValidationArguments import com.meloda.fast.auth.login.model.LoginUserBannedArguments import com.meloda.fast.auth.login.model.LoginValidationResult import com.meloda.fast.auth.login.validation.LoginValidator @@ -36,10 +36,10 @@ interface LoginViewModel { val screenState: StateFlow val loginError: StateFlow - val twoFaCode: StateFlow - val twoFaArguments: StateFlow + val validationCode: StateFlow + val validationArguments: StateFlow val captchaCode: StateFlow - val captchaArguments: StateFlow + val captchaArguments: StateFlow val userBannedArguments: StateFlow val isNeedToOpenMain: StateFlow @@ -55,9 +55,9 @@ interface LoginViewModel { fun onNavigatedToMain() fun onNavigatedToUserBanned() fun onNavigatedToCaptcha() - fun onNavigatedToTwoFa() + fun onNavigatedToValidation() - fun onTwoFaCodeReceived(code: String) + fun onValidationCodeReceived(code: String) fun onCaptchaCodeReceived(code: String) fun onLogoLongClicked() @@ -73,10 +73,10 @@ class LoginViewModelImpl( override val screenState = MutableStateFlow(LoginScreenState.EMPTY) override val loginError = MutableStateFlow(null) - override val twoFaCode = MutableStateFlow(null) - override val twoFaArguments = MutableStateFlow(null) + override val validationCode = MutableStateFlow(null) + override val validationArguments = MutableStateFlow(null) override val captchaCode = MutableStateFlow(null) - override val captchaArguments = MutableStateFlow(null) + override val captchaArguments = MutableStateFlow(null) override val userBannedArguments = MutableStateFlow(null) override val isNeedToOpenMain = MutableStateFlow(false) @@ -125,12 +125,12 @@ class LoginViewModelImpl( captchaArguments.update { null } } - override fun onNavigatedToTwoFa() { - twoFaArguments.update { null } + override fun onNavigatedToValidation() { + validationArguments.update { null } } - override fun onTwoFaCodeReceived(code: String) { - twoFaCode.update { code } + override fun onValidationCodeReceived(code: String) { + validationCode.update { code } login() } @@ -186,7 +186,7 @@ class LoginViewModelImpl( "LoginViewModel", "auth: login: ${currentState.login}; " + "password: ${currentState.password}; " + - "2fa code: ${twoFaCode.value}; " + + "2fa code: ${validationCode.value}; " + "captcha code: ${captchaCode.value}" ) @@ -197,7 +197,7 @@ class LoginViewModelImpl( login = currentState.login, password = currentState.password, forceSms = forceSms, - twoFaCode = twoFaCode.value, + validationCode = validationCode.value, captchaSid = captchaArguments.value?.captchaSid, captchaKey = captchaCode.value ).listenValue { state -> @@ -205,7 +205,7 @@ class LoginViewModelImpl( error = { error -> Log.d("LoginViewModelImpl", "login: error: $error") - twoFaCode.update { null } + validationCode.update { null } captchaCode.update { null } parseError(error) @@ -229,7 +229,7 @@ class LoginViewModelImpl( userId = userId, accessToken = accessToken, fastToken = null, - trustedHash = response.twoFaHash + trustedHash = response.validationHash ).also { account -> UserConfig.currentUserId = account.userId UserConfig.userId = account.userId @@ -243,8 +243,8 @@ class LoginViewModelImpl( captchaArguments.update { null } captchaCode.update { null } - twoFaArguments.update { null } - twoFaCode.update { null } + validationArguments.update { null } + validationCode.update { null } screenState.setValue { old -> old.copy( @@ -265,7 +265,7 @@ class LoginViewModelImpl( is State.Error.OAuthError -> { when (val error = stateError.error) { is OAuthErrorDomain.ValidationRequiredError -> { - val arguments = LoginTwoFaArguments( + val arguments = LoginValidationArguments( validationSid = error.validationSid, redirectUri = error.redirectUri, phoneMask = error.phoneMask, @@ -273,13 +273,13 @@ class LoginViewModelImpl( canResendSms = error.validationResend == "sms", wrongCodeError = null ) - twoFaArguments.update { arguments } + validationArguments.update { arguments } } is OAuthErrorDomain.CaptchaRequiredError -> { - val arguments = LoginCaptchaArguments( + val arguments = CaptchaArguments( captchaSid = error.captchaSid, - captchaImage = error.captchaImageUrl + captchaImageUrl = error.captchaImageUrl ) captchaArguments.update { arguments } } @@ -298,12 +298,12 @@ class LoginViewModelImpl( userBannedArguments.update { arguments } } - OAuthErrorDomain.WrongTwoFaCode -> { - loginError.update { LoginError.WrongTwoFaCode } + OAuthErrorDomain.WrongValidationCode -> { + loginError.update { LoginError.WrongValidationCode } } - OAuthErrorDomain.WrongTwoFaCodeFormat -> { - loginError.update { LoginError.WrongTwoFaCodeFormat } + OAuthErrorDomain.WrongValidationCodeFormat -> { + loginError.update { LoginError.WrongValidationCodeFormat } } OAuthErrorDomain.TooManyTriesError -> { @@ -320,102 +320,6 @@ class LoginViewModelImpl( else -> false } - - -// return when (val error = -// (stateError as? State.Error.OAuthError<*>)?.error) { -// null -> false - -// is CaptchaRequiredError -> { -// val captchaArguments = CaptchaArguments( -// captchaSid = error.captchaSid, -// captchaImage = error.captchaImage, -// ) -// -// screenState.setValue { old -> -// old.copy( -// isNeedToOpenCaptcha = true, -// captchaArguments = captchaArguments -// ) -// } -// -// true -// } -// -// is InvalidCredentialsError -> { -// screenState.setValue { old -> old.copy(error = LoginError.WrongCredentials) } -// -// true -// } -// -// is UserBannedError -> { -// val banInfo = error.banInfo -// -// val userBannedArguments = UserBannedArguments( -// name = banInfo.memberName, -// message = banInfo.message, -// restoreUrl = banInfo.restoreUrl, -// accessToken = banInfo.accessToken -// ) -// -// screenState.setValue { old -> -// old.copy( -// isNeedToOpenUserBanned = true, -// userBannedArguments = userBannedArguments -// ) -// } -// -// true -// } -// -// is ValidationRequiredError -> { -// val twoFaArguments = TwoFaArguments( -// validationSid = error.validationSid, -// redirectUri = error.redirectUri, -// phoneMask = error.phoneMask, -// validationType = error.validationType, -// canResendSms = error.validationResend == "sms", -// wrongCodeError = null -// ) -// -// screenState.setValue { old -> -// old.copy( -// isNeedToOpenTwoFa = true, -// twoFaArguments = twoFaArguments -// ) -// } -// -// true -// } -// -// is WrongTwoFaCode -> { -// screenState.setValue { old -> -// old.copy( -// isNeedToOpenTwoFa = true, -// twoFaArguments = old.twoFaArguments?.copy( -// wrongCodeError = UiText.Simple("Wrong code") -// ) -// ) -// } -// -// true -// } -// -// is WrongTwoFaCodeFormat -> { -// screenState.setValue { old -> -// old.copy( -// isNeedToOpenTwoFa = true, -// twoFaArguments = old.twoFaArguments?.copy( -// wrongCodeError = UiText.Simple("Wrong code format") -// ) -// ) -// } -// -// true -// } - -// else -> false -// } } private fun processValidation() { diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCase.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCase.kt index c3f37a8d..a1874906 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCase.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCase.kt @@ -10,7 +10,7 @@ interface OAuthUseCase { login: String, password: String, forceSms: Boolean, - twoFaCode: String?, + validationCode: String?, captchaSid: String?, captchaKey: String? ): Flow> diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCaseImpl.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCaseImpl.kt index c16baba5..6a465525 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCaseImpl.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCaseImpl.kt @@ -18,7 +18,7 @@ class OAuthUseCaseImpl( login: String, password: String, forceSms: Boolean, - twoFaCode: String?, + validationCode: String?, captchaSid: String?, captchaKey: String? ): Flow> = flow { @@ -27,7 +27,7 @@ class OAuthUseCaseImpl( val response = oAuthRepository.auth( login = login, password = password, - twoFaCode = twoFaCode, + validationCode = validationCode, captchaSid = captchaSid, captchaKey = captchaKey, forceSms = forceSms @@ -39,7 +39,7 @@ class OAuthUseCaseImpl( AuthInfo( userId = response.userId, accessToken = response.accessToken, - twoFaHash = response.twoFaHash + validationHash = response.validationHash ) ) } @@ -92,11 +92,11 @@ class OAuthUseCaseImpl( VkOAuthErrors.INVALID_REQUEST -> { when (response.errorType) { VkErrorTypes.WRONG_OTP -> { - State.Error.OAuthError(OAuthErrorDomain.WrongTwoFaCode) + State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode) } VkErrorTypes.WRONG_OTP_FORMAT -> { - State.Error.OAuthError(OAuthErrorDomain.WrongTwoFaCodeFormat) + State.Error.OAuthError(OAuthErrorDomain.WrongValidationCodeFormat) } else -> { diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/AuthInfo.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/AuthInfo.kt index 3ee24ed4..c4f93a97 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/AuthInfo.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/AuthInfo.kt @@ -3,5 +3,5 @@ package com.meloda.fast.auth.login.model data class AuthInfo( val userId: Int?, val accessToken: String?, - val twoFaHash: String? + val validationHash: String? ) diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginCaptchaArguments.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/CaptchaArguments.kt similarity index 77% rename from feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginCaptchaArguments.kt rename to feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/CaptchaArguments.kt index b8837e89..b83a7860 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginCaptchaArguments.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/CaptchaArguments.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable @Serializable @Parcelize -data class LoginCaptchaArguments( +data class CaptchaArguments( val captchaSid: String, - val captchaImage: String + val captchaImageUrl: String ) : Parcelable diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginError.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginError.kt index 6dbdcd96..340914d9 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginError.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginError.kt @@ -7,6 +7,6 @@ sealed class LoginError { data object Unknown : LoginError() data object WrongCredentials : LoginError() data object TooManyTries : LoginError() - data object WrongTwoFaCode : LoginError() - data object WrongTwoFaCodeFormat : LoginError() + data object WrongValidationCode : LoginError() + data object WrongValidationCodeFormat : LoginError() } diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginTwoFaArguments.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginValidationArguments.kt similarity index 90% rename from feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginTwoFaArguments.kt rename to feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginValidationArguments.kt index 9f21166a..12ac5b46 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginTwoFaArguments.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginValidationArguments.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable @Serializable @Parcelize -data class LoginTwoFaArguments( +data class LoginValidationArguments( val validationSid: String, val redirectUri: String, val phoneMask: String, diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginRoute.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginNavigation.kt similarity index 62% rename from feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginRoute.kt rename to feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginNavigation.kt index ed346db6..edbf4613 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginRoute.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginNavigation.kt @@ -5,14 +5,13 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable 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.LoginCaptchaArguments -import com.meloda.fast.auth.login.model.LoginTwoFaArguments +import com.meloda.fast.auth.login.model.CaptchaArguments +import com.meloda.fast.auth.login.model.LoginValidationArguments 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 com.meloda.fast.auth.login.presentation.LoginRoute +import com.meloda.fast.auth.login.presentation.LogoRoute import kotlinx.serialization.Serializable @Serializable @@ -21,10 +20,9 @@ object Login @Serializable object Logo -fun NavGraphBuilder.loginRoute( - onError: (BaseError) -> Unit, - onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit, - onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit, +fun NavGraphBuilder.loginScreen( + onNavigateToCaptcha: (CaptchaArguments) -> Unit, + onNavigateToValidation: (LoginValidationArguments) -> Unit, onNavigateToMain: () -> Unit, onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit, onNavigateToCredentials: () -> Unit, @@ -34,25 +32,24 @@ fun NavGraphBuilder.loginRoute( val viewModel: LoginViewModel = backStackEntry.sharedViewModel(navController = navController) - val twoFaCode = backStackEntry.getTwoFaResult() + val validationCode = backStackEntry.getValidationResult() val captchaCode = backStackEntry.getCaptchaResult() - LoginScreen( - onError = onError, + LoginRoute( onNavigateToUserBanned = onNavigateToUserBanned, onNavigateToMain = onNavigateToMain, onNavigateToCaptcha = onNavigateToCaptcha, - onNavigateToTwoFa = onNavigateToTwoFa, - twoFaCode = twoFaCode, + onNavigateToValidation = onNavigateToValidation, + validationCode = validationCode, captchaCode = captchaCode, viewModel = viewModel ) } composable { - LogoScreen( + LogoRoute( onNavigateToMain = onNavigateToMain, - onShowCredentials = onNavigateToCredentials + onGoNextButtonClicked = onNavigateToCredentials ) } } @@ -61,10 +58,10 @@ fun NavController.navigateToLogin() { this.navigate(route = Login) } -fun NavBackStackEntry.getTwoFaResult(): String? { - return savedStateHandle["twofacode"] +fun NavBackStackEntry.getValidationResult(): String? { + return savedStateHandle["validation_code"] } fun NavBackStackEntry.getCaptchaResult(): String? { - return savedStateHandle["captchacode"] + return savedStateHandle["captcha_code"] } diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt index cde4d644..49556741 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt @@ -55,25 +55,23 @@ import com.meloda.app.fast.designsystem.connectNode import com.meloda.app.fast.designsystem.defaultFocusChangeAutoFill import com.meloda.app.fast.designsystem.handleEnterKey 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.LoginCaptchaArguments +import com.meloda.fast.auth.login.model.CaptchaArguments import com.meloda.fast.auth.login.model.LoginError -import com.meloda.fast.auth.login.model.LoginTwoFaArguments +import com.meloda.fast.auth.login.model.LoginScreenState +import com.meloda.fast.auth.login.model.LoginValidationArguments import com.meloda.fast.auth.login.model.LoginUserBannedArguments import org.koin.androidx.compose.koinViewModel import com.meloda.app.fast.designsystem.R as UiR -@OptIn(ExperimentalComposeUiApi::class) @Composable -fun LoginScreen( - onError: (BaseError) -> Unit, +fun LoginRoute( onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit, onNavigateToMain: () -> Unit, - onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit, - onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit, - twoFaCode: String?, + onNavigateToCaptcha: (CaptchaArguments) -> Unit, + onNavigateToValidation: (LoginValidationArguments) -> Unit, + validationCode: String?, captchaCode: String?, viewModel: LoginViewModel = koinViewModel() ) { @@ -81,7 +79,7 @@ fun LoginScreen( 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 validationArguments by viewModel.validationArguments.collectAsStateWithLifecycle() val loginError by viewModel.loginError.collectAsStateWithLifecycle() LaunchedEffect(isNeedToOpenMain) { @@ -105,16 +103,16 @@ fun LoginScreen( } } - LaunchedEffect(twoFaArguments) { - twoFaArguments?.let { arguments -> - viewModel.onNavigatedToTwoFa() - onNavigateToTwoFa(arguments) + LaunchedEffect(validationArguments) { + validationArguments?.let { arguments -> + viewModel.onNavigatedToValidation() + onNavigateToValidation(arguments) } } - LaunchedEffect(twoFaCode) { - if (twoFaCode != null) { - viewModel.onTwoFaCodeReceived(twoFaCode) + LaunchedEffect(validationCode) { + if (validationCode != null) { + viewModel.onValidationCodeReceived(validationCode) } } @@ -124,9 +122,41 @@ fun LoginScreen( } } + LoginScreen( + screenState = screenState, + onLoginAutoFilled = viewModel::onLoginInputChanged, + onPasswordAutoFilled = viewModel::onPasswordInputChanged, + onLoginInputChanged = viewModel::onLoginInputChanged, + onPasswordInputChanged = viewModel::onPasswordInputChanged, + onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked, + onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked, + onPasswordFieldGoAction = viewModel::onSignInButtonClicked, + onSignInButtonClicked = viewModel::onSignInButtonClicked + ) + + HandleError( + onDismiss = viewModel::onErrorDialogDismissed, + error = loginError + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun LoginScreen( + screenState: LoginScreenState = LoginScreenState.EMPTY, + onLoginAutoFilled: (String) -> Unit = {}, + onPasswordAutoFilled: (String) -> Unit = {}, + onLoginInputChanged: (String) -> Unit = {}, + onPasswordInputChanged: (String) -> Unit = {}, + onPasswordFieldEnterKeyClicked: () -> Unit = {}, + onPasswordVisibilityButtonClicked: () -> Unit = {}, + onPasswordFieldGoAction: () -> Unit = {}, + onSignInButtonClicked: () -> Unit = {} +) { val focusManager = LocalFocusManager.current val (loginFocusable, passwordFocusable) = FocusRequester.createRefs() + // TODO: 13/07/2024, Danil Nikolaev: remove var loginText by remember { mutableStateOf(TextFieldValue(screenState.login)) } val showLoginError = screenState.loginError @@ -135,7 +165,7 @@ fun LoginScreen( onFill = { value -> loginText = TextFieldValue(text = value, selection = TextRange(value.length)) - viewModel.onLoginInputChanged(value) + onLoginAutoFilled(value) } ) @@ -147,7 +177,7 @@ fun LoginScreen( onFill = { value -> passwordText = TextFieldValue(text = value, selection = TextRange(value.length)) - viewModel.onPasswordInputChanged(value) + onPasswordAutoFilled(value) } ) @@ -200,7 +230,7 @@ fun LoginScreen( } loginText = newText - viewModel.onLoginInputChanged(text) + onLoginInputChanged(text) }, label = { Text(text = stringResource(id = UiR.string.login_hint)) }, placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) }, @@ -236,7 +266,7 @@ fun LoginScreen( .clip(RoundedCornerShape(10.dp)) .handleEnterKey { focusManager.clearFocus() - viewModel.onSignInButtonClicked() + onPasswordFieldEnterKeyClicked() true } .focusRequester(passwordFocusable) @@ -250,7 +280,7 @@ fun LoginScreen( } passwordText = newText - viewModel.onPasswordInputChanged(text) + onPasswordInputChanged(text) }, label = { Text(text = stringResource(id = UiR.string.password_login_hint)) }, placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) }, @@ -271,7 +301,7 @@ fun LoginScreen( else UiR.drawable.round_visibility_24 ) - IconButton(onClick = viewModel::onPasswordVisibilityButtonClicked) { + IconButton(onClick = onPasswordVisibilityButtonClicked) { Icon( painter = imagePainter, contentDescription = if (screenState.passwordVisible) "Password visible icon" @@ -286,7 +316,7 @@ fun LoginScreen( keyboardActions = KeyboardActions( onGo = { focusManager.clearFocus() - viewModel.onSignInButtonClicked() + onPasswordFieldGoAction() } ), isError = showPasswordError, @@ -310,10 +340,10 @@ fun LoginScreen( FloatingActionButton( onClick = { focusManager.clearFocus() - viewModel.onSignInButtonClicked() + onSignInButtonClicked() }, containerColor = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.testTag("Sign in button") + modifier = Modifier.testTag("sing_in_fab") ) { Icon( painter = painterResource(id = UiR.drawable.ic_arrow_end), @@ -332,11 +362,6 @@ fun LoginScreen( } } } - - HandleError( - onDismiss = viewModel::onErrorDialogDismissed, - error = loginError - ) } @Composable @@ -375,7 +400,7 @@ fun HandleError( } - LoginError.WrongTwoFaCode -> { + LoginError.WrongValidationCode -> { MaterialDialog( onDismissAction = onDismiss, title = UiText.Simple("Error"), @@ -384,7 +409,7 @@ fun HandleError( ) } - LoginError.WrongTwoFaCodeFormat -> { + LoginError.WrongValidationCodeFormat -> { MaterialDialog( onDismissAction = onDismiss, title = UiText.Simple("Error"), diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt index fdbed2b1..8064d2cf 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt @@ -34,11 +34,10 @@ import com.meloda.fast.auth.login.LoginViewModelImpl import org.koin.androidx.compose.koinViewModel import com.meloda.app.fast.designsystem.R as UiR -@OptIn(ExperimentalFoundationApi::class) @Composable -fun LogoScreen( +fun LogoRoute( onNavigateToMain: () -> Unit, - onShowCredentials: () -> Unit, + onGoNextButtonClicked: () -> Unit, viewModel: LoginViewModel = koinViewModel() ) { val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle() @@ -50,15 +49,37 @@ fun LogoScreen( } } + LogoScreen( + onLogoLongClicked = viewModel::onLogoLongClicked, + onGoNextButtonClicked = onGoNextButtonClicked + ) +} + + +// TODO: 13/07/2024, Danil Nikolaev: replace with scaffold? +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun LogoScreen( + onLogoLongClicked: () -> Unit = {}, + onGoNextButtonClicked: () -> Unit = {} +) { Scaffold { padding -> - val topPadding by animateDpAsState(targetValue = padding.calculateTopPadding()) - val bottomPadding by animateDpAsState(targetValue = padding.calculateBottomPadding()) + val topPadding by animateDpAsState( + targetValue = padding.calculateTopPadding(), + label = "topPaddingAnimation" + ) + val bottomPadding by animateDpAsState( + targetValue = padding.calculateBottomPadding(), + label = "bottomPaddingAnimation" + ) val endPadding by animateDpAsState( - targetValue = padding.calculateEndPadding(LayoutDirection.Ltr) + targetValue = padding.calculateEndPadding(LayoutDirection.Ltr), + label = "endPaddingAnimation" ) val startPadding by animateDpAsState( - targetValue = padding.calculateStartPadding(LayoutDirection.Ltr) + targetValue = padding.calculateStartPadding(LayoutDirection.Ltr), + label = "startPaddingAnimation" ) Box( @@ -85,7 +106,7 @@ fun LogoScreen( modifier = Modifier.combinedClickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - onLongClick = viewModel::onLogoLongClicked, + onLongClick = onLogoLongClicked, onClick = {} ) ) @@ -98,7 +119,7 @@ fun LogoScreen( } FloatingActionButton( - onClick = onShowCredentials, + onClick = onGoNextButtonClicked, containerColor = MaterialTheme.colorScheme.secondaryContainer, modifier = Modifier.align(Alignment.BottomCenter) ) { diff --git a/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt b/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt index 144f5ccf..560bf72e 100644 --- a/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt +++ b/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt @@ -3,20 +3,19 @@ package com.meloda.app.fast.auth import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation -import com.meloda.app.fast.auth.captcha.model.CaptchaArguments -import com.meloda.app.fast.auth.captcha.navigation.captchaRoute +import com.meloda.app.fast.auth.captcha.navigation.captchaScreen import com.meloda.app.fast.auth.captcha.navigation.navigateToCaptcha import com.meloda.app.fast.auth.captcha.navigation.setCaptchaResult -import com.meloda.app.fast.auth.twofa.model.TwoFaArguments -import com.meloda.app.fast.auth.twofa.navigation.navigateToTwoFa -import com.meloda.app.fast.auth.twofa.navigation.setTwoFaResult -import com.meloda.app.fast.auth.twofa.navigation.twoFaRoute +import com.meloda.app.fast.auth.validation.model.ValidationArguments +import com.meloda.app.fast.auth.validation.navigation.navigateToValidation +import com.meloda.app.fast.auth.validation.navigation.setValidationResult +import com.meloda.app.fast.auth.validation.navigation.validationScreen import com.meloda.app.fast.model.BaseError import com.meloda.app.fast.userbanned.model.UserBannedArguments import com.meloda.app.fast.userbanned.navigation.navigateToUserBanned import com.meloda.app.fast.userbanned.navigation.userBannedRoute import com.meloda.fast.auth.login.navigation.Logo -import com.meloda.fast.auth.login.navigation.loginRoute +import com.meloda.fast.auth.login.navigation.loginScreen import com.meloda.fast.auth.login.navigation.navigateToLogin import kotlinx.serialization.Serializable import java.net.URLEncoder @@ -25,26 +24,21 @@ import java.net.URLEncoder object AuthGraph fun NavGraphBuilder.authNavGraph( - onError: (BaseError) -> Unit, onNavigateToMain: () -> Unit, navController: NavController ) { navigation( startDestination = Logo ) { - loginRoute( - onError = onError, + loginScreen( onNavigateToCaptcha = { arguments -> navController.navigateToCaptcha( - CaptchaArguments( - arguments.captchaSid, - URLEncoder.encode(arguments.captchaImage, "utf-8") - ) + captchaImageUrl = URLEncoder.encode(arguments.captchaImageUrl, "utf-8") ) }, - onNavigateToTwoFa = { arguments -> - navController.navigateToTwoFa( - TwoFaArguments( + onNavigateToValidation = { arguments -> + navController.navigateToValidation( + ValidationArguments( validationSid = arguments.validationSid, redirectUri = URLEncoder.encode(arguments.redirectUri, "utf-8"), phoneMask = arguments.phoneMask, @@ -58,7 +52,7 @@ fun NavGraphBuilder.authNavGraph( onNavigateToUserBanned = { arguments -> navController.navigateToUserBanned( UserBannedArguments( - name = arguments.name, + userName = arguments.name, message = arguments.message, restoreUrl = arguments.restoreUrl, accessToken = arguments.accessToken @@ -69,18 +63,18 @@ fun NavGraphBuilder.authNavGraph( navController = navController ) - twoFaRoute( + validationScreen( onBack = { navController.navigateUp() - navController.setTwoFaResult(null) + navController.setValidationResult(null) }, onResult = { code -> navController.popBackStack() - navController.setTwoFaResult(code) + navController.setValidationResult(code) } ) - captchaRoute( + captchaScreen( onBack = { navController.navigateUp() navController.setCaptchaResult(null) diff --git a/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthModule.kt b/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthModule.kt index e5ef7e29..d818230f 100644 --- a/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthModule.kt +++ b/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthModule.kt @@ -1,14 +1,14 @@ package com.meloda.app.fast.auth import com.meloda.app.fast.auth.captcha.di.captchaModule -import com.meloda.app.fast.auth.twofa.di.twoFaModule +import com.meloda.app.fast.auth.validation.di.validationModule import com.meloda.fast.auth.login.di.loginModule import org.koin.dsl.module val authModule = module { includes( loginModule, - twoFaModule, + validationModule, captchaModule, ) } diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/di/TwoFaModule.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/di/TwoFaModule.kt deleted file mode 100644 index aa777bc0..00000000 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/di/TwoFaModule.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.app.fast.auth.twofa.di - -import com.meloda.app.fast.auth.twofa.AuthUseCase -import com.meloda.app.fast.auth.twofa.AuthUseCaseImpl -import com.meloda.app.fast.auth.twofa.TwoFaViewModel -import com.meloda.app.fast.auth.twofa.TwoFaViewModelImpl -import com.meloda.app.fast.auth.twofa.validation.TwoFaValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.bind -import org.koin.dsl.module - -val twoFaModule = module { - singleOf(::TwoFaValidator) - viewModelOf(::TwoFaViewModelImpl) bind TwoFaViewModel::class - singleOf(::AuthUseCaseImpl) bind AuthUseCase::class -} diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaScreenState.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaScreenState.kt deleted file mode 100644 index fb90afdb..00000000 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaScreenState.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.app.fast.auth.twofa.model - -import com.meloda.app.fast.common.UiText - -data class TwoFaScreenState( - val twoFaSid: String, - val twoFaCode: String?, - val twoFaText: UiText, - val canResendSms: Boolean, - val codeError: String?, - val delayTime: Int, - val phoneMask: String -) { - - companion object { - val EMPTY = TwoFaScreenState( - twoFaSid = "", - twoFaCode = null, - twoFaText = UiText.Simple(""), - canResendSms = false, - codeError = null, - delayTime = 0, - phoneMask = "" - ) - } -} diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaValidationResult.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaValidationResult.kt deleted file mode 100644 index 1b5fd8b2..00000000 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaValidationResult.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.app.fast.auth.twofa.model - -sealed class TwoFaValidationResult { - data object Empty : TwoFaValidationResult() - data object Valid : TwoFaValidationResult() - - fun isValid() = this == Valid -} diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaValidationType.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaValidationType.kt deleted file mode 100644 index 68939d54..00000000 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaValidationType.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.meloda.app.fast.auth.twofa.model - -sealed class TwoFaValidationType(val value: String) { - - data object Sms : TwoFaValidationType(TYPE_SMS) - - data object TwoFaApp : TwoFaValidationType(TYPE_TWO_FA_APP) - - data class Another(val type: String) : TwoFaValidationType(type) - - companion object { - private const val TYPE_SMS = "sms" - private const val TYPE_TWO_FA_APP = "2fa_app" - - fun parse(validationType: String): TwoFaValidationType { - return when (validationType) { - TYPE_SMS -> Sms - TYPE_TWO_FA_APP -> TwoFaApp - else -> Another(validationType) - } - } - } -} diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/navigation/TwoFaRoute.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/navigation/TwoFaRoute.kt deleted file mode 100644 index 8c4bfe9a..00000000 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/navigation/TwoFaRoute.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.meloda.app.fast.auth.twofa.navigation - -import androidx.lifecycle.SavedStateHandle -import androidx.navigation.NavController -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.presentation.TwoFaScreen -import com.meloda.app.fast.common.customNavType -import kotlinx.serialization.Serializable -import kotlin.reflect.typeOf - -@Serializable -data class TwoFa(val arguments: TwoFaArguments) { - companion object { - val typeMap = mapOf(typeOf() to customNavType()) - - fun from(savedStateHandle: SavedStateHandle) = - savedStateHandle.toRoute(typeMap) - } -} - -fun NavGraphBuilder.twoFaRoute( - onBack: () -> Unit, - onResult: (String) -> Unit -) { - composable(typeMap = TwoFa.typeMap) { - TwoFaScreen( - onBack = onBack, - onCodeResult = onResult - ) - } -} - -fun NavController.navigateToTwoFa(arguments: TwoFaArguments) { - this.navigate(TwoFa(arguments)) -} - -fun NavController.setTwoFaResult(code: String?) { - this.currentBackStackEntry - ?.savedStateHandle - ?.set("twofacode", code) -} - - diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/validation/TwoFaValidator.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/validation/TwoFaValidator.kt deleted file mode 100644 index d514fe6a..00000000 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/validation/TwoFaValidator.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.app.fast.auth.twofa.validation - -import com.meloda.app.fast.auth.twofa.model.TwoFaScreenState -import com.meloda.app.fast.auth.twofa.model.TwoFaValidationResult - -class TwoFaValidator { - - fun validate(screenState: TwoFaScreenState): TwoFaValidationResult { - return when { - screenState.twoFaCode.isNullOrEmpty() -> TwoFaValidationResult.Empty - else -> TwoFaValidationResult.Valid - } - } -} diff --git a/feature/auth/userbanned/build.gradle.kts b/feature/auth/userbanned/build.gradle.kts index 74a22e4a..96db9dfd 100644 --- a/feature/auth/userbanned/build.gradle.kts +++ b/feature/auth/userbanned/build.gradle.kts @@ -57,4 +57,6 @@ dependencies { implementation(libs.coil.compose) implementation(libs.androidx.navigation.compose) implementation(libs.kotlin.serialization) + + debugImplementation(libs.androidx.ui.tooling) } diff --git a/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/model/UserBannedArguments.kt b/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/model/UserBannedArguments.kt index 57154715..e114e0a7 100644 --- a/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/model/UserBannedArguments.kt +++ b/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/model/UserBannedArguments.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable @Serializable @Parcelize data class UserBannedArguments( - val name: String, + val userName: String, val message: String, val restoreUrl: String, val accessToken: String diff --git a/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/model/UserBannedScreenState.kt b/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/model/UserBannedScreenState.kt new file mode 100644 index 00000000..80d2b48c --- /dev/null +++ b/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/model/UserBannedScreenState.kt @@ -0,0 +1,14 @@ +package com.meloda.app.fast.userbanned.model + +data class UserBannedScreenState( + val userName: String, + val message: String +) { + + companion object { + val EMPTY: UserBannedScreenState = UserBannedScreenState( + userName = "", + message = "" + ) + } +} diff --git a/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/navigation/UserBannedRoute.kt b/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/navigation/UserBannedNavigation.kt similarity index 87% rename from feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/navigation/UserBannedRoute.kt rename to feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/navigation/UserBannedNavigation.kt index 27581d86..bed6e811 100644 --- a/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/navigation/UserBannedRoute.kt +++ b/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/navigation/UserBannedNavigation.kt @@ -8,7 +8,7 @@ import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.meloda.app.fast.userbanned.model.UserBannedArguments -import com.meloda.app.fast.userbanned.presentation.UserBannedScreen +import com.meloda.app.fast.userbanned.presentation.UserBannedRoute import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -32,18 +32,16 @@ val UserBannedNavType = object : NavType(isNullableAllowed override val name: String = "UserBannedArguments" } -fun NavGraphBuilder.userBannedRoute( - onBack: () -> Unit -) { +fun NavGraphBuilder.userBannedRoute(onBack: () -> Unit) { composable( typeMap = mapOf(typeOf() to UserBannedNavType) ) { backStackEntry -> val arguments: UserBannedArguments = backStackEntry.toRoute() - UserBannedScreen( + UserBannedRoute( onBack = onBack, - name = arguments.name, - message = arguments.message, + userName = arguments.userName, + message = arguments.message ) } } diff --git a/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/presentation/UserBannedScreen.kt b/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/presentation/UserBannedScreen.kt index 4a2d8f05..714a08c2 100644 --- a/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/presentation/UserBannedScreen.kt +++ b/feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/presentation/UserBannedScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -22,27 +23,44 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.meloda.app.fast.designsystem.AppTheme +import com.meloda.app.fast.userbanned.model.UserBannedScreenState import com.meloda.app.fast.designsystem.R as UiR @Preview @Composable fun UserBannedScreenPreview() { - AppTheme { - UserBannedScreen( - onBack = {}, - name = "Calvin Harris", - message = "Eto konets" + UserBannedScreen( + screenState = UserBannedScreenState( + userName = "Andre Shultz", + message = "Bruteforce" + ) + ) +} + +@Composable +fun UserBannedRoute( + onBack: () -> Unit, + userName: String, + message: String +) { + val screenState = remember(userName, message) { + UserBannedScreenState( + userName = userName, + message = message ) } + + UserBannedScreen( + screenState = screenState, + onBack = onBack + ) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun UserBannedScreen( - onBack: () -> Unit, - name: String, - message: String, + screenState: UserBannedScreenState = UserBannedScreenState.EMPTY, + onBack: () -> Unit = {}, ) { Scaffold( topBar = { @@ -80,7 +98,7 @@ fun UserBannedScreen( append(": ") } - append(name) + append(screenState.userName) } ) Text( @@ -89,7 +107,7 @@ fun UserBannedScreen( append(stringResource(id = UiR.string.blocking_reason_title)) append(": ") } - append(message) + append(screenState.message) } ) } diff --git a/feature/auth/twofa/.gitignore b/feature/auth/validation/.gitignore similarity index 100% rename from feature/auth/twofa/.gitignore rename to feature/auth/validation/.gitignore diff --git a/feature/auth/twofa/build.gradle.kts b/feature/auth/validation/build.gradle.kts similarity index 96% rename from feature/auth/twofa/build.gradle.kts rename to feature/auth/validation/build.gradle.kts index 5e82d129..8921644a 100644 --- a/feature/auth/twofa/build.gradle.kts +++ b/feature/auth/validation/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } android { - namespace = "com.meloda.app.fast.twofa" + namespace = "com.meloda.app.fast.validation" compileSdk = Configs.compileSdk defaultConfig { diff --git a/feature/auth/twofa/consumer-rules.pro b/feature/auth/validation/consumer-rules.pro similarity index 100% rename from feature/auth/twofa/consumer-rules.pro rename to feature/auth/validation/consumer-rules.pro diff --git a/feature/auth/twofa/proguard-rules.pro b/feature/auth/validation/proguard-rules.pro similarity index 100% rename from feature/auth/twofa/proguard-rules.pro rename to feature/auth/validation/proguard-rules.pro diff --git a/feature/auth/twofa/src/main/AndroidManifest.xml b/feature/auth/validation/src/main/AndroidManifest.xml similarity index 100% rename from feature/auth/twofa/src/main/AndroidManifest.xml rename to feature/auth/validation/src/main/AndroidManifest.xml diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/AuthUseCase.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/AuthUseCase.kt similarity index 84% rename from feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/AuthUseCase.kt rename to feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/AuthUseCase.kt index 9f0c51c9..92d6f605 100644 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/AuthUseCase.kt +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/AuthUseCase.kt @@ -1,4 +1,4 @@ -package com.meloda.app.fast.auth.twofa +package com.meloda.app.fast.auth.validation import com.meloda.app.fast.data.State import com.meloda.app.fast.model.api.responses.SendSmsResponse diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/AuthUseCaseImpl.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/AuthUseCaseImpl.kt similarity index 95% rename from feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/AuthUseCaseImpl.kt rename to feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/AuthUseCaseImpl.kt index 0b55d557..942b84a6 100644 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/AuthUseCaseImpl.kt +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/AuthUseCaseImpl.kt @@ -1,4 +1,4 @@ -package com.meloda.app.fast.auth.twofa +package com.meloda.app.fast.auth.validation import com.meloda.app.fast.data.State import com.meloda.app.fast.data.api.auth.AuthRepository diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/TwoFaViewModel.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/ValidationViewModel.kt similarity index 69% rename from feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/TwoFaViewModel.kt rename to feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/ValidationViewModel.kt index a808d190..edf21b20 100644 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/TwoFaViewModel.kt +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/ValidationViewModel.kt @@ -1,12 +1,12 @@ -package com.meloda.app.fast.auth.twofa +package com.meloda.app.fast.auth.validation import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -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 -import com.meloda.app.fast.auth.twofa.validation.TwoFaValidator +import com.meloda.app.fast.auth.validation.model.ValidationScreenState +import com.meloda.app.fast.auth.validation.model.ValidationType +import com.meloda.app.fast.auth.validation.navigation.Validation +import com.meloda.app.fast.auth.validation.validation.ValidationValidator import com.meloda.app.fast.common.UiText import com.meloda.app.fast.common.extensions.createTimerFlow import com.meloda.app.fast.common.extensions.listenValue @@ -22,9 +22,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -interface TwoFaViewModel { +interface ValidationViewModel { - val screenState: StateFlow + val screenState: StateFlow val isNeedToOpenLogin: StateFlow @@ -33,36 +33,39 @@ interface TwoFaViewModel { fun onBackButtonClicked() fun onCancelButtonClicked() fun onRequestSmsButtonClicked() - fun onTextFieldDoneClicked() + fun onTextFieldDoneAction() fun onDoneButtonClicked() fun onNavigatedToLogin() } -class TwoFaViewModelImpl( - private val validator: TwoFaValidator, +class ValidationViewModelImpl( + private val validator: ValidationValidator, private val authUseCase: AuthUseCase, savedStateHandle: SavedStateHandle -) : TwoFaViewModel, ViewModel() { +) : ValidationViewModel, ViewModel() { - override val screenState = MutableStateFlow(TwoFaScreenState.EMPTY) + override val screenState = MutableStateFlow(ValidationScreenState.EMPTY) override val isNeedToOpenLogin = MutableStateFlow(false) + private var validationSid: String? = null + private var delayJob: Job? = null init { // TODO: 08/07/2024, Danil Nikolaev: use when fixed - //savedStateHandle.toRoute().arguments + //savedStateHandle.toRoute().arguments - val arguments = TwoFa.from(savedStateHandle).arguments + val arguments = Validation.from(savedStateHandle).arguments + + validationSid = arguments.validationSid screenState.setValue { old -> old.copy( - twoFaSid = arguments.validationSid, - canResendSms = arguments.canResendSms, + isSmsButtonVisible = arguments.canResendSms, codeError = arguments.wrongCodeError, - twoFaText = getTwoFaText(TwoFaValidationType.parse(arguments.validationType)), + validationText = getValidationText(ValidationType.parse(arguments.validationType)), phoneMask = arguments.phoneMask ) } @@ -71,7 +74,7 @@ class TwoFaViewModelImpl( override fun onCodeInputChanged(newCode: String) { screenState.updateValue( screenState.value.copy( - twoFaCode = newCode.trim(), + code = newCode.trim(), codeError = null ) ) @@ -89,7 +92,7 @@ class TwoFaViewModelImpl( } override fun onCancelButtonClicked() { - screenState.setValue { old -> old.copy(twoFaCode = null) } + screenState.setValue { old -> old.copy(code = null) } isNeedToOpenLogin.update { true } } @@ -97,7 +100,7 @@ class TwoFaViewModelImpl( sendValidationCode() } - override fun onTextFieldDoneClicked() { + override fun onTextFieldDoneAction() { onDoneButtonClicked() } @@ -108,7 +111,7 @@ class TwoFaViewModelImpl( } override fun onNavigatedToLogin() { - screenState.updateValue(TwoFaScreenState.EMPTY) + screenState.updateValue(ValidationScreenState.EMPTY) isNeedToOpenLogin.update { false } } @@ -126,9 +129,7 @@ class TwoFaViewModelImpl( } private fun sendValidationCode() { - val validationSid = screenState.value.twoFaSid - - authUseCase.sendSms(validationSid) + authUseCase.sendSms(validationSid.orEmpty()) .listenValue { state -> state.processState( error = { error -> @@ -140,9 +141,9 @@ class TwoFaViewModelImpl( screenState.setValue { old -> old.copy( - canResendSms = newCanResendSms, - twoFaText = getTwoFaText( - TwoFaValidationType.parse(newValidationType.orEmpty()) + isSmsButtonVisible = newCanResendSms, + validationText = getValidationText( + ValidationType.parse(newValidationType.orEmpty()) ) ) } @@ -152,7 +153,7 @@ class TwoFaViewModelImpl( ) if (state.isLoading()) { - screenState.emit(screenState.value.copy(canResendSms = false)) + screenState.emit(screenState.value.copy(isSmsButtonVisible = false)) } } } @@ -164,7 +165,7 @@ class TwoFaViewModelImpl( time = delay, onStartAction = { screenState.updateValue( - screenState.value.copy(canResendSms = false) + screenState.value.copy(isSmsButtonVisible = false) ) }, onTickAction = { remainedTime -> @@ -175,24 +176,24 @@ class TwoFaViewModelImpl( onTimeoutAction = { screenState.updateValue( screenState.value.copy( - canResendSms = true + isSmsButtonVisible = true ) ) }, ).launchIn(viewModelScope) } - private fun getTwoFaText(validationType: TwoFaValidationType): UiText { + private fun getValidationText(validationType: ValidationType): UiText { return when (validationType) { - TwoFaValidationType.Sms -> { + ValidationType.Sms -> { UiText.Simple("SMS with the code is sent to ${screenState.value.phoneMask}") } - TwoFaValidationType.TwoFaApp -> { + ValidationType.App -> { UiText.Simple("Enter the code from the code generator application") } - is TwoFaValidationType.Another -> UiText.Simple(validationType.type) + is ValidationType.Other -> UiText.Simple(validationType.type) } } } diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/di/ValidationModule.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/di/ValidationModule.kt new file mode 100644 index 00000000..671066c3 --- /dev/null +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/di/ValidationModule.kt @@ -0,0 +1,17 @@ +package com.meloda.app.fast.auth.validation.di + +import com.meloda.app.fast.auth.validation.AuthUseCase +import com.meloda.app.fast.auth.validation.AuthUseCaseImpl +import com.meloda.app.fast.auth.validation.ValidationViewModel +import com.meloda.app.fast.auth.validation.ValidationViewModelImpl +import com.meloda.app.fast.auth.validation.validation.ValidationValidator +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +val validationModule = module { + singleOf(::ValidationValidator) + viewModelOf(::ValidationViewModelImpl) bind ValidationViewModel::class + singleOf(::AuthUseCaseImpl) bind AuthUseCase::class +} diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaArguments.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationArguments.kt similarity index 80% rename from feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaArguments.kt rename to feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationArguments.kt index 650da578..16ab7831 100644 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaArguments.kt +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationArguments.kt @@ -1,4 +1,4 @@ -package com.meloda.app.fast.auth.twofa.model +package com.meloda.app.fast.auth.validation.model import android.os.Parcelable import kotlinx.parcelize.Parcelize @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable @Serializable @Parcelize -data class TwoFaArguments( +data class ValidationArguments( val validationSid: String, val redirectUri: String, val phoneMask: String, diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationScreenState.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationScreenState.kt new file mode 100644 index 00000000..5e56fe1c --- /dev/null +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationScreenState.kt @@ -0,0 +1,27 @@ +package com.meloda.app.fast.auth.validation.model + +import com.meloda.app.fast.common.UiText + +data class ValidationScreenState( + val code: String?, + val codeError: String?, + val isSmsButtonVisible: Boolean, + val delayTime: Int, + val phoneMask: String, + + // TODO: 13/07/2024, Danil Nikolaev: check wtf is this + val validationText: UiText, +) { + + companion object { + val EMPTY = ValidationScreenState( + code = null, + codeError = null, + isSmsButtonVisible = false, + delayTime = 0, + phoneMask = "", + + validationText = UiText.Simple("") + ) + } +} diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationType.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationType.kt new file mode 100644 index 00000000..871a62b9 --- /dev/null +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationType.kt @@ -0,0 +1,23 @@ +package com.meloda.app.fast.auth.validation.model + +sealed class ValidationType(val value: String) { + + data object Sms : ValidationType(TYPE_SMS) + + data object App : ValidationType(TYPE_TWO_FA_APP) + + data class Other(val type: String) : ValidationType(type) + + companion object { + private const val TYPE_SMS = "sms" + private const val TYPE_TWO_FA_APP = "2fa_app" + + fun parse(validationType: String): ValidationType { + return when (validationType) { + TYPE_SMS -> Sms + TYPE_TWO_FA_APP -> App + else -> Other(validationType) + } + } + } +} diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationValidationResult.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationValidationResult.kt new file mode 100644 index 00000000..3955e6c6 --- /dev/null +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationValidationResult.kt @@ -0,0 +1,8 @@ +package com.meloda.app.fast.auth.validation.model + +sealed class ValidationValidationResult { + data object Empty : ValidationValidationResult() + data object Valid : ValidationValidationResult() + + fun isValid() = this == Valid +} diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/navigation/ValidationNavigation.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/navigation/ValidationNavigation.kt new file mode 100644 index 00000000..8b28d99f --- /dev/null +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/navigation/ValidationNavigation.kt @@ -0,0 +1,46 @@ +package com.meloda.app.fast.auth.validation.navigation + +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.meloda.app.fast.auth.validation.model.ValidationArguments +import com.meloda.app.fast.auth.validation.presentation.ValidationRoute +import com.meloda.app.fast.common.customNavType +import kotlinx.serialization.Serializable +import kotlin.reflect.typeOf + +@Serializable +data class Validation(val arguments: ValidationArguments) { + companion object { + val typeMap = mapOf(typeOf() to customNavType()) + + fun from(savedStateHandle: SavedStateHandle) = + savedStateHandle.toRoute(typeMap) + } +} + +fun NavGraphBuilder.validationScreen( + onBack: () -> Unit, + onResult: (String) -> Unit +) { + composable(typeMap = Validation.typeMap) { + ValidationRoute( + onBack = onBack, + onResult = onResult + ) + } +} + +fun NavController.navigateToValidation(arguments: ValidationArguments) { + this.navigate(Validation(arguments)) +} + +fun NavController.setValidationResult(code: String?) { + this.currentBackStackEntry + ?.savedStateHandle + ?.set("validation_code", code) +} + + diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/presentation/TwoFaScreen.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/presentation/ValidationScreen.kt similarity index 82% rename from feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/presentation/TwoFaScreen.kt rename to feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/presentation/ValidationScreen.kt index 35344f1d..77030af6 100644 --- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/presentation/TwoFaScreen.kt +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/presentation/ValidationScreen.kt @@ -1,4 +1,4 @@ -package com.meloda.app.fast.auth.twofa.presentation +package com.meloda.app.fast.auth.validation.presentation import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility @@ -27,6 +27,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -42,8 +43,9 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue 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.validation.ValidationViewModel +import com.meloda.app.fast.auth.validation.ValidationViewModelImpl +import com.meloda.app.fast.auth.validation.model.ValidationScreenState import com.meloda.app.fast.common.UiText import com.meloda.app.fast.designsystem.MaterialDialog import com.meloda.app.fast.designsystem.TextFieldErrorText @@ -52,17 +54,48 @@ import org.koin.androidx.compose.koinViewModel import com.meloda.app.fast.designsystem.R as UiR @Composable -fun TwoFaScreen( +fun ValidationRoute( onBack: () -> Unit, - onCodeResult: (code: String) -> Unit, - viewModel: TwoFaViewModel = koinViewModel(), + onResult: (String) -> Unit, + viewModel: ValidationViewModel = koinViewModel() +) { + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle() + + LaunchedEffect(isNeedToOpenLogin) { + if (isNeedToOpenLogin) { + viewModel.onNavigatedToLogin() + + val code = screenState.code + if (code == null) { + onBack() + } else { + onResult(code) + } + } + } + + ValidationScreen( + screenState = screenState, + onBack = onBack, + onCodeInputChanged = viewModel::onCodeInputChanged, + onTextFieldDoneAction = viewModel::onTextFieldDoneAction, + onRequestSmsButtonClicked = viewModel::onRequestSmsButtonClicked, + onDoneButtonClicked = viewModel::onDoneButtonClicked + ) +} + +@Composable +fun ValidationScreen( + screenState: ValidationScreenState = ValidationScreenState.EMPTY, + onBack: () -> Unit = {}, + onCodeInputChanged: (String) -> Unit = {}, + onTextFieldDoneAction: () -> Unit = {}, + onRequestSmsButtonClicked: () -> Unit = {}, + onDoneButtonClicked: () -> Unit = {} ) { val focusManager = LocalFocusManager.current - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - - val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle() - var confirmedExit by rememberSaveable { mutableStateOf(false) } @@ -96,20 +129,7 @@ fun TwoFaScreen( ) } - LaunchedEffect(isNeedToOpenLogin) { - if (isNeedToOpenLogin) { - viewModel.onNavigatedToLogin() - - val code = screenState.twoFaCode - if (code == null) { - onBack() - } else { - onCodeResult(code) - } - } - } - - var code by remember { mutableStateOf(TextFieldValue(screenState.twoFaCode.orEmpty())) } + var code by remember { mutableStateOf(TextFieldValue(screenState.code.orEmpty())) } val codeError = screenState.codeError Scaffold { padding -> @@ -140,7 +160,6 @@ fun TwoFaScreen( Column( modifier = Modifier.fillMaxWidth(), ) { - Text( text = "Two-Factor\nAuthentication", style = MaterialTheme.typography.displayMedium, @@ -148,16 +167,18 @@ fun TwoFaScreen( ) Spacer(modifier = Modifier.height(38.dp)) Text( - text = screenState.twoFaText.getString().orEmpty(), + text = screenState.validationText.getString().orEmpty(), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onBackground ) Spacer(modifier = Modifier.height(10.dp)) - val delayRemainedTime = screenState.delayTime - AnimatedVisibility(visible = delayRemainedTime > 0) { + val isResendTextVisible by remember { + derivedStateOf { screenState.delayTime > 0 } + } + AnimatedVisibility(visible = isResendTextVisible) { Text( - text = "Can resend after $delayRemainedTime seconds", + text = "Can resend after ${screenState.delayTime} seconds", style = MaterialTheme.typography.bodySmall ) } @@ -169,7 +190,7 @@ fun TwoFaScreen( if (newText.text.length > 6) return@TextField code = newText - viewModel.onCodeInputChanged((newText.text)) + onCodeInputChanged((newText.text)) }, label = { Text(text = "Code") }, placeholder = { Text(text = "Code") }, @@ -195,7 +216,7 @@ fun TwoFaScreen( keyboardActions = KeyboardActions( onDone = { focusManager.clearFocus() - viewModel.onTextFieldDoneClicked() + onTextFieldDoneAction() } ), isError = codeError != null @@ -211,13 +232,13 @@ fun TwoFaScreen( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - val canResendSms = screenState.canResendSms + val canResendSms = screenState.isSmsButtonVisible AnimatedVisibility( visible = canResendSms, ) { ExtendedFloatingActionButton( - onClick = viewModel::onRequestSmsButtonClicked, + onClick = onRequestSmsButtonClicked, text = { Text( text = "Request SMS", @@ -238,7 +259,7 @@ fun TwoFaScreen( Spacer(modifier = Modifier.width(16.dp)) FloatingActionButton( - onClick = viewModel::onDoneButtonClicked, + onClick = onDoneButtonClicked, containerColor = MaterialTheme.colorScheme.secondaryContainer, ) { Icon( diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/validation/ValidationValidator.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/validation/ValidationValidator.kt new file mode 100644 index 00000000..ea9af800 --- /dev/null +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/validation/ValidationValidator.kt @@ -0,0 +1,14 @@ +package com.meloda.app.fast.auth.validation.validation + +import com.meloda.app.fast.auth.validation.model.ValidationScreenState +import com.meloda.app.fast.auth.validation.model.ValidationValidationResult + +class ValidationValidator { + + fun validate(screenState: ValidationScreenState): ValidationValidationResult { + return when { + screenState.code.isNullOrEmpty() -> ValidationValidationResult.Empty + else -> ValidationValidationResult.Valid + } + } +} diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsRoute.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt similarity index 88% rename from feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsRoute.kt rename to feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt index 5a12ecde..a92ba5d2 100644 --- a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsRoute.kt +++ b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt @@ -5,7 +5,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.meloda.app.fast.chatmaterials.presentation.ChatMaterialsScreen +import com.meloda.app.fast.chatmaterials.presentation.ChatMaterialsRoute import kotlinx.serialization.Serializable @Serializable @@ -19,13 +19,11 @@ data class ChatMaterials( } } -fun NavGraphBuilder.chatMaterialsRoute( +fun NavGraphBuilder.chatMaterialsScreen( onBack: () -> Unit ) { composable { - ChatMaterialsScreen( - onBack = onBack - ) + ChatMaterialsRoute(onBack = onBack) } } diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialsScreen.kt index eed5b4e5..354958bb 100644 --- a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialsScreen.kt @@ -66,6 +66,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.imageLoader import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModel import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModelImpl +import com.meloda.app.fast.chatmaterials.model.ChatMaterialsScreenState import com.meloda.app.fast.designsystem.LocalTheme import com.meloda.app.fast.designsystem.R import dev.chrisbanes.haze.HazeState @@ -75,6 +76,22 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import org.koin.androidx.compose.koinViewModel +@Composable +fun ChatMaterialsRoute( + onBack: () -> Unit, + viewModel: ChatMaterialsViewModel = koinViewModel() +) { + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + + ChatMaterialsScreen( + screenState = screenState, + onBack = onBack, + onTypeChanged = viewModel::onTypeChanged, + onRefreshDropdownItemClicked = viewModel::onRefresh, + onRefresh = viewModel::onRefresh + ) +} + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn( ExperimentalMaterial3Api::class, @@ -82,11 +99,14 @@ import org.koin.androidx.compose.koinViewModel ) @Composable fun ChatMaterialsScreen( - onBack: () -> Unit, - viewModel: ChatMaterialsViewModel = koinViewModel() + screenState: ChatMaterialsScreenState, + onBack: () -> Unit = {}, + onTypeChanged: (String) -> Unit = {}, + onRefreshDropdownItemClicked: () -> Unit = {}, + onRefresh: () -> Unit = {} ) { val currentTheme = LocalTheme.current - val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val attachments = screenState.materials val imageLoader = LocalContext.current.imageLoader @@ -106,7 +126,7 @@ fun ChatMaterialsScreen( } LaunchedEffect(checkedTypeIndex) { - viewModel.onTypeChanged( + onTypeChanged( when (checkedTypeIndex) { 0 -> "photo" 1 -> "video" @@ -213,7 +233,7 @@ fun ChatMaterialsScreen( ) { DropdownMenuItem( onClick = { - viewModel.onRefresh() + onRefreshDropdownItemClicked() dropDownMenuExpanded = false }, text = { @@ -342,7 +362,7 @@ fun ChatMaterialsScreen( if (pullToRefreshState.isRefreshing) { LaunchedEffect(true) { - viewModel.onRefresh() + onRefresh() } } diff --git a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/ConversationsViewModel.kt b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/ConversationsViewModel.kt index 175d9615..f1141909 100644 --- a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/ConversationsViewModel.kt +++ b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/ConversationsViewModel.kt @@ -44,7 +44,7 @@ interface ConversationsViewModel { val currentOffset: StateFlow val canPaginate: StateFlow - fun onMetPaginationCondition() + fun onPaginationConditionsMet() fun onDeleteDialogDismissed() @@ -52,7 +52,7 @@ interface ConversationsViewModel { fun onRefresh() - fun onConversationItemClick(conversationId: Int) + fun onConversationItemClick() fun onConversationItemLongClick(conversation: UiConversation) fun onPinDialogDismissed() @@ -76,7 +76,7 @@ class ConversationsViewModelImpl( override val currentOffset = MutableStateFlow(0) override val canPaginate = MutableStateFlow(false) - override fun onMetPaginationCondition() { + override fun onPaginationConditionsMet() { currentOffset.update { screenState.value.conversations.size } loadConversations() } @@ -113,7 +113,7 @@ class ConversationsViewModelImpl( loadConversations(offset = 0) } - override fun onConversationItemClick(conversationId: Int) { + override fun onConversationItemClick() { screenState.setValue { old -> old.copy( conversations = old.conversations.map { item -> @@ -225,25 +225,14 @@ class ConversationsViewModelImpl( conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset).listenValue { state -> state.processState( error = { error -> - when (error) { - is State.Error.ApiError -> { - val (code, message) = error - - when (code) { - VkErrorCodes.UserAuthorizationFailed -> { - baseError.setValue { BaseError.SessionExpired } - } - - else -> { - Unit - } + if (error is State.Error.ApiError) { + when (error.errorCode) { + VkErrorCodes.UserAuthorizationFailed -> { + baseError.setValue { BaseError.SessionExpired } } - } - State.Error.ConnectionError -> TODO() - State.Error.InternalError -> TODO() - is State.Error.OAuthError -> TODO() - State.Error.Unknown -> TODO() + else -> Unit + } } }, success = { response -> diff --git a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/Dots.kt b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/Dots.kt index 130e426f..195ad5bc 100644 --- a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/Dots.kt +++ b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/Dots.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.meloda.app.fast.designsystem.AppTheme const val numberOfDots = 3 val dotSize = 6.dp @@ -298,7 +297,7 @@ fun DotsCollision() { @Preview(showBackground = true) @Composable -fun DotsPreview() = AppTheme { +fun DotsPreview() { Column( modifier = Modifier .padding(4.dp) diff --git a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/navigation/ConversationRoute.kt b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/navigation/ConversationsNavigation.kt similarity index 75% rename from feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/navigation/ConversationRoute.kt rename to feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/navigation/ConversationsNavigation.kt index 85668070..1866249b 100644 --- a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/navigation/ConversationRoute.kt +++ b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/navigation/ConversationsNavigation.kt @@ -6,27 +6,25 @@ import androidx.navigation.compose.composable import com.meloda.app.fast.common.extensions.navigation.sharedViewModel import com.meloda.app.fast.conversations.ConversationsViewModel import com.meloda.app.fast.conversations.ConversationsViewModelImpl -import com.meloda.app.fast.conversations.presentation.ConversationsScreen +import com.meloda.app.fast.conversations.presentation.ConversationsRoute import com.meloda.app.fast.model.BaseError import kotlinx.serialization.Serializable @Serializable object Conversations -fun NavGraphBuilder.conversationsRoute( +fun NavGraphBuilder.conversationsScreen( onError: (BaseError) -> Unit, - onNavigateToMessagesHistory: (id: Int) -> Unit, - onListScrollingUp: (Boolean) -> Unit, + onConversationItemClicked: (id: Int) -> Unit, navController: NavController, ) { composable { val viewModel: ConversationsViewModel = it.sharedViewModel(navController = navController) - ConversationsScreen( + ConversationsRoute( onError = onError, - onNavigateToMessagesHistory = onNavigateToMessagesHistory, - onListScrollingUp = onListScrollingUp, + onConversationItemClicked = onConversationItemClicked, viewModel = viewModel ) } diff --git a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt index 85f2fd3e..af1bce3b 100644 --- a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt @@ -72,6 +72,7 @@ import coil.request.ImageRequest import com.meloda.app.fast.common.UiText import com.meloda.app.fast.conversations.ConversationsViewModel import com.meloda.app.fast.conversations.ConversationsViewModelImpl +import com.meloda.app.fast.conversations.model.ConversationOption import com.meloda.app.fast.conversations.model.ConversationsScreenState import com.meloda.app.fast.conversations.model.UiConversation import com.meloda.app.fast.designsystem.LocalBottomPadding @@ -89,34 +90,70 @@ import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import com.meloda.app.fast.designsystem.R as UiR +@Composable +fun ConversationsRoute( + onError: (BaseError) -> Unit, + onConversationItemClicked: (conversationId: Int) -> Unit, + viewModel: ConversationsViewModel = koinViewModel() +) { + val context = LocalContext.current + + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() + val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + + val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle() + LaunchedEffect(imagesToPreload) { + imagesToPreload.forEach { url -> + context.imageLoader.enqueue( + ImageRequest.Builder(context) + .data(url) + .build() + ) + } + } + + ConversationsScreen( + screenState = screenState, + baseError = baseError, + canPaginate = canPaginate, + onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, + onConversationItemClicked = { id -> + onConversationItemClicked(id) + viewModel.onConversationItemClick() + }, + onConversationItemLongClicked = viewModel::onConversationItemLongClick, + onOptionClicked = viewModel::onOptionClicked, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet, + onRefreshDropdownItemClicked = viewModel::onRefresh, + onRefresh = viewModel::onRefresh + ) + + + HandleDialogs( + screenState = screenState, + viewModel = viewModel + ) +} + @OptIn( ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, ) @Composable fun ConversationsScreen( - onError: (BaseError) -> Unit, - onNavigateToMessagesHistory: (conversationId: Int) -> Unit, - onListScrollingUp: (Boolean) -> Unit, - viewModel: ConversationsViewModel = koinViewModel() + screenState: ConversationsScreenState = ConversationsScreenState.EMPTY, + baseError: BaseError? = null, + canPaginate: Boolean = false, + onSessionExpiredLogOutButtonClicked: () -> Unit, + onConversationItemClicked: (conversationId: Int) -> Unit = {}, + onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {}, + onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> }, + onPaginationConditionsMet: () -> Unit = {}, + onRefreshDropdownItemClicked: () -> Unit = {}, + onRefresh: () -> Unit = {} ) { - val baseError by viewModel.baseError.collectAsStateWithLifecycle() - - val context = LocalContext.current - - val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle() - imagesToPreload.forEach { url -> - context.imageLoader.enqueue( - ImageRequest.Builder(context) - .data(url) - .build() - ) - } - val view = LocalView.current - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() - val currentTheme = LocalTheme.current val maxLines by remember { @@ -129,10 +166,6 @@ fun ConversationsScreen( val isListScrollingUp = listState.isScrollingUp() - LaunchedEffect(isListScrollingUp) { - onListScrollingUp(isListScrollingUp) - } - val paginationConditionMet by remember { derivedStateOf { canPaginate && @@ -143,7 +176,7 @@ fun ConversationsScreen( LaunchedEffect(paginationConditionMet) { if (paginationConditionMet && !screenState.isPaginating) { - viewModel.onMetPaginationCondition() + onPaginationConditionsMet() } } @@ -213,7 +246,7 @@ fun ConversationsScreen( ) { DropdownMenuItem( onClick = { - viewModel.onRefresh() + onRefreshDropdownItemClicked() dropDownMenuExpanded = false }, text = { @@ -301,7 +334,7 @@ fun ConversationsScreen( ErrorView( text = "Session expired", buttonText = "Log out", - onButtonClick = { onError(BaseError.SessionExpired) } + onButtonClick = onSessionExpiredLogOutButtonClicked ) } @@ -320,10 +353,10 @@ fun ConversationsScreen( ) { ConversationsListComposable( onConversationsClick = { id -> - onNavigateToMessagesHistory(id) - viewModel.onConversationItemClick(id) + onConversationItemClicked(id) + }, - onConversationsLongClick = viewModel::onConversationItemLongClick, + onConversationsLongClick = onConversationItemLongClicked, screenState = screenState, state = listState, maxLines = maxLines, @@ -335,13 +368,13 @@ fun ConversationsScreen( } else { Modifier }.fillMaxSize(), - onOptionClicked = viewModel::onOptionClicked, + onOptionClicked = onOptionClicked, padding = padding ) if (pullToRefreshState.isRefreshing) { LaunchedEffect(true) { - viewModel.onRefresh() + onRefresh() } } @@ -362,11 +395,6 @@ fun ConversationsScreen( } } } - - HandleDialogs( - screenState = screenState, - viewModel = viewModel - ) } } diff --git a/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/FriendsViewModel.kt b/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/FriendsViewModel.kt index 6f7fedf4..b42f17b3 100644 --- a/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/FriendsViewModel.kt +++ b/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/FriendsViewModel.kt @@ -1,7 +1,6 @@ package com.meloda.app.fast.friends import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.meloda.app.fast.common.extensions.listenValue import com.meloda.app.fast.common.extensions.setValue import com.meloda.app.fast.data.State @@ -9,30 +8,24 @@ import com.meloda.app.fast.data.api.friends.FriendsUseCase import com.meloda.app.fast.data.processState import com.meloda.app.fast.datastore.UserSettings import com.meloda.app.fast.friends.model.FriendsScreenState -import com.meloda.app.fast.friends.model.UiFriend import com.meloda.app.fast.friends.util.asPresentation import com.meloda.app.fast.model.BaseError import com.meloda.app.fast.model.api.domain.VkUser import com.meloda.app.fast.network.VkErrorCodes import kotlinx.coroutines.flow.MutableStateFlow -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 // TODO: 13/07/2024, Danil Nikolaev: separate two lists and their pagination interface FriendsViewModel { val screenState: StateFlow - val uiFriends: StateFlow> - val uiOnlineFriends: StateFlow> val baseError: StateFlow val imagesToPreload: StateFlow> val currentOffset: StateFlow val canPaginate: StateFlow - fun onMetPaginationCondition() + fun onPaginationConditionsMet() fun onRefresh() @@ -46,11 +39,6 @@ class FriendsViewModelImpl( override val screenState = MutableStateFlow(FriendsScreenState.EMPTY) - override val uiFriends = screenState.map { it.friends } - .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) - - override val uiOnlineFriends = MutableStateFlow>(emptyList()) - override val baseError = MutableStateFlow(null) override val imagesToPreload = MutableStateFlow>(emptyList()) override val currentOffset = MutableStateFlow(0) @@ -64,7 +52,7 @@ class FriendsViewModelImpl( loadFriends() } - override fun onMetPaginationCondition() { + override fun onPaginationConditionsMet() { currentOffset.update { screenState.value.friends.size } loadFriends() } @@ -130,19 +118,19 @@ class FriendsViewModelImpl( if (offset == 0) { friends.emit(response) screenState.setValue { - newState.copy(friends = loadedFriends) + newState.copy( + friends = loadedFriends, + onlineFriends = loadedOnlineFriends + ) } - uiOnlineFriends.setValue { loadedOnlineFriends } } else { friends.emit(friends.value.plus(response)) screenState.setValue { newState.copy( - friends = newState.friends.plus(loadedFriends) + friends = newState.friends.plus(loadedFriends), + onlineFriends = newState.onlineFriends.plus(loadedOnlineFriends) ) } - uiOnlineFriends.setValue { old -> - old.plus(loadedFriends) - } } } ) @@ -164,17 +152,19 @@ class FriendsViewModelImpl( conversation.asPresentation(useContactNames) } - val onlineUiFriends = uiOnlineFriends.value.mapNotNull { friend -> + val onlineUiFriends = screenState.value.onlineFriends.mapNotNull { friend -> uiFriends.find { it.userId == friend.userId } } screenState.setValue { old -> - old.copy(friends = uiFriends) + old.copy( + friends = uiFriends, + onlineFriends = onlineUiFriends + ) } - uiOnlineFriends.setValue { onlineUiFriends } } companion object { - const val LOAD_COUNT = 30 + const val LOAD_COUNT = 60 } } diff --git a/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/model/FriendsScreenState.kt b/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/model/FriendsScreenState.kt index b3f1c01d..edc8a82e 100644 --- a/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/model/FriendsScreenState.kt +++ b/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/model/FriendsScreenState.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.Immutable data class FriendsScreenState( val isLoading: Boolean, val friends: List, + val onlineFriends: List, val isPaginating: Boolean, val isPaginationExhausted: Boolean ) { @@ -14,6 +15,7 @@ data class FriendsScreenState( val EMPTY: FriendsScreenState = FriendsScreenState( isLoading = true, friends = emptyList(), + onlineFriends = emptyList(), isPaginating = false, isPaginationExhausted = false ) diff --git a/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/navigation/FriendsRoute.kt b/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/navigation/FriendsNavigation.kt similarity index 86% rename from feature/friends/src/main/kotlin/com/meloda/app/fast/friends/navigation/FriendsRoute.kt rename to feature/friends/src/main/kotlin/com/meloda/app/fast/friends/navigation/FriendsNavigation.kt index 23530b7f..6e240e4d 100644 --- a/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/navigation/FriendsRoute.kt +++ b/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/navigation/FriendsNavigation.kt @@ -6,14 +6,14 @@ import androidx.navigation.compose.composable import com.meloda.app.fast.common.extensions.navigation.sharedViewModel import com.meloda.app.fast.friends.FriendsViewModel import com.meloda.app.fast.friends.FriendsViewModelImpl -import com.meloda.app.fast.friends.presentation.FriendsScreen +import com.meloda.app.fast.friends.presentation.FriendsRoute import com.meloda.app.fast.model.BaseError import kotlinx.serialization.Serializable @Serializable object Friends -fun NavGraphBuilder.friendsRoute( +fun NavGraphBuilder.friendsScreen( onError: (BaseError) -> Unit, navController: NavController ) { @@ -21,7 +21,7 @@ fun NavGraphBuilder.friendsRoute( val viewModel: FriendsViewModel = it.sharedViewModel(navController = navController) - FriendsScreen( + FriendsRoute( onError = onError, viewModel = viewModel ) diff --git a/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/presentation/FriendsScreen.kt b/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/presentation/FriendsScreen.kt index 69d79e3c..6c5aa8b2 100644 --- a/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/presentation/FriendsScreen.kt +++ b/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/presentation/FriendsScreen.kt @@ -58,6 +58,7 @@ import com.meloda.app.fast.designsystem.components.FullScreenLoader import com.meloda.app.fast.designsystem.components.NoItemsView import com.meloda.app.fast.friends.FriendsViewModel import com.meloda.app.fast.friends.FriendsViewModelImpl +import com.meloda.app.fast.friends.model.FriendsScreenState import com.meloda.app.fast.model.BaseError import com.meloda.app.fast.ui.ErrorView import dev.chrisbanes.haze.haze @@ -67,30 +68,53 @@ import dev.chrisbanes.haze.materials.HazeMaterials import org.koin.androidx.compose.koinViewModel import com.meloda.app.fast.designsystem.R as UiR -@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @Composable -fun FriendsScreen( +fun FriendsRoute( onError: (BaseError) -> Unit, viewModel: FriendsViewModel = koinViewModel() ) { - val baseError by viewModel.baseError.collectAsStateWithLifecycle() - val context = LocalContext.current - val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle() - imagesToPreload.forEach { url -> - context.imageLoader.enqueue( - ImageRequest.Builder(context) - .data(url) - .build() - ) - } - + val baseError by viewModel.baseError.collectAsStateWithLifecycle() val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val friends by viewModel.uiFriends.collectAsStateWithLifecycle() - val onlineFriends by viewModel.uiOnlineFriends.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle() + LaunchedEffect(imagesToPreload) { + imagesToPreload.forEach { url -> + context.imageLoader.enqueue( + ImageRequest.Builder(context) + .data(url) + .build() + ) + } + } + + FriendsScreen( + screenState = screenState, + baseError = baseError, + canPaginate = canPaginate, + onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet, + onRefresh = viewModel::onRefresh + ) +} + + +// TODO: 13/07/2024, Danil Nikolaev: support for online +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalHazeMaterialsApi::class +) +@Composable +fun FriendsScreen( + screenState: FriendsScreenState = FriendsScreenState.EMPTY, + baseError: BaseError? = null, + canPaginate: Boolean = false, + onSessionExpiredLogOutButtonClicked: () -> Unit = {}, + onPaginationConditionsMet: () -> Unit = {}, + onRefresh: () -> Unit = {} +) { val currentTheme = LocalTheme.current val maxLines by remember { @@ -111,7 +135,7 @@ fun FriendsScreen( LaunchedEffect(paginationConditionMet) { if (paginationConditionMet && !screenState.isPaginating) { - viewModel.onMetPaginationCondition() + onPaginationConditionsMet() } } @@ -223,11 +247,11 @@ fun FriendsScreen( ErrorView( text = "Session expired", buttonText = "Log out", - onButtonClick = { onError(BaseError.SessionExpired) } + onButtonClick = onSessionExpiredLogOutButtonClicked ) } - screenState.isLoading && friends.isEmpty() -> FullScreenLoader() + screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader() else -> { val pullToRefreshState = rememberPullToRefreshState() @@ -259,8 +283,7 @@ fun FriendsScreen( .padding(bottom = padding.calculateBottomPadding()) .nestedScroll(pullToRefreshState.nestedScrollConnection) ) { - val friendsToDisplay = if (index == 0) friends - else onlineFriends + val friendsToDisplay = screenState.friends FriendsList( modifier = if (currentTheme.usingBlur) { @@ -289,7 +312,7 @@ fun FriendsScreen( if (pullToRefreshState.isRefreshing) { LaunchedEffect(true) { - viewModel.onRefresh() + onRefresh() } } diff --git a/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/LanguagePickerViewModel.kt b/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/LanguagePickerViewModel.kt index a5f01bac..cae6a7f7 100644 --- a/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/LanguagePickerViewModel.kt +++ b/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/LanguagePickerViewModel.kt @@ -1,9 +1,13 @@ package com.meloda.app.fast.languagepicker +import android.content.res.Resources import androidx.appcompat.app.AppCompatDelegate import androidx.core.os.LocaleListCompat import androidx.lifecycle.ViewModel +import com.meloda.app.fast.common.UiText import com.meloda.app.fast.common.extensions.setValue +import com.meloda.app.fast.common.parseString +import com.meloda.app.fast.designsystem.R import com.meloda.app.fast.languagepicker.model.LanguagePickerScreenState import com.meloda.app.fast.languagepicker.model.SelectableLanguage import kotlinx.coroutines.flow.MutableStateFlow @@ -12,25 +16,54 @@ import kotlinx.coroutines.flow.StateFlow interface LanguagePickerViewModel { val screenState: StateFlow - fun setLanguages(languages: List) - fun onLanguagePicked(newLanguage: SelectableLanguage) - fun onApplyButtonClicked() - fun updateCurrentLocale(locale: String) } -class LanguagePickerViewModelImpl : LanguagePickerViewModel, ViewModel() { +class LanguagePickerViewModelImpl( + private val resources: Resources +) : LanguagePickerViewModel, ViewModel() { - override val screenState = MutableStateFlow( - LanguagePickerScreenState( - languages = emptyList(), - currentLanguage = AppCompatDelegate.getApplicationLocales().toLanguageTags() - ) - ) + override val screenState = MutableStateFlow(LanguagePickerScreenState.EMPTY) + + init { + val languages = listOf( + Triple( + "", + UiText.Resource(R.string.language_key_system), + UiText.Resource(R.string.language_system) + ), + Triple( + "en-US", + UiText.Resource(R.string.language_key_english), + UiText.Resource(R.string.language_english), + ), + Triple( + "ru-RU", + UiText.Resource(R.string.language_key_russian), + UiText.Resource(R.string.language_russian) + ), + Triple( + "uk-UA", + UiText.Resource(R.string.language_key_ukrainian), + UiText.Resource(R.string.language_ukrainian) + ) + ).map { (key, language, local) -> + Triple( + key, + language.parseString(resources).orEmpty(), + local.parseString(resources).orEmpty() + ) + }.map { (key, language, local) -> + SelectableLanguage( + local = local, + language = language, + key = key, + isSelected = key == AppCompatDelegate.getApplicationLocales().toLanguageTags() + ) + } - override fun setLanguages(languages: List) { screenState.setValue { old -> old.copy(languages = languages) } } diff --git a/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/model/LanguagePickerScreenState.kt b/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/model/LanguagePickerScreenState.kt index 0521793f..f6110221 100644 --- a/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/model/LanguagePickerScreenState.kt +++ b/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/model/LanguagePickerScreenState.kt @@ -6,4 +6,12 @@ import androidx.compose.runtime.Immutable data class LanguagePickerScreenState( val languages: List, val currentLanguage: String?, -) +) { + + companion object { + val EMPTY: LanguagePickerScreenState = LanguagePickerScreenState( + languages = emptyList(), + currentLanguage = null + ) + } +} diff --git a/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/navigation/LanguagePickerNavigation.kt b/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/navigation/LanguagePickerNavigation.kt new file mode 100644 index 00000000..65260d5b --- /dev/null +++ b/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/navigation/LanguagePickerNavigation.kt @@ -0,0 +1,22 @@ +package com.meloda.app.fast.languagepicker.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.meloda.app.fast.languagepicker.presentation.LanguagePickerRoute +import kotlinx.serialization.Serializable + +@Serializable +object LanguagePicker + +fun NavGraphBuilder.languagePickerScreen( + onBack: () -> Unit, +) { + composable { + LanguagePickerRoute(onBack = onBack) + } +} + +fun NavController.navigateToLanguagePicker() { + this.navigate(LanguagePicker) +} diff --git a/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/navigation/LanguagePickerRoute.kt b/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/navigation/LanguagePickerRoute.kt deleted file mode 100644 index 40b102f6..00000000 --- a/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/navigation/LanguagePickerRoute.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.meloda.app.fast.languagepicker.navigation - -import android.content.res.Resources -import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.ui.platform.LocalContext -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.meloda.app.fast.common.UiText -import com.meloda.app.fast.common.parseString -import com.meloda.app.fast.designsystem.R -import com.meloda.app.fast.languagepicker.LanguagePickerViewModel -import com.meloda.app.fast.languagepicker.LanguagePickerViewModelImpl -import com.meloda.app.fast.languagepicker.model.SelectableLanguage -import com.meloda.app.fast.languagepicker.presentation.LanguagePickerScreen -import kotlinx.serialization.Serializable -import org.koin.androidx.compose.koinViewModel - -@Serializable -object LanguagePicker - -private fun getLanguages(resources: Resources): List { - return listOf( - Triple( - "", - UiText.Resource(R.string.language_key_system), - UiText.Resource(R.string.language_system) - ), - Triple( - "en-US", - UiText.Resource(R.string.language_key_english), - UiText.Resource(R.string.language_english), - ), - Triple( - "ru-RU", - UiText.Resource(R.string.language_key_russian), - UiText.Resource(R.string.language_russian) - ), - Triple( - "uk-UA", - UiText.Resource(R.string.language_key_ukrainian), - UiText.Resource(R.string.language_ukrainian) - ) - ).map { (key, language, local) -> - Triple( - key, - language.parseString(resources).orEmpty(), - local.parseString(resources).orEmpty() - ) - }.map { (key, language, local) -> - SelectableLanguage( - local = local, - language = language, - key = key, - isSelected = key == AppCompatDelegate.getApplicationLocales().toLanguageTags() - ) - } -} - -fun NavGraphBuilder.languagePickerRoute( - onBack: () -> Unit, -) { - composable { - val languages = getLanguages(LocalContext.current.resources) - - val viewModel: LanguagePickerViewModel = koinViewModel() - viewModel.setLanguages(languages) - - LanguagePickerScreen( - onBack = onBack, - viewModel = viewModel - ) - } -} - -fun NavController.navigateToLanguagePicker() { - this.navigate(LanguagePicker) -} diff --git a/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/presentation/LanguagePickerScreen.kt b/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/presentation/LanguagePickerScreen.kt index f86a3ecf..a9899c2d 100644 --- a/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/presentation/LanguagePickerScreen.kt +++ b/feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/presentation/LanguagePickerScreen.kt @@ -56,31 +56,46 @@ import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.meloda.app.fast.languagepicker.LanguagePickerViewModel import com.meloda.app.fast.languagepicker.LanguagePickerViewModelImpl +import com.meloda.app.fast.languagepicker.model.LanguagePickerScreenState import com.meloda.app.fast.languagepicker.model.SelectableLanguage import org.koin.androidx.compose.koinViewModel import com.meloda.app.fast.designsystem.R as UiR -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun LanguagePickerScreen( +fun LanguagePickerRoute( onBack: () -> Unit, viewModel: LanguagePickerViewModel = koinViewModel() ) { - val context = LocalContext.current - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val languages = screenState.languages - LifecycleResumeEffect(true) { viewModel.updateCurrentLocale(AppCompatDelegate.getApplicationLocales().toLanguageTags()) - onPauseOrDispose {} } + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + + LanguagePickerScreen( + screenState = screenState, + onBack = onBack, + onLanguagePicked = viewModel::onLanguagePicked, + onApplyButtonClicked = viewModel::onApplyButtonClicked + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LanguagePickerScreen( + screenState: LanguagePickerScreenState = LanguagePickerScreenState.EMPTY, + onBack: () -> Unit = {}, + onLanguagePicked: (SelectableLanguage) -> Unit = {}, + onApplyButtonClicked: () -> Unit = {} +) { + val context = LocalContext.current + val isButtonEnabled by remember(screenState) { derivedStateOf { screenState.currentLanguage != null && - languages.isNotEmpty() && - languages.find(SelectableLanguage::isSelected)?.key != screenState.currentLanguage + screenState.languages.isNotEmpty() && + screenState.languages.find(SelectableLanguage::isSelected)?.key != screenState.currentLanguage } } @@ -165,10 +180,13 @@ fun LanguagePickerScreen( LazyColumn( modifier = Modifier.fillMaxSize() ) { - items(screenState.languages.toList()) { item -> + items( + items = screenState.languages.toList(), + key = SelectableLanguage::key + ) { item -> LanguageItem( item = item, - onClick = viewModel::onLanguagePicked + onClick = onLanguagePicked ) } @@ -183,7 +201,7 @@ fun LanguagePickerScreen( } Button( - onClick = viewModel::onApplyButtonClicked, + onClick = onApplyButtonClicked, enabled = isButtonEnabled, modifier = Modifier .fillMaxWidth() diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/MessagesHistoryViewModel.kt index 8481cbed..91b15199 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/MessagesHistoryViewModel.kt @@ -4,6 +4,7 @@ import android.content.SharedPreferences import android.content.res.Resources import android.util.Log import androidx.core.content.edit +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.conena.nanokt.collections.indexOfFirstOrNull @@ -20,11 +21,10 @@ import com.meloda.app.fast.data.api.messages.MessagesUseCase import com.meloda.app.fast.data.processState import com.meloda.app.fast.datastore.SettingsKeys import com.meloda.app.fast.messageshistory.model.ActionMode -import com.meloda.app.fast.messageshistory.model.MessagesHistoryArguments import com.meloda.app.fast.messageshistory.model.MessagesHistoryScreenState +import com.meloda.app.fast.messageshistory.navigation.MessagesHistory import com.meloda.app.fast.messageshistory.util.asPresentation import com.meloda.app.fast.messageshistory.util.extractAvatar -import com.meloda.app.fast.messageshistory.util.extractShowName import com.meloda.app.fast.messageshistory.util.extractTitle import com.meloda.app.fast.model.BaseError import com.meloda.app.fast.model.LongPollEvent @@ -48,17 +48,14 @@ interface MessagesHistoryViewModel { val canPaginate: StateFlow + fun onRefresh() fun onAttachmentButtonClicked() - fun onInputChanged(newText: String) + fun onMessageInputChanged(newText: String) fun onEmojiButtonClicked() fun onActionButtonClicked() - fun onTopAppBarMenuClicked(id: Int) - fun setArguments(arguments: MessagesHistoryArguments) - fun onMetPaginationCondition() - fun onShowDatesClicked(showDates: Boolean) - fun onShowNamesClicked(showNames: Boolean) - fun onEnableAnimationsClicked(enableAnimations: Boolean) + fun onPaginationConditionsMet() + fun onToggleAnimationsDropdownItemClicked(enableAnimations: Boolean) } class MessagesHistoryViewModelImpl( @@ -67,6 +64,7 @@ class MessagesHistoryViewModelImpl( private val preferences: SharedPreferences, private val resources: Resources, updatesParser: LongPollUpdatesParser, + savedStateHandle: SavedStateHandle ) : MessagesHistoryViewModel, ViewModel() { override val screenState = MutableStateFlow(MessagesHistoryScreenState.EMPTY) @@ -85,17 +83,26 @@ class MessagesHistoryViewModelImpl( private val sendingMessages: MutableList = mutableListOf() init { + val arguments = MessagesHistory.from(savedStateHandle).arguments + + screenState.setValue { old -> old.copy(conversationId = arguments.conversationId) } + loadMessagesHistory() + updatesParser.onNewMessage(::handleNewMessage) updatesParser.onMessageEdited(::handleEditedMessage) updatesParser.onMessageIncomingRead(::handleReadIncomingEvent) updatesParser.onMessageOutgoingRead(::handleReadOutgoingEvent) } + override fun onRefresh() { + loadMessagesHistory(offset = 0) + } + override fun onAttachmentButtonClicked() { } - override fun onInputChanged(newText: String) { + override fun onMessageInputChanged(newText: String) { screenState.setValue { old -> old.copy( message = newText, @@ -131,58 +138,12 @@ class MessagesHistoryViewModelImpl( } } - override fun onTopAppBarMenuClicked(id: Int) { - when (id) { - 0 -> loadMessagesHistory(0) - else -> Unit - } - } - - override fun setArguments(arguments: MessagesHistoryArguments) { - if (arguments.conversationId == screenState.value.conversationId) return - - screenState.setValue { old -> old.copy(conversationId = arguments.conversationId) } - loadMessagesHistory() - } - - override fun onMetPaginationCondition() { + override fun onPaginationConditionsMet() { currentOffset.update { screenState.value.messages.size } loadMessagesHistory() } - override fun onShowDatesClicked(showDates: Boolean) { - preferences.edit { putBoolean(SettingsKeys.KEY_SHOW_DATE_UNDER_BUBBLES, showDates) } - - screenState.setValue { old -> - old.copy( - messages = old.messages.map { message -> - message.copy(showDate = showDates) - } - ) - } - } - - override fun onShowNamesClicked(showNames: Boolean) { - preferences.edit { putBoolean(SettingsKeys.KEY_SHOW_NAME_IN_BUBBLES, showNames) } - - screenState.setValue { old -> - old.copy( - messages = old.messages.map { message -> - message.copy( - showName = if (showNames) { - val index = messages.value.indexOfFirst { it.id == message.id } - val domainMessage = messages.value[index] - val prevMessage = messages.value.getOrNull(index + 1) - - domainMessage.extractShowName(prevMessage) - } else false - ) - } - ) - } - } - - override fun onEnableAnimationsClicked(enableAnimations: Boolean) { + override fun onToggleAnimationsDropdownItemClicked(enableAnimations: Boolean) { preferences.edit { putBoolean( SettingsKeys.KEY_ENABLE_ANIMATIONS_IN_MESSAGES, @@ -285,15 +246,10 @@ class MessagesHistoryViewModelImpl( messagesUseCase.storeMessages(messages) conversationsUseCase.storeConversations(conversations) - val showDate = - preferences.getBoolean(SettingsKeys.KEY_SHOW_DATE_UNDER_BUBBLES, false) - val showName = - preferences.getBoolean(SettingsKeys.KEY_SHOW_NAME_IN_BUBBLES, false) - val loadedMessages = fullMessages.mapIndexed { index, message -> message.asPresentation( - showDate = showDate, - showName = showName, + showDate = false, + showName = false, prevMessage = messages.getOrNull(index + 1), nextMessage = messages.getOrNull(index - 1), ) diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryNavigation.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryNavigation.kt new file mode 100644 index 00000000..121926ab --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryNavigation.kt @@ -0,0 +1,43 @@ +package com.meloda.app.fast.messageshistory.navigation + +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.meloda.app.fast.common.customNavType +import com.meloda.app.fast.messageshistory.model.MessagesHistoryArguments +import com.meloda.app.fast.messageshistory.presentation.MessagesHistoryRoute +import com.meloda.app.fast.model.BaseError +import kotlinx.serialization.Serializable +import kotlin.reflect.typeOf + +@Serializable +data class MessagesHistory(val arguments: MessagesHistoryArguments) { + + companion object { + val typeMap = + mapOf(typeOf() to customNavType()) + + fun from(savedStateHandle: SavedStateHandle) = + savedStateHandle.toRoute(typeMap) + } +} + +fun NavGraphBuilder.messagesHistoryScreen( + onError: (BaseError) -> Unit, + onBack: () -> Unit, + onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit +) { + composable(typeMap = MessagesHistory.typeMap) { + MessagesHistoryRoute( + onError = onError, + onBack = onBack, + onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked, + ) + } +} + +fun NavController.navigateToMessagesHistory(conversationId: Int) { + this.navigate(MessagesHistory(MessagesHistoryArguments(conversationId))) +} diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryRoute.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryRoute.kt deleted file mode 100644 index 4b516198..00000000 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryRoute.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.meloda.app.fast.messageshistory.navigation - -import android.os.Bundle -import androidx.core.os.BundleCompat -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.toRoute -import com.meloda.app.fast.messageshistory.MessagesHistoryViewModel -import com.meloda.app.fast.messageshistory.MessagesHistoryViewModelImpl -import com.meloda.app.fast.messageshistory.model.MessagesHistoryArguments -import com.meloda.app.fast.messageshistory.presentation.MessagesHistoryScreen -import com.meloda.app.fast.model.BaseError -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.koin.androidx.compose.koinViewModel -import kotlin.reflect.typeOf - -@Serializable -data class MessagesHistory(val arguments: MessagesHistoryArguments) - -val MessagesHistoryNavType = object : NavType(isNullableAllowed = false) { - override fun get(bundle: Bundle, key: String): MessagesHistoryArguments? = - BundleCompat.getParcelable(bundle, key, MessagesHistoryArguments::class.java) - - override fun parseValue(value: String): MessagesHistoryArguments = Json.decodeFromString(value) - - override fun serializeAsValue(value: MessagesHistoryArguments): String = - Json.encodeToString(value) - - override fun put(bundle: Bundle, key: String, value: MessagesHistoryArguments) { - bundle.putParcelable(key, value) - } - - override val name: String = "MessagesHistoryArguments" -} - -fun NavGraphBuilder.messagesHistoryRoute( - onError: (BaseError) -> Unit, - onBack: () -> Unit, - onNavigateToChatAttachments: (peerId: Int, conversationMessageId: Int) -> Unit -) { - composable( - typeMap = mapOf(typeOf() to MessagesHistoryNavType) - ) { backStackEntry -> - val arguments: MessagesHistory = backStackEntry.toRoute() - - val viewModel: MessagesHistoryViewModel = koinViewModel() - viewModel.setArguments(arguments.arguments) - - MessagesHistoryScreen( - onError = onError, - onBack = onBack, - onNavigateToChatMaterials = onNavigateToChatAttachments, - viewModel = viewModel - ) - } -} - -fun NavController.navigateToMessagesHistory(conversationId: Int) { - this.navigate(MessagesHistory(MessagesHistoryArguments(conversationId))) -} diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt index 60af7c23..6e28a7fe 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -71,6 +71,7 @@ import com.meloda.app.fast.designsystem.LocalTheme import com.meloda.app.fast.messageshistory.MessagesHistoryViewModel import com.meloda.app.fast.messageshistory.MessagesHistoryViewModelImpl import com.meloda.app.fast.messageshistory.model.ActionMode +import com.meloda.app.fast.messageshistory.model.MessagesHistoryScreenState import com.meloda.app.fast.model.BaseError import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeChild @@ -81,6 +82,32 @@ import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject import com.meloda.app.fast.designsystem.R as UiR +@Composable +fun MessagesHistoryRoute( + onError: (BaseError) -> Unit, + onBack: () -> Unit, + onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit, + viewModel: MessagesHistoryViewModel = koinViewModel() +) { + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() + val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + + MessagesHistoryScreen( + screenState = screenState, + baseError = baseError, + canPaginate = canPaginate, + onBack = onBack, + onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked, + onRefreshDropdownItemClicked = viewModel::onRefresh, + onToggleAnimationsDropdownItemClicked = viewModel::onToggleAnimationsDropdownItemClicked, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet, + onMessageInputChanged = viewModel::onMessageInputChanged, + onAttachmentButtonClicked = viewModel::onAttachmentButtonClicked, + onActionButtonClicked = viewModel::onActionButtonClicked + ) +} + @OptIn( ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, @@ -88,21 +115,23 @@ import com.meloda.app.fast.designsystem.R as UiR ) @Composable fun MessagesHistoryScreen( - onError: (BaseError) -> Unit, - onBack: () -> Unit, - onNavigateToChatMaterials: (peerId: Int, conversationMessageId: Int) -> Unit, - viewModel: MessagesHistoryViewModel = koinViewModel() + screenState: MessagesHistoryScreenState = MessagesHistoryScreenState.EMPTY, + baseError: BaseError? = null, + canPaginate: Boolean = false, + onBack: () -> Unit = {}, + onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit = { _, _ -> }, + onRefreshDropdownItemClicked: () -> Unit = {}, + onToggleAnimationsDropdownItemClicked: (Boolean) -> Unit = {}, + onPaginationConditionsMet: () -> Unit = {}, + onMessageInputChanged: (String) -> Unit = {}, + onAttachmentButtonClicked: () -> Unit = {}, + onActionButtonClicked: () -> Unit = {} ) { val view = LocalView.current val preferences: SharedPreferences = koinInject() val currentTheme = LocalTheme.current - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() - - val messages = screenState.messages - val listState = rememberLazyListState() val paginationConditionMet by remember { @@ -115,7 +144,7 @@ fun MessagesHistoryScreen( LaunchedEffect(paginationConditionMet) { if (paginationConditionMet && !screenState.isPaginating) { - viewModel.onMetPaginationCondition() + onPaginationConditionsMet() } } @@ -125,24 +154,6 @@ fun MessagesHistoryScreen( val hazeSate = remember { HazeState() } - var datesShown by remember { - mutableStateOf( - preferences.getBoolean( - SettingsKeys.KEY_SHOW_DATE_UNDER_BUBBLES, - false - ) - ) - } - - var namesShown by remember { - mutableStateOf( - preferences.getBoolean( - SettingsKeys.KEY_SHOW_NAME_IN_BUBBLES, - false - ) - ) - } - var animationsEnabled by remember { mutableStateOf( preferences.getBoolean( @@ -217,7 +228,8 @@ fun MessagesHistoryScreen( dropDownMenuExpanded = false // TODO: 11/07/2024, Danil Nikolaev: to VM - onNavigateToChatMaterials( + + onChatMaterialsDropdownItemClicked( screenState.conversationId, screenState.messages.first().conversationMessageId ) @@ -228,7 +240,7 @@ fun MessagesHistoryScreen( ) DropdownMenuItem( onClick = { - viewModel.onTopAppBarMenuClicked(0) + onRefreshDropdownItemClicked() dropDownMenuExpanded = false }, text = { @@ -248,27 +260,6 @@ fun MessagesHistoryScreen( ) ) { HorizontalDivider() - DropdownMenuItem( - text = { - Text(text = if (datesShown) "Hide dates" else "Show dates") - }, - onClick = { - dropDownMenuExpanded = false - datesShown = !datesShown - viewModel.onShowDatesClicked(datesShown) - } - ) - - DropdownMenuItem( - text = { - Text(text = if (namesShown) "Hide names" else "Show names") - }, - onClick = { - dropDownMenuExpanded = false - namesShown = !namesShown - viewModel.onShowNamesClicked(namesShown) - } - ) DropdownMenuItem( text = { @@ -277,14 +268,14 @@ fun MessagesHistoryScreen( onClick = { dropDownMenuExpanded = false animationsEnabled = !animationsEnabled - viewModel.onEnableAnimationsClicked(animationsEnabled) + onToggleAnimationsDropdownItemClicked(animationsEnabled) } ) } } } ) - if (screenState.isLoading && messages.isNotEmpty()) { + if (screenState.isLoading && screenState.messages.isNotEmpty()) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } } @@ -300,7 +291,7 @@ fun MessagesHistoryScreen( MessagesList( hazeState = hazeSate, listState = listState, - immutableMessages = ImmutableList.copyOf(messages), + immutableMessages = ImmutableList.copyOf(screenState.messages), isPaginating = screenState.isPaginating, enableAnimations = animationsEnabled ) @@ -372,7 +363,7 @@ fun MessagesHistoryScreen( TextField( modifier = Modifier.weight(1f), value = screenState.message, - onValueChange = viewModel::onInputChanged, + onValueChange = onMessageInputChanged, colors = TextFieldDefaults.colors( unfocusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent, @@ -382,7 +373,7 @@ fun MessagesHistoryScreen( placeholder = { Text(text = stringResource(id = UiR.string.message_input_hint)) } ) - IconButton(onClick = viewModel::onAttachmentButtonClicked) { + IconButton(onClick = onAttachmentButtonClicked) { Icon( painter = painterResource(id = UiR.drawable.round_attach_file_24), contentDescription = "Add attachment button", @@ -414,7 +405,7 @@ fun MessagesHistoryScreen( } } } else { - viewModel.onActionButtonClicked() + onActionButtonClicked() } }, modifier = Modifier.rotate(rotation.value) @@ -445,7 +436,7 @@ fun MessagesHistoryScreen( } } - if (screenState.isLoading && messages.isEmpty()) { + if (screenState.isLoading && screenState.messages.isEmpty()) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } } diff --git a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/ProfileViewModel.kt b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/ProfileViewModel.kt index fb0cf5bd..3ccf0e93 100644 --- a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/ProfileViewModel.kt +++ b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/ProfileViewModel.kt @@ -5,14 +5,18 @@ import com.meloda.app.fast.common.UserConfig import com.meloda.app.fast.common.VkConstants import com.meloda.app.fast.common.extensions.listenValue import com.meloda.app.fast.common.extensions.setValue +import com.meloda.app.fast.data.State import com.meloda.app.fast.data.api.users.UsersUseCase import com.meloda.app.fast.data.processState +import com.meloda.app.fast.model.BaseError +import com.meloda.app.fast.network.VkErrorCodes import com.meloda.app.fast.profile.model.ProfileScreenState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow interface ProfileViewModel { val screenState: StateFlow + val baseError: StateFlow } class ProfileViewModelImpl( @@ -20,6 +24,7 @@ class ProfileViewModelImpl( ) : ViewModel(), ProfileViewModel { override val screenState = MutableStateFlow(ProfileScreenState.EMPTY) + override val baseError = MutableStateFlow(null) init { getLocalAccountInfo() @@ -30,6 +35,16 @@ class ProfileViewModelImpl( .listenValue { state -> state.processState( error = { error -> + if (error is State.Error.ApiError) { + when (error.errorCode) { + VkErrorCodes.UserAuthorizationFailed -> { + baseError.setValue { BaseError.SessionExpired } + } + + else -> Unit + } + } + screenState.setValue { old -> old.copy( avatarUrl = null, diff --git a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/navigation/ProfileRoute.kt b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/navigation/ProfileRoute.kt index 703765cf..19bc584f 100644 --- a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/navigation/ProfileRoute.kt +++ b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/navigation/ProfileRoute.kt @@ -7,24 +7,24 @@ import com.meloda.app.fast.common.extensions.navigation.sharedViewModel import com.meloda.app.fast.model.BaseError import com.meloda.app.fast.profile.ProfileViewModel import com.meloda.app.fast.profile.ProfileViewModelImpl -import com.meloda.app.fast.profile.presentation.ProfileScreen +import com.meloda.app.fast.profile.presentation.ProfileRoute import kotlinx.serialization.Serializable @Serializable object Profile -fun NavGraphBuilder.profileRoute( +fun NavGraphBuilder.profileScreen( onError: (BaseError) -> Unit, - onNavigateToSettings: () -> Unit, + onSettingsButtonClicked: () -> Unit, navController: NavController ) { composable { val viewModel: ProfileViewModel = it.sharedViewModel(navController = navController) - ProfileScreen( + ProfileRoute( onError = onError, - onNavigateToSettings = onNavigateToSettings, + onSettingsButtonClicked = onSettingsButtonClicked, viewModel = viewModel ) } diff --git a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/presentation/ProfileScreen.kt b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/presentation/ProfileScreen.kt index 6c2d71b9..045bbe69 100644 --- a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/presentation/ProfileScreen.kt +++ b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/presentation/ProfileScreen.kt @@ -1,6 +1,5 @@ package com.meloda.app.fast.profile.presentation -import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -38,27 +37,42 @@ import coil.compose.AsyncImage import com.meloda.app.fast.model.BaseError import com.meloda.app.fast.profile.ProfileViewModel import com.meloda.app.fast.profile.ProfileViewModelImpl +import com.meloda.app.fast.profile.model.ProfileScreenState import org.koin.androidx.compose.koinViewModel import com.meloda.app.fast.designsystem.R as UiR -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun ProfileScreen( +fun ProfileRoute( onError: (BaseError) -> Unit, - onNavigateToSettings: () -> Unit, + onSettingsButtonClicked: () -> Unit, viewModel: ProfileViewModel = koinViewModel() ) { val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() - Log.d("ProfileScreen", "isLoading: ${screenState.isLoading}") + ProfileScreen( + screenState = screenState, + baseError = baseError, + onSettingsButtonClicked = onSettingsButtonClicked + ) +} + +// TODO: 13/07/2024, Danil Nikolaev: handle expired session +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProfileScreen( + screenState: ProfileScreenState = ProfileScreenState.EMPTY, + baseError: BaseError? = null, + onSettingsButtonClicked: () -> Unit = {}, +) { Scaffold( topBar = { TopAppBar( title = {}, actions = { - IconButton(onClick = onNavigateToSettings) { + IconButton(onClick = onSettingsButtonClicked) { Icon( imageVector = Icons.Rounded.Settings, contentDescription = null diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt index 652c96a3..d3f128c9 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt @@ -12,6 +12,7 @@ import com.meloda.app.fast.common.extensions.setValue import com.meloda.app.fast.data.db.AccountsRepository import com.meloda.app.fast.datastore.SettingsController import com.meloda.app.fast.datastore.SettingsKeys +import com.meloda.app.fast.datastore.UserSettings import com.meloda.app.fast.datastore.isDebugSettingsShown import com.meloda.app.fast.model.database.AccountEntity import com.meloda.app.fast.settings.model.SettingsItem @@ -44,13 +45,14 @@ interface SettingsViewModel { fun onSettingsItemLongClicked(key: String) fun onSettingsItemChanged(key: String, newValue: Any?) - fun onHapticsUsed() + fun onHapticPerformed() fun onNotificationsPermissionRequested() } class SettingsViewModelImpl( private val accountsRepository: AccountsRepository, + private val userSettings: UserSettings ) : SettingsViewModel, ViewModel() { override val screenState = MutableStateFlow(SettingsScreenState.EMPTY) @@ -159,6 +161,7 @@ class SettingsViewModelImpl( when (key) { SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> { val isEnabled = (newValue as? Boolean) == true + userSettings.setLongPollBackground(isEnabled) if (isEnabled) { // TODO: 26/11/2023, Danil Nikolaev: implement @@ -169,10 +172,41 @@ class SettingsViewModelImpl( } } } + + SettingsKeys.KEY_APPEARANCE_MULTILINE -> { + val isUsing = newValue as? Boolean ?: false + userSettings.useMultiline(isUsing) + } + + + SettingsKeys.KEY_APPEARANCE_AMOLED_THEME -> { + val isUsing = newValue as? Boolean ?: false + userSettings.useAmoledThemeChanged(isUsing) + } + + SettingsKeys.KEY_USE_DYNAMIC_COLORS -> { + val isUsing = newValue as? Boolean ?: false + userSettings.useDynamicColorsChanged(isUsing) + } + + SettingsKeys.KEY_APPEARANCE_BLUR -> { + val isUsing = newValue as? Boolean ?: false + userSettings.useBlurChanged(isUsing) + } + + SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> { + val isUsing = newValue as? Boolean ?: false + userSettings.setOnline(isUsing) + } + + SettingsKeys.KEY_USE_CONTACT_NAMES -> { + val isUsing = newValue as? Boolean ?: false + userSettings.onUseContactNamesChanged(isUsing) + } } } - override fun onHapticsUsed() { + override fun onHapticPerformed() { screenState.setValue { old -> old.copy(useHaptics = HapticType.None) } } diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/navigation/SettingsNavigation.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/navigation/SettingsNavigation.kt new file mode 100644 index 00000000..0cfd1fb5 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/navigation/SettingsNavigation.kt @@ -0,0 +1,32 @@ +package com.meloda.app.fast.settings.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.meloda.app.fast.datastore.SettingsKeys +import com.meloda.app.fast.model.BaseError +import com.meloda.app.fast.settings.model.OnSettingsClickListener +import com.meloda.app.fast.settings.presentation.SettingsRoute +import com.meloda.app.fast.settings.presentation.SettingsScreen +import kotlinx.serialization.Serializable + +@Serializable +object Settings + +fun NavGraphBuilder.settingsScreen( + onBack: () -> Unit, + onLogOutButtonClicked: () -> Unit, + onLanguageItemClicked: () -> Unit +) { + composable { + SettingsRoute( + onBack = onBack, + onLogOutButtonClicked = onLogOutButtonClicked, + onLanguageItemClicked = onLanguageItemClicked + ) + } +} + +fun NavController.navigateToSettings() { + this.navigate(Settings) +} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsRoute.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsRoute.kt deleted file mode 100644 index 5293cb4a..00000000 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsRoute.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.app.fast.settings.presentation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.meloda.app.fast.model.BaseError -import kotlinx.serialization.Serializable - -@Serializable -object Settings - -fun NavGraphBuilder.settingsRoute( - onError: (BaseError) -> Unit, - onBack: () -> Unit, - onNavigateToAuth: () -> Unit, - onNavigateToLanguagePicker: () -> Unit -) { - composable { - SettingsScreen( - onError = onError, - onBack = onBack, - onNavigateToAuth = onNavigateToAuth, - onNavigateToLanguagePicker = onNavigateToLanguagePicker - ) - } -} - -fun NavController.navigateToSettings() { - this.navigate(Settings) -} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsScreen.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsScreen.kt index 72c60a92..50c9b706 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -23,7 +24,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -41,13 +41,9 @@ import com.meloda.app.fast.datastore.UserSettings import com.meloda.app.fast.datastore.isUsingDarkMode import com.meloda.app.fast.designsystem.LocalTheme import com.meloda.app.fast.designsystem.MaterialDialog -import com.meloda.app.fast.model.BaseError import com.meloda.app.fast.settings.HapticType import com.meloda.app.fast.settings.SettingsViewModel import com.meloda.app.fast.settings.SettingsViewModelImpl -import com.meloda.app.fast.settings.model.OnSettingsChangeListener -import com.meloda.app.fast.settings.model.OnSettingsClickListener -import com.meloda.app.fast.settings.model.OnSettingsLongClickListener import com.meloda.app.fast.settings.model.SettingsItem import com.meloda.app.fast.settings.model.SettingsScreenState import com.meloda.app.fast.settings.presentation.items.EditTextSettingsItem @@ -64,102 +60,94 @@ import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject import com.meloda.app.fast.designsystem.R as UiR -@OptIn( - ExperimentalMaterial3Api::class, - ExperimentalHazeMaterialsApi::class -) @Composable -fun SettingsScreen( - onError: (BaseError) -> Unit, +fun SettingsRoute( onBack: () -> Unit, - onNavigateToAuth: () -> Unit, - onNavigateToLanguagePicker: () -> Unit, + onLogOutButtonClicked: () -> Unit, + onLanguageItemClicked: () -> Unit, viewModel: SettingsViewModel = koinViewModel() ) { val context = LocalContext.current - val view = LocalView.current val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val hapticType = screenState.useHaptics - if (hapticType != HapticType.None) { - view.performHapticFeedback(hapticType.getHaptic()) - viewModel.onHapticsUsed() - } - val userSettings: UserSettings = koinInject() LaunchedEffect(true) { userSettings.enableDebugSettings(screenState.showDebugOptions) } + SettingsScreen(screenState = screenState, + onBack = onBack, + onHapticPerformed = viewModel::onHapticPerformed, + onSettingsItemClicked = { key -> + when (key) { + SettingsKeys.KEY_APPEARANCE_LANGUAGE -> { + onLanguageItemClicked() + } + + else -> viewModel.onSettingsItemClicked(key) + } + }, + onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked, + onSettingsItemValueChanged = { key, newValue -> + when (key) { + SettingsKeys.KEY_APPEARANCE_DARK_THEME -> { + val newMode = newValue as? Int ?: 0 + AppCompatDelegate.setDefaultNightMode(newMode) + + val isUsing = context.getSystemService()?.let { manager -> + isUsingDarkMode( + context.resources, + manager + ) + } ?: false + + userSettings.useDarkThemeChanged(isUsing) + } + + else -> viewModel.onSettingsItemChanged(key, newValue) + } + } + ) + + HandlePopups( + performCrashPositiveClick = viewModel::onPerformCrashPositiveButtonClicked, + performCrashDismissed = viewModel::onPerformCrashAlertDismissed, + logoutPositiveClick = { + viewModel.onLogOutAlertPositiveClick() + onLogOutButtonClicked() + }, + logoutDismissed = viewModel::onLogOutAlertDismissed, + longPollingPositiveClick = viewModel::onLongPollingAlertPositiveClicked, + longPollingDismissed = viewModel::onLongPollingAlertDismissed, + screenState = screenState + ) +} + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalHazeMaterialsApi::class +) +@Composable +fun SettingsScreen( + screenState: SettingsScreenState = SettingsScreenState.EMPTY, + onBack: () -> Unit = {}, + onHapticPerformed: () -> Unit = {}, + onSettingsItemClicked: (key: String) -> Unit = {}, + onSettingsItemLongClicked: (key: String) -> Unit = {}, + onSettingsItemValueChanged: (key: String, newValue: Any?) -> Unit = { _, _ -> } +) { + val view = LocalView.current + val hapticType = screenState.useHaptics + + LaunchedEffect(hapticType) { + if (hapticType != HapticType.None) { + view.performHapticFeedback(hapticType.getHaptic()) + onHapticPerformed() + } + } + val currentTheme = LocalTheme.current - val settingsList = screenState.settings - - val clickListener = OnSettingsClickListener { key -> - when (key) { - SettingsKeys.KEY_APPEARANCE_LANGUAGE -> { - onNavigateToLanguagePicker() - } - - else -> viewModel.onSettingsItemClicked(key) - } - - } - val longClickListener = OnSettingsLongClickListener(viewModel::onSettingsItemLongClicked) - val changeListener = OnSettingsChangeListener { key, newValue -> - when (key) { - SettingsKeys.KEY_APPEARANCE_MULTILINE -> { - val isUsing = newValue as? Boolean ?: false - userSettings.useMultiline(isUsing) - } - - SettingsKeys.KEY_APPEARANCE_DARK_THEME -> { - val newMode = newValue as? Int ?: return@OnSettingsChangeListener - AppCompatDelegate.setDefaultNightMode(newMode) - - val isUsing = context.getSystemService()?.let { manager -> - isUsingDarkMode( - context.resources, - manager - ) - } ?: false - - userSettings.useDarkThemeChanged(isUsing) - } - - SettingsKeys.KEY_APPEARANCE_AMOLED_THEME -> { - val isUsing = newValue as? Boolean ?: false - userSettings.useAmoledThemeChanged(isUsing) - } - - SettingsKeys.KEY_USE_DYNAMIC_COLORS -> { - val isUsing = newValue as? Boolean ?: false - userSettings.useDynamicColorsChanged(isUsing) - } - - SettingsKeys.KEY_APPEARANCE_BLUR -> { - val isUsing = newValue as? Boolean ?: false - userSettings.useBlurChanged(isUsing) - } - - SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> { - val isUsing = newValue as? Boolean ?: false - userSettings.setLongPollBackground(isUsing) - } - - SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> { - val isUsing = newValue as? Boolean ?: false - userSettings.setOnline(isUsing) - } - - SettingsKeys.KEY_USE_CONTACT_NAMES -> { - val isUsing = newValue as? Boolean ?: false - userSettings.onUseContactNamesChanged(isUsing) - } - - else -> viewModel.onSettingsItemChanged(key, newValue) - } - } val hazeState = remember { HazeState() } @@ -167,19 +155,16 @@ fun SettingsScreen( modifier = Modifier.fillMaxSize(), contentWindowInsets = WindowInsets.statusBars, topBar = { - val title = @Composable { Text(text = stringResource(id = UiR.string.title_settings)) } - val navigationIcon = @Composable { - IconButton(onClick = onBack) { - Icon( - painter = painterResource(id = UiR.drawable.ic_round_arrow_back_24), - contentDescription = "Back button" - ) - } - } - TopAppBar( - title = title, - navigationIcon = navigationIcon, + title = { Text(text = stringResource(id = UiR.string.title_settings)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(id = UiR.drawable.ic_round_arrow_back_24), + contentDescription = "Back button" + ) + } + }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface.copy( alpha = if (currentTheme.usingBlur) 0f else 1f @@ -215,33 +200,23 @@ fun SettingsScreen( .padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)) .padding(bottom = padding.calculateBottomPadding()) ) { + item { + Spacer(modifier = Modifier.height(padding.calculateTopPadding())) + } items( - count = settingsList.size, -// key = { index -> -// val item = settingsList[index] -// requireNotNull(item.title ?: item.summary) -// }, - contentType = { index -> - when (settingsList[index]) { - is SettingsItem.ListItem -> "listitem" + items = screenState.settings, + key = { item -> item.key }, + contentType = { item -> + when (item) { + is SettingsItem.ListItem -> "list_item" is SettingsItem.Switch -> "switch" - is SettingsItem.TextField -> "textfield" + is SettingsItem.TextField -> "text_field" is SettingsItem.Title -> "title" - is SettingsItem.TitleSummary -> "titlesummary" + is SettingsItem.TitleSummary -> "title_summary" } } - ) { index -> - val needToShowSpacer by remember { - derivedStateOf { - index == 0 - } - } - - if (needToShowSpacer) { - Spacer(modifier = Modifier.height(padding.calculateTopPadding())) - } - - when (val item = settingsList[index]) { + ) { item -> + when (item) { is SettingsItem.Title -> TitleSettingsItem( item = item, isMultiline = currentTheme.multiline, @@ -251,67 +226,47 @@ fun SettingsScreen( is SettingsItem.TitleSummary -> TitleSummarySettingsItem( item = item, isMultiline = currentTheme.multiline, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener, + onSettingsClickListener = onSettingsItemClicked, + onSettingsLongClickListener = onSettingsItemLongClicked, modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) ) is SettingsItem.Switch -> SwitchSettingsItem( item = item, isMultiline = currentTheme.multiline, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener, - onSettingsChangeListener = changeListener, + onSettingsClickListener = onSettingsItemClicked, + onSettingsLongClickListener = onSettingsItemLongClicked, + onSettingsChangeListener = onSettingsItemValueChanged, modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) ) is SettingsItem.TextField -> EditTextSettingsItem( item = item, isMultiline = currentTheme.multiline, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener, - onSettingsChangeListener = changeListener, + onSettingsClickListener = onSettingsItemClicked, + onSettingsLongClickListener = onSettingsItemLongClicked, + onSettingsChangeListener = onSettingsItemValueChanged, modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) ) is SettingsItem.ListItem -> ListSettingsItem( item = item, isMultiline = currentTheme.multiline, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener, - onSettingsChangeListener = changeListener, + onSettingsClickListener = onSettingsItemClicked, + onSettingsLongClickListener = onSettingsItemLongClicked, + onSettingsChangeListener = onSettingsItemValueChanged, modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) ) } + } - val showBottomNavigationBarsSpacer by remember { - derivedStateOf { - index == settingsList.size - 1 - } - } - - if (showBottomNavigationBarsSpacer) { - Spacer(modifier = Modifier.navigationBarsPadding()) - } + item { + Spacer(modifier = Modifier.navigationBarsPadding()) } } } - - HandlePopups( - performCrashPositiveClick = viewModel::onPerformCrashPositiveButtonClicked, - performCrashDismissed = viewModel::onPerformCrashAlertDismissed, - logoutPositiveClick = { - viewModel.onLogOutAlertPositiveClick() - onNavigateToAuth() - }, - logoutDismissed = viewModel::onLogOutAlertDismissed, - longPollingPositiveClick = viewModel::onLongPollingAlertPositiveClicked, - longPollingDismissed = viewModel::onLongPollingAlertDismissed, - screenState = screenState - ) } -// TODO: 12/04/2024, Danil Nikolaev: rewrite to UiAction @Composable fun HandlePopups( performCrashPositiveClick: () -> Unit, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 882404c3..518be339 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,16 +7,18 @@ kotlin = "2.0.0" ksp = "2.0.0-1.0.22" vkompose = "0.5.4-k2" + +compose-bom = "2024.06.00" +koin = "3.5.6" + accompanist = "0.34.0" coil = "2.6.0" -compose-bom = "2024.06.00" coroutines = "1.9.0-RC" junit = "4.13.2" chucker = "4.0.0" guava = "33.2.1-jre" lifecycle = "2.8.3" core-ktx = "1.13.1" -koin = "3.5.6" material = "1.12.0" loggingInterceptor = "5.0.0-alpha.14" moshi = "1.15.1" @@ -30,24 +32,13 @@ appcompat = "1.7.0" androidx-navigation = "2.8.0-beta05" serialization = "1.7.1" rebugger = "1.0.0-rc03" +uiTooling = "1.6.8" [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshi" } -compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } -compose-material3 = { module = "androidx.compose.material3:material3" } -compose-ui = { module = "androidx.compose.ui:ui" } -compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } -compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } -compose-material3-windowsize = { module = "androidx.compose.material3:material3-window-size-class" } -compose-activity = { module = "androidx.activity:activity-compose" } -compose-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose" } -compose-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-compose" } -compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable" } -compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } -compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } eithernet = { module = "com.slack.eithernet:eithernet", version.ref = "eithernet" } haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } @@ -59,9 +50,6 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } -koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } -koin-androidx-compose-navigation = { module = "io.insert-koin:koin-androidx-compose-navigation", version.ref = "koin" } -koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } material = { module = "com.google.android.material:material", version.ref = "material" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } @@ -84,6 +72,30 @@ kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-j rebugger = { module = "io.github.theapache64:rebugger-android", version.ref = "rebugger" } +compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } +compose-material3 = { module = "androidx.compose.material3:material3" } +compose-ui = { module = "androidx.compose.ui:ui" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +compose-material3-windowsize = { module = "androidx.compose.material3:material3-window-size-class" } +compose-activity = { module = "androidx.activity:activity-compose" } +compose-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose" } +compose-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-compose" } +compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } + +# TODO: remove version from non-bom dependencies +koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin" } +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-core-coroutines = { module = "io.insert-koin:koin-core-coroutines", version.ref = "koin" } +koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-android-test = { module = "io.insert-koin:koin-android-test", version.ref = "koin" } +koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } +koin-androidx-compose-navigation = { module = "io.insert-koin:koin-androidx-compose-navigation", version.ref = "koin" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" } + [bundles] compose = [ "compose-material3", @@ -95,6 +107,13 @@ compose = [ "compose-lifecycle-runtime", "compose-runtime-saveable" ] +koin = [ + "koin-core", + "koin-core-coroutines", + "koin-android", + "koin-androidx-compose", + "koin-androidx-compose-navigation" +] [plugins] com-android-application = { id = "com.android.application", version.ref = "agp" } @@ -104,4 +123,3 @@ 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" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3ba8b5c1..5bf2267c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,7 +34,7 @@ include(":feature:languagepicker") include(":feature:photoviewer") include(":feature:settings") include(":feature:auth:login") -include(":feature:auth:twofa") +include(":feature:auth:validation") include(":feature:auth:captcha") include(":feature:auth:userbanned") include(":feature:friends")