twoFa -> validation naming; fixes for preview for screens (separating view model from ui); some improvements & fixes

This commit is contained in:
2024-07-13 22:45:49 +03:00
parent dfdc48b682
commit 733627f935
98 changed files with 1611 additions and 1637 deletions
+1 -1
View File
@@ -20,7 +20,7 @@
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:name=".presentation.MainActivity"
android:exported="true"
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.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
interface MainViewModel {
val screenState: StateFlow<MainScreenState>
val isNeedToOpenAuth: StateFlow<Boolean>
val longPollState: StateFlow<LongPollState>
val startOnlineService: StateFlow<Boolean>
@@ -38,7 +40,7 @@ interface MainViewModel {
fun onError(error: BaseError)
fun onAuthOpened()
fun onNavigatedToAuth()
}
class MainViewModelImpl(
@@ -51,6 +53,7 @@ class MainViewModelImpl(
}
override val screenState = MutableStateFlow(MainScreenState.EMPTY)
override val isNeedToOpenAuth = MutableStateFlow(false)
override val longPollState = MutableStateFlow(
if (SettingsController.getBoolean(
@@ -109,13 +112,13 @@ class MainViewModelImpl(
override fun onError(error: BaseError) {
when (error) {
BaseError.SessionExpired -> {
screenState.setValue { old -> old.copy(isNeedToOpenAuth = true) }
isNeedToOpenAuth.update { true }
}
}
}
override fun onAuthOpened() {
screenState.setValue { old -> old.copy(isNeedToOpenAuth = false) }
override fun onNavigatedToAuth() {
isNeedToOpenAuth.update { false }
}
private fun loadAccounts() {
@@ -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 useDynamicColors: Boolean,
val isNeedToRequestNotifications: Boolean,
val isNeedToOpenAppPermissions: Boolean,
val isNeedToOpenAuth: Boolean,
val isNeedToOpenAppPermissions: Boolean
) {
companion object {
val EMPTY: MainScreenState = MainScreenState(
accounts = emptyList(),
accountsLoaded = false,
// TODO: 05/05/2024, Danil Nikolaev: implement
useDarkTheme = false,
useDynamicColors = false,
isNeedToRequestNotifications = false,
isNeedToOpenAppPermissions = false,
isNeedToOpenAuth = false,
isNeedToOpenAppPermissions = false
)
}
}
@@ -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,
)
}
}
@@ -1,4 +1,4 @@
package com.meloda.app.fast
package com.meloda.app.fast.presentation
import android.Manifest
import android.app.NotificationChannel
@@ -26,6 +26,7 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import com.meloda.app.fast.MainViewModel
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.extensions.ifEmpty
import com.meloda.app.fast.common.extensions.isSdkAtLeast
@@ -100,13 +101,15 @@ class MainActivity : AppCompatActivity() {
multiline = theme.multiline
)
) {
val currentTheme = LocalTheme.current
AppTheme(
useDarkTheme = LocalTheme.current.usingDarkStyle,
useDynamicColors = LocalTheme.current.usingDynamicColors,
selectedColorScheme = LocalTheme.current.selectedColorScheme,
useAmoledBackground = LocalTheme.current.usingAmoledBackground,
useDarkTheme = currentTheme.usingDarkStyle,
useDynamicColors = currentTheme.usingDynamicColors,
selectedColorScheme = currentTheme.selectedColorScheme,
useAmoledBackground = currentTheme.usingAmoledBackground,
) {
RootGraph()
RootScreen()
}
}
}
@@ -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
)
}
}
}
}
}
}
@@ -1,10 +1,10 @@
package com.meloda.app.fast
package com.meloda.app.fast.presentation
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -12,33 +12,40 @@ import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.meloda.app.fast.MainViewModel
import com.meloda.app.fast.MainViewModelImpl
import com.meloda.app.fast.auth.AuthGraph
import com.meloda.app.fast.auth.authNavGraph
import com.meloda.app.fast.auth.navigateToAuth
import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsRoute
import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsScreen
import com.meloda.app.fast.chatmaterials.navigation.navigateToChatMaterials
import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.languagepicker.navigation.languagePickerRoute
import com.meloda.app.fast.languagepicker.navigation.languagePickerScreen
import com.meloda.app.fast.languagepicker.navigation.navigateToLanguagePicker
import com.meloda.app.fast.messageshistory.navigation.messagesHistoryRoute
import com.meloda.app.fast.messageshistory.navigation.messagesHistoryScreen
import com.meloda.app.fast.messageshistory.navigation.navigateToMessagesHistory
import com.meloda.app.fast.settings.presentation.navigateToSettings
import com.meloda.app.fast.settings.presentation.settingsRoute
import com.meloda.app.fast.navigation.Main
import com.meloda.app.fast.navigation.mainScreen
import com.meloda.app.fast.settings.navigation.navigateToSettings
import com.meloda.app.fast.settings.navigation.settingsScreen
import org.koin.androidx.compose.koinViewModel
@Composable
fun RootGraph(navController: NavHostController = rememberNavController()) {
fun RootScreen(navController: NavHostController = rememberNavController()) {
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val isNeedToOpenAuth by viewModel.isNeedToOpenAuth.collectAsStateWithLifecycle()
if (screenState.isNeedToOpenAuth) {
viewModel.onAuthOpened()
navController.navigateToAuth(clearBackStack = true)
LaunchedEffect(isNeedToOpenAuth) {
if (isNeedToOpenAuth) {
viewModel.onNavigatedToAuth()
navController.navigateToAuth(clearBackStack = true)
}
}
if (screenState.accountsLoaded) {
val isNeedToShowConversations by remember {
derivedStateOf { screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn() }
val isNeedToShowConversations = remember {
screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn()
}
NavHost(
@@ -48,32 +55,30 @@ fun RootGraph(navController: NavHostController = rememberNavController()) {
exitTransition = { fadeOut(animationSpec = tween(200)) }
) {
authNavGraph(
onError = viewModel::onError,
onNavigateToMain = navController::navigateToMain,
navController = navController
)
mainScreen(
onError = viewModel::onError,
onNavigateToSettings = navController::navigateToSettings,
onNavigateToMessagesHistory = navController::navigateToMessagesHistory
onSettingsButtonClicked = navController::navigateToSettings,
onConversationClicked = navController::navigateToMessagesHistory
)
messagesHistoryRoute(
messagesHistoryScreen(
onError = viewModel::onError,
onBack = navController::navigateUp,
onNavigateToChatAttachments = navController::navigateToChatMaterials
onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials
)
chatMaterialsRoute(
chatMaterialsScreen(
onBack = navController::navigateUp
)
settingsRoute(
onError = viewModel::onError,
settingsScreen(
onBack = navController::navigateUp,
onNavigateToAuth = { navController.navigateToAuth(true) },
onNavigateToLanguagePicker = navController::navigateToLanguagePicker
onLogOutButtonClicked = { navController.navigateToAuth(true) },
onLanguageItemClicked = navController::navigateToLanguagePicker
)
languagePickerRoute(onBack = navController::navigateUp)
languagePickerScreen(onBack = navController::navigateUp)
}
}