feat: Replace custom header with TopAppBar for improved layout and smoother transitions in ChatsListScreen
This commit is contained in:
@@ -24,10 +24,12 @@ import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.data.AccountManager
|
||||
import com.rosetta.messenger.data.DecryptedAccount
|
||||
import com.rosetta.messenger.data.PreferencesManager
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
import com.rosetta.messenger.ui.auth.AccountInfo
|
||||
import com.rosetta.messenger.ui.auth.AuthFlow
|
||||
import com.rosetta.messenger.ui.chats.ChatsListScreen
|
||||
import com.rosetta.messenger.ui.chats.ChatDetailScreen
|
||||
import com.rosetta.messenger.ui.chats.SearchScreen
|
||||
import com.rosetta.messenger.network.SearchUser
|
||||
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
||||
import com.rosetta.messenger.ui.splash.SplashScreen
|
||||
@@ -218,12 +220,16 @@ fun MainScreen(
|
||||
val accountPrivateKey = account?.privateKey ?: ""
|
||||
val privateKeyHash = account?.privateKeyHash ?: ""
|
||||
|
||||
// Состояние протокола для передачи в SearchScreen
|
||||
val protocolState by ProtocolManager.state.collectAsState()
|
||||
|
||||
// Навигация между экранами
|
||||
var selectedUser by remember { mutableStateOf<SearchUser?>(null) }
|
||||
var showSearchScreen by remember { mutableStateOf(false) }
|
||||
|
||||
// Анимированный переход между чатами - плавный crossfade без прыжков
|
||||
// Анимированный переход между экранами
|
||||
AnimatedContent(
|
||||
targetState = selectedUser,
|
||||
targetState = Triple(selectedUser, showSearchScreen, Unit),
|
||||
transitionSpec = {
|
||||
// Плавный crossfade для избежания "прыжков" header'а
|
||||
val enterAnim = fadeIn(
|
||||
@@ -232,7 +238,7 @@ fun MainScreen(
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
) + slideInHorizontally(
|
||||
initialOffsetX = { if (targetState != null) it / 4 else -it / 4 },
|
||||
initialOffsetX = { it / 4 },
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = FastOutSlowInEasing
|
||||
@@ -245,7 +251,7 @@ fun MainScreen(
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
) + slideOutHorizontally(
|
||||
targetOffsetX = { if (targetState != null) -it / 6 else it / 6 },
|
||||
targetOffsetX = { -it / 6 },
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = FastOutSlowInEasing
|
||||
@@ -254,58 +260,75 @@ fun MainScreen(
|
||||
|
||||
enterAnim togetherWith exitAnim using SizeTransform(clip = false)
|
||||
},
|
||||
label = "chatNavigation"
|
||||
) { user ->
|
||||
if (user != null) {
|
||||
// Экран чата
|
||||
ChatDetailScreen(
|
||||
user = user,
|
||||
currentUserPublicKey = accountPublicKey,
|
||||
currentUserPrivateKey = accountPrivateKey,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onBack = { selectedUser = null }
|
||||
)
|
||||
} else {
|
||||
// Список чатов
|
||||
ChatsListScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
accountName = accountName,
|
||||
accountPhone = accountPhone,
|
||||
accountPublicKey = accountPublicKey,
|
||||
privateKeyHash = privateKeyHash,
|
||||
onToggleTheme = onToggleTheme,
|
||||
onProfileClick = {
|
||||
// TODO: Navigate to profile
|
||||
},
|
||||
onNewGroupClick = {
|
||||
// TODO: Navigate to new group
|
||||
},
|
||||
onContactsClick = {
|
||||
// TODO: Navigate to contacts
|
||||
},
|
||||
onCallsClick = {
|
||||
// TODO: Navigate to calls
|
||||
},
|
||||
onSavedMessagesClick = {
|
||||
// TODO: Navigate to saved messages
|
||||
},
|
||||
onSettingsClick = {
|
||||
// TODO: Navigate to settings
|
||||
},
|
||||
onInviteFriendsClick = {
|
||||
// TODO: Share invite link
|
||||
},
|
||||
onSearchClick = {
|
||||
// TODO: Show search
|
||||
},
|
||||
onNewChat = {
|
||||
// TODO: Show new chat screen
|
||||
},
|
||||
onUserSelect = { user ->
|
||||
selectedUser = user
|
||||
},
|
||||
onLogout = onLogout
|
||||
)
|
||||
label = "screenNavigation"
|
||||
) { (user, isSearchOpen, _) ->
|
||||
when {
|
||||
user != null -> {
|
||||
// Экран чата
|
||||
ChatDetailScreen(
|
||||
user = user,
|
||||
currentUserPublicKey = accountPublicKey,
|
||||
currentUserPrivateKey = accountPrivateKey,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onBack = { selectedUser = null }
|
||||
)
|
||||
}
|
||||
isSearchOpen -> {
|
||||
// Экран поиска
|
||||
SearchScreen(
|
||||
privateKeyHash = privateKeyHash,
|
||||
currentUserPublicKey = accountPublicKey,
|
||||
isDarkTheme = isDarkTheme,
|
||||
protocolState = protocolState,
|
||||
onBackClick = { showSearchScreen = false },
|
||||
onUserSelect = { user ->
|
||||
showSearchScreen = false
|
||||
selectedUser = user
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// Список чатов
|
||||
ChatsListScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
accountName = accountName,
|
||||
accountPhone = accountPhone,
|
||||
accountPublicKey = accountPublicKey,
|
||||
privateKeyHash = privateKeyHash,
|
||||
onToggleTheme = onToggleTheme,
|
||||
onProfileClick = {
|
||||
// TODO: Navigate to profile
|
||||
},
|
||||
onNewGroupClick = {
|
||||
// TODO: Navigate to new group
|
||||
},
|
||||
onContactsClick = {
|
||||
// TODO: Navigate to contacts
|
||||
},
|
||||
onCallsClick = {
|
||||
// TODO: Navigate to calls
|
||||
},
|
||||
onSavedMessagesClick = {
|
||||
// TODO: Navigate to saved messages
|
||||
},
|
||||
onSettingsClick = {
|
||||
// TODO: Navigate to settings
|
||||
},
|
||||
onInviteFriendsClick = {
|
||||
// TODO: Share invite link
|
||||
},
|
||||
onSearchClick = {
|
||||
showSearchScreen = true
|
||||
},
|
||||
onNewChat = {
|
||||
// TODO: Show new chat screen
|
||||
},
|
||||
onUserSelect = { user ->
|
||||
selectedUser = user
|
||||
},
|
||||
onLogout = onLogout
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,20 +194,6 @@ fun ChatsListScreen(
|
||||
// Status dialog state
|
||||
var showStatusDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// Search state - используем ViewModel для поиска пользователей
|
||||
val searchViewModel = remember { SearchUsersViewModel() }
|
||||
val searchQuery by searchViewModel.searchQuery.collectAsState()
|
||||
val searchResults by searchViewModel.searchResults.collectAsState()
|
||||
val isSearching by searchViewModel.isSearching.collectAsState()
|
||||
val isSearchExpanded by searchViewModel.isSearchExpanded.collectAsState()
|
||||
|
||||
// Устанавливаем privateKeyHash для поиска
|
||||
LaunchedEffect(privateKeyHash) {
|
||||
if (privateKeyHash.isNotEmpty()) {
|
||||
searchViewModel.setPrivateKeyHash(privateKeyHash)
|
||||
}
|
||||
}
|
||||
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) { visible = true }
|
||||
@@ -416,299 +402,84 @@ fun ChatsListScreen(
|
||||
)
|
||||
) {
|
||||
key(isDarkTheme) {
|
||||
// Custom header с фиксированной структурой для плавной анимации без скачков
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding(),
|
||||
color = backgroundColor
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
) {
|
||||
// Burger menu - всегда на месте, только меняет alpha
|
||||
val burgerAlpha by animateFloatAsState(
|
||||
targetValue = if (isSearchExpanded) 0f else 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 250,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "burgerAlpha"
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (!isSearchExpanded) {
|
||||
scope.launch { drawerState.open() }
|
||||
}
|
||||
},
|
||||
enabled = !isSearchExpanded,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.padding(start = 4.dp)
|
||||
.graphicsLayer {
|
||||
alpha = burgerAlpha
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Menu,
|
||||
contentDescription = "Menu",
|
||||
tint = textColor
|
||||
)
|
||||
}
|
||||
|
||||
// Center content - Title и Search в одном месте
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 60.dp)
|
||||
) {
|
||||
val focusRequester = remember {
|
||||
FocusRequester()
|
||||
} // Auto-focus when search opens
|
||||
LaunchedEffect(isSearchExpanded) {
|
||||
if (isSearchExpanded) {
|
||||
kotlinx.coroutines.delay(100)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch { drawerState.open() }
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Menu,
|
||||
contentDescription = "Menu",
|
||||
tint = textColor
|
||||
)
|
||||
}
|
||||
|
||||
// Crossfade for smooth transition without position jumps
|
||||
Crossfade(
|
||||
targetState = isSearchExpanded,
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = "searchCrossfade"
|
||||
) { expanded ->
|
||||
if (expanded) {
|
||||
// Search Mode
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Back arrow
|
||||
IconButton(
|
||||
onClick = {
|
||||
searchViewModel.collapseSearch()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
contentDescription =
|
||||
"Close search",
|
||||
tint = textColor
|
||||
)
|
||||
}
|
||||
|
||||
// Search input with underline animation
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
TextField(
|
||||
value = searchQuery,
|
||||
onValueChange = {
|
||||
searchViewModel
|
||||
.onSearchQueryChange(
|
||||
it
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
"Search",
|
||||
color =
|
||||
secondaryTextColor
|
||||
.copy(
|
||||
alpha =
|
||||
0.7f
|
||||
)
|
||||
)
|
||||
},
|
||||
colors =
|
||||
TextFieldDefaults
|
||||
.colors(
|
||||
focusedContainerColor =
|
||||
Color.Transparent,
|
||||
unfocusedContainerColor =
|
||||
Color.Transparent,
|
||||
focusedTextColor =
|
||||
textColor,
|
||||
unfocusedTextColor =
|
||||
textColor,
|
||||
cursorColor =
|
||||
PrimaryBlue,
|
||||
focusedIndicatorColor =
|
||||
Color.Transparent,
|
||||
unfocusedIndicatorColor =
|
||||
Color.Transparent
|
||||
),
|
||||
singleLine = true,
|
||||
enabled =
|
||||
protocolState ==
|
||||
ProtocolState
|
||||
.AUTHENTICATED,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.focusRequester(
|
||||
focusRequester
|
||||
)
|
||||
)
|
||||
|
||||
// Underline
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.align(
|
||||
Alignment
|
||||
.BottomCenter
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.height(2.dp)
|
||||
.background(
|
||||
PrimaryBlue
|
||||
.copy(
|
||||
alpha = 0.8f
|
||||
),
|
||||
RoundedCornerShape(
|
||||
1.dp
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Clear button with fade-in animation
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
val clearAlpha by
|
||||
animateFloatAsState(
|
||||
targetValue = 1f,
|
||||
animationSpec =
|
||||
tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "clearAlpha"
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
searchViewModel
|
||||
.clearSearchQuery()
|
||||
},
|
||||
modifier =
|
||||
Modifier.graphicsLayer {
|
||||
alpha = clearAlpha
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Clear,
|
||||
contentDescription =
|
||||
"Clear",
|
||||
tint = secondaryTextColor
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.clickable {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastClickTime < 500) {
|
||||
titleClickCount++
|
||||
if (titleClickCount >= 3) {
|
||||
showDevConsole = true
|
||||
titleClickCount = 0
|
||||
}
|
||||
} else {
|
||||
titleClickCount = 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Title Mode - Triple click to open dev console
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val currentTime =
|
||||
System.currentTimeMillis()
|
||||
if (currentTime -
|
||||
lastClickTime <
|
||||
500
|
||||
) {
|
||||
titleClickCount++
|
||||
if (titleClickCount >=
|
||||
3
|
||||
) {
|
||||
showDevConsole =
|
||||
true
|
||||
titleClickCount =
|
||||
0
|
||||
}
|
||||
} else {
|
||||
titleClickCount =
|
||||
1
|
||||
}
|
||||
lastClickTime =
|
||||
currentTime
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
"Rosetta",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
// Status indicator dot
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
when (protocolState) {
|
||||
ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50) // Green
|
||||
ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFFC107) // Yellow
|
||||
ProtocolState.DISCONNECTED -> Color(0xFFF44336) // Red
|
||||
}
|
||||
)
|
||||
.clickable {
|
||||
showStatusDialog = true
|
||||
lastClickTime = currentTime
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
"Rosetta",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
when (protocolState) {
|
||||
ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50)
|
||||
ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFFC107)
|
||||
ProtocolState.DISCONNECTED -> Color(0xFFF44336)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
.clickable { showStatusDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Search button - справа, всегда на месте
|
||||
val searchButtonScale by animateFloatAsState(
|
||||
targetValue = if (isSearchExpanded) 0f else 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 250,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "searchButtonScale"
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (protocolState == ProtocolState.AUTHENTICATED) {
|
||||
onSearchClick()
|
||||
}
|
||||
},
|
||||
enabled = protocolState == ProtocolState.AUTHENTICATED
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
tint = if (protocolState == ProtocolState.AUTHENTICATED)
|
||||
textColor else textColor.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = backgroundColor,
|
||||
scrolledContainerColor = backgroundColor,
|
||||
navigationIconContentColor = textColor,
|
||||
titleContentColor = textColor,
|
||||
actionIconContentColor = textColor
|
||||
)
|
||||
val searchButtonAlpha by animateFloatAsState(
|
||||
targetValue = if (isSearchExpanded) 0f else 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 250,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "searchButtonAlpha"
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (protocolState == ProtocolState.AUTHENTICATED) {
|
||||
searchViewModel.expandSearch()
|
||||
}
|
||||
},
|
||||
enabled = protocolState == ProtocolState.AUTHENTICATED && !isSearchExpanded,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 4.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = searchButtonScale
|
||||
scaleY = searchButtonScale
|
||||
alpha = searchButtonAlpha * if (protocolState == ProtocolState.AUTHENTICATED) 1f else 0.5f
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
tint = textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -735,30 +506,7 @@ fun ChatsListScreen(
|
||||
) { paddingValues ->
|
||||
// Main content
|
||||
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
|
||||
// Show search results when search is expanded
|
||||
if (isSearchExpanded) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Search Results List
|
||||
SearchResultsList(
|
||||
searchResults = searchResults,
|
||||
isSearching = isSearching,
|
||||
currentUserPublicKey = accountPublicKey,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onUserClick = { user ->
|
||||
// Логируем выбор пользователя
|
||||
ProtocolManager.addLog(
|
||||
"🎯 User selected: ${user.title.ifEmpty { user.publicKey.take(10) }}"
|
||||
)
|
||||
ProtocolManager.addLog(
|
||||
" PublicKey: ${user.publicKey.take(20)}..."
|
||||
)
|
||||
// Закрываем поиск и вызываем callback
|
||||
searchViewModel.collapseSearch()
|
||||
onUserSelect(user)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if (dialogsList.isEmpty()) {
|
||||
if (dialogsList.isEmpty()) {
|
||||
// Empty state with Lottie animation
|
||||
EmptyChatsState(
|
||||
isDarkTheme = isDarkTheme,
|
||||
|
||||
177
app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt
Normal file
177
app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt
Normal file
@@ -0,0 +1,177 @@
|
||||
package com.rosetta.messenger.ui.chats
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.network.ProtocolState
|
||||
import com.rosetta.messenger.network.SearchUser
|
||||
|
||||
// Primary Blue color
|
||||
private val PrimaryBlue = Color(0xFF54A9EB)
|
||||
|
||||
/**
|
||||
* Отдельная страница поиска пользователей
|
||||
* Хедер на всю ширину с полем ввода
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
privateKeyHash: String,
|
||||
currentUserPublicKey: String,
|
||||
isDarkTheme: Boolean,
|
||||
protocolState: ProtocolState,
|
||||
onBackClick: () -> Unit,
|
||||
onUserSelect: (SearchUser) -> Unit
|
||||
) {
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF121212) else Color(0xFFF8F9FA)
|
||||
val textColor = if (isDarkTheme) Color.White else Color(0xFF1a1a1a)
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFFB0B0B0) else Color(0xFF6c757d)
|
||||
val surfaceColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color.White
|
||||
|
||||
// Search ViewModel
|
||||
val searchViewModel = remember { SearchUsersViewModel() }
|
||||
val searchQuery by searchViewModel.searchQuery.collectAsState()
|
||||
val searchResults by searchViewModel.searchResults.collectAsState()
|
||||
val isSearching by searchViewModel.isSearching.collectAsState()
|
||||
|
||||
// Устанавливаем privateKeyHash
|
||||
LaunchedEffect(privateKeyHash) {
|
||||
if (privateKeyHash.isNotEmpty()) {
|
||||
searchViewModel.setPrivateKeyHash(privateKeyHash)
|
||||
}
|
||||
}
|
||||
|
||||
// Focus requester для автофокуса
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
// Автофокус при открытии
|
||||
LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(100)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
// Кастомный header с полем ввода на всю ширину
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = backgroundColor,
|
||||
shadowElevation = 4.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.height(64.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Кнопка назад
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = textColor
|
||||
)
|
||||
}
|
||||
|
||||
// Поле ввода на всю оставшуюся ширину
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
TextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchViewModel.onSearchQueryChange(it) },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Search users...",
|
||||
color = secondaryTextColor.copy(alpha = 0.7f),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedTextColor = textColor,
|
||||
unfocusedTextColor = textColor,
|
||||
cursorColor = PrimaryBlue,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
),
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
),
|
||||
singleLine = true,
|
||||
enabled = protocolState == ProtocolState.AUTHENTICATED,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
|
||||
// Подчеркивание
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.height(2.dp)
|
||||
.background(
|
||||
PrimaryBlue.copy(alpha = 0.8f),
|
||||
RoundedCornerShape(1.dp)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Кнопка очистки
|
||||
AnimatedVisibility(
|
||||
visible = searchQuery.isNotEmpty(),
|
||||
enter = fadeIn(tween(150)) + scaleIn(tween(150)),
|
||||
exit = fadeOut(tween(150)) + scaleOut(tween(150))
|
||||
) {
|
||||
IconButton(onClick = { searchViewModel.clearSearchQuery() }) {
|
||||
Icon(
|
||||
Icons.Default.Clear,
|
||||
contentDescription = "Clear",
|
||||
tint = secondaryTextColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = backgroundColor
|
||||
) { paddingValues ->
|
||||
// Контент - результаты поиска
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
SearchResultsList(
|
||||
searchResults = searchResults,
|
||||
isSearching = isSearching,
|
||||
currentUserPublicKey = currentUserPublicKey,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onUserClick = { user ->
|
||||
onUserSelect(user)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user