forked from melod1n/fast-messenger
twoFa -> validation naming; fixes for preview for screens (separating view model from ui); some improvements & fixes
This commit is contained in:
@@ -112,11 +112,11 @@ android {
|
|||||||
useLiveLiterals = true
|
useLiveLiterals = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// packaging {
|
packaging {
|
||||||
// resources {
|
resources {
|
||||||
// excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".presentation.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
|
||||||
|
|||||||
@@ -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<Main> {
|
|
||||||
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<MainGraph>(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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,11 +16,13 @@ import com.meloda.app.fast.model.MainScreenState
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
interface MainViewModel {
|
interface MainViewModel {
|
||||||
|
|
||||||
val screenState: StateFlow<MainScreenState>
|
val screenState: StateFlow<MainScreenState>
|
||||||
|
val isNeedToOpenAuth: StateFlow<Boolean>
|
||||||
|
|
||||||
val longPollState: StateFlow<LongPollState>
|
val longPollState: StateFlow<LongPollState>
|
||||||
val startOnlineService: StateFlow<Boolean>
|
val startOnlineService: StateFlow<Boolean>
|
||||||
@@ -38,7 +40,7 @@ interface MainViewModel {
|
|||||||
|
|
||||||
fun onError(error: BaseError)
|
fun onError(error: BaseError)
|
||||||
|
|
||||||
fun onAuthOpened()
|
fun onNavigatedToAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainViewModelImpl(
|
class MainViewModelImpl(
|
||||||
@@ -51,6 +53,7 @@ class MainViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(MainScreenState.EMPTY)
|
override val screenState = MutableStateFlow(MainScreenState.EMPTY)
|
||||||
|
override val isNeedToOpenAuth = MutableStateFlow(false)
|
||||||
|
|
||||||
override val longPollState = MutableStateFlow(
|
override val longPollState = MutableStateFlow(
|
||||||
if (SettingsController.getBoolean(
|
if (SettingsController.getBoolean(
|
||||||
@@ -109,13 +112,13 @@ class MainViewModelImpl(
|
|||||||
override fun onError(error: BaseError) {
|
override fun onError(error: BaseError) {
|
||||||
when (error) {
|
when (error) {
|
||||||
BaseError.SessionExpired -> {
|
BaseError.SessionExpired -> {
|
||||||
screenState.setValue { old -> old.copy(isNeedToOpenAuth = true) }
|
isNeedToOpenAuth.update { true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthOpened() {
|
override fun onNavigatedToAuth() {
|
||||||
screenState.setValue { old -> old.copy(isNeedToOpenAuth = false) }
|
isNeedToOpenAuth.update { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAccounts() {
|
private fun loadAccounts() {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -10,21 +10,17 @@ data class MainScreenState(
|
|||||||
val useDarkTheme: Boolean,
|
val useDarkTheme: Boolean,
|
||||||
val useDynamicColors: Boolean,
|
val useDynamicColors: Boolean,
|
||||||
val isNeedToRequestNotifications: Boolean,
|
val isNeedToRequestNotifications: Boolean,
|
||||||
val isNeedToOpenAppPermissions: Boolean,
|
val isNeedToOpenAppPermissions: Boolean
|
||||||
val isNeedToOpenAuth: Boolean,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY: MainScreenState = MainScreenState(
|
val EMPTY: MainScreenState = MainScreenState(
|
||||||
accounts = emptyList(),
|
accounts = emptyList(),
|
||||||
accountsLoaded = false,
|
accountsLoaded = false,
|
||||||
|
|
||||||
// TODO: 05/05/2024, Danil Nikolaev: implement
|
|
||||||
useDarkTheme = false,
|
useDarkTheme = false,
|
||||||
useDynamicColors = false,
|
useDynamicColors = false,
|
||||||
isNeedToRequestNotifications = false,
|
isNeedToRequestNotifications = false,
|
||||||
isNeedToOpenAppPermissions = false,
|
isNeedToOpenAppPermissions = false
|
||||||
isNeedToOpenAuth = false,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Main> {
|
||||||
|
MainScreen(
|
||||||
|
navigationItems = navigationItems,
|
||||||
|
onError = onError,
|
||||||
|
onSettingsButtonClicked = onSettingsButtonClicked,
|
||||||
|
onConversationItemClicked = onConversationClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-6
@@ -1,4 +1,4 @@
|
|||||||
package com.meloda.app.fast
|
package com.meloda.app.fast.presentation
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
@@ -26,6 +26,7 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
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.UiText
|
||||||
import com.meloda.app.fast.common.extensions.ifEmpty
|
import com.meloda.app.fast.common.extensions.ifEmpty
|
||||||
import com.meloda.app.fast.common.extensions.isSdkAtLeast
|
import com.meloda.app.fast.common.extensions.isSdkAtLeast
|
||||||
@@ -100,13 +101,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
multiline = theme.multiline
|
multiline = theme.multiline
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
val currentTheme = LocalTheme.current
|
||||||
|
|
||||||
AppTheme(
|
AppTheme(
|
||||||
useDarkTheme = LocalTheme.current.usingDarkStyle,
|
useDarkTheme = currentTheme.usingDarkStyle,
|
||||||
useDynamicColors = LocalTheme.current.usingDynamicColors,
|
useDynamicColors = currentTheme.usingDynamicColors,
|
||||||
selectedColorScheme = LocalTheme.current.selectedColorScheme,
|
selectedColorScheme = currentTheme.selectedColorScheme,
|
||||||
useAmoledBackground = LocalTheme.current.usingAmoledBackground,
|
useAmoledBackground = currentTheme.usingAmoledBackground,
|
||||||
) {
|
) {
|
||||||
RootGraph()
|
RootScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<BottomNavigationItem>,
|
||||||
|
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<MainGraph>(startDestination = Conversations) {
|
||||||
|
friendsScreen(
|
||||||
|
onError = onError,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
conversationsScreen(
|
||||||
|
onError = onError,
|
||||||
|
onConversationItemClicked = onConversationItemClicked,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
profileScreen(
|
||||||
|
onError = onError,
|
||||||
|
onSettingsButtonClicked = onSettingsButtonClicked,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-23
@@ -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.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -12,33 +12,40 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.rememberNavController
|
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.AuthGraph
|
||||||
import com.meloda.app.fast.auth.authNavGraph
|
import com.meloda.app.fast.auth.authNavGraph
|
||||||
import com.meloda.app.fast.auth.navigateToAuth
|
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.chatmaterials.navigation.navigateToChatMaterials
|
||||||
import com.meloda.app.fast.common.UserConfig
|
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.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.messageshistory.navigation.navigateToMessagesHistory
|
||||||
import com.meloda.app.fast.settings.presentation.navigateToSettings
|
import com.meloda.app.fast.navigation.Main
|
||||||
import com.meloda.app.fast.settings.presentation.settingsRoute
|
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
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RootGraph(navController: NavHostController = rememberNavController()) {
|
fun RootScreen(navController: NavHostController = rememberNavController()) {
|
||||||
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
|
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
|
val isNeedToOpenAuth by viewModel.isNeedToOpenAuth.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
if (screenState.isNeedToOpenAuth) {
|
LaunchedEffect(isNeedToOpenAuth) {
|
||||||
viewModel.onAuthOpened()
|
if (isNeedToOpenAuth) {
|
||||||
|
viewModel.onNavigatedToAuth()
|
||||||
navController.navigateToAuth(clearBackStack = true)
|
navController.navigateToAuth(clearBackStack = true)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (screenState.accountsLoaded) {
|
if (screenState.accountsLoaded) {
|
||||||
val isNeedToShowConversations by remember {
|
val isNeedToShowConversations = remember {
|
||||||
derivedStateOf { screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn() }
|
screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
@@ -48,32 +55,30 @@ fun RootGraph(navController: NavHostController = rememberNavController()) {
|
|||||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||||
) {
|
) {
|
||||||
authNavGraph(
|
authNavGraph(
|
||||||
onError = viewModel::onError,
|
|
||||||
onNavigateToMain = navController::navigateToMain,
|
onNavigateToMain = navController::navigateToMain,
|
||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
mainScreen(
|
mainScreen(
|
||||||
onError = viewModel::onError,
|
onError = viewModel::onError,
|
||||||
onNavigateToSettings = navController::navigateToSettings,
|
onSettingsButtonClicked = navController::navigateToSettings,
|
||||||
onNavigateToMessagesHistory = navController::navigateToMessagesHistory
|
onConversationClicked = navController::navigateToMessagesHistory
|
||||||
)
|
)
|
||||||
|
|
||||||
messagesHistoryRoute(
|
messagesHistoryScreen(
|
||||||
onError = viewModel::onError,
|
onError = viewModel::onError,
|
||||||
onBack = navController::navigateUp,
|
onBack = navController::navigateUp,
|
||||||
onNavigateToChatAttachments = navController::navigateToChatMaterials
|
onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials
|
||||||
)
|
)
|
||||||
chatMaterialsRoute(
|
chatMaterialsScreen(
|
||||||
onBack = navController::navigateUp
|
onBack = navController::navigateUp
|
||||||
)
|
)
|
||||||
|
|
||||||
settingsRoute(
|
settingsScreen(
|
||||||
onError = viewModel::onError,
|
|
||||||
onBack = navController::navigateUp,
|
onBack = navController::navigateUp,
|
||||||
onNavigateToAuth = { navController.navigateToAuth(true) },
|
onLogOutButtonClicked = { navController.navigateToAuth(true) },
|
||||||
onNavigateToLanguagePicker = navController::navigateToLanguagePicker
|
onLanguageItemClicked = navController::navigateToLanguagePicker
|
||||||
)
|
)
|
||||||
languagePickerRoute(onBack = navController::navigateUp)
|
languagePickerScreen(onBack = navController::navigateUp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ interface OAuthRepository {
|
|||||||
login: String,
|
login: String,
|
||||||
password: String,
|
password: String,
|
||||||
forceSms: Boolean,
|
forceSms: Boolean,
|
||||||
twoFaCode: String?,
|
validationCode: String?,
|
||||||
captchaSid: String?,
|
captchaSid: String?,
|
||||||
captchaKey: String?
|
captchaKey: String?
|
||||||
): AuthDirectResponse
|
): AuthDirectResponse
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class OAuthRepositoryImpl(
|
|||||||
login: String,
|
login: String,
|
||||||
password: String,
|
password: String,
|
||||||
forceSms: Boolean,
|
forceSms: Boolean,
|
||||||
twoFaCode: String?,
|
validationCode: String?,
|
||||||
captchaSid: String?,
|
captchaSid: String?,
|
||||||
captchaKey: String?
|
captchaKey: String?
|
||||||
): AuthDirectResponse = withContext(Dispatchers.IO) {
|
): AuthDirectResponse = withContext(Dispatchers.IO) {
|
||||||
@@ -27,8 +27,8 @@ class OAuthRepositoryImpl(
|
|||||||
username = login,
|
username = login,
|
||||||
password = password,
|
password = password,
|
||||||
scope = VkConstants.Auth.SCOPE,
|
scope = VkConstants.Auth.SCOPE,
|
||||||
twoFaForceSms = forceSms,
|
validationForceSms = forceSms,
|
||||||
twoFaCode = twoFaCode,
|
validationCode = validationCode,
|
||||||
captchaSid = captchaSid,
|
captchaSid = captchaSid,
|
||||||
captchaKey = captchaKey,
|
captchaKey = captchaKey,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ object SettingsKeys {
|
|||||||
const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash"
|
const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash"
|
||||||
const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert"
|
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_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_ENABLE_ANIMATIONS_IN_MESSAGES = "debug_enable_animations_in_messages"
|
||||||
|
|
||||||
const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category"
|
const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category"
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ import androidx.compose.ui.text.font.FontStyle
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.WindowCompat
|
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.model.ThemeConfig
|
||||||
import com.meloda.app.fast.datastore.selectedColorScheme
|
|
||||||
import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme
|
import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
|
||||||
@@ -128,10 +125,10 @@ val LocalBottomPadding = compositionLocalOf {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppTheme(
|
fun AppTheme(
|
||||||
predefinedColorScheme: ColorScheme? = null,
|
predefinedColorScheme: ColorScheme? = null,
|
||||||
useDarkTheme: Boolean = isUsingDarkTheme(),
|
useDarkTheme: Boolean = false,
|
||||||
useDynamicColors: Boolean = isUsingDynamicColors(),
|
useDynamicColors: Boolean = false,
|
||||||
selectedColorScheme: Int = selectedColorScheme(),
|
useAmoledBackground: Boolean = false,
|
||||||
useAmoledBackground: Boolean = isUsingAmoledBackground(),
|
selectedColorScheme: Int = 0,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme: ColorScheme = when {
|
val colorScheme: ColorScheme = when {
|
||||||
|
|||||||
+1
-19
@@ -81,7 +81,7 @@ fun MaterialDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppTheme {
|
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
// AlertAnimation(visible = isVisible) {
|
// AlertAnimation(visible = isVisible) {
|
||||||
BasicAlertDialog(
|
BasicAlertDialog(
|
||||||
@@ -247,7 +247,6 @@ fun MaterialDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AlertAnimation(
|
fun AlertAnimation(
|
||||||
@@ -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
|
@Composable
|
||||||
private fun AlertItems(
|
private fun AlertItems(
|
||||||
selectionType: ItemsSelectionType,
|
selectionType: ItemsSelectionType,
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ data class AuthDirectRequest(
|
|||||||
val username: String,
|
val username: String,
|
||||||
val password: String,
|
val password: String,
|
||||||
val scope: String,
|
val scope: String,
|
||||||
val twoFaSupported: Boolean = true,
|
val validationSupported: Boolean = true,
|
||||||
val twoFaForceSms: Boolean = false,
|
val validationForceSms: Boolean = false,
|
||||||
val twoFaCode: String? = null,
|
val validationCode: String? = null,
|
||||||
val captchaSid: String? = null,
|
val captchaSid: String? = null,
|
||||||
val captchaKey: String? = null,
|
val captchaKey: String? = null,
|
||||||
val trustedHash: String? = null
|
val trustedHash: String? = null
|
||||||
@@ -23,11 +23,11 @@ data class AuthDirectRequest(
|
|||||||
"username" to username,
|
"username" to username,
|
||||||
"password" to password,
|
"password" to password,
|
||||||
"scope" to scope,
|
"scope" to scope,
|
||||||
"2fa_supported" to if (twoFaSupported) "1" else "0",
|
"2fa_supported" to if (validationSupported) "1" else "0",
|
||||||
"force_sms" to if (twoFaForceSms) "1" else "0"
|
"force_sms" to if (validationForceSms) "1" else "0"
|
||||||
)
|
)
|
||||||
.apply {
|
.apply {
|
||||||
twoFaCode?.let { this["code"] = it }
|
validationCode?.let { this["code"] = it }
|
||||||
captchaSid?.let { this["captcha_sid"] = it }
|
captchaSid?.let { this["captcha_sid"] = it }
|
||||||
captchaKey?.let { this["captcha_key"] = it }
|
captchaKey?.let { this["captcha_key"] = it }
|
||||||
trustedHash?.let { this["trusted_hash"] = it }
|
trustedHash?.let { this["trusted_hash"] = it }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.squareup.moshi.JsonClass
|
|||||||
data class AuthDirectResponse(
|
data class AuthDirectResponse(
|
||||||
@Json(name = "access_token") val accessToken: String?,
|
@Json(name = "access_token") val accessToken: String?,
|
||||||
@Json(name = "user_id") val userId: Int?,
|
@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_sid") val validationSid: String?,
|
||||||
@Json(name = "validation_type") val validationType: String?,
|
@Json(name = "validation_type") val validationType: String?,
|
||||||
@Json(name = "phone_mask") val phoneMask: String?,
|
@Json(name = "phone_mask") val phoneMask: String?,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ data class InvalidCredentialsError(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class WrongTwoFaCodeError(
|
data class WrongValidationCodeError(
|
||||||
@Json(name = "error") override val error: String, // "invalid_request"
|
@Json(name = "error") override val error: String, // "invalid_request"
|
||||||
@Json(name = "error_description") override val errorDescription: String,
|
@Json(name = "error_description") override val errorDescription: String,
|
||||||
@Json(name = "error_type") override val errorType: String // "wrong_otp"
|
@Json(name = "error_type") override val errorType: String // "wrong_otp"
|
||||||
@@ -86,7 +86,7 @@ data class WrongTwoFaCodeError(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class WrongTwoFaCodeFormatError(
|
data class WrongValidationCodeFormatError(
|
||||||
@Json(name = "error") override val error: String, // "invalid_request"
|
@Json(name = "error") override val error: String, // "invalid_request"
|
||||||
@Json(name = "error_description") override val errorDescription: String,
|
@Json(name = "error_description") override val errorDescription: String,
|
||||||
@Json(name = "error_type") override val errorType: String // "otp_format_is_incorrect"
|
@Json(name = "error_type") override val errorType: String // "otp_format_is_incorrect"
|
||||||
@@ -140,12 +140,12 @@ fun OAuthError.toDomain(): OAuthErrorDomain? = when (this) {
|
|||||||
OAuthErrorDomain.InvalidCredentialsError
|
OAuthErrorDomain.InvalidCredentialsError
|
||||||
}
|
}
|
||||||
|
|
||||||
is WrongTwoFaCodeError -> {
|
is WrongValidationCodeError -> {
|
||||||
OAuthErrorDomain.WrongTwoFaCode
|
OAuthErrorDomain.WrongValidationCode
|
||||||
}
|
}
|
||||||
|
|
||||||
is WrongTwoFaCodeFormatError -> {
|
is WrongValidationCodeFormatError -> {
|
||||||
OAuthErrorDomain.WrongTwoFaCodeFormat
|
OAuthErrorDomain.WrongValidationCodeFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
is TooManyTriesError -> {
|
is TooManyTriesError -> {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ sealed class OAuthErrorDomain {
|
|||||||
) : OAuthErrorDomain()
|
) : OAuthErrorDomain()
|
||||||
|
|
||||||
data object InvalidCredentialsError : OAuthErrorDomain()
|
data object InvalidCredentialsError : OAuthErrorDomain()
|
||||||
data object WrongTwoFaCode : OAuthErrorDomain()
|
data object WrongValidationCode : OAuthErrorDomain()
|
||||||
data object WrongTwoFaCodeFormat : OAuthErrorDomain()
|
data object WrongValidationCodeFormat : OAuthErrorDomain()
|
||||||
data object TooManyTriesError: OAuthErrorDomain()
|
data object TooManyTriesError: OAuthErrorDomain()
|
||||||
|
|
||||||
data object UnknownError : OAuthErrorDomain()
|
data object UnknownError : OAuthErrorDomain()
|
||||||
|
|||||||
@@ -128,12 +128,12 @@ internal class ResultCall<R : Any, E : OAuthError>(
|
|||||||
"invalid_request" -> {
|
"invalid_request" -> {
|
||||||
when (val type = baseError.errorType) {
|
when (val type = baseError.errorType) {
|
||||||
"wrong_otp" -> {
|
"wrong_otp" -> {
|
||||||
moshi.adapter(WrongTwoFaCodeError::class.java)
|
moshi.adapter(WrongValidationCodeError::class.java)
|
||||||
.fromJson(errorBodyString.orEmpty())
|
.fromJson(errorBodyString.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
"otp_format_is_incorrect" -> {
|
"otp_format_is_incorrect" -> {
|
||||||
moshi.adapter(WrongTwoFaCodeFormatError::class.java)
|
moshi.adapter(WrongValidationCodeFormatError::class.java)
|
||||||
.fromJson(errorBodyString.orEmpty())
|
.fromJson(errorBodyString.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(projects.feature.auth.login)
|
implementation(projects.feature.auth.login)
|
||||||
implementation(projects.feature.auth.captcha)
|
implementation(projects.feature.auth.captcha)
|
||||||
implementation(projects.feature.auth.twofa)
|
implementation(projects.feature.auth.validation)
|
||||||
implementation(projects.feature.auth.userbanned)
|
implementation(projects.feature.auth.userbanned)
|
||||||
|
|
||||||
implementation(libs.koin.androidx.compose)
|
implementation(libs.koin.androidx.compose)
|
||||||
|
|||||||
+6
-8
@@ -18,7 +18,7 @@ interface CaptchaViewModel {
|
|||||||
|
|
||||||
fun onCodeInputChanged(newCode: String)
|
fun onCodeInputChanged(newCode: String)
|
||||||
|
|
||||||
fun onTextFieldDoneClicked()
|
fun onTextFieldDoneAction()
|
||||||
fun onDoneButtonClicked()
|
fun onDoneButtonClicked()
|
||||||
|
|
||||||
fun onNavigatedToLogin()
|
fun onNavigatedToLogin()
|
||||||
@@ -32,24 +32,22 @@ class CaptchaViewModelImpl(
|
|||||||
override val screenState = MutableStateFlow(CaptchaScreenState.EMPTY)
|
override val screenState = MutableStateFlow(CaptchaScreenState.EMPTY)
|
||||||
override val isNeedToOpenLogin = MutableStateFlow(false)
|
override val isNeedToOpenLogin = MutableStateFlow(false)
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val arguments = Captcha.from(savedStateHandle).arguments
|
val captchaImage = Captcha.from(savedStateHandle).captchaImageUrl
|
||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(captchaImageUrl = URLDecoder.decode(captchaImage, "utf-8"))
|
||||||
captchaSid = arguments.captchaSid,
|
|
||||||
captchaImage = URLDecoder.decode(arguments.captchaImage, "utf-8")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCodeInputChanged(newCode: String) {
|
override fun onCodeInputChanged(newCode: String) {
|
||||||
val newState = screenState.value.copy(captchaCode = newCode.trim())
|
val newState = screenState.value.copy(code = newCode.trim())
|
||||||
screenState.update { newState }
|
screenState.update { newState }
|
||||||
processValidation()
|
processValidation()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTextFieldDoneClicked() {
|
override fun onTextFieldDoneAction() {
|
||||||
onDoneButtonClicked()
|
onDoneButtonClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-12
@@ -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
|
|
||||||
+4
-6
@@ -1,17 +1,15 @@
|
|||||||
package com.meloda.app.fast.auth.captcha.model
|
package com.meloda.app.fast.auth.captcha.model
|
||||||
|
|
||||||
data class CaptchaScreenState(
|
data class CaptchaScreenState(
|
||||||
val captchaSid: String,
|
val captchaImageUrl: String,
|
||||||
val captchaImage: String,
|
val code: String,
|
||||||
val captchaCode: String,
|
|
||||||
val codeError: Boolean
|
val codeError: Boolean
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY = CaptchaScreenState(
|
val EMPTY = CaptchaScreenState(
|
||||||
captchaSid = "",
|
captchaImageUrl = "",
|
||||||
captchaImage = "",
|
code = "",
|
||||||
captchaCode = "",
|
|
||||||
codeError = false
|
codeError = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+40
@@ -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<Captcha>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun NavGraphBuilder.captchaScreen(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onResult: (String) -> Unit
|
||||||
|
) {
|
||||||
|
composable<Captcha> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
-48
@@ -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<CaptchaArguments>() to customNavType<CaptchaArguments>())
|
|
||||||
|
|
||||||
fun from(savedStateHandle: SavedStateHandle) =
|
|
||||||
savedStateHandle.toRoute<Captcha>(typeMap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun NavGraphBuilder.captchaRoute(
|
|
||||||
onBack: () -> Unit,
|
|
||||||
onResult: (String) -> Unit
|
|
||||||
) {
|
|
||||||
composable<Captcha>(
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
+31
-13
@@ -49,6 +49,7 @@ import coil.compose.AsyncImage
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.meloda.app.fast.auth.captcha.CaptchaViewModel
|
import com.meloda.app.fast.auth.captcha.CaptchaViewModel
|
||||||
import com.meloda.app.fast.auth.captcha.CaptchaViewModelImpl
|
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.common.UiText
|
||||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||||
import com.meloda.app.fast.designsystem.TextFieldErrorText
|
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
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CaptchaScreen(
|
fun CaptchaRoute(
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onResult: (String) -> Unit,
|
onResult: (String) -> Unit,
|
||||||
viewModel: CaptchaViewModel = koinViewModel<CaptchaViewModelImpl>()
|
viewModel: CaptchaViewModel = koinViewModel<CaptchaViewModelImpl>()
|
||||||
@@ -64,6 +65,30 @@ fun CaptchaScreen(
|
|||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.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 {
|
var confirmedExit by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@@ -97,13 +122,6 @@ fun CaptchaScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(isNeedToOpenLogin) {
|
|
||||||
if (isNeedToOpenLogin) {
|
|
||||||
viewModel.onNavigatedToLogin()
|
|
||||||
onResult(screenState.captchaCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
Scaffold { padding ->
|
Scaffold { padding ->
|
||||||
@@ -171,7 +189,7 @@ fun CaptchaScreen(
|
|||||||
} else {
|
} else {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(screenState.captchaImage)
|
.data(screenState.captchaImageUrl)
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = "Captcha image",
|
contentDescription = "Captcha image",
|
||||||
@@ -183,14 +201,14 @@ fun CaptchaScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(30.dp))
|
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
|
val showError = screenState.codeError
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
value = code,
|
value = code,
|
||||||
onValueChange = { newText ->
|
onValueChange = { newText ->
|
||||||
code = newText
|
code = newText
|
||||||
viewModel.onCodeInputChanged(newText.text)
|
onCodeInputChanged(newText.text)
|
||||||
},
|
},
|
||||||
label = { Text(text = "Code") },
|
label = { Text(text = "Code") },
|
||||||
placeholder = { Text(text = "Code") },
|
placeholder = { Text(text = "Code") },
|
||||||
@@ -213,7 +231,7 @@ fun CaptchaScreen(
|
|||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onDone = {
|
onDone = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
viewModel.onTextFieldDoneClicked()
|
onTextFieldDoneAction()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
isError = showError
|
isError = showError
|
||||||
@@ -225,7 +243,7 @@ fun CaptchaScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = viewModel::onDoneButtonClicked,
|
onClick = onDoneButtonClicked,
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
) {
|
) {
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ class CaptchaValidator {
|
|||||||
|
|
||||||
fun validate(screenState: CaptchaScreenState): CaptchaValidationResult {
|
fun validate(screenState: CaptchaScreenState): CaptchaValidationResult {
|
||||||
return when {
|
return when {
|
||||||
screenState.captchaCode.isEmpty() -> CaptchaValidationResult.Empty
|
screenState.code.trim().isEmpty() -> CaptchaValidationResult.Empty
|
||||||
else -> CaptchaValidationResult.Valid
|
else -> CaptchaValidationResult.Valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,4 +91,7 @@ dependencies {
|
|||||||
implementation(libs.kotlin.serialization)
|
implementation(libs.kotlin.serialization)
|
||||||
|
|
||||||
implementation(libs.rebugger)
|
implementation(libs.rebugger)
|
||||||
|
|
||||||
|
androidTestImplementation(libs.compose.ui.test.junit4)
|
||||||
|
debugImplementation(libs.compose.ui.test.manifest)
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,10 +15,10 @@ import com.meloda.app.fast.data.db.AccountsRepository
|
|||||||
import com.meloda.app.fast.data.processState
|
import com.meloda.app.fast.data.processState
|
||||||
import com.meloda.app.fast.model.database.AccountEntity
|
import com.meloda.app.fast.model.database.AccountEntity
|
||||||
import com.meloda.app.fast.network.OAuthErrorDomain
|
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.LoginError
|
||||||
import com.meloda.fast.auth.login.model.LoginScreenState
|
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.LoginUserBannedArguments
|
||||||
import com.meloda.fast.auth.login.model.LoginValidationResult
|
import com.meloda.fast.auth.login.model.LoginValidationResult
|
||||||
import com.meloda.fast.auth.login.validation.LoginValidator
|
import com.meloda.fast.auth.login.validation.LoginValidator
|
||||||
@@ -36,10 +36,10 @@ interface LoginViewModel {
|
|||||||
val screenState: StateFlow<LoginScreenState>
|
val screenState: StateFlow<LoginScreenState>
|
||||||
val loginError: StateFlow<LoginError?>
|
val loginError: StateFlow<LoginError?>
|
||||||
|
|
||||||
val twoFaCode: StateFlow<String?>
|
val validationCode: StateFlow<String?>
|
||||||
val twoFaArguments: StateFlow<LoginTwoFaArguments?>
|
val validationArguments: StateFlow<LoginValidationArguments?>
|
||||||
val captchaCode: StateFlow<String?>
|
val captchaCode: StateFlow<String?>
|
||||||
val captchaArguments: StateFlow<LoginCaptchaArguments?>
|
val captchaArguments: StateFlow<CaptchaArguments?>
|
||||||
val userBannedArguments: StateFlow<LoginUserBannedArguments?>
|
val userBannedArguments: StateFlow<LoginUserBannedArguments?>
|
||||||
val isNeedToOpenMain: StateFlow<Boolean>
|
val isNeedToOpenMain: StateFlow<Boolean>
|
||||||
|
|
||||||
@@ -55,9 +55,9 @@ interface LoginViewModel {
|
|||||||
fun onNavigatedToMain()
|
fun onNavigatedToMain()
|
||||||
fun onNavigatedToUserBanned()
|
fun onNavigatedToUserBanned()
|
||||||
fun onNavigatedToCaptcha()
|
fun onNavigatedToCaptcha()
|
||||||
fun onNavigatedToTwoFa()
|
fun onNavigatedToValidation()
|
||||||
|
|
||||||
fun onTwoFaCodeReceived(code: String)
|
fun onValidationCodeReceived(code: String)
|
||||||
fun onCaptchaCodeReceived(code: String)
|
fun onCaptchaCodeReceived(code: String)
|
||||||
|
|
||||||
fun onLogoLongClicked()
|
fun onLogoLongClicked()
|
||||||
@@ -73,10 +73,10 @@ class LoginViewModelImpl(
|
|||||||
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||||
override val loginError = MutableStateFlow<LoginError?>(null)
|
override val loginError = MutableStateFlow<LoginError?>(null)
|
||||||
|
|
||||||
override val twoFaCode = MutableStateFlow<String?>(null)
|
override val validationCode = MutableStateFlow<String?>(null)
|
||||||
override val twoFaArguments = MutableStateFlow<LoginTwoFaArguments?>(null)
|
override val validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
||||||
override val captchaCode = MutableStateFlow<String?>(null)
|
override val captchaCode = MutableStateFlow<String?>(null)
|
||||||
override val captchaArguments = MutableStateFlow<LoginCaptchaArguments?>(null)
|
override val captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
||||||
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
||||||
override val isNeedToOpenMain = MutableStateFlow(false)
|
override val isNeedToOpenMain = MutableStateFlow(false)
|
||||||
|
|
||||||
@@ -125,12 +125,12 @@ class LoginViewModelImpl(
|
|||||||
captchaArguments.update { null }
|
captchaArguments.update { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToTwoFa() {
|
override fun onNavigatedToValidation() {
|
||||||
twoFaArguments.update { null }
|
validationArguments.update { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTwoFaCodeReceived(code: String) {
|
override fun onValidationCodeReceived(code: String) {
|
||||||
twoFaCode.update { code }
|
validationCode.update { code }
|
||||||
|
|
||||||
login()
|
login()
|
||||||
}
|
}
|
||||||
@@ -186,7 +186,7 @@ class LoginViewModelImpl(
|
|||||||
"LoginViewModel",
|
"LoginViewModel",
|
||||||
"auth: login: ${currentState.login}; " +
|
"auth: login: ${currentState.login}; " +
|
||||||
"password: ${currentState.password}; " +
|
"password: ${currentState.password}; " +
|
||||||
"2fa code: ${twoFaCode.value}; " +
|
"2fa code: ${validationCode.value}; " +
|
||||||
"captcha code: ${captchaCode.value}"
|
"captcha code: ${captchaCode.value}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ class LoginViewModelImpl(
|
|||||||
login = currentState.login,
|
login = currentState.login,
|
||||||
password = currentState.password,
|
password = currentState.password,
|
||||||
forceSms = forceSms,
|
forceSms = forceSms,
|
||||||
twoFaCode = twoFaCode.value,
|
validationCode = validationCode.value,
|
||||||
captchaSid = captchaArguments.value?.captchaSid,
|
captchaSid = captchaArguments.value?.captchaSid,
|
||||||
captchaKey = captchaCode.value
|
captchaKey = captchaCode.value
|
||||||
).listenValue { state ->
|
).listenValue { state ->
|
||||||
@@ -205,7 +205,7 @@ class LoginViewModelImpl(
|
|||||||
error = { error ->
|
error = { error ->
|
||||||
Log.d("LoginViewModelImpl", "login: error: $error")
|
Log.d("LoginViewModelImpl", "login: error: $error")
|
||||||
|
|
||||||
twoFaCode.update { null }
|
validationCode.update { null }
|
||||||
captchaCode.update { null }
|
captchaCode.update { null }
|
||||||
|
|
||||||
parseError(error)
|
parseError(error)
|
||||||
@@ -229,7 +229,7 @@ class LoginViewModelImpl(
|
|||||||
userId = userId,
|
userId = userId,
|
||||||
accessToken = accessToken,
|
accessToken = accessToken,
|
||||||
fastToken = null,
|
fastToken = null,
|
||||||
trustedHash = response.twoFaHash
|
trustedHash = response.validationHash
|
||||||
).also { account ->
|
).also { account ->
|
||||||
UserConfig.currentUserId = account.userId
|
UserConfig.currentUserId = account.userId
|
||||||
UserConfig.userId = account.userId
|
UserConfig.userId = account.userId
|
||||||
@@ -243,8 +243,8 @@ class LoginViewModelImpl(
|
|||||||
captchaArguments.update { null }
|
captchaArguments.update { null }
|
||||||
captchaCode.update { null }
|
captchaCode.update { null }
|
||||||
|
|
||||||
twoFaArguments.update { null }
|
validationArguments.update { null }
|
||||||
twoFaCode.update { null }
|
validationCode.update { null }
|
||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
@@ -265,7 +265,7 @@ class LoginViewModelImpl(
|
|||||||
is State.Error.OAuthError -> {
|
is State.Error.OAuthError -> {
|
||||||
when (val error = stateError.error) {
|
when (val error = stateError.error) {
|
||||||
is OAuthErrorDomain.ValidationRequiredError -> {
|
is OAuthErrorDomain.ValidationRequiredError -> {
|
||||||
val arguments = LoginTwoFaArguments(
|
val arguments = LoginValidationArguments(
|
||||||
validationSid = error.validationSid,
|
validationSid = error.validationSid,
|
||||||
redirectUri = error.redirectUri,
|
redirectUri = error.redirectUri,
|
||||||
phoneMask = error.phoneMask,
|
phoneMask = error.phoneMask,
|
||||||
@@ -273,13 +273,13 @@ class LoginViewModelImpl(
|
|||||||
canResendSms = error.validationResend == "sms",
|
canResendSms = error.validationResend == "sms",
|
||||||
wrongCodeError = null
|
wrongCodeError = null
|
||||||
)
|
)
|
||||||
twoFaArguments.update { arguments }
|
validationArguments.update { arguments }
|
||||||
}
|
}
|
||||||
|
|
||||||
is OAuthErrorDomain.CaptchaRequiredError -> {
|
is OAuthErrorDomain.CaptchaRequiredError -> {
|
||||||
val arguments = LoginCaptchaArguments(
|
val arguments = CaptchaArguments(
|
||||||
captchaSid = error.captchaSid,
|
captchaSid = error.captchaSid,
|
||||||
captchaImage = error.captchaImageUrl
|
captchaImageUrl = error.captchaImageUrl
|
||||||
)
|
)
|
||||||
captchaArguments.update { arguments }
|
captchaArguments.update { arguments }
|
||||||
}
|
}
|
||||||
@@ -298,12 +298,12 @@ class LoginViewModelImpl(
|
|||||||
userBannedArguments.update { arguments }
|
userBannedArguments.update { arguments }
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.WrongTwoFaCode -> {
|
OAuthErrorDomain.WrongValidationCode -> {
|
||||||
loginError.update { LoginError.WrongTwoFaCode }
|
loginError.update { LoginError.WrongValidationCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.WrongTwoFaCodeFormat -> {
|
OAuthErrorDomain.WrongValidationCodeFormat -> {
|
||||||
loginError.update { LoginError.WrongTwoFaCodeFormat }
|
loginError.update { LoginError.WrongValidationCodeFormat }
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.TooManyTriesError -> {
|
OAuthErrorDomain.TooManyTriesError -> {
|
||||||
@@ -320,102 +320,6 @@ class LoginViewModelImpl(
|
|||||||
|
|
||||||
else -> false
|
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() {
|
private fun processValidation() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface OAuthUseCase {
|
|||||||
login: String,
|
login: String,
|
||||||
password: String,
|
password: String,
|
||||||
forceSms: Boolean,
|
forceSms: Boolean,
|
||||||
twoFaCode: String?,
|
validationCode: String?,
|
||||||
captchaSid: String?,
|
captchaSid: String?,
|
||||||
captchaKey: String?
|
captchaKey: String?
|
||||||
): Flow<State<AuthInfo>>
|
): Flow<State<AuthInfo>>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class OAuthUseCaseImpl(
|
|||||||
login: String,
|
login: String,
|
||||||
password: String,
|
password: String,
|
||||||
forceSms: Boolean,
|
forceSms: Boolean,
|
||||||
twoFaCode: String?,
|
validationCode: String?,
|
||||||
captchaSid: String?,
|
captchaSid: String?,
|
||||||
captchaKey: String?
|
captchaKey: String?
|
||||||
): Flow<State<AuthInfo>> = flow {
|
): Flow<State<AuthInfo>> = flow {
|
||||||
@@ -27,7 +27,7 @@ class OAuthUseCaseImpl(
|
|||||||
val response = oAuthRepository.auth(
|
val response = oAuthRepository.auth(
|
||||||
login = login,
|
login = login,
|
||||||
password = password,
|
password = password,
|
||||||
twoFaCode = twoFaCode,
|
validationCode = validationCode,
|
||||||
captchaSid = captchaSid,
|
captchaSid = captchaSid,
|
||||||
captchaKey = captchaKey,
|
captchaKey = captchaKey,
|
||||||
forceSms = forceSms
|
forceSms = forceSms
|
||||||
@@ -39,7 +39,7 @@ class OAuthUseCaseImpl(
|
|||||||
AuthInfo(
|
AuthInfo(
|
||||||
userId = response.userId,
|
userId = response.userId,
|
||||||
accessToken = response.accessToken,
|
accessToken = response.accessToken,
|
||||||
twoFaHash = response.twoFaHash
|
validationHash = response.validationHash
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -92,11 +92,11 @@ class OAuthUseCaseImpl(
|
|||||||
VkOAuthErrors.INVALID_REQUEST -> {
|
VkOAuthErrors.INVALID_REQUEST -> {
|
||||||
when (response.errorType) {
|
when (response.errorType) {
|
||||||
VkErrorTypes.WRONG_OTP -> {
|
VkErrorTypes.WRONG_OTP -> {
|
||||||
State.Error.OAuthError(OAuthErrorDomain.WrongTwoFaCode)
|
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
VkErrorTypes.WRONG_OTP_FORMAT -> {
|
VkErrorTypes.WRONG_OTP_FORMAT -> {
|
||||||
State.Error.OAuthError(OAuthErrorDomain.WrongTwoFaCodeFormat)
|
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCodeFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package com.meloda.fast.auth.login.model
|
|||||||
data class AuthInfo(
|
data class AuthInfo(
|
||||||
val userId: Int?,
|
val userId: Int?,
|
||||||
val accessToken: String?,
|
val accessToken: String?,
|
||||||
val twoFaHash: String?
|
val validationHash: String?
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class LoginCaptchaArguments(
|
data class CaptchaArguments(
|
||||||
val captchaSid: String,
|
val captchaSid: String,
|
||||||
val captchaImage: String
|
val captchaImageUrl: String
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -7,6 +7,6 @@ sealed class LoginError {
|
|||||||
data object Unknown : LoginError()
|
data object Unknown : LoginError()
|
||||||
data object WrongCredentials : LoginError()
|
data object WrongCredentials : LoginError()
|
||||||
data object TooManyTries : LoginError()
|
data object TooManyTries : LoginError()
|
||||||
data object WrongTwoFaCode : LoginError()
|
data object WrongValidationCode : LoginError()
|
||||||
data object WrongTwoFaCodeFormat : LoginError()
|
data object WrongValidationCodeFormat : LoginError()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class LoginTwoFaArguments(
|
data class LoginValidationArguments(
|
||||||
val validationSid: String,
|
val validationSid: String,
|
||||||
val redirectUri: String,
|
val redirectUri: String,
|
||||||
val phoneMask: String,
|
val phoneMask: String,
|
||||||
+16
-19
@@ -5,14 +5,13 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import com.meloda.app.fast.common.extensions.navigation.sharedViewModel
|
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.LoginViewModel
|
||||||
import com.meloda.fast.auth.login.LoginViewModelImpl
|
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.LoginTwoFaArguments
|
import com.meloda.fast.auth.login.model.LoginValidationArguments
|
||||||
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||||
import com.meloda.fast.auth.login.presentation.LoginScreen
|
import com.meloda.fast.auth.login.presentation.LoginRoute
|
||||||
import com.meloda.fast.auth.login.presentation.LogoScreen
|
import com.meloda.fast.auth.login.presentation.LogoRoute
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -21,10 +20,9 @@ object Login
|
|||||||
@Serializable
|
@Serializable
|
||||||
object Logo
|
object Logo
|
||||||
|
|
||||||
fun NavGraphBuilder.loginRoute(
|
fun NavGraphBuilder.loginScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
||||||
onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit,
|
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
||||||
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
|
|
||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||||
onNavigateToCredentials: () -> Unit,
|
onNavigateToCredentials: () -> Unit,
|
||||||
@@ -34,25 +32,24 @@ fun NavGraphBuilder.loginRoute(
|
|||||||
val viewModel: LoginViewModel =
|
val viewModel: LoginViewModel =
|
||||||
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
||||||
|
|
||||||
val twoFaCode = backStackEntry.getTwoFaResult()
|
val validationCode = backStackEntry.getValidationResult()
|
||||||
val captchaCode = backStackEntry.getCaptchaResult()
|
val captchaCode = backStackEntry.getCaptchaResult()
|
||||||
|
|
||||||
LoginScreen(
|
LoginRoute(
|
||||||
onError = onError,
|
|
||||||
onNavigateToUserBanned = onNavigateToUserBanned,
|
onNavigateToUserBanned = onNavigateToUserBanned,
|
||||||
onNavigateToMain = onNavigateToMain,
|
onNavigateToMain = onNavigateToMain,
|
||||||
onNavigateToCaptcha = onNavigateToCaptcha,
|
onNavigateToCaptcha = onNavigateToCaptcha,
|
||||||
onNavigateToTwoFa = onNavigateToTwoFa,
|
onNavigateToValidation = onNavigateToValidation,
|
||||||
twoFaCode = twoFaCode,
|
validationCode = validationCode,
|
||||||
captchaCode = captchaCode,
|
captchaCode = captchaCode,
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Logo> {
|
composable<Logo> {
|
||||||
LogoScreen(
|
LogoRoute(
|
||||||
onNavigateToMain = onNavigateToMain,
|
onNavigateToMain = onNavigateToMain,
|
||||||
onShowCredentials = onNavigateToCredentials
|
onGoNextButtonClicked = onNavigateToCredentials
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,10 +58,10 @@ fun NavController.navigateToLogin() {
|
|||||||
this.navigate(route = Login)
|
this.navigate(route = Login)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavBackStackEntry.getTwoFaResult(): String? {
|
fun NavBackStackEntry.getValidationResult(): String? {
|
||||||
return savedStateHandle["twofacode"]
|
return savedStateHandle["validation_code"]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavBackStackEntry.getCaptchaResult(): String? {
|
fun NavBackStackEntry.getCaptchaResult(): String? {
|
||||||
return savedStateHandle["captchacode"]
|
return savedStateHandle["captcha_code"]
|
||||||
}
|
}
|
||||||
+58
-33
@@ -55,25 +55,23 @@ import com.meloda.app.fast.designsystem.connectNode
|
|||||||
import com.meloda.app.fast.designsystem.defaultFocusChangeAutoFill
|
import com.meloda.app.fast.designsystem.defaultFocusChangeAutoFill
|
||||||
import com.meloda.app.fast.designsystem.handleEnterKey
|
import com.meloda.app.fast.designsystem.handleEnterKey
|
||||||
import com.meloda.app.fast.designsystem.handleTabKey
|
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.LoginViewModel
|
||||||
import com.meloda.fast.auth.login.LoginViewModelImpl
|
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.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 com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(
|
fun LoginRoute(
|
||||||
onError: (BaseError) -> Unit,
|
|
||||||
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit,
|
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
||||||
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
|
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
||||||
twoFaCode: String?,
|
validationCode: String?,
|
||||||
captchaCode: String?,
|
captchaCode: String?,
|
||||||
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
@@ -81,7 +79,7 @@ fun LoginScreen(
|
|||||||
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
||||||
val userBannedArguments by viewModel.userBannedArguments.collectAsStateWithLifecycle()
|
val userBannedArguments by viewModel.userBannedArguments.collectAsStateWithLifecycle()
|
||||||
val captchaArguments by viewModel.captchaArguments.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()
|
val loginError by viewModel.loginError.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(isNeedToOpenMain) {
|
LaunchedEffect(isNeedToOpenMain) {
|
||||||
@@ -105,16 +103,16 @@ fun LoginScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(twoFaArguments) {
|
LaunchedEffect(validationArguments) {
|
||||||
twoFaArguments?.let { arguments ->
|
validationArguments?.let { arguments ->
|
||||||
viewModel.onNavigatedToTwoFa()
|
viewModel.onNavigatedToValidation()
|
||||||
onNavigateToTwoFa(arguments)
|
onNavigateToValidation(arguments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(twoFaCode) {
|
LaunchedEffect(validationCode) {
|
||||||
if (twoFaCode != null) {
|
if (validationCode != null) {
|
||||||
viewModel.onTwoFaCodeReceived(twoFaCode)
|
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 focusManager = LocalFocusManager.current
|
||||||
val (loginFocusable, passwordFocusable) = FocusRequester.createRefs()
|
val (loginFocusable, passwordFocusable) = FocusRequester.createRefs()
|
||||||
|
|
||||||
|
// TODO: 13/07/2024, Danil Nikolaev: remove
|
||||||
var loginText by remember { mutableStateOf(TextFieldValue(screenState.login)) }
|
var loginText by remember { mutableStateOf(TextFieldValue(screenState.login)) }
|
||||||
val showLoginError = screenState.loginError
|
val showLoginError = screenState.loginError
|
||||||
|
|
||||||
@@ -135,7 +165,7 @@ fun LoginScreen(
|
|||||||
onFill = { value ->
|
onFill = { value ->
|
||||||
loginText =
|
loginText =
|
||||||
TextFieldValue(text = value, selection = TextRange(value.length))
|
TextFieldValue(text = value, selection = TextRange(value.length))
|
||||||
viewModel.onLoginInputChanged(value)
|
onLoginAutoFilled(value)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,7 +177,7 @@ fun LoginScreen(
|
|||||||
onFill = { value ->
|
onFill = { value ->
|
||||||
passwordText =
|
passwordText =
|
||||||
TextFieldValue(text = value, selection = TextRange(value.length))
|
TextFieldValue(text = value, selection = TextRange(value.length))
|
||||||
viewModel.onPasswordInputChanged(value)
|
onPasswordAutoFilled(value)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -200,7 +230,7 @@ fun LoginScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
loginText = newText
|
loginText = newText
|
||||||
viewModel.onLoginInputChanged(text)
|
onLoginInputChanged(text)
|
||||||
},
|
},
|
||||||
label = { Text(text = stringResource(id = UiR.string.login_hint)) },
|
label = { Text(text = stringResource(id = UiR.string.login_hint)) },
|
||||||
placeholder = { 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))
|
.clip(RoundedCornerShape(10.dp))
|
||||||
.handleEnterKey {
|
.handleEnterKey {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
viewModel.onSignInButtonClicked()
|
onPasswordFieldEnterKeyClicked()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
.focusRequester(passwordFocusable)
|
.focusRequester(passwordFocusable)
|
||||||
@@ -250,7 +280,7 @@ fun LoginScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
passwordText = newText
|
passwordText = newText
|
||||||
viewModel.onPasswordInputChanged(text)
|
onPasswordInputChanged(text)
|
||||||
},
|
},
|
||||||
label = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
|
label = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
|
||||||
placeholder = { 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
|
else UiR.drawable.round_visibility_24
|
||||||
)
|
)
|
||||||
|
|
||||||
IconButton(onClick = viewModel::onPasswordVisibilityButtonClicked) {
|
IconButton(onClick = onPasswordVisibilityButtonClicked) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = imagePainter,
|
painter = imagePainter,
|
||||||
contentDescription = if (screenState.passwordVisible) "Password visible icon"
|
contentDescription = if (screenState.passwordVisible) "Password visible icon"
|
||||||
@@ -286,7 +316,7 @@ fun LoginScreen(
|
|||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onGo = {
|
onGo = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
viewModel.onSignInButtonClicked()
|
onPasswordFieldGoAction()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
isError = showPasswordError,
|
isError = showPasswordError,
|
||||||
@@ -310,10 +340,10 @@ fun LoginScreen(
|
|||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
viewModel.onSignInButtonClicked()
|
onSignInButtonClicked()
|
||||||
},
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
modifier = Modifier.testTag("Sign in button")
|
modifier = Modifier.testTag("sing_in_fab")
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = UiR.drawable.ic_arrow_end),
|
painter = painterResource(id = UiR.drawable.ic_arrow_end),
|
||||||
@@ -332,11 +362,6 @@ fun LoginScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleError(
|
|
||||||
onDismiss = viewModel::onErrorDialogDismissed,
|
|
||||||
error = loginError
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -375,7 +400,7 @@ fun HandleError(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
LoginError.WrongTwoFaCode -> {
|
LoginError.WrongValidationCode -> {
|
||||||
MaterialDialog(
|
MaterialDialog(
|
||||||
onDismissAction = onDismiss,
|
onDismissAction = onDismiss,
|
||||||
title = UiText.Simple("Error"),
|
title = UiText.Simple("Error"),
|
||||||
@@ -384,7 +409,7 @@ fun HandleError(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginError.WrongTwoFaCodeFormat -> {
|
LoginError.WrongValidationCodeFormat -> {
|
||||||
MaterialDialog(
|
MaterialDialog(
|
||||||
onDismissAction = onDismiss,
|
onDismissAction = onDismiss,
|
||||||
title = UiText.Simple("Error"),
|
title = UiText.Simple("Error"),
|
||||||
|
|||||||
+30
-9
@@ -34,11 +34,10 @@ import com.meloda.fast.auth.login.LoginViewModelImpl
|
|||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogoScreen(
|
fun LogoRoute(
|
||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
onShowCredentials: () -> Unit,
|
onGoNextButtonClicked: () -> Unit,
|
||||||
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
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 ->
|
Scaffold { padding ->
|
||||||
val topPadding by animateDpAsState(targetValue = padding.calculateTopPadding())
|
val topPadding by animateDpAsState(
|
||||||
val bottomPadding by animateDpAsState(targetValue = padding.calculateBottomPadding())
|
targetValue = padding.calculateTopPadding(),
|
||||||
|
label = "topPaddingAnimation"
|
||||||
|
)
|
||||||
|
val bottomPadding by animateDpAsState(
|
||||||
|
targetValue = padding.calculateBottomPadding(),
|
||||||
|
label = "bottomPaddingAnimation"
|
||||||
|
)
|
||||||
|
|
||||||
val endPadding by animateDpAsState(
|
val endPadding by animateDpAsState(
|
||||||
targetValue = padding.calculateEndPadding(LayoutDirection.Ltr)
|
targetValue = padding.calculateEndPadding(LayoutDirection.Ltr),
|
||||||
|
label = "endPaddingAnimation"
|
||||||
)
|
)
|
||||||
val startPadding by animateDpAsState(
|
val startPadding by animateDpAsState(
|
||||||
targetValue = padding.calculateStartPadding(LayoutDirection.Ltr)
|
targetValue = padding.calculateStartPadding(LayoutDirection.Ltr),
|
||||||
|
label = "startPaddingAnimation"
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@@ -85,7 +106,7 @@ fun LogoScreen(
|
|||||||
modifier = Modifier.combinedClickable(
|
modifier = Modifier.combinedClickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = null,
|
indication = null,
|
||||||
onLongClick = viewModel::onLogoLongClicked,
|
onLongClick = onLogoLongClicked,
|
||||||
onClick = {}
|
onClick = {}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -98,7 +119,7 @@ fun LogoScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = onShowCredentials,
|
onClick = onGoNextButtonClicked,
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -3,20 +3,19 @@ package com.meloda.app.fast.auth
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.navigation
|
import androidx.navigation.navigation
|
||||||
import com.meloda.app.fast.auth.captcha.model.CaptchaArguments
|
import com.meloda.app.fast.auth.captcha.navigation.captchaScreen
|
||||||
import com.meloda.app.fast.auth.captcha.navigation.captchaRoute
|
|
||||||
import com.meloda.app.fast.auth.captcha.navigation.navigateToCaptcha
|
import com.meloda.app.fast.auth.captcha.navigation.navigateToCaptcha
|
||||||
import com.meloda.app.fast.auth.captcha.navigation.setCaptchaResult
|
import com.meloda.app.fast.auth.captcha.navigation.setCaptchaResult
|
||||||
import com.meloda.app.fast.auth.twofa.model.TwoFaArguments
|
import com.meloda.app.fast.auth.validation.model.ValidationArguments
|
||||||
import com.meloda.app.fast.auth.twofa.navigation.navigateToTwoFa
|
import com.meloda.app.fast.auth.validation.navigation.navigateToValidation
|
||||||
import com.meloda.app.fast.auth.twofa.navigation.setTwoFaResult
|
import com.meloda.app.fast.auth.validation.navigation.setValidationResult
|
||||||
import com.meloda.app.fast.auth.twofa.navigation.twoFaRoute
|
import com.meloda.app.fast.auth.validation.navigation.validationScreen
|
||||||
import com.meloda.app.fast.model.BaseError
|
import com.meloda.app.fast.model.BaseError
|
||||||
import com.meloda.app.fast.userbanned.model.UserBannedArguments
|
import com.meloda.app.fast.userbanned.model.UserBannedArguments
|
||||||
import com.meloda.app.fast.userbanned.navigation.navigateToUserBanned
|
import com.meloda.app.fast.userbanned.navigation.navigateToUserBanned
|
||||||
import com.meloda.app.fast.userbanned.navigation.userBannedRoute
|
import com.meloda.app.fast.userbanned.navigation.userBannedRoute
|
||||||
import com.meloda.fast.auth.login.navigation.Logo
|
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 com.meloda.fast.auth.login.navigation.navigateToLogin
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
@@ -25,26 +24,21 @@ import java.net.URLEncoder
|
|||||||
object AuthGraph
|
object AuthGraph
|
||||||
|
|
||||||
fun NavGraphBuilder.authNavGraph(
|
fun NavGraphBuilder.authNavGraph(
|
||||||
onError: (BaseError) -> Unit,
|
|
||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
navigation<AuthGraph>(
|
navigation<AuthGraph>(
|
||||||
startDestination = Logo
|
startDestination = Logo
|
||||||
) {
|
) {
|
||||||
loginRoute(
|
loginScreen(
|
||||||
onError = onError,
|
|
||||||
onNavigateToCaptcha = { arguments ->
|
onNavigateToCaptcha = { arguments ->
|
||||||
navController.navigateToCaptcha(
|
navController.navigateToCaptcha(
|
||||||
CaptchaArguments(
|
captchaImageUrl = URLEncoder.encode(arguments.captchaImageUrl, "utf-8")
|
||||||
arguments.captchaSid,
|
|
||||||
URLEncoder.encode(arguments.captchaImage, "utf-8")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onNavigateToTwoFa = { arguments ->
|
onNavigateToValidation = { arguments ->
|
||||||
navController.navigateToTwoFa(
|
navController.navigateToValidation(
|
||||||
TwoFaArguments(
|
ValidationArguments(
|
||||||
validationSid = arguments.validationSid,
|
validationSid = arguments.validationSid,
|
||||||
redirectUri = URLEncoder.encode(arguments.redirectUri, "utf-8"),
|
redirectUri = URLEncoder.encode(arguments.redirectUri, "utf-8"),
|
||||||
phoneMask = arguments.phoneMask,
|
phoneMask = arguments.phoneMask,
|
||||||
@@ -58,7 +52,7 @@ fun NavGraphBuilder.authNavGraph(
|
|||||||
onNavigateToUserBanned = { arguments ->
|
onNavigateToUserBanned = { arguments ->
|
||||||
navController.navigateToUserBanned(
|
navController.navigateToUserBanned(
|
||||||
UserBannedArguments(
|
UserBannedArguments(
|
||||||
name = arguments.name,
|
userName = arguments.name,
|
||||||
message = arguments.message,
|
message = arguments.message,
|
||||||
restoreUrl = arguments.restoreUrl,
|
restoreUrl = arguments.restoreUrl,
|
||||||
accessToken = arguments.accessToken
|
accessToken = arguments.accessToken
|
||||||
@@ -69,18 +63,18 @@ fun NavGraphBuilder.authNavGraph(
|
|||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
|
|
||||||
twoFaRoute(
|
validationScreen(
|
||||||
onBack = {
|
onBack = {
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
navController.setTwoFaResult(null)
|
navController.setValidationResult(null)
|
||||||
},
|
},
|
||||||
onResult = { code ->
|
onResult = { code ->
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
navController.setTwoFaResult(code)
|
navController.setValidationResult(code)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
captchaRoute(
|
captchaScreen(
|
||||||
onBack = {
|
onBack = {
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
navController.setCaptchaResult(null)
|
navController.setCaptchaResult(null)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.meloda.app.fast.auth
|
package com.meloda.app.fast.auth
|
||||||
|
|
||||||
import com.meloda.app.fast.auth.captcha.di.captchaModule
|
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 com.meloda.fast.auth.login.di.loginModule
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val authModule = module {
|
val authModule = module {
|
||||||
includes(
|
includes(
|
||||||
loginModule,
|
loginModule,
|
||||||
twoFaModule,
|
validationModule,
|
||||||
captchaModule,
|
captchaModule,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
-26
@@ -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 = ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-8
@@ -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
|
|
||||||
}
|
|
||||||
-23
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-46
@@ -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<TwoFaArguments>() to customNavType<TwoFaArguments>())
|
|
||||||
|
|
||||||
fun from(savedStateHandle: SavedStateHandle) =
|
|
||||||
savedStateHandle.toRoute<TwoFa>(typeMap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavGraphBuilder.twoFaRoute(
|
|
||||||
onBack: () -> Unit,
|
|
||||||
onResult: (String) -> Unit
|
|
||||||
) {
|
|
||||||
composable<TwoFa>(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
-14
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -57,4 +57,6 @@ dependencies {
|
|||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.kotlin.serialization)
|
implementation(libs.kotlin.serialization)
|
||||||
|
|
||||||
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class UserBannedArguments(
|
data class UserBannedArguments(
|
||||||
val name: String,
|
val userName: String,
|
||||||
val message: String,
|
val message: String,
|
||||||
val restoreUrl: String,
|
val restoreUrl: String,
|
||||||
val accessToken: String
|
val accessToken: String
|
||||||
|
|||||||
+14
@@ -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 = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-7
@@ -8,7 +8,7 @@ import androidx.navigation.NavType
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import com.meloda.app.fast.userbanned.model.UserBannedArguments
|
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.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -32,18 +32,16 @@ val UserBannedNavType = object : NavType<UserBannedArguments>(isNullableAllowed
|
|||||||
override val name: String = "UserBannedArguments"
|
override val name: String = "UserBannedArguments"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavGraphBuilder.userBannedRoute(
|
fun NavGraphBuilder.userBannedRoute(onBack: () -> Unit) {
|
||||||
onBack: () -> Unit
|
|
||||||
) {
|
|
||||||
composable<UserBanned>(
|
composable<UserBanned>(
|
||||||
typeMap = mapOf(typeOf<UserBannedArguments>() to UserBannedNavType)
|
typeMap = mapOf(typeOf<UserBannedArguments>() to UserBannedNavType)
|
||||||
) { backStackEntry ->
|
) { backStackEntry ->
|
||||||
val arguments: UserBannedArguments = backStackEntry.toRoute()
|
val arguments: UserBannedArguments = backStackEntry.toRoute()
|
||||||
|
|
||||||
UserBannedScreen(
|
UserBannedRoute(
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
name = arguments.name,
|
userName = arguments.userName,
|
||||||
message = arguments.message,
|
message = arguments.message
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+28
-10
@@ -14,6 +14,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
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.text.withStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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
|
import com.meloda.app.fast.userbanned.model.UserBannedScreenState
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun UserBannedScreenPreview() {
|
fun UserBannedScreenPreview() {
|
||||||
AppTheme {
|
|
||||||
UserBannedScreen(
|
UserBannedScreen(
|
||||||
onBack = {},
|
screenState = UserBannedScreenState(
|
||||||
name = "Calvin Harris",
|
userName = "Andre Shultz",
|
||||||
message = "Eto konets"
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun UserBannedScreen(
|
fun UserBannedScreen(
|
||||||
onBack: () -> Unit,
|
screenState: UserBannedScreenState = UserBannedScreenState.EMPTY,
|
||||||
name: String,
|
onBack: () -> Unit = {},
|
||||||
message: String,
|
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -80,7 +98,7 @@ fun UserBannedScreen(
|
|||||||
append(": ")
|
append(": ")
|
||||||
}
|
}
|
||||||
|
|
||||||
append(name)
|
append(screenState.userName)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
@@ -89,7 +107,7 @@ fun UserBannedScreen(
|
|||||||
append(stringResource(id = UiR.string.blocking_reason_title))
|
append(stringResource(id = UiR.string.blocking_reason_title))
|
||||||
append(": ")
|
append(": ")
|
||||||
}
|
}
|
||||||
append(message)
|
append(screenState.message)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.meloda.app.fast.twofa"
|
namespace = "com.meloda.app.fast.validation"
|
||||||
compileSdk = Configs.compileSdk
|
compileSdk = Configs.compileSdk
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
+1
-1
@@ -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.State
|
||||||
import com.meloda.app.fast.model.api.responses.SendSmsResponse
|
import com.meloda.app.fast.model.api.responses.SendSmsResponse
|
||||||
+1
-1
@@ -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.State
|
||||||
import com.meloda.app.fast.data.api.auth.AuthRepository
|
import com.meloda.app.fast.data.api.auth.AuthRepository
|
||||||
+35
-34
@@ -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.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.meloda.app.fast.auth.twofa.model.TwoFaScreenState
|
import com.meloda.app.fast.auth.validation.model.ValidationScreenState
|
||||||
import com.meloda.app.fast.auth.twofa.model.TwoFaValidationType
|
import com.meloda.app.fast.auth.validation.model.ValidationType
|
||||||
import com.meloda.app.fast.auth.twofa.navigation.TwoFa
|
import com.meloda.app.fast.auth.validation.navigation.Validation
|
||||||
import com.meloda.app.fast.auth.twofa.validation.TwoFaValidator
|
import com.meloda.app.fast.auth.validation.validation.ValidationValidator
|
||||||
import com.meloda.app.fast.common.UiText
|
import com.meloda.app.fast.common.UiText
|
||||||
import com.meloda.app.fast.common.extensions.createTimerFlow
|
import com.meloda.app.fast.common.extensions.createTimerFlow
|
||||||
import com.meloda.app.fast.common.extensions.listenValue
|
import com.meloda.app.fast.common.extensions.listenValue
|
||||||
@@ -22,9 +22,9 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
interface TwoFaViewModel {
|
interface ValidationViewModel {
|
||||||
|
|
||||||
val screenState: StateFlow<TwoFaScreenState>
|
val screenState: StateFlow<ValidationScreenState>
|
||||||
|
|
||||||
val isNeedToOpenLogin: StateFlow<Boolean>
|
val isNeedToOpenLogin: StateFlow<Boolean>
|
||||||
|
|
||||||
@@ -33,36 +33,39 @@ interface TwoFaViewModel {
|
|||||||
fun onBackButtonClicked()
|
fun onBackButtonClicked()
|
||||||
fun onCancelButtonClicked()
|
fun onCancelButtonClicked()
|
||||||
fun onRequestSmsButtonClicked()
|
fun onRequestSmsButtonClicked()
|
||||||
fun onTextFieldDoneClicked()
|
fun onTextFieldDoneAction()
|
||||||
fun onDoneButtonClicked()
|
fun onDoneButtonClicked()
|
||||||
|
|
||||||
fun onNavigatedToLogin()
|
fun onNavigatedToLogin()
|
||||||
}
|
}
|
||||||
|
|
||||||
class TwoFaViewModelImpl(
|
class ValidationViewModelImpl(
|
||||||
private val validator: TwoFaValidator,
|
private val validator: ValidationValidator,
|
||||||
private val authUseCase: AuthUseCase,
|
private val authUseCase: AuthUseCase,
|
||||||
savedStateHandle: SavedStateHandle
|
savedStateHandle: SavedStateHandle
|
||||||
) : TwoFaViewModel, ViewModel() {
|
) : ValidationViewModel, ViewModel() {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(TwoFaScreenState.EMPTY)
|
override val screenState = MutableStateFlow(ValidationScreenState.EMPTY)
|
||||||
|
|
||||||
override val isNeedToOpenLogin = MutableStateFlow(false)
|
override val isNeedToOpenLogin = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private var validationSid: String? = null
|
||||||
|
|
||||||
private var delayJob: Job? = null
|
private var delayJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// TODO: 08/07/2024, Danil Nikolaev: use when fixed
|
// TODO: 08/07/2024, Danil Nikolaev: use when fixed
|
||||||
//savedStateHandle.toRoute<TwoFa>().arguments
|
//savedStateHandle.toRoute<Validation>().arguments
|
||||||
|
|
||||||
val arguments = TwoFa.from(savedStateHandle).arguments
|
val arguments = Validation.from(savedStateHandle).arguments
|
||||||
|
|
||||||
|
validationSid = arguments.validationSid
|
||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
twoFaSid = arguments.validationSid,
|
isSmsButtonVisible = arguments.canResendSms,
|
||||||
canResendSms = arguments.canResendSms,
|
|
||||||
codeError = arguments.wrongCodeError,
|
codeError = arguments.wrongCodeError,
|
||||||
twoFaText = getTwoFaText(TwoFaValidationType.parse(arguments.validationType)),
|
validationText = getValidationText(ValidationType.parse(arguments.validationType)),
|
||||||
phoneMask = arguments.phoneMask
|
phoneMask = arguments.phoneMask
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -71,7 +74,7 @@ class TwoFaViewModelImpl(
|
|||||||
override fun onCodeInputChanged(newCode: String) {
|
override fun onCodeInputChanged(newCode: String) {
|
||||||
screenState.updateValue(
|
screenState.updateValue(
|
||||||
screenState.value.copy(
|
screenState.value.copy(
|
||||||
twoFaCode = newCode.trim(),
|
code = newCode.trim(),
|
||||||
codeError = null
|
codeError = null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -89,7 +92,7 @@ class TwoFaViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancelButtonClicked() {
|
override fun onCancelButtonClicked() {
|
||||||
screenState.setValue { old -> old.copy(twoFaCode = null) }
|
screenState.setValue { old -> old.copy(code = null) }
|
||||||
isNeedToOpenLogin.update { true }
|
isNeedToOpenLogin.update { true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +100,7 @@ class TwoFaViewModelImpl(
|
|||||||
sendValidationCode()
|
sendValidationCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTextFieldDoneClicked() {
|
override fun onTextFieldDoneAction() {
|
||||||
onDoneButtonClicked()
|
onDoneButtonClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +111,7 @@ class TwoFaViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToLogin() {
|
override fun onNavigatedToLogin() {
|
||||||
screenState.updateValue(TwoFaScreenState.EMPTY)
|
screenState.updateValue(ValidationScreenState.EMPTY)
|
||||||
isNeedToOpenLogin.update { false }
|
isNeedToOpenLogin.update { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,9 +129,7 @@ class TwoFaViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sendValidationCode() {
|
private fun sendValidationCode() {
|
||||||
val validationSid = screenState.value.twoFaSid
|
authUseCase.sendSms(validationSid.orEmpty())
|
||||||
|
|
||||||
authUseCase.sendSms(validationSid)
|
|
||||||
.listenValue { state ->
|
.listenValue { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = { error ->
|
error = { error ->
|
||||||
@@ -140,9 +141,9 @@ class TwoFaViewModelImpl(
|
|||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
canResendSms = newCanResendSms,
|
isSmsButtonVisible = newCanResendSms,
|
||||||
twoFaText = getTwoFaText(
|
validationText = getValidationText(
|
||||||
TwoFaValidationType.parse(newValidationType.orEmpty())
|
ValidationType.parse(newValidationType.orEmpty())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -152,7 +153,7 @@ class TwoFaViewModelImpl(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (state.isLoading()) {
|
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,
|
time = delay,
|
||||||
onStartAction = {
|
onStartAction = {
|
||||||
screenState.updateValue(
|
screenState.updateValue(
|
||||||
screenState.value.copy(canResendSms = false)
|
screenState.value.copy(isSmsButtonVisible = false)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onTickAction = { remainedTime ->
|
onTickAction = { remainedTime ->
|
||||||
@@ -175,24 +176,24 @@ class TwoFaViewModelImpl(
|
|||||||
onTimeoutAction = {
|
onTimeoutAction = {
|
||||||
screenState.updateValue(
|
screenState.updateValue(
|
||||||
screenState.value.copy(
|
screenState.value.copy(
|
||||||
canResendSms = true
|
isSmsButtonVisible = true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
).launchIn(viewModelScope)
|
).launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTwoFaText(validationType: TwoFaValidationType): UiText {
|
private fun getValidationText(validationType: ValidationType): UiText {
|
||||||
return when (validationType) {
|
return when (validationType) {
|
||||||
TwoFaValidationType.Sms -> {
|
ValidationType.Sms -> {
|
||||||
UiText.Simple("SMS with the code is sent to ${screenState.value.phoneMask}")
|
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")
|
UiText.Simple("Enter the code from the code generator application")
|
||||||
}
|
}
|
||||||
|
|
||||||
is TwoFaValidationType.Another -> UiText.Simple(validationType.type)
|
is ValidationType.Other -> UiText.Simple(validationType.type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+17
@@ -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
|
||||||
|
}
|
||||||
+2
-2
@@ -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 android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class TwoFaArguments(
|
data class ValidationArguments(
|
||||||
val validationSid: String,
|
val validationSid: String,
|
||||||
val redirectUri: String,
|
val redirectUri: String,
|
||||||
val phoneMask: String,
|
val phoneMask: String,
|
||||||
+27
@@ -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("")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -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
|
||||||
|
}
|
||||||
+46
@@ -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<ValidationArguments>() to customNavType<ValidationArguments>())
|
||||||
|
|
||||||
|
fun from(savedStateHandle: SavedStateHandle) =
|
||||||
|
savedStateHandle.toRoute<Validation>(typeMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.validationScreen(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onResult: (String) -> Unit
|
||||||
|
) {
|
||||||
|
composable<Validation>(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+55
-34
@@ -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.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
@@ -27,6 +27,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.meloda.app.fast.auth.twofa.TwoFaViewModel
|
import com.meloda.app.fast.auth.validation.ValidationViewModel
|
||||||
import com.meloda.app.fast.auth.twofa.TwoFaViewModelImpl
|
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.common.UiText
|
||||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||||
import com.meloda.app.fast.designsystem.TextFieldErrorText
|
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
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TwoFaScreen(
|
fun ValidationRoute(
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onCodeResult: (code: String) -> Unit,
|
onResult: (String) -> Unit,
|
||||||
viewModel: TwoFaViewModel = koinViewModel<TwoFaViewModelImpl>(),
|
viewModel: ValidationViewModel = koinViewModel<ValidationViewModelImpl>()
|
||||||
|
) {
|
||||||
|
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 focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
var confirmedExit by rememberSaveable {
|
var confirmedExit by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@@ -96,20 +129,7 @@ fun TwoFaScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(isNeedToOpenLogin) {
|
var code by remember { mutableStateOf(TextFieldValue(screenState.code.orEmpty())) }
|
||||||
if (isNeedToOpenLogin) {
|
|
||||||
viewModel.onNavigatedToLogin()
|
|
||||||
|
|
||||||
val code = screenState.twoFaCode
|
|
||||||
if (code == null) {
|
|
||||||
onBack()
|
|
||||||
} else {
|
|
||||||
onCodeResult(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var code by remember { mutableStateOf(TextFieldValue(screenState.twoFaCode.orEmpty())) }
|
|
||||||
val codeError = screenState.codeError
|
val codeError = screenState.codeError
|
||||||
|
|
||||||
Scaffold { padding ->
|
Scaffold { padding ->
|
||||||
@@ -140,7 +160,6 @@ fun TwoFaScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Two-Factor\nAuthentication",
|
text = "Two-Factor\nAuthentication",
|
||||||
style = MaterialTheme.typography.displayMedium,
|
style = MaterialTheme.typography.displayMedium,
|
||||||
@@ -148,16 +167,18 @@ fun TwoFaScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(38.dp))
|
Spacer(modifier = Modifier.height(38.dp))
|
||||||
Text(
|
Text(
|
||||||
text = screenState.twoFaText.getString().orEmpty(),
|
text = screenState.validationText.getString().orEmpty(),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
val delayRemainedTime = screenState.delayTime
|
val isResendTextVisible by remember {
|
||||||
AnimatedVisibility(visible = delayRemainedTime > 0) {
|
derivedStateOf { screenState.delayTime > 0 }
|
||||||
|
}
|
||||||
|
AnimatedVisibility(visible = isResendTextVisible) {
|
||||||
Text(
|
Text(
|
||||||
text = "Can resend after $delayRemainedTime seconds",
|
text = "Can resend after ${screenState.delayTime} seconds",
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -169,7 +190,7 @@ fun TwoFaScreen(
|
|||||||
if (newText.text.length > 6) return@TextField
|
if (newText.text.length > 6) return@TextField
|
||||||
|
|
||||||
code = newText
|
code = newText
|
||||||
viewModel.onCodeInputChanged((newText.text))
|
onCodeInputChanged((newText.text))
|
||||||
},
|
},
|
||||||
label = { Text(text = "Code") },
|
label = { Text(text = "Code") },
|
||||||
placeholder = { Text(text = "Code") },
|
placeholder = { Text(text = "Code") },
|
||||||
@@ -195,7 +216,7 @@ fun TwoFaScreen(
|
|||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onDone = {
|
onDone = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
viewModel.onTextFieldDoneClicked()
|
onTextFieldDoneAction()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
isError = codeError != null
|
isError = codeError != null
|
||||||
@@ -211,13 +232,13 @@ fun TwoFaScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
val canResendSms = screenState.canResendSms
|
val canResendSms = screenState.isSmsButtonVisible
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = canResendSms,
|
visible = canResendSms,
|
||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = viewModel::onRequestSmsButtonClicked,
|
onClick = onRequestSmsButtonClicked,
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = "Request SMS",
|
text = "Request SMS",
|
||||||
@@ -238,7 +259,7 @@ fun TwoFaScreen(
|
|||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = viewModel::onDoneButtonClicked,
|
onClick = onDoneButtonClicked,
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
+14
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-5
@@ -5,7 +5,7 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import com.meloda.app.fast.chatmaterials.presentation.ChatMaterialsScreen
|
import com.meloda.app.fast.chatmaterials.presentation.ChatMaterialsRoute
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -19,13 +19,11 @@ data class ChatMaterials(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavGraphBuilder.chatMaterialsRoute(
|
fun NavGraphBuilder.chatMaterialsScreen(
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
composable<ChatMaterials> {
|
composable<ChatMaterials> {
|
||||||
ChatMaterialsScreen(
|
ChatMaterialsRoute(onBack = onBack)
|
||||||
onBack = onBack
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+26
-6
@@ -66,6 +66,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModel
|
import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModel
|
||||||
import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModelImpl
|
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.LocalTheme
|
||||||
import com.meloda.app.fast.designsystem.R
|
import com.meloda.app.fast.designsystem.R
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
@@ -75,6 +76,22 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatMaterialsRoute(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: ChatMaterialsViewModel = koinViewModel<ChatMaterialsViewModelImpl>()
|
||||||
|
) {
|
||||||
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
ChatMaterialsScreen(
|
||||||
|
screenState = screenState,
|
||||||
|
onBack = onBack,
|
||||||
|
onTypeChanged = viewModel::onTypeChanged,
|
||||||
|
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
||||||
|
onRefresh = viewModel::onRefresh
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
@@ -82,11 +99,14 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatMaterialsScreen(
|
fun ChatMaterialsScreen(
|
||||||
onBack: () -> Unit,
|
screenState: ChatMaterialsScreenState,
|
||||||
viewModel: ChatMaterialsViewModel = koinViewModel<ChatMaterialsViewModelImpl>()
|
onBack: () -> Unit = {},
|
||||||
|
onTypeChanged: (String) -> Unit = {},
|
||||||
|
onRefreshDropdownItemClicked: () -> Unit = {},
|
||||||
|
onRefresh: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val currentTheme = LocalTheme.current
|
val currentTheme = LocalTheme.current
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
|
||||||
val attachments = screenState.materials
|
val attachments = screenState.materials
|
||||||
|
|
||||||
val imageLoader = LocalContext.current.imageLoader
|
val imageLoader = LocalContext.current.imageLoader
|
||||||
@@ -106,7 +126,7 @@ fun ChatMaterialsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(checkedTypeIndex) {
|
LaunchedEffect(checkedTypeIndex) {
|
||||||
viewModel.onTypeChanged(
|
onTypeChanged(
|
||||||
when (checkedTypeIndex) {
|
when (checkedTypeIndex) {
|
||||||
0 -> "photo"
|
0 -> "photo"
|
||||||
1 -> "video"
|
1 -> "video"
|
||||||
@@ -213,7 +233,7 @@ fun ChatMaterialsScreen(
|
|||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onRefresh()
|
onRefreshDropdownItemClicked()
|
||||||
dropDownMenuExpanded = false
|
dropDownMenuExpanded = false
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
@@ -342,7 +362,7 @@ fun ChatMaterialsScreen(
|
|||||||
|
|
||||||
if (pullToRefreshState.isRefreshing) {
|
if (pullToRefreshState.isRefreshing) {
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
viewModel.onRefresh()
|
onRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-18
@@ -44,7 +44,7 @@ interface ConversationsViewModel {
|
|||||||
val currentOffset: StateFlow<Int>
|
val currentOffset: StateFlow<Int>
|
||||||
val canPaginate: StateFlow<Boolean>
|
val canPaginate: StateFlow<Boolean>
|
||||||
|
|
||||||
fun onMetPaginationCondition()
|
fun onPaginationConditionsMet()
|
||||||
|
|
||||||
fun onDeleteDialogDismissed()
|
fun onDeleteDialogDismissed()
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ interface ConversationsViewModel {
|
|||||||
|
|
||||||
fun onRefresh()
|
fun onRefresh()
|
||||||
|
|
||||||
fun onConversationItemClick(conversationId: Int)
|
fun onConversationItemClick()
|
||||||
fun onConversationItemLongClick(conversation: UiConversation)
|
fun onConversationItemLongClick(conversation: UiConversation)
|
||||||
|
|
||||||
fun onPinDialogDismissed()
|
fun onPinDialogDismissed()
|
||||||
@@ -76,7 +76,7 @@ class ConversationsViewModelImpl(
|
|||||||
override val currentOffset = MutableStateFlow(0)
|
override val currentOffset = MutableStateFlow(0)
|
||||||
override val canPaginate = MutableStateFlow(false)
|
override val canPaginate = MutableStateFlow(false)
|
||||||
|
|
||||||
override fun onMetPaginationCondition() {
|
override fun onPaginationConditionsMet() {
|
||||||
currentOffset.update { screenState.value.conversations.size }
|
currentOffset.update { screenState.value.conversations.size }
|
||||||
loadConversations()
|
loadConversations()
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ class ConversationsViewModelImpl(
|
|||||||
loadConversations(offset = 0)
|
loadConversations(offset = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConversationItemClick(conversationId: Int) {
|
override fun onConversationItemClick() {
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
conversations = old.conversations.map { item ->
|
conversations = old.conversations.map { item ->
|
||||||
@@ -225,26 +225,15 @@ class ConversationsViewModelImpl(
|
|||||||
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset).listenValue { state ->
|
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset).listenValue { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = { error ->
|
error = { error ->
|
||||||
when (error) {
|
if (error is State.Error.ApiError) {
|
||||||
is State.Error.ApiError -> {
|
when (error.errorCode) {
|
||||||
val (code, message) = error
|
|
||||||
|
|
||||||
when (code) {
|
|
||||||
VkErrorCodes.UserAuthorizationFailed -> {
|
VkErrorCodes.UserAuthorizationFailed -> {
|
||||||
baseError.setValue { BaseError.SessionExpired }
|
baseError.setValue { BaseError.SessionExpired }
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> Unit
|
||||||
Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
State.Error.ConnectionError -> TODO()
|
|
||||||
State.Error.InternalError -> TODO()
|
|
||||||
is State.Error.OAuthError -> TODO()
|
|
||||||
State.Error.Unknown -> TODO()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
success = { response ->
|
success = { response ->
|
||||||
val itemsCountSufficient = response.size == LOAD_COUNT
|
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.meloda.app.fast.designsystem.AppTheme
|
|
||||||
|
|
||||||
const val numberOfDots = 3
|
const val numberOfDots = 3
|
||||||
val dotSize = 6.dp
|
val dotSize = 6.dp
|
||||||
@@ -298,7 +297,7 @@ fun DotsCollision() {
|
|||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun DotsPreview() = AppTheme {
|
fun DotsPreview() {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
|
|||||||
+5
-7
@@ -6,27 +6,25 @@ import androidx.navigation.compose.composable
|
|||||||
import com.meloda.app.fast.common.extensions.navigation.sharedViewModel
|
import com.meloda.app.fast.common.extensions.navigation.sharedViewModel
|
||||||
import com.meloda.app.fast.conversations.ConversationsViewModel
|
import com.meloda.app.fast.conversations.ConversationsViewModel
|
||||||
import com.meloda.app.fast.conversations.ConversationsViewModelImpl
|
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 com.meloda.app.fast.model.BaseError
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
object Conversations
|
object Conversations
|
||||||
|
|
||||||
fun NavGraphBuilder.conversationsRoute(
|
fun NavGraphBuilder.conversationsScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onNavigateToMessagesHistory: (id: Int) -> Unit,
|
onConversationItemClicked: (id: Int) -> Unit,
|
||||||
onListScrollingUp: (Boolean) -> Unit,
|
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
) {
|
) {
|
||||||
composable<Conversations> {
|
composable<Conversations> {
|
||||||
val viewModel: ConversationsViewModel =
|
val viewModel: ConversationsViewModel =
|
||||||
it.sharedViewModel<ConversationsViewModelImpl>(navController = navController)
|
it.sharedViewModel<ConversationsViewModelImpl>(navController = navController)
|
||||||
|
|
||||||
ConversationsScreen(
|
ConversationsRoute(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
onConversationItemClicked = onConversationItemClicked,
|
||||||
onListScrollingUp = onListScrollingUp,
|
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+57
-29
@@ -72,6 +72,7 @@ import coil.request.ImageRequest
|
|||||||
import com.meloda.app.fast.common.UiText
|
import com.meloda.app.fast.common.UiText
|
||||||
import com.meloda.app.fast.conversations.ConversationsViewModel
|
import com.meloda.app.fast.conversations.ConversationsViewModel
|
||||||
import com.meloda.app.fast.conversations.ConversationsViewModelImpl
|
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.ConversationsScreenState
|
||||||
import com.meloda.app.fast.conversations.model.UiConversation
|
import com.meloda.app.fast.conversations.model.UiConversation
|
||||||
import com.meloda.app.fast.designsystem.LocalBottomPadding
|
import com.meloda.app.fast.designsystem.LocalBottomPadding
|
||||||
@@ -89,22 +90,20 @@ import kotlinx.coroutines.launch
|
|||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@OptIn(
|
|
||||||
ExperimentalMaterial3Api::class,
|
|
||||||
ExperimentalHazeMaterialsApi::class,
|
|
||||||
)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConversationsScreen(
|
fun ConversationsRoute(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onNavigateToMessagesHistory: (conversationId: Int) -> Unit,
|
onConversationItemClicked: (conversationId: Int) -> Unit,
|
||||||
onListScrollingUp: (Boolean) -> Unit,
|
|
||||||
viewModel: ConversationsViewModel = koinViewModel<ConversationsViewModelImpl>()
|
viewModel: ConversationsViewModel = koinViewModel<ConversationsViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
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()
|
val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle()
|
||||||
|
LaunchedEffect(imagesToPreload) {
|
||||||
imagesToPreload.forEach { url ->
|
imagesToPreload.forEach { url ->
|
||||||
context.imageLoader.enqueue(
|
context.imageLoader.enqueue(
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
@@ -112,11 +111,49 @@ fun ConversationsScreen(
|
|||||||
.build()
|
.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(
|
||||||
|
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 view = LocalView.current
|
val view = LocalView.current
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
|
||||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
val currentTheme = LocalTheme.current
|
val currentTheme = LocalTheme.current
|
||||||
|
|
||||||
val maxLines by remember {
|
val maxLines by remember {
|
||||||
@@ -129,10 +166,6 @@ fun ConversationsScreen(
|
|||||||
|
|
||||||
val isListScrollingUp = listState.isScrollingUp()
|
val isListScrollingUp = listState.isScrollingUp()
|
||||||
|
|
||||||
LaunchedEffect(isListScrollingUp) {
|
|
||||||
onListScrollingUp(isListScrollingUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
val paginationConditionMet by remember {
|
val paginationConditionMet by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
canPaginate &&
|
canPaginate &&
|
||||||
@@ -143,7 +176,7 @@ fun ConversationsScreen(
|
|||||||
|
|
||||||
LaunchedEffect(paginationConditionMet) {
|
LaunchedEffect(paginationConditionMet) {
|
||||||
if (paginationConditionMet && !screenState.isPaginating) {
|
if (paginationConditionMet && !screenState.isPaginating) {
|
||||||
viewModel.onMetPaginationCondition()
|
onPaginationConditionsMet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +246,7 @@ fun ConversationsScreen(
|
|||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onRefresh()
|
onRefreshDropdownItemClicked()
|
||||||
dropDownMenuExpanded = false
|
dropDownMenuExpanded = false
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
@@ -301,7 +334,7 @@ fun ConversationsScreen(
|
|||||||
ErrorView(
|
ErrorView(
|
||||||
text = "Session expired",
|
text = "Session expired",
|
||||||
buttonText = "Log out",
|
buttonText = "Log out",
|
||||||
onButtonClick = { onError(BaseError.SessionExpired) }
|
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,10 +353,10 @@ fun ConversationsScreen(
|
|||||||
) {
|
) {
|
||||||
ConversationsListComposable(
|
ConversationsListComposable(
|
||||||
onConversationsClick = { id ->
|
onConversationsClick = { id ->
|
||||||
onNavigateToMessagesHistory(id)
|
onConversationItemClicked(id)
|
||||||
viewModel.onConversationItemClick(id)
|
|
||||||
},
|
},
|
||||||
onConversationsLongClick = viewModel::onConversationItemLongClick,
|
onConversationsLongClick = onConversationItemLongClicked,
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
state = listState,
|
state = listState,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
@@ -335,13 +368,13 @@ fun ConversationsScreen(
|
|||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}.fillMaxSize(),
|
}.fillMaxSize(),
|
||||||
onOptionClicked = viewModel::onOptionClicked,
|
onOptionClicked = onOptionClicked,
|
||||||
padding = padding
|
padding = padding
|
||||||
)
|
)
|
||||||
|
|
||||||
if (pullToRefreshState.isRefreshing) {
|
if (pullToRefreshState.isRefreshing) {
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
viewModel.onRefresh()
|
onRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,11 +395,6 @@ fun ConversationsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleDialogs(
|
|
||||||
screenState = screenState,
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.meloda.app.fast.friends
|
package com.meloda.app.fast.friends
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.meloda.app.fast.common.extensions.listenValue
|
import com.meloda.app.fast.common.extensions.listenValue
|
||||||
import com.meloda.app.fast.common.extensions.setValue
|
import com.meloda.app.fast.common.extensions.setValue
|
||||||
import com.meloda.app.fast.data.State
|
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.data.processState
|
||||||
import com.meloda.app.fast.datastore.UserSettings
|
import com.meloda.app.fast.datastore.UserSettings
|
||||||
import com.meloda.app.fast.friends.model.FriendsScreenState
|
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.friends.util.asPresentation
|
||||||
import com.meloda.app.fast.model.BaseError
|
import com.meloda.app.fast.model.BaseError
|
||||||
import com.meloda.app.fast.model.api.domain.VkUser
|
import com.meloda.app.fast.model.api.domain.VkUser
|
||||||
import com.meloda.app.fast.network.VkErrorCodes
|
import com.meloda.app.fast.network.VkErrorCodes
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
// TODO: 13/07/2024, Danil Nikolaev: separate two lists and their pagination
|
// TODO: 13/07/2024, Danil Nikolaev: separate two lists and their pagination
|
||||||
interface FriendsViewModel {
|
interface FriendsViewModel {
|
||||||
|
|
||||||
val screenState: StateFlow<FriendsScreenState>
|
val screenState: StateFlow<FriendsScreenState>
|
||||||
val uiFriends: StateFlow<List<UiFriend>>
|
|
||||||
val uiOnlineFriends: StateFlow<List<UiFriend>>
|
|
||||||
val baseError: StateFlow<BaseError?>
|
val baseError: StateFlow<BaseError?>
|
||||||
val imagesToPreload: StateFlow<List<String>>
|
val imagesToPreload: StateFlow<List<String>>
|
||||||
val currentOffset: StateFlow<Int>
|
val currentOffset: StateFlow<Int>
|
||||||
val canPaginate: StateFlow<Boolean>
|
val canPaginate: StateFlow<Boolean>
|
||||||
|
|
||||||
fun onMetPaginationCondition()
|
fun onPaginationConditionsMet()
|
||||||
|
|
||||||
fun onRefresh()
|
fun onRefresh()
|
||||||
|
|
||||||
@@ -46,11 +39,6 @@ class FriendsViewModelImpl(
|
|||||||
|
|
||||||
override val screenState = MutableStateFlow(FriendsScreenState.EMPTY)
|
override val screenState = MutableStateFlow(FriendsScreenState.EMPTY)
|
||||||
|
|
||||||
override val uiFriends = screenState.map { it.friends }
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
|
||||||
|
|
||||||
override val uiOnlineFriends = MutableStateFlow<List<UiFriend>>(emptyList())
|
|
||||||
|
|
||||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||||
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
|
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
|
||||||
override val currentOffset = MutableStateFlow(0)
|
override val currentOffset = MutableStateFlow(0)
|
||||||
@@ -64,7 +52,7 @@ class FriendsViewModelImpl(
|
|||||||
loadFriends()
|
loadFriends()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMetPaginationCondition() {
|
override fun onPaginationConditionsMet() {
|
||||||
currentOffset.update { screenState.value.friends.size }
|
currentOffset.update { screenState.value.friends.size }
|
||||||
loadFriends()
|
loadFriends()
|
||||||
}
|
}
|
||||||
@@ -130,19 +118,19 @@ class FriendsViewModelImpl(
|
|||||||
if (offset == 0) {
|
if (offset == 0) {
|
||||||
friends.emit(response)
|
friends.emit(response)
|
||||||
screenState.setValue {
|
screenState.setValue {
|
||||||
newState.copy(friends = loadedFriends)
|
newState.copy(
|
||||||
|
friends = loadedFriends,
|
||||||
|
onlineFriends = loadedOnlineFriends
|
||||||
|
)
|
||||||
}
|
}
|
||||||
uiOnlineFriends.setValue { loadedOnlineFriends }
|
|
||||||
} else {
|
} else {
|
||||||
friends.emit(friends.value.plus(response))
|
friends.emit(friends.value.plus(response))
|
||||||
screenState.setValue {
|
screenState.setValue {
|
||||||
newState.copy(
|
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)
|
conversation.asPresentation(useContactNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
val onlineUiFriends = uiOnlineFriends.value.mapNotNull { friend ->
|
val onlineUiFriends = screenState.value.onlineFriends.mapNotNull { friend ->
|
||||||
uiFriends.find { it.userId == friend.userId }
|
uiFriends.find { it.userId == friend.userId }
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(friends = uiFriends)
|
old.copy(
|
||||||
|
friends = uiFriends,
|
||||||
|
onlineFriends = onlineUiFriends
|
||||||
|
)
|
||||||
}
|
}
|
||||||
uiOnlineFriends.setValue { onlineUiFriends }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val LOAD_COUNT = 30
|
const val LOAD_COUNT = 60
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -6,6 +6,7 @@ import androidx.compose.runtime.Immutable
|
|||||||
data class FriendsScreenState(
|
data class FriendsScreenState(
|
||||||
val isLoading: Boolean,
|
val isLoading: Boolean,
|
||||||
val friends: List<UiFriend>,
|
val friends: List<UiFriend>,
|
||||||
|
val onlineFriends: List<UiFriend>,
|
||||||
val isPaginating: Boolean,
|
val isPaginating: Boolean,
|
||||||
val isPaginationExhausted: Boolean
|
val isPaginationExhausted: Boolean
|
||||||
) {
|
) {
|
||||||
@@ -14,6 +15,7 @@ data class FriendsScreenState(
|
|||||||
val EMPTY: FriendsScreenState = FriendsScreenState(
|
val EMPTY: FriendsScreenState = FriendsScreenState(
|
||||||
isLoading = true,
|
isLoading = true,
|
||||||
friends = emptyList(),
|
friends = emptyList(),
|
||||||
|
onlineFriends = emptyList(),
|
||||||
isPaginating = false,
|
isPaginating = false,
|
||||||
isPaginationExhausted = false
|
isPaginationExhausted = false
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -6,14 +6,14 @@ import androidx.navigation.compose.composable
|
|||||||
import com.meloda.app.fast.common.extensions.navigation.sharedViewModel
|
import com.meloda.app.fast.common.extensions.navigation.sharedViewModel
|
||||||
import com.meloda.app.fast.friends.FriendsViewModel
|
import com.meloda.app.fast.friends.FriendsViewModel
|
||||||
import com.meloda.app.fast.friends.FriendsViewModelImpl
|
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 com.meloda.app.fast.model.BaseError
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
object Friends
|
object Friends
|
||||||
|
|
||||||
fun NavGraphBuilder.friendsRoute(
|
fun NavGraphBuilder.friendsScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
@@ -21,7 +21,7 @@ fun NavGraphBuilder.friendsRoute(
|
|||||||
val viewModel: FriendsViewModel =
|
val viewModel: FriendsViewModel =
|
||||||
it.sharedViewModel<FriendsViewModelImpl>(navController = navController)
|
it.sharedViewModel<FriendsViewModelImpl>(navController = navController)
|
||||||
|
|
||||||
FriendsScreen(
|
FriendsRoute(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
+37
-14
@@ -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.designsystem.components.NoItemsView
|
||||||
import com.meloda.app.fast.friends.FriendsViewModel
|
import com.meloda.app.fast.friends.FriendsViewModel
|
||||||
import com.meloda.app.fast.friends.FriendsViewModelImpl
|
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.model.BaseError
|
||||||
import com.meloda.app.fast.ui.ErrorView
|
import com.meloda.app.fast.ui.ErrorView
|
||||||
import dev.chrisbanes.haze.haze
|
import dev.chrisbanes.haze.haze
|
||||||
@@ -67,17 +68,19 @@ import dev.chrisbanes.haze.materials.HazeMaterials
|
|||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FriendsScreen(
|
fun FriendsRoute(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
|
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||||
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
|
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle()
|
val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle()
|
||||||
|
LaunchedEffect(imagesToPreload) {
|
||||||
imagesToPreload.forEach { url ->
|
imagesToPreload.forEach { url ->
|
||||||
context.imageLoader.enqueue(
|
context.imageLoader.enqueue(
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
@@ -85,12 +88,33 @@ fun FriendsScreen(
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
FriendsScreen(
|
||||||
val friends by viewModel.uiFriends.collectAsStateWithLifecycle()
|
screenState = screenState,
|
||||||
val onlineFriends by viewModel.uiOnlineFriends.collectAsStateWithLifecycle()
|
baseError = baseError,
|
||||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
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 currentTheme = LocalTheme.current
|
||||||
|
|
||||||
val maxLines by remember {
|
val maxLines by remember {
|
||||||
@@ -111,7 +135,7 @@ fun FriendsScreen(
|
|||||||
|
|
||||||
LaunchedEffect(paginationConditionMet) {
|
LaunchedEffect(paginationConditionMet) {
|
||||||
if (paginationConditionMet && !screenState.isPaginating) {
|
if (paginationConditionMet && !screenState.isPaginating) {
|
||||||
viewModel.onMetPaginationCondition()
|
onPaginationConditionsMet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,11 +247,11 @@ fun FriendsScreen(
|
|||||||
ErrorView(
|
ErrorView(
|
||||||
text = "Session expired",
|
text = "Session expired",
|
||||||
buttonText = "Log out",
|
buttonText = "Log out",
|
||||||
onButtonClick = { onError(BaseError.SessionExpired) }
|
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.isLoading && friends.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val pullToRefreshState = rememberPullToRefreshState()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
@@ -259,8 +283,7 @@ fun FriendsScreen(
|
|||||||
.padding(bottom = padding.calculateBottomPadding())
|
.padding(bottom = padding.calculateBottomPadding())
|
||||||
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
val friendsToDisplay = if (index == 0) friends
|
val friendsToDisplay = screenState.friends
|
||||||
else onlineFriends
|
|
||||||
|
|
||||||
FriendsList(
|
FriendsList(
|
||||||
modifier = if (currentTheme.usingBlur) {
|
modifier = if (currentTheme.usingBlur) {
|
||||||
@@ -289,7 +312,7 @@ fun FriendsScreen(
|
|||||||
|
|
||||||
if (pullToRefreshState.isRefreshing) {
|
if (pullToRefreshState.isRefreshing) {
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
viewModel.onRefresh()
|
onRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-12
@@ -1,9 +1,13 @@
|
|||||||
package com.meloda.app.fast.languagepicker
|
package com.meloda.app.fast.languagepicker
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.lifecycle.ViewModel
|
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.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.LanguagePickerScreenState
|
||||||
import com.meloda.app.fast.languagepicker.model.SelectableLanguage
|
import com.meloda.app.fast.languagepicker.model.SelectableLanguage
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -12,25 +16,54 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
interface LanguagePickerViewModel {
|
interface LanguagePickerViewModel {
|
||||||
val screenState: StateFlow<LanguagePickerScreenState>
|
val screenState: StateFlow<LanguagePickerScreenState>
|
||||||
|
|
||||||
fun setLanguages(languages: List<SelectableLanguage>)
|
|
||||||
|
|
||||||
fun onLanguagePicked(newLanguage: SelectableLanguage)
|
fun onLanguagePicked(newLanguage: SelectableLanguage)
|
||||||
|
|
||||||
fun onApplyButtonClicked()
|
fun onApplyButtonClicked()
|
||||||
|
|
||||||
fun updateCurrentLocale(locale: String)
|
fun updateCurrentLocale(locale: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
class LanguagePickerViewModelImpl : LanguagePickerViewModel, ViewModel() {
|
class LanguagePickerViewModelImpl(
|
||||||
|
private val resources: Resources
|
||||||
|
) : LanguagePickerViewModel, ViewModel() {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(
|
override val screenState = MutableStateFlow(LanguagePickerScreenState.EMPTY)
|
||||||
LanguagePickerScreenState(
|
|
||||||
languages = emptyList(),
|
init {
|
||||||
currentLanguage = AppCompatDelegate.getApplicationLocales().toLanguageTags()
|
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<SelectableLanguage>) {
|
|
||||||
screenState.setValue { old -> old.copy(languages = languages) }
|
screenState.setValue { old -> old.copy(languages = languages) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
@@ -6,4 +6,12 @@ import androidx.compose.runtime.Immutable
|
|||||||
data class LanguagePickerScreenState(
|
data class LanguagePickerScreenState(
|
||||||
val languages: List<SelectableLanguage>,
|
val languages: List<SelectableLanguage>,
|
||||||
val currentLanguage: String?,
|
val currentLanguage: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val EMPTY: LanguagePickerScreenState = LanguagePickerScreenState(
|
||||||
|
languages = emptyList(),
|
||||||
|
currentLanguage = null
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+22
@@ -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<LanguagePicker> {
|
||||||
|
LanguagePickerRoute(onBack = onBack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavController.navigateToLanguagePicker() {
|
||||||
|
this.navigate(LanguagePicker)
|
||||||
|
}
|
||||||
-78
@@ -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<SelectableLanguage> {
|
|
||||||
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<LanguagePicker> {
|
|
||||||
val languages = getLanguages(LocalContext.current.resources)
|
|
||||||
|
|
||||||
val viewModel: LanguagePickerViewModel = koinViewModel<LanguagePickerViewModelImpl>()
|
|
||||||
viewModel.setLanguages(languages)
|
|
||||||
|
|
||||||
LanguagePickerScreen(
|
|
||||||
onBack = onBack,
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavController.navigateToLanguagePicker() {
|
|
||||||
this.navigate(LanguagePicker)
|
|
||||||
}
|
|
||||||
+30
-12
@@ -56,31 +56,46 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.meloda.app.fast.languagepicker.LanguagePickerViewModel
|
import com.meloda.app.fast.languagepicker.LanguagePickerViewModel
|
||||||
import com.meloda.app.fast.languagepicker.LanguagePickerViewModelImpl
|
import com.meloda.app.fast.languagepicker.LanguagePickerViewModelImpl
|
||||||
|
import com.meloda.app.fast.languagepicker.model.LanguagePickerScreenState
|
||||||
import com.meloda.app.fast.languagepicker.model.SelectableLanguage
|
import com.meloda.app.fast.languagepicker.model.SelectableLanguage
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LanguagePickerScreen(
|
fun LanguagePickerRoute(
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
viewModel: LanguagePickerViewModel = koinViewModel<LanguagePickerViewModelImpl>()
|
viewModel: LanguagePickerViewModel = koinViewModel<LanguagePickerViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
|
||||||
val languages = screenState.languages
|
|
||||||
|
|
||||||
LifecycleResumeEffect(true) {
|
LifecycleResumeEffect(true) {
|
||||||
viewModel.updateCurrentLocale(AppCompatDelegate.getApplicationLocales().toLanguageTags())
|
viewModel.updateCurrentLocale(AppCompatDelegate.getApplicationLocales().toLanguageTags())
|
||||||
|
|
||||||
onPauseOrDispose {}
|
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) {
|
val isButtonEnabled by remember(screenState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
screenState.currentLanguage != null &&
|
screenState.currentLanguage != null &&
|
||||||
languages.isNotEmpty() &&
|
screenState.languages.isNotEmpty() &&
|
||||||
languages.find(SelectableLanguage::isSelected)?.key != screenState.currentLanguage
|
screenState.languages.find(SelectableLanguage::isSelected)?.key != screenState.currentLanguage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +180,13 @@ fun LanguagePickerScreen(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
items(screenState.languages.toList()) { item ->
|
items(
|
||||||
|
items = screenState.languages.toList(),
|
||||||
|
key = SelectableLanguage::key
|
||||||
|
) { item ->
|
||||||
LanguageItem(
|
LanguageItem(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = viewModel::onLanguagePicked
|
onClick = onLanguagePicked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +201,7 @@ fun LanguagePickerScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = viewModel::onApplyButtonClicked,
|
onClick = onApplyButtonClicked,
|
||||||
enabled = isButtonEnabled,
|
enabled = isButtonEnabled,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
+21
-65
@@ -4,6 +4,7 @@ import android.content.SharedPreferences
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.conena.nanokt.collections.indexOfFirstOrNull
|
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.data.processState
|
||||||
import com.meloda.app.fast.datastore.SettingsKeys
|
import com.meloda.app.fast.datastore.SettingsKeys
|
||||||
import com.meloda.app.fast.messageshistory.model.ActionMode
|
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.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.asPresentation
|
||||||
import com.meloda.app.fast.messageshistory.util.extractAvatar
|
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.messageshistory.util.extractTitle
|
||||||
import com.meloda.app.fast.model.BaseError
|
import com.meloda.app.fast.model.BaseError
|
||||||
import com.meloda.app.fast.model.LongPollEvent
|
import com.meloda.app.fast.model.LongPollEvent
|
||||||
@@ -48,17 +48,14 @@ interface MessagesHistoryViewModel {
|
|||||||
|
|
||||||
val canPaginate: StateFlow<Boolean>
|
val canPaginate: StateFlow<Boolean>
|
||||||
|
|
||||||
|
fun onRefresh()
|
||||||
fun onAttachmentButtonClicked()
|
fun onAttachmentButtonClicked()
|
||||||
fun onInputChanged(newText: String)
|
fun onMessageInputChanged(newText: String)
|
||||||
fun onEmojiButtonClicked()
|
fun onEmojiButtonClicked()
|
||||||
fun onActionButtonClicked()
|
fun onActionButtonClicked()
|
||||||
fun onTopAppBarMenuClicked(id: Int)
|
|
||||||
fun setArguments(arguments: MessagesHistoryArguments)
|
|
||||||
|
|
||||||
fun onMetPaginationCondition()
|
fun onPaginationConditionsMet()
|
||||||
fun onShowDatesClicked(showDates: Boolean)
|
fun onToggleAnimationsDropdownItemClicked(enableAnimations: Boolean)
|
||||||
fun onShowNamesClicked(showNames: Boolean)
|
|
||||||
fun onEnableAnimationsClicked(enableAnimations: Boolean)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessagesHistoryViewModelImpl(
|
class MessagesHistoryViewModelImpl(
|
||||||
@@ -67,6 +64,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
private val preferences: SharedPreferences,
|
private val preferences: SharedPreferences,
|
||||||
private val resources: Resources,
|
private val resources: Resources,
|
||||||
updatesParser: LongPollUpdatesParser,
|
updatesParser: LongPollUpdatesParser,
|
||||||
|
savedStateHandle: SavedStateHandle
|
||||||
) : MessagesHistoryViewModel, ViewModel() {
|
) : MessagesHistoryViewModel, ViewModel() {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(MessagesHistoryScreenState.EMPTY)
|
override val screenState = MutableStateFlow(MessagesHistoryScreenState.EMPTY)
|
||||||
@@ -85,17 +83,26 @@ class MessagesHistoryViewModelImpl(
|
|||||||
private val sendingMessages: MutableList<VkMessage> = mutableListOf()
|
private val sendingMessages: MutableList<VkMessage> = mutableListOf()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
val arguments = MessagesHistory.from(savedStateHandle).arguments
|
||||||
|
|
||||||
|
screenState.setValue { old -> old.copy(conversationId = arguments.conversationId) }
|
||||||
|
loadMessagesHistory()
|
||||||
|
|
||||||
updatesParser.onNewMessage(::handleNewMessage)
|
updatesParser.onNewMessage(::handleNewMessage)
|
||||||
updatesParser.onMessageEdited(::handleEditedMessage)
|
updatesParser.onMessageEdited(::handleEditedMessage)
|
||||||
updatesParser.onMessageIncomingRead(::handleReadIncomingEvent)
|
updatesParser.onMessageIncomingRead(::handleReadIncomingEvent)
|
||||||
updatesParser.onMessageOutgoingRead(::handleReadOutgoingEvent)
|
updatesParser.onMessageOutgoingRead(::handleReadOutgoingEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRefresh() {
|
||||||
|
loadMessagesHistory(offset = 0)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttachmentButtonClicked() {
|
override fun onAttachmentButtonClicked() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInputChanged(newText: String) {
|
override fun onMessageInputChanged(newText: String) {
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
message = newText,
|
message = newText,
|
||||||
@@ -131,58 +138,12 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTopAppBarMenuClicked(id: Int) {
|
override fun onPaginationConditionsMet() {
|
||||||
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() {
|
|
||||||
currentOffset.update { screenState.value.messages.size }
|
currentOffset.update { screenState.value.messages.size }
|
||||||
loadMessagesHistory()
|
loadMessagesHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowDatesClicked(showDates: Boolean) {
|
override fun onToggleAnimationsDropdownItemClicked(enableAnimations: 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) {
|
|
||||||
preferences.edit {
|
preferences.edit {
|
||||||
putBoolean(
|
putBoolean(
|
||||||
SettingsKeys.KEY_ENABLE_ANIMATIONS_IN_MESSAGES,
|
SettingsKeys.KEY_ENABLE_ANIMATIONS_IN_MESSAGES,
|
||||||
@@ -285,15 +246,10 @@ class MessagesHistoryViewModelImpl(
|
|||||||
messagesUseCase.storeMessages(messages)
|
messagesUseCase.storeMessages(messages)
|
||||||
conversationsUseCase.storeConversations(conversations)
|
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 ->
|
val loadedMessages = fullMessages.mapIndexed { index, message ->
|
||||||
message.asPresentation(
|
message.asPresentation(
|
||||||
showDate = showDate,
|
showDate = false,
|
||||||
showName = showName,
|
showName = false,
|
||||||
prevMessage = messages.getOrNull(index + 1),
|
prevMessage = messages.getOrNull(index + 1),
|
||||||
nextMessage = messages.getOrNull(index - 1),
|
nextMessage = messages.getOrNull(index - 1),
|
||||||
)
|
)
|
||||||
|
|||||||
+43
@@ -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<MessagesHistoryArguments>() to customNavType<MessagesHistoryArguments>())
|
||||||
|
|
||||||
|
fun from(savedStateHandle: SavedStateHandle) =
|
||||||
|
savedStateHandle.toRoute<MessagesHistory>(typeMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.messagesHistoryScreen(
|
||||||
|
onError: (BaseError) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit
|
||||||
|
) {
|
||||||
|
composable<MessagesHistory>(typeMap = MessagesHistory.typeMap) {
|
||||||
|
MessagesHistoryRoute(
|
||||||
|
onError = onError,
|
||||||
|
onBack = onBack,
|
||||||
|
onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavController.navigateToMessagesHistory(conversationId: Int) {
|
||||||
|
this.navigate(MessagesHistory(MessagesHistoryArguments(conversationId)))
|
||||||
|
}
|
||||||
-64
@@ -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<MessagesHistoryArguments>(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<MessagesHistory>(
|
|
||||||
typeMap = mapOf(typeOf<MessagesHistoryArguments>() to MessagesHistoryNavType)
|
|
||||||
) { backStackEntry ->
|
|
||||||
val arguments: MessagesHistory = backStackEntry.toRoute()
|
|
||||||
|
|
||||||
val viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
|
|
||||||
viewModel.setArguments(arguments.arguments)
|
|
||||||
|
|
||||||
MessagesHistoryScreen(
|
|
||||||
onError = onError,
|
|
||||||
onBack = onBack,
|
|
||||||
onNavigateToChatMaterials = onNavigateToChatAttachments,
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavController.navigateToMessagesHistory(conversationId: Int) {
|
|
||||||
this.navigate(MessagesHistory(MessagesHistoryArguments(conversationId)))
|
|
||||||
}
|
|
||||||
+49
-58
@@ -71,6 +71,7 @@ import com.meloda.app.fast.designsystem.LocalTheme
|
|||||||
import com.meloda.app.fast.messageshistory.MessagesHistoryViewModel
|
import com.meloda.app.fast.messageshistory.MessagesHistoryViewModel
|
||||||
import com.meloda.app.fast.messageshistory.MessagesHistoryViewModelImpl
|
import com.meloda.app.fast.messageshistory.MessagesHistoryViewModelImpl
|
||||||
import com.meloda.app.fast.messageshistory.model.ActionMode
|
import com.meloda.app.fast.messageshistory.model.ActionMode
|
||||||
|
import com.meloda.app.fast.messageshistory.model.MessagesHistoryScreenState
|
||||||
import com.meloda.app.fast.model.BaseError
|
import com.meloda.app.fast.model.BaseError
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeChild
|
import dev.chrisbanes.haze.hazeChild
|
||||||
@@ -81,6 +82,32 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
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<MessagesHistoryViewModelImpl>()
|
||||||
|
) {
|
||||||
|
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(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
ExperimentalHazeMaterialsApi::class,
|
ExperimentalHazeMaterialsApi::class,
|
||||||
@@ -88,21 +115,23 @@ import com.meloda.app.fast.designsystem.R as UiR
|
|||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesHistoryScreen(
|
fun MessagesHistoryScreen(
|
||||||
onError: (BaseError) -> Unit,
|
screenState: MessagesHistoryScreenState = MessagesHistoryScreenState.EMPTY,
|
||||||
onBack: () -> Unit,
|
baseError: BaseError? = null,
|
||||||
onNavigateToChatMaterials: (peerId: Int, conversationMessageId: Int) -> Unit,
|
canPaginate: Boolean = false,
|
||||||
viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
|
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 view = LocalView.current
|
||||||
|
|
||||||
val preferences: SharedPreferences = koinInject()
|
val preferences: SharedPreferences = koinInject()
|
||||||
val currentTheme = LocalTheme.current
|
val currentTheme = LocalTheme.current
|
||||||
|
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
|
||||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
val messages = screenState.messages
|
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
val paginationConditionMet by remember {
|
val paginationConditionMet by remember {
|
||||||
@@ -115,7 +144,7 @@ fun MessagesHistoryScreen(
|
|||||||
|
|
||||||
LaunchedEffect(paginationConditionMet) {
|
LaunchedEffect(paginationConditionMet) {
|
||||||
if (paginationConditionMet && !screenState.isPaginating) {
|
if (paginationConditionMet && !screenState.isPaginating) {
|
||||||
viewModel.onMetPaginationCondition()
|
onPaginationConditionsMet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,24 +154,6 @@ fun MessagesHistoryScreen(
|
|||||||
|
|
||||||
val hazeSate = remember { HazeState() }
|
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 {
|
var animationsEnabled by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
preferences.getBoolean(
|
preferences.getBoolean(
|
||||||
@@ -217,7 +228,8 @@ fun MessagesHistoryScreen(
|
|||||||
dropDownMenuExpanded = false
|
dropDownMenuExpanded = false
|
||||||
|
|
||||||
// TODO: 11/07/2024, Danil Nikolaev: to VM
|
// TODO: 11/07/2024, Danil Nikolaev: to VM
|
||||||
onNavigateToChatMaterials(
|
|
||||||
|
onChatMaterialsDropdownItemClicked(
|
||||||
screenState.conversationId,
|
screenState.conversationId,
|
||||||
screenState.messages.first().conversationMessageId
|
screenState.messages.first().conversationMessageId
|
||||||
)
|
)
|
||||||
@@ -228,7 +240,7 @@ fun MessagesHistoryScreen(
|
|||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onTopAppBarMenuClicked(0)
|
onRefreshDropdownItemClicked()
|
||||||
dropDownMenuExpanded = false
|
dropDownMenuExpanded = false
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
@@ -248,27 +260,6 @@ fun MessagesHistoryScreen(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
HorizontalDivider()
|
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(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
@@ -277,14 +268,14 @@ fun MessagesHistoryScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
dropDownMenuExpanded = false
|
dropDownMenuExpanded = false
|
||||||
animationsEnabled = !animationsEnabled
|
animationsEnabled = !animationsEnabled
|
||||||
viewModel.onEnableAnimationsClicked(animationsEnabled)
|
onToggleAnimationsDropdownItemClicked(animationsEnabled)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (screenState.isLoading && messages.isNotEmpty()) {
|
if (screenState.isLoading && screenState.messages.isNotEmpty()) {
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,7 +291,7 @@ fun MessagesHistoryScreen(
|
|||||||
MessagesList(
|
MessagesList(
|
||||||
hazeState = hazeSate,
|
hazeState = hazeSate,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
immutableMessages = ImmutableList.copyOf(messages),
|
immutableMessages = ImmutableList.copyOf(screenState.messages),
|
||||||
isPaginating = screenState.isPaginating,
|
isPaginating = screenState.isPaginating,
|
||||||
enableAnimations = animationsEnabled
|
enableAnimations = animationsEnabled
|
||||||
)
|
)
|
||||||
@@ -372,7 +363,7 @@ fun MessagesHistoryScreen(
|
|||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
value = screenState.message,
|
value = screenState.message,
|
||||||
onValueChange = viewModel::onInputChanged,
|
onValueChange = onMessageInputChanged,
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
@@ -382,7 +373,7 @@ fun MessagesHistoryScreen(
|
|||||||
placeholder = { Text(text = stringResource(id = UiR.string.message_input_hint)) }
|
placeholder = { Text(text = stringResource(id = UiR.string.message_input_hint)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
IconButton(onClick = viewModel::onAttachmentButtonClicked) {
|
IconButton(onClick = onAttachmentButtonClicked) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = UiR.drawable.round_attach_file_24),
|
painter = painterResource(id = UiR.drawable.round_attach_file_24),
|
||||||
contentDescription = "Add attachment button",
|
contentDescription = "Add attachment button",
|
||||||
@@ -414,7 +405,7 @@ fun MessagesHistoryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
viewModel.onActionButtonClicked()
|
onActionButtonClicked()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.rotate(rotation.value)
|
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))
|
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ import com.meloda.app.fast.common.UserConfig
|
|||||||
import com.meloda.app.fast.common.VkConstants
|
import com.meloda.app.fast.common.VkConstants
|
||||||
import com.meloda.app.fast.common.extensions.listenValue
|
import com.meloda.app.fast.common.extensions.listenValue
|
||||||
import com.meloda.app.fast.common.extensions.setValue
|
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.api.users.UsersUseCase
|
||||||
import com.meloda.app.fast.data.processState
|
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 com.meloda.app.fast.profile.model.ProfileScreenState
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface ProfileViewModel {
|
interface ProfileViewModel {
|
||||||
val screenState: StateFlow<ProfileScreenState>
|
val screenState: StateFlow<ProfileScreenState>
|
||||||
|
val baseError: StateFlow<BaseError?>
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProfileViewModelImpl(
|
class ProfileViewModelImpl(
|
||||||
@@ -20,6 +24,7 @@ class ProfileViewModelImpl(
|
|||||||
) : ViewModel(), ProfileViewModel {
|
) : ViewModel(), ProfileViewModel {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(ProfileScreenState.EMPTY)
|
override val screenState = MutableStateFlow(ProfileScreenState.EMPTY)
|
||||||
|
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getLocalAccountInfo()
|
getLocalAccountInfo()
|
||||||
@@ -30,6 +35,16 @@ class ProfileViewModelImpl(
|
|||||||
.listenValue { state ->
|
.listenValue { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = { error ->
|
error = { error ->
|
||||||
|
if (error is State.Error.ApiError) {
|
||||||
|
when (error.errorCode) {
|
||||||
|
VkErrorCodes.UserAuthorizationFailed -> {
|
||||||
|
baseError.setValue { BaseError.SessionExpired }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
|
|||||||
+5
-5
@@ -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.model.BaseError
|
||||||
import com.meloda.app.fast.profile.ProfileViewModel
|
import com.meloda.app.fast.profile.ProfileViewModel
|
||||||
import com.meloda.app.fast.profile.ProfileViewModelImpl
|
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
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
object Profile
|
object Profile
|
||||||
|
|
||||||
fun NavGraphBuilder.profileRoute(
|
fun NavGraphBuilder.profileScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onNavigateToSettings: () -> Unit,
|
onSettingsButtonClicked: () -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
composable<Profile> {
|
composable<Profile> {
|
||||||
val viewModel: ProfileViewModel =
|
val viewModel: ProfileViewModel =
|
||||||
it.sharedViewModel<ProfileViewModelImpl>(navController = navController)
|
it.sharedViewModel<ProfileViewModelImpl>(navController = navController)
|
||||||
|
|
||||||
ProfileScreen(
|
ProfileRoute(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
onNavigateToSettings = onNavigateToSettings,
|
onSettingsButtonClicked = onSettingsButtonClicked,
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-6
@@ -1,6 +1,5 @@
|
|||||||
package com.meloda.app.fast.profile.presentation
|
package com.meloda.app.fast.profile.presentation
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.model.BaseError
|
||||||
import com.meloda.app.fast.profile.ProfileViewModel
|
import com.meloda.app.fast.profile.ProfileViewModel
|
||||||
import com.meloda.app.fast.profile.ProfileViewModelImpl
|
import com.meloda.app.fast.profile.ProfileViewModelImpl
|
||||||
|
import com.meloda.app.fast.profile.model.ProfileScreenState
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(
|
fun ProfileRoute(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onNavigateToSettings: () -> Unit,
|
onSettingsButtonClicked: () -> Unit,
|
||||||
viewModel: ProfileViewModel = koinViewModel<ProfileViewModelImpl>()
|
viewModel: ProfileViewModel = koinViewModel<ProfileViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {},
|
title = {},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onNavigateToSettings) {
|
IconButton(onClick = onSettingsButtonClicked) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Settings,
|
imageVector = Icons.Rounded.Settings,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
|
|||||||
@@ -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.data.db.AccountsRepository
|
||||||
import com.meloda.app.fast.datastore.SettingsController
|
import com.meloda.app.fast.datastore.SettingsController
|
||||||
import com.meloda.app.fast.datastore.SettingsKeys
|
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.datastore.isDebugSettingsShown
|
||||||
import com.meloda.app.fast.model.database.AccountEntity
|
import com.meloda.app.fast.model.database.AccountEntity
|
||||||
import com.meloda.app.fast.settings.model.SettingsItem
|
import com.meloda.app.fast.settings.model.SettingsItem
|
||||||
@@ -44,13 +45,14 @@ interface SettingsViewModel {
|
|||||||
fun onSettingsItemLongClicked(key: String)
|
fun onSettingsItemLongClicked(key: String)
|
||||||
fun onSettingsItemChanged(key: String, newValue: Any?)
|
fun onSettingsItemChanged(key: String, newValue: Any?)
|
||||||
|
|
||||||
fun onHapticsUsed()
|
fun onHapticPerformed()
|
||||||
|
|
||||||
fun onNotificationsPermissionRequested()
|
fun onNotificationsPermissionRequested()
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsViewModelImpl(
|
class SettingsViewModelImpl(
|
||||||
private val accountsRepository: AccountsRepository,
|
private val accountsRepository: AccountsRepository,
|
||||||
|
private val userSettings: UserSettings
|
||||||
) : SettingsViewModel, ViewModel() {
|
) : SettingsViewModel, ViewModel() {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
|
override val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
|
||||||
@@ -159,6 +161,7 @@ class SettingsViewModelImpl(
|
|||||||
when (key) {
|
when (key) {
|
||||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> {
|
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> {
|
||||||
val isEnabled = (newValue as? Boolean) == true
|
val isEnabled = (newValue as? Boolean) == true
|
||||||
|
userSettings.setLongPollBackground(isEnabled)
|
||||||
|
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
// TODO: 26/11/2023, Danil Nikolaev: implement
|
// 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) }
|
screenState.setValue { old -> old.copy(useHaptics = HapticType.None) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
@@ -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<Settings> {
|
||||||
|
SettingsRoute(
|
||||||
|
onBack = onBack,
|
||||||
|
onLogOutButtonClicked = onLogOutButtonClicked,
|
||||||
|
onLanguageItemClicked = onLanguageItemClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavController.navigateToSettings() {
|
||||||
|
this.navigate(Settings)
|
||||||
|
}
|
||||||
-30
@@ -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<Settings> {
|
|
||||||
SettingsScreen(
|
|
||||||
onError = onError,
|
|
||||||
onBack = onBack,
|
|
||||||
onNavigateToAuth = onNavigateToAuth,
|
|
||||||
onNavigateToLanguagePicker = onNavigateToLanguagePicker
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavController.navigateToSettings() {
|
|
||||||
this.navigate(Settings)
|
|
||||||
}
|
|
||||||
+84
-129
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -23,7 +24,6 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
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.datastore.isUsingDarkMode
|
||||||
import com.meloda.app.fast.designsystem.LocalTheme
|
import com.meloda.app.fast.designsystem.LocalTheme
|
||||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
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.HapticType
|
||||||
import com.meloda.app.fast.settings.SettingsViewModel
|
import com.meloda.app.fast.settings.SettingsViewModel
|
||||||
import com.meloda.app.fast.settings.SettingsViewModelImpl
|
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.SettingsItem
|
||||||
import com.meloda.app.fast.settings.model.SettingsScreenState
|
import com.meloda.app.fast.settings.model.SettingsScreenState
|
||||||
import com.meloda.app.fast.settings.presentation.items.EditTextSettingsItem
|
import com.meloda.app.fast.settings.presentation.items.EditTextSettingsItem
|
||||||
@@ -64,57 +60,39 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@OptIn(
|
|
||||||
ExperimentalMaterial3Api::class,
|
|
||||||
ExperimentalHazeMaterialsApi::class
|
|
||||||
)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsRoute(
|
||||||
onError: (BaseError) -> Unit,
|
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onNavigateToAuth: () -> Unit,
|
onLogOutButtonClicked: () -> Unit,
|
||||||
onNavigateToLanguagePicker: () -> Unit,
|
onLanguageItemClicked: () -> Unit,
|
||||||
viewModel: SettingsViewModel = koinViewModel<SettingsViewModelImpl>()
|
viewModel: SettingsViewModel = koinViewModel<SettingsViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val view = LocalView.current
|
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val hapticType = screenState.useHaptics
|
|
||||||
if (hapticType != HapticType.None) {
|
|
||||||
view.performHapticFeedback(hapticType.getHaptic())
|
|
||||||
viewModel.onHapticsUsed()
|
|
||||||
}
|
|
||||||
|
|
||||||
val userSettings: UserSettings = koinInject()
|
val userSettings: UserSettings = koinInject()
|
||||||
|
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
userSettings.enableDebugSettings(screenState.showDebugOptions)
|
userSettings.enableDebugSettings(screenState.showDebugOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentTheme = LocalTheme.current
|
SettingsScreen(screenState = screenState,
|
||||||
val settingsList = screenState.settings
|
onBack = onBack,
|
||||||
|
onHapticPerformed = viewModel::onHapticPerformed,
|
||||||
val clickListener = OnSettingsClickListener { key ->
|
onSettingsItemClicked = { key ->
|
||||||
when (key) {
|
when (key) {
|
||||||
SettingsKeys.KEY_APPEARANCE_LANGUAGE -> {
|
SettingsKeys.KEY_APPEARANCE_LANGUAGE -> {
|
||||||
onNavigateToLanguagePicker()
|
onLanguageItemClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> viewModel.onSettingsItemClicked(key)
|
else -> viewModel.onSettingsItemClicked(key)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
onSettingsItemLongClicked = viewModel::onSettingsItemLongClicked,
|
||||||
val longClickListener = OnSettingsLongClickListener(viewModel::onSettingsItemLongClicked)
|
onSettingsItemValueChanged = { key, newValue ->
|
||||||
val changeListener = OnSettingsChangeListener { key, newValue ->
|
|
||||||
when (key) {
|
when (key) {
|
||||||
SettingsKeys.KEY_APPEARANCE_MULTILINE -> {
|
|
||||||
val isUsing = newValue as? Boolean ?: false
|
|
||||||
userSettings.useMultiline(isUsing)
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsKeys.KEY_APPEARANCE_DARK_THEME -> {
|
SettingsKeys.KEY_APPEARANCE_DARK_THEME -> {
|
||||||
val newMode = newValue as? Int ?: return@OnSettingsChangeListener
|
val newMode = newValue as? Int ?: 0
|
||||||
AppCompatDelegate.setDefaultNightMode(newMode)
|
AppCompatDelegate.setDefaultNightMode(newMode)
|
||||||
|
|
||||||
val isUsing = context.getSystemService<PowerManager>()?.let { manager ->
|
val isUsing = context.getSystemService<PowerManager>()?.let { manager ->
|
||||||
@@ -127,39 +105,49 @@ fun SettingsScreen(
|
|||||||
userSettings.useDarkThemeChanged(isUsing)
|
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)
|
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 hazeState = remember { HazeState() }
|
val hazeState = remember { HazeState() }
|
||||||
|
|
||||||
@@ -167,19 +155,16 @@ fun SettingsScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentWindowInsets = WindowInsets.statusBars,
|
contentWindowInsets = WindowInsets.statusBars,
|
||||||
topBar = {
|
topBar = {
|
||||||
val title = @Composable { Text(text = stringResource(id = UiR.string.title_settings)) }
|
TopAppBar(
|
||||||
val navigationIcon = @Composable {
|
title = { Text(text = stringResource(id = UiR.string.title_settings)) },
|
||||||
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = UiR.drawable.ic_round_arrow_back_24),
|
painter = painterResource(id = UiR.drawable.ic_round_arrow_back_24),
|
||||||
contentDescription = "Back button"
|
contentDescription = "Back button"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
TopAppBar(
|
|
||||||
title = title,
|
|
||||||
navigationIcon = navigationIcon,
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface.copy(
|
containerColor = MaterialTheme.colorScheme.surface.copy(
|
||||||
alpha = if (currentTheme.usingBlur) 0f else 1f
|
alpha = if (currentTheme.usingBlur) 0f else 1f
|
||||||
@@ -215,33 +200,23 @@ fun SettingsScreen(
|
|||||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
|
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
|
||||||
.padding(bottom = padding.calculateBottomPadding())
|
.padding(bottom = padding.calculateBottomPadding())
|
||||||
) {
|
) {
|
||||||
items(
|
item {
|
||||||
count = settingsList.size,
|
|
||||||
// key = { index ->
|
|
||||||
// val item = settingsList[index]
|
|
||||||
// requireNotNull(item.title ?: item.summary)
|
|
||||||
// },
|
|
||||||
contentType = { index ->
|
|
||||||
when (settingsList[index]) {
|
|
||||||
is SettingsItem.ListItem -> "listitem"
|
|
||||||
is SettingsItem.Switch -> "switch"
|
|
||||||
is SettingsItem.TextField -> "textfield"
|
|
||||||
is SettingsItem.Title -> "title"
|
|
||||||
is SettingsItem.TitleSummary -> "titlesummary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { index ->
|
|
||||||
val needToShowSpacer by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
index == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needToShowSpacer) {
|
|
||||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||||
}
|
}
|
||||||
|
items(
|
||||||
when (val item = settingsList[index]) {
|
items = screenState.settings,
|
||||||
|
key = { item -> item.key },
|
||||||
|
contentType = { item ->
|
||||||
|
when (item) {
|
||||||
|
is SettingsItem.ListItem -> "list_item"
|
||||||
|
is SettingsItem.Switch -> "switch"
|
||||||
|
is SettingsItem.TextField -> "text_field"
|
||||||
|
is SettingsItem.Title -> "title"
|
||||||
|
is SettingsItem.TitleSummary -> "title_summary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { item ->
|
||||||
|
when (item) {
|
||||||
is SettingsItem.Title -> TitleSettingsItem(
|
is SettingsItem.Title -> TitleSettingsItem(
|
||||||
item = item,
|
item = item,
|
||||||
isMultiline = currentTheme.multiline,
|
isMultiline = currentTheme.multiline,
|
||||||
@@ -251,67 +226,47 @@ fun SettingsScreen(
|
|||||||
is SettingsItem.TitleSummary -> TitleSummarySettingsItem(
|
is SettingsItem.TitleSummary -> TitleSummarySettingsItem(
|
||||||
item = item,
|
item = item,
|
||||||
isMultiline = currentTheme.multiline,
|
isMultiline = currentTheme.multiline,
|
||||||
onSettingsClickListener = clickListener,
|
onSettingsClickListener = onSettingsItemClicked,
|
||||||
onSettingsLongClickListener = longClickListener,
|
onSettingsLongClickListener = onSettingsItemLongClicked,
|
||||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsItem.Switch -> SwitchSettingsItem(
|
is SettingsItem.Switch -> SwitchSettingsItem(
|
||||||
item = item,
|
item = item,
|
||||||
isMultiline = currentTheme.multiline,
|
isMultiline = currentTheme.multiline,
|
||||||
onSettingsClickListener = clickListener,
|
onSettingsClickListener = onSettingsItemClicked,
|
||||||
onSettingsLongClickListener = longClickListener,
|
onSettingsLongClickListener = onSettingsItemLongClicked,
|
||||||
onSettingsChangeListener = changeListener,
|
onSettingsChangeListener = onSettingsItemValueChanged,
|
||||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsItem.TextField -> EditTextSettingsItem(
|
is SettingsItem.TextField -> EditTextSettingsItem(
|
||||||
item = item,
|
item = item,
|
||||||
isMultiline = currentTheme.multiline,
|
isMultiline = currentTheme.multiline,
|
||||||
onSettingsClickListener = clickListener,
|
onSettingsClickListener = onSettingsItemClicked,
|
||||||
onSettingsLongClickListener = longClickListener,
|
onSettingsLongClickListener = onSettingsItemLongClicked,
|
||||||
onSettingsChangeListener = changeListener,
|
onSettingsChangeListener = onSettingsItemValueChanged,
|
||||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsItem.ListItem -> ListSettingsItem(
|
is SettingsItem.ListItem -> ListSettingsItem(
|
||||||
item = item,
|
item = item,
|
||||||
isMultiline = currentTheme.multiline,
|
isMultiline = currentTheme.multiline,
|
||||||
onSettingsClickListener = clickListener,
|
onSettingsClickListener = onSettingsItemClicked,
|
||||||
onSettingsLongClickListener = longClickListener,
|
onSettingsLongClickListener = onSettingsItemLongClicked,
|
||||||
onSettingsChangeListener = changeListener,
|
onSettingsChangeListener = onSettingsItemValueChanged,
|
||||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val showBottomNavigationBarsSpacer by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
index == settingsList.size - 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showBottomNavigationBarsSpacer) {
|
item {
|
||||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
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
|
@Composable
|
||||||
fun HandlePopups(
|
fun HandlePopups(
|
||||||
performCrashPositiveClick: () -> Unit,
|
performCrashPositiveClick: () -> Unit,
|
||||||
|
|||||||
+36
-18
@@ -7,16 +7,18 @@ kotlin = "2.0.0"
|
|||||||
ksp = "2.0.0-1.0.22"
|
ksp = "2.0.0-1.0.22"
|
||||||
vkompose = "0.5.4-k2"
|
vkompose = "0.5.4-k2"
|
||||||
|
|
||||||
|
|
||||||
|
compose-bom = "2024.06.00"
|
||||||
|
koin = "3.5.6"
|
||||||
|
|
||||||
accompanist = "0.34.0"
|
accompanist = "0.34.0"
|
||||||
coil = "2.6.0"
|
coil = "2.6.0"
|
||||||
compose-bom = "2024.06.00"
|
|
||||||
coroutines = "1.9.0-RC"
|
coroutines = "1.9.0-RC"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
chucker = "4.0.0"
|
chucker = "4.0.0"
|
||||||
guava = "33.2.1-jre"
|
guava = "33.2.1-jre"
|
||||||
lifecycle = "2.8.3"
|
lifecycle = "2.8.3"
|
||||||
core-ktx = "1.13.1"
|
core-ktx = "1.13.1"
|
||||||
koin = "3.5.6"
|
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
loggingInterceptor = "5.0.0-alpha.14"
|
loggingInterceptor = "5.0.0-alpha.14"
|
||||||
moshi = "1.15.1"
|
moshi = "1.15.1"
|
||||||
@@ -30,24 +32,13 @@ appcompat = "1.7.0"
|
|||||||
androidx-navigation = "2.8.0-beta05"
|
androidx-navigation = "2.8.0-beta05"
|
||||||
serialization = "1.7.1"
|
serialization = "1.7.1"
|
||||||
rebugger = "1.0.0-rc03"
|
rebugger = "1.0.0-rc03"
|
||||||
|
uiTooling = "1.6.8"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||||
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
||||||
coil-compose = { module = "io.coil-kt:coil-compose", 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" }
|
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" }
|
eithernet = { module = "com.slack.eithernet:eithernet", version.ref = "eithernet" }
|
||||||
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
|
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
|
||||||
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", 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-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||||
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-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" }
|
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" }
|
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||||
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
|
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
|
||||||
moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
|
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" }
|
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]
|
[bundles]
|
||||||
compose = [
|
compose = [
|
||||||
"compose-material3",
|
"compose-material3",
|
||||||
@@ -95,6 +107,13 @@ compose = [
|
|||||||
"compose-lifecycle-runtime",
|
"compose-lifecycle-runtime",
|
||||||
"compose-runtime-saveable"
|
"compose-runtime-saveable"
|
||||||
]
|
]
|
||||||
|
koin = [
|
||||||
|
"koin-core",
|
||||||
|
"koin-core-coroutines",
|
||||||
|
"koin-android",
|
||||||
|
"koin-androidx-compose",
|
||||||
|
"koin-androidx-compose-navigation"
|
||||||
|
]
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
com-android-application = { id = "com.android.application", version.ref = "agp" }
|
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" }
|
org-jetbrains-kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
com-vk-vkompose = { id = "com.vk.vkompose", version.ref = "vkompose" }
|
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ include(":feature:languagepicker")
|
|||||||
include(":feature:photoviewer")
|
include(":feature:photoviewer")
|
||||||
include(":feature:settings")
|
include(":feature:settings")
|
||||||
include(":feature:auth:login")
|
include(":feature:auth:login")
|
||||||
include(":feature:auth:twofa")
|
include(":feature:auth:validation")
|
||||||
include(":feature:auth:captcha")
|
include(":feature:auth:captcha")
|
||||||
include(":feature:auth:userbanned")
|
include(":feature:auth:userbanned")
|
||||||
include(":feature:friends")
|
include(":feature:friends")
|
||||||
|
|||||||
Reference in New Issue
Block a user