android: unify main tabs shell and hide bottom bar on scroll
This commit is contained in:
@@ -474,3 +474,11 @@
|
|||||||
- attachment/media markers (`movie`, `attach file`).
|
- attachment/media markers (`movie`, `attach file`).
|
||||||
- Replaced chat list management FAB glyph toggle (`+`/`×`) with Material `Add`/`Close` icons.
|
- Replaced chat list management FAB glyph toggle (`+`/`×`) with Material `Add`/`Close` icons.
|
||||||
- Added `androidx.compose.material:material-icons-extended` dependency for consistent icon usage.
|
- Added `androidx.compose.material:material-icons-extended` dependency for consistent icon usage.
|
||||||
|
|
||||||
|
### Step 76 - Shared main tabs shell with scroll-aware visibility
|
||||||
|
- Moved `Chats / Contacts / Settings / Profile` bottom panel to a shared navigation shell (`AppNavGraph`) so it behaves as global page navigation.
|
||||||
|
- Added dedicated `Contacts` page route and wired it into main tabs.
|
||||||
|
- Removed local duplicated bottom panel from chat list screen.
|
||||||
|
- Implemented scroll-direction behavior for all 4 main pages:
|
||||||
|
- hide panel on downward scroll,
|
||||||
|
- show panel on upward scroll / at top.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.material3.AssistChip
|
import androidx.compose.material3.AssistChip
|
||||||
@@ -37,6 +38,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
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
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -44,8 +46,6 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
|
||||||
import androidx.compose.ui.semantics.semantics
|
|
||||||
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.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
@@ -53,6 +53,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import ru.daemonlord.messenger.domain.chat.model.ChatItem
|
import ru.daemonlord.messenger.domain.chat.model.ChatItem
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -62,10 +63,9 @@ import java.time.format.DateTimeFormatter
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatListRoute(
|
fun ChatListRoute(
|
||||||
onOpenChat: (Long) -> Unit,
|
onOpenChat: (Long) -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
|
||||||
onOpenProfile: () -> Unit,
|
|
||||||
inviteToken: String?,
|
inviteToken: String?,
|
||||||
onInviteTokenConsumed: () -> Unit,
|
onInviteTokenConsumed: () -> Unit,
|
||||||
|
onMainBarVisibilityChanged: (Boolean) -> Unit,
|
||||||
viewModel: ChatListViewModel = hiltViewModel(),
|
viewModel: ChatListViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
@@ -87,9 +87,8 @@ fun ChatListRoute(
|
|||||||
onSearchChanged = viewModel::onSearchChanged,
|
onSearchChanged = viewModel::onSearchChanged,
|
||||||
onGlobalSearchChanged = viewModel::onGlobalSearchChanged,
|
onGlobalSearchChanged = viewModel::onGlobalSearchChanged,
|
||||||
onRefresh = viewModel::onPullToRefresh,
|
onRefresh = viewModel::onPullToRefresh,
|
||||||
onOpenSettings = onOpenSettings,
|
|
||||||
onOpenProfile = onOpenProfile,
|
|
||||||
onOpenChat = onOpenChat,
|
onOpenChat = onOpenChat,
|
||||||
|
onMainBarVisibilityChanged = onMainBarVisibilityChanged,
|
||||||
onCreateGroup = viewModel::createGroup,
|
onCreateGroup = viewModel::createGroup,
|
||||||
onCreateChannel = viewModel::createChannel,
|
onCreateChannel = viewModel::createChannel,
|
||||||
onDiscoverChats = viewModel::discoverChats,
|
onDiscoverChats = viewModel::discoverChats,
|
||||||
@@ -114,9 +113,8 @@ fun ChatListScreen(
|
|||||||
onSearchChanged: (String) -> Unit,
|
onSearchChanged: (String) -> Unit,
|
||||||
onGlobalSearchChanged: (String) -> Unit,
|
onGlobalSearchChanged: (String) -> Unit,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
|
||||||
onOpenProfile: () -> Unit,
|
|
||||||
onOpenChat: (Long) -> Unit,
|
onOpenChat: (Long) -> Unit,
|
||||||
|
onMainBarVisibilityChanged: (Boolean) -> Unit,
|
||||||
onCreateGroup: (String, List<Long>) -> Unit,
|
onCreateGroup: (String, List<Long>) -> Unit,
|
||||||
onCreateChannel: (String, String, String?) -> Unit,
|
onCreateChannel: (String, String, String?) -> Unit,
|
||||||
onDiscoverChats: (String?) -> Unit,
|
onDiscoverChats: (String?) -> Unit,
|
||||||
@@ -140,6 +138,27 @@ fun ChatListScreen(
|
|||||||
var manageUserIdText by remember { mutableStateOf("") }
|
var manageUserIdText by remember { mutableStateOf("") }
|
||||||
var manageRoleText by remember { mutableStateOf("member") }
|
var manageRoleText by remember { mutableStateOf("member") }
|
||||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
onMainBarVisibilityChanged(true)
|
||||||
|
}
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
var prevIndex = 0
|
||||||
|
var prevOffset = 0
|
||||||
|
snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||||
|
.collectLatest { (index, offset) ->
|
||||||
|
val movedDown = index > prevIndex || (index == prevIndex && offset > prevOffset)
|
||||||
|
val movedUp = index < prevIndex || (index == prevIndex && offset < prevOffset)
|
||||||
|
when {
|
||||||
|
index == 0 && offset == 0 -> onMainBarVisibilityChanged(true)
|
||||||
|
movedDown -> onMainBarVisibilityChanged(false)
|
||||||
|
movedUp -> onMainBarVisibilityChanged(true)
|
||||||
|
}
|
||||||
|
prevIndex = index
|
||||||
|
prevOffset = offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -271,6 +290,7 @@ fun ChatListScreen(
|
|||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = listState,
|
||||||
) {
|
) {
|
||||||
if (state.selectedTab == ChatTab.ALL && state.archivedChatsCount > 0) {
|
if (state.selectedTab == ChatTab.ALL && state.archivedChatsCount > 0) {
|
||||||
item(key = "archive_row") {
|
item(key = "archive_row") {
|
||||||
@@ -305,23 +325,6 @@ fun ChatListScreen(
|
|||||||
contentDescription = if (managementExpanded) "Close management" else "Open management",
|
contentDescription = if (managementExpanded) "Close management" else "Open management",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
|
||||||
shape = CircleShape,
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.92f),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
BottomNavPill(label = "Chats", selected = true, onClick = {})
|
|
||||||
BottomNavPill(label = "Contacts", selected = false, onClick = {})
|
|
||||||
BottomNavPill(label = "Settings", selected = false, onClick = onOpenSettings)
|
|
||||||
BottomNavPill(label = "Profile", selected = false, onClick = onOpenProfile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (managementExpanded) {
|
if (managementExpanded) {
|
||||||
Surface(
|
Surface(
|
||||||
@@ -672,36 +675,6 @@ private fun ArchiveRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BottomNavPill(
|
|
||||||
label: String,
|
|
||||||
selected: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
shape = CircleShape,
|
|
||||||
color = if (selected) {
|
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.surface
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.semantics { contentDescription = "$label tab" },
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = if (selected) {
|
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurface
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CenterState(
|
private fun CenterState(
|
||||||
text: String?,
|
text: String?,
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package ru.daemonlord.messenger.ui.contacts
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactsRoute(
|
||||||
|
onMainBarVisibilityChanged: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
ContactsScreen(onMainBarVisibilityChanged = onMainBarVisibilityChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContactsScreen(
|
||||||
|
onMainBarVisibilityChanged: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||||
|
var query by remember { mutableStateOf("") }
|
||||||
|
val contacts = remember {
|
||||||
|
listOf(
|
||||||
|
"Alex", "Marta", "Danya", "Ilya", "Mila", "Artem", "Nika", "Vlad",
|
||||||
|
"Sasha", "Roma", "Katya", "Nastya", "Boris", "Pavel", "Alyona",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val filtered = remember(query) {
|
||||||
|
if (query.isBlank()) contacts else contacts.filter { it.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
onMainBarVisibilityChanged(true)
|
||||||
|
}
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
var prevIndex = 0
|
||||||
|
var prevOffset = 0
|
||||||
|
snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||||
|
.collectLatest { (index, offset) ->
|
||||||
|
val movedDown = index > prevIndex || (index == prevIndex && offset > prevOffset)
|
||||||
|
val movedUp = index < prevIndex || (index == prevIndex && offset < prevOffset)
|
||||||
|
when {
|
||||||
|
index == 0 && offset == 0 -> onMainBarVisibilityChanged(true)
|
||||||
|
movedDown -> onMainBarVisibilityChanged(false)
|
||||||
|
movedUp -> onMainBarVisibilityChanged(true)
|
||||||
|
}
|
||||||
|
prevIndex = index
|
||||||
|
prevOffset = offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing),
|
||||||
|
contentAlignment = Alignment.TopCenter,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.then(if (isTabletLayout) Modifier.widthIn(max = 820.dp) else Modifier)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Contacts",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = { query = it },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
label = { Text("Search contacts") },
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = listState,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
items(filtered) { name ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
) {
|
||||||
|
Text(text = name, fontWeight = FontWeight.SemiBold)
|
||||||
|
Text(
|
||||||
|
text = "last seen recently",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,23 +6,49 @@ import android.os.Build
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||||
|
import androidx.compose.material.icons.filled.Contacts
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
@@ -35,6 +61,7 @@ import ru.daemonlord.messenger.ui.auth.reset.ResetPasswordRoute
|
|||||||
import ru.daemonlord.messenger.ui.auth.verify.VerifyEmailRoute
|
import ru.daemonlord.messenger.ui.auth.verify.VerifyEmailRoute
|
||||||
import ru.daemonlord.messenger.ui.chat.ChatRoute
|
import ru.daemonlord.messenger.ui.chat.ChatRoute
|
||||||
import ru.daemonlord.messenger.ui.chats.ChatListRoute
|
import ru.daemonlord.messenger.ui.chats.ChatListRoute
|
||||||
|
import ru.daemonlord.messenger.ui.contacts.ContactsRoute
|
||||||
import ru.daemonlord.messenger.ui.profile.ProfileRoute
|
import ru.daemonlord.messenger.ui.profile.ProfileRoute
|
||||||
import ru.daemonlord.messenger.ui.settings.SettingsRoute
|
import ru.daemonlord.messenger.ui.settings.SettingsRoute
|
||||||
|
|
||||||
@@ -44,6 +71,7 @@ private object Routes {
|
|||||||
const val VerifyEmail = "verify_email"
|
const val VerifyEmail = "verify_email"
|
||||||
const val ResetPassword = "reset_password"
|
const val ResetPassword = "reset_password"
|
||||||
const val Chats = "chats"
|
const val Chats = "chats"
|
||||||
|
const val Contacts = "contacts"
|
||||||
const val Settings = "settings"
|
const val Settings = "settings"
|
||||||
const val Profile = "profile"
|
const val Profile = "profile"
|
||||||
const val Chat = "chat"
|
const val Chat = "chat"
|
||||||
@@ -68,6 +96,13 @@ fun MessengerNavHost(
|
|||||||
val notificationPermissionLauncher = rememberLauncherForActivityResult(
|
val notificationPermissionLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.RequestPermission(),
|
contract = ActivityResultContracts.RequestPermission(),
|
||||||
) {}
|
) {}
|
||||||
|
var isMainBarVisible by remember { mutableStateOf(true) }
|
||||||
|
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
val currentRoute = backStackEntry?.destination?.route
|
||||||
|
val mainTabRoutes = remember {
|
||||||
|
setOf(Routes.Chats, Routes.Contacts, Routes.Settings, Routes.Profile)
|
||||||
|
}
|
||||||
|
val showMainBar = currentRoute in mainTabRoutes
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return@LaunchedEffect
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return@LaunchedEffect
|
||||||
@@ -127,97 +162,205 @@ fun MessengerNavHost(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NavHost(
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
navController = navController,
|
NavHost(
|
||||||
startDestination = Routes.AuthGraph,
|
navController = navController,
|
||||||
) {
|
startDestination = Routes.AuthGraph,
|
||||||
navigation(
|
|
||||||
route = Routes.AuthGraph,
|
|
||||||
startDestination = Routes.Login,
|
|
||||||
) {
|
) {
|
||||||
composable(route = Routes.Login) {
|
navigation(
|
||||||
if (uiState.isCheckingSession) {
|
route = Routes.AuthGraph,
|
||||||
SessionCheckingScreen()
|
startDestination = Routes.Login,
|
||||||
} else {
|
) {
|
||||||
LoginScreen(
|
composable(route = Routes.Login) {
|
||||||
state = uiState,
|
if (uiState.isCheckingSession) {
|
||||||
onEmailChanged = viewModel::onEmailChanged,
|
SessionCheckingScreen()
|
||||||
onPasswordChanged = viewModel::onPasswordChanged,
|
} else {
|
||||||
onLoginClick = viewModel::login,
|
LoginScreen(
|
||||||
onOpenVerifyEmail = { navController.navigate(Routes.VerifyEmail) },
|
state = uiState,
|
||||||
onOpenResetPassword = { navController.navigate(Routes.ResetPassword) },
|
onEmailChanged = viewModel::onEmailChanged,
|
||||||
)
|
onPasswordChanged = viewModel::onPasswordChanged,
|
||||||
|
onLoginClick = viewModel::login,
|
||||||
|
onOpenVerifyEmail = { navController.navigate(Routes.VerifyEmail) },
|
||||||
|
onOpenResetPassword = { navController.navigate(Routes.ResetPassword) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${Routes.VerifyEmail}?token={token}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("token") {
|
||||||
|
type = NavType.StringType
|
||||||
|
nullable = true
|
||||||
|
defaultValue = null
|
||||||
|
}
|
||||||
|
),
|
||||||
|
) { entry ->
|
||||||
|
VerifyEmailRoute(
|
||||||
|
token = entry.arguments?.getString("token"),
|
||||||
|
onBackToLogin = { navController.navigate(Routes.Login) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${Routes.ResetPassword}?token={token}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("token") {
|
||||||
|
type = NavType.StringType
|
||||||
|
nullable = true
|
||||||
|
defaultValue = null
|
||||||
|
}
|
||||||
|
),
|
||||||
|
) { entry ->
|
||||||
|
ResetPasswordRoute(
|
||||||
|
token = entry.arguments?.getString("token"),
|
||||||
|
onBackToLogin = { navController.navigate(Routes.Login) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(route = Routes.Chats) {
|
||||||
|
ChatListRoute(
|
||||||
|
inviteToken = inviteToken,
|
||||||
|
onInviteTokenConsumed = onInviteTokenConsumed,
|
||||||
|
onOpenChat = { chatId ->
|
||||||
|
navController.navigate("${Routes.Chat}/$chatId")
|
||||||
|
},
|
||||||
|
onMainBarVisibilityChanged = { isMainBarVisible = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(route = Routes.Contacts) {
|
||||||
|
ContactsRoute(onMainBarVisibilityChanged = { isMainBarVisible = it })
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(route = Routes.Settings) {
|
||||||
|
SettingsRoute(
|
||||||
|
onBackToChats = { navController.navigate(Routes.Chats) },
|
||||||
|
onOpenProfile = { navController.navigate(Routes.Profile) },
|
||||||
|
onLogout = viewModel::logout,
|
||||||
|
onMainBarVisibilityChanged = { isMainBarVisible = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(route = Routes.Profile) {
|
||||||
|
ProfileRoute(
|
||||||
|
onBackToChats = { navController.navigate(Routes.Chats) },
|
||||||
|
onOpenSettings = { navController.navigate(Routes.Settings) },
|
||||||
|
onMainBarVisibilityChanged = { isMainBarVisible = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${Routes.Chat}/{chatId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("chatId") { type = NavType.LongType }
|
||||||
|
),
|
||||||
|
) { backStackEntry ->
|
||||||
|
ChatRoute(
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(
|
AnimatedVisibility(
|
||||||
route = "${Routes.VerifyEmail}?token={token}",
|
visible = showMainBar && isMainBarVisible,
|
||||||
arguments = listOf(
|
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||||
navArgument("token") {
|
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||||
type = NavType.StringType
|
modifier = Modifier
|
||||||
nullable = true
|
.align(Alignment.BottomCenter)
|
||||||
defaultValue = null
|
.navigationBarsPadding()
|
||||||
}
|
.padding(bottom = 12.dp),
|
||||||
),
|
) {
|
||||||
) { entry ->
|
MainBottomBar(
|
||||||
VerifyEmailRoute(
|
currentRoute = currentRoute,
|
||||||
token = entry.arguments?.getString("token"),
|
onNavigate = { route ->
|
||||||
onBackToLogin = { navController.navigate(Routes.Login) },
|
if (currentRoute == route) return@MainBottomBar
|
||||||
|
navController.navigate(route) {
|
||||||
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
composable(
|
@Composable
|
||||||
route = "${Routes.ResetPassword}?token={token}",
|
private fun MainBottomBar(
|
||||||
arguments = listOf(
|
currentRoute: String?,
|
||||||
navArgument("token") {
|
onNavigate: (String) -> Unit,
|
||||||
type = NavType.StringType
|
) {
|
||||||
nullable = true
|
Surface(
|
||||||
defaultValue = null
|
shape = CircleShape,
|
||||||
}
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.92f),
|
||||||
),
|
) {
|
||||||
) { entry ->
|
androidx.compose.foundation.layout.Row(
|
||||||
ResetPasswordRoute(
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
token = entry.arguments?.getString("token"),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
onBackToLogin = { navController.navigate(Routes.Login) },
|
) {
|
||||||
|
MainTabPill(
|
||||||
|
label = "Chats",
|
||||||
|
selected = currentRoute == Routes.Chats,
|
||||||
|
icon = { Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) },
|
||||||
|
onClick = { onNavigate(Routes.Chats) },
|
||||||
|
)
|
||||||
|
MainTabPill(
|
||||||
|
label = "Contacts",
|
||||||
|
selected = currentRoute == Routes.Contacts,
|
||||||
|
icon = { Icon(Icons.Filled.Contacts, contentDescription = null) },
|
||||||
|
onClick = { onNavigate(Routes.Contacts) },
|
||||||
|
)
|
||||||
|
MainTabPill(
|
||||||
|
label = "Settings",
|
||||||
|
selected = currentRoute == Routes.Settings,
|
||||||
|
icon = { Icon(Icons.Filled.Settings, contentDescription = null) },
|
||||||
|
onClick = { onNavigate(Routes.Settings) },
|
||||||
|
)
|
||||||
|
MainTabPill(
|
||||||
|
label = "Profile",
|
||||||
|
selected = currentRoute == Routes.Profile,
|
||||||
|
icon = { Icon(Icons.Filled.Person, contentDescription = null) },
|
||||||
|
onClick = { onNavigate(Routes.Profile) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
composable(route = Routes.Chats) {
|
@Composable
|
||||||
ChatListRoute(
|
private fun MainTabPill(
|
||||||
inviteToken = inviteToken,
|
label: String,
|
||||||
onInviteTokenConsumed = onInviteTokenConsumed,
|
selected: Boolean,
|
||||||
onOpenSettings = { navController.navigate(Routes.Settings) },
|
icon: @Composable () -> Unit,
|
||||||
onOpenProfile = { navController.navigate(Routes.Profile) },
|
onClick: () -> Unit,
|
||||||
onOpenChat = { chatId ->
|
) {
|
||||||
navController.navigate("${Routes.Chat}/$chatId")
|
Surface(
|
||||||
}
|
shape = CircleShape,
|
||||||
)
|
color = if (selected) {
|
||||||
}
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
} else {
|
||||||
composable(route = Routes.Settings) {
|
MaterialTheme.colorScheme.surface
|
||||||
SettingsRoute(
|
},
|
||||||
onBackToChats = { navController.popBackStack() },
|
modifier = Modifier
|
||||||
onOpenProfile = { navController.navigate(Routes.Profile) },
|
.clickable(onClick = onClick)
|
||||||
onLogout = viewModel::logout,
|
.semantics { contentDescription = "$label tab" },
|
||||||
)
|
) {
|
||||||
}
|
androidx.compose.foundation.layout.Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
composable(route = Routes.Profile) {
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
ProfileRoute(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
onBackToChats = { navController.popBackStack() },
|
) {
|
||||||
onOpenSettings = { navController.navigate(Routes.Settings) },
|
icon()
|
||||||
)
|
Text(
|
||||||
}
|
text = label,
|
||||||
|
color = if (selected) {
|
||||||
composable(
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
route = "${Routes.Chat}/{chatId}",
|
} else {
|
||||||
arguments = listOf(
|
MaterialTheme.colorScheme.onSurface
|
||||||
navArgument("chatId") { type = NavType.LongType }
|
},
|
||||||
),
|
|
||||||
) { backStackEntry ->
|
|
||||||
ChatRoute(
|
|
||||||
onBack = { navController.popBackStack() },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ 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
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -45,6 +46,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import ru.daemonlord.messenger.ui.account.AccountViewModel
|
import ru.daemonlord.messenger.ui.account.AccountViewModel
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
@@ -53,11 +55,13 @@ import java.io.ByteArrayOutputStream
|
|||||||
fun ProfileRoute(
|
fun ProfileRoute(
|
||||||
onBackToChats: () -> Unit,
|
onBackToChats: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
|
onMainBarVisibilityChanged: (Boolean) -> Unit,
|
||||||
viewModel: AccountViewModel = hiltViewModel(),
|
viewModel: AccountViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
ProfileScreen(
|
ProfileScreen(
|
||||||
onBackToChats = onBackToChats,
|
onBackToChats = onBackToChats,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
|
onMainBarVisibilityChanged = onMainBarVisibilityChanged,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -66,6 +70,7 @@ fun ProfileRoute(
|
|||||||
fun ProfileScreen(
|
fun ProfileScreen(
|
||||||
onBackToChats: () -> Unit,
|
onBackToChats: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
|
onMainBarVisibilityChanged: (Boolean) -> Unit,
|
||||||
viewModel: AccountViewModel,
|
viewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
@@ -74,14 +79,27 @@ fun ProfileScreen(
|
|||||||
var username by remember(profile?.username) { mutableStateOf(profile?.username.orEmpty()) }
|
var username by remember(profile?.username) { mutableStateOf(profile?.username.orEmpty()) }
|
||||||
var bio by remember(profile?.bio) { mutableStateOf(profile?.bio.orEmpty()) }
|
var bio by remember(profile?.bio) { mutableStateOf(profile?.bio.orEmpty()) }
|
||||||
var avatarUrl by remember(profile?.avatarUrl) { mutableStateOf(profile?.avatarUrl.orEmpty()) }
|
var avatarUrl by remember(profile?.avatarUrl) { mutableStateOf(profile?.avatarUrl.orEmpty()) }
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
|
onMainBarVisibilityChanged(true)
|
||||||
|
}
|
||||||
|
LaunchedEffect(scrollState) {
|
||||||
|
var prevOffset = 0
|
||||||
|
snapshotFlow { scrollState.value }
|
||||||
|
.collectLatest { offset ->
|
||||||
|
when {
|
||||||
|
offset == 0 -> onMainBarVisibilityChanged(true)
|
||||||
|
offset > prevOffset -> onMainBarVisibilityChanged(false)
|
||||||
|
offset < prevOffset -> onMainBarVisibilityChanged(true)
|
||||||
|
}
|
||||||
|
prevOffset = offset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
val pickAvatarLauncher = rememberLauncherForActivityResult(
|
val pickAvatarLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.GetContent(),
|
contract = ActivityResultContracts.GetContent(),
|
||||||
) { uri ->
|
) { uri ->
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ 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
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -34,6 +35,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import ru.daemonlord.messenger.ui.account.AccountViewModel
|
import ru.daemonlord.messenger.ui.account.AccountViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -41,12 +43,14 @@ fun SettingsRoute(
|
|||||||
onBackToChats: () -> Unit,
|
onBackToChats: () -> Unit,
|
||||||
onOpenProfile: () -> Unit,
|
onOpenProfile: () -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
|
onMainBarVisibilityChanged: (Boolean) -> Unit,
|
||||||
viewModel: AccountViewModel = hiltViewModel(),
|
viewModel: AccountViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
onBackToChats = onBackToChats,
|
onBackToChats = onBackToChats,
|
||||||
onOpenProfile = onOpenProfile,
|
onOpenProfile = onOpenProfile,
|
||||||
onLogout = onLogout,
|
onLogout = onLogout,
|
||||||
|
onMainBarVisibilityChanged = onMainBarVisibilityChanged,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -56,6 +60,7 @@ fun SettingsScreen(
|
|||||||
onBackToChats: () -> Unit,
|
onBackToChats: () -> Unit,
|
||||||
onOpenProfile: () -> Unit,
|
onOpenProfile: () -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
|
onMainBarVisibilityChanged: (Boolean) -> Unit,
|
||||||
viewModel: AccountViewModel,
|
viewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
@@ -74,6 +79,19 @@ fun SettingsScreen(
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
viewModel.refreshRecoveryStatus()
|
viewModel.refreshRecoveryStatus()
|
||||||
|
onMainBarVisibilityChanged(true)
|
||||||
|
}
|
||||||
|
LaunchedEffect(scrollState) {
|
||||||
|
var prevOffset = 0
|
||||||
|
snapshotFlow { scrollState.value }
|
||||||
|
.collectLatest { offset ->
|
||||||
|
when {
|
||||||
|
offset == 0 -> onMainBarVisibilityChanged(true)
|
||||||
|
offset > prevOffset -> onMainBarVisibilityChanged(false)
|
||||||
|
offset < prevOffset -> onMainBarVisibilityChanged(true)
|
||||||
|
}
|
||||||
|
prevOffset = offset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
## P0 — Navigation + Layout Shell
|
## P0 — Navigation + Layout Shell
|
||||||
- [x] Floating bottom navigation в rounded контейнере с полупрозрачным тёмным фоном.
|
- [x] Floating bottom navigation в rounded контейнере с полупрозрачным тёмным фоном.
|
||||||
- [x] Активный tab в фиолетовом pill-состоянии, неактивные — белые/серые.
|
- [x] Активный tab в фиолетовом pill-состоянии, неактивные — белые/серые.
|
||||||
|
- [x] Глобальная панель `Chats / Contacts / Settings / Profile` на уровне app-shell (одна на все 4 страницы).
|
||||||
|
- [x] Поведение панели: скрывается только при скролле вниз, возвращается при скролле вверх/в начале списка.
|
||||||
- [ ] Safe area для status/nav bars на всех экранах списка/настроек/профиля.
|
- [ ] Safe area для status/nav bars на всех экранах списка/настроек/профиля.
|
||||||
|
|
||||||
## P0 — Settings Visual System
|
## P0 — Settings Visual System
|
||||||
|
|||||||
Reference in New Issue
Block a user