Refactor UI components in ChatsListScreen, ForwardChatPickerBottomSheet, and SearchScreen for improved readability and maintainability; adjust text color alpha values, streamline imports, and enhance keyboard handling functionality.

This commit is contained in:
2026-01-17 21:09:47 +05:00
parent c9136ed499
commit a3810af4a0
11 changed files with 3763 additions and 3226 deletions

View File

@@ -534,38 +534,10 @@ fun ChatsListScreen(
Spacer(modifier = Modifier.height(6.dp))
// Connection status indicator
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.clickable { showStatusDialog = true }
) {
val statusColor =
when (protocolState) {
ProtocolState.AUTHENTICATED -> Color(0xFF4ADE80)
ProtocolState.CONNECTING,
ProtocolState.CONNECTED,
ProtocolState.HANDSHAKING -> Color(0xFFFBBF24)
else -> Color(0xFFF87171)
}
val statusText =
when (protocolState) {
ProtocolState.AUTHENTICATED -> "Online"
ProtocolState.CONNECTING,
ProtocolState.CONNECTED,
ProtocolState.HANDSHAKING -> "Connecting..."
else -> "Offline"
}
Box(
modifier =
Modifier.size(8.dp)
.clip(CircleShape)
.background(statusColor)
)
Spacer(modifier = Modifier.width(6.dp))
// Username display
if (accountName.isNotEmpty()) {
Text(
text = statusText,
text = "@$accountName",
fontSize = 13.sp,
color = Color.White.copy(alpha = 0.85f)
)
@@ -864,7 +836,9 @@ fun ChatsListScreen(
ProtocolState
.AUTHENTICATED
)
textColor.copy(alpha = 0.6f)
textColor.copy(
alpha = 0.6f
)
else
textColor.copy(
alpha = 0.5f

View File

@@ -9,14 +9,13 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@@ -24,13 +23,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.data.ForwardManager
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import kotlinx.coroutines.launch
/**
* 📨 BottomSheet для выбора чата при Forward сообщений
*
*
* Логика как в десктопной версии:
* 1. Показывает список диалогов
* 2. При выборе диалога - переходит в чат с сообщениями в Reply панели
@@ -38,25 +36,23 @@ import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ForwardChatPickerBottomSheet(
dialogs: List<DialogUiModel>,
isDarkTheme: Boolean,
currentUserPublicKey: String,
onDismiss: () -> Unit,
onChatSelected: (String) -> Unit
dialogs: List<DialogUiModel>,
isDarkTheme: Boolean,
currentUserPublicKey: String,
onDismiss: () -> Unit,
onChatSelected: (String) -> Unit
) {
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = false
)
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
val scope = rememberCoroutineScope()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
val forwardMessages by ForwardManager.forwardMessages.collectAsState()
val messagesCount = forwardMessages.size
// 🔥 Функция для красивого закрытия с анимацией
fun dismissWithAnimation() {
scope.launch {
@@ -64,249 +60,232 @@ fun ForwardChatPickerBottomSheet(
onDismiss()
}
}
ModalBottomSheet(
onDismissRequest = { dismissWithAnimation() },
sheetState = sheetState,
containerColor = backgroundColor,
dragHandle = {
// Кастомный handle
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.width(36.dp)
.height(5.dp)
.clip(RoundedCornerShape(2.5.dp))
.background(
if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD1D1D6)
)
)
Spacer(modifier = Modifier.height(16.dp))
}
},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
onDismissRequest = { dismissWithAnimation() },
sheetState = sheetState,
containerColor = backgroundColor,
dragHandle = {
// Кастомный handle
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier =
Modifier.width(36.dp)
.height(5.dp)
.clip(RoundedCornerShape(2.5.dp))
.background(
if (isDarkTheme) Color(0xFF4A4A4A)
else Color(0xFFD1D1D6)
)
)
Spacer(modifier = Modifier.height(16.dp))
}
},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
) {
Column(modifier = Modifier.fillMaxWidth().navigationBarsPadding()) {
// Header
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Иконка и заголовок
Row(
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// 🔥 Красивая иконка Forward
Icon(
Icons.Filled.ArrowForward,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier
.size(24.dp)
Icons.Filled.ArrowForward,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = "Forward to",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = textColor
text = "Forward to",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = textColor
)
Text(
text = "$messagesCount message${if (messagesCount > 1) "s" else ""} selected",
fontSize = 14.sp,
color = secondaryTextColor
text =
"$messagesCount message${if (messagesCount > 1) "s" else ""} selected",
fontSize = 14.sp,
color = secondaryTextColor
)
}
}
// Кнопка закрытия с анимацией
IconButton(onClick = { dismissWithAnimation() }) {
Icon(
Icons.Default.Close,
contentDescription = "Close",
tint = secondaryTextColor.copy(alpha = 0.6f)
Icons.Default.Close,
contentDescription = "Close",
tint = secondaryTextColor.copy(alpha = 0.6f)
)
}
}
Spacer(modifier = Modifier.height(12.dp))
Divider(
color = dividerColor,
thickness = 0.5.dp
)
Divider(color = dividerColor, thickness = 0.5.dp)
// Список диалогов
if (dialogs.isEmpty()) {
// Empty state
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
contentAlignment = Alignment.Center
modifier = Modifier.fillMaxWidth().height(200.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "No chats yet",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = secondaryTextColor
text = "No chats yet",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = secondaryTextColor
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Start a conversation first",
fontSize = 14.sp,
color = secondaryTextColor.copy(alpha = 0.7f)
text = "Start a conversation first",
fontSize = 14.sp,
color = secondaryTextColor.copy(alpha = 0.7f)
)
}
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 300.dp, max = 400.dp) // 🔥 Минимальная высота для лучшего UX
modifier =
Modifier.fillMaxWidth()
.heightIn(
min = 300.dp,
max = 400.dp
) // 🔥 Минимальная высота для лучшего UX
) {
items(dialogs, key = { it.opponentKey }) { dialog ->
ForwardDialogItem(
dialog = dialog,
isDarkTheme = isDarkTheme,
isSavedMessages = dialog.opponentKey == currentUserPublicKey,
onClick = {
onChatSelected(dialog.opponentKey)
}
dialog = dialog,
isDarkTheme = isDarkTheme,
isSavedMessages = dialog.opponentKey == currentUserPublicKey,
onClick = { onChatSelected(dialog.opponentKey) }
)
// Сепаратор между диалогами
if (dialog != dialogs.last()) {
Divider(
modifier = Modifier.padding(start = 76.dp),
color = dividerColor,
thickness = 0.5.dp
modifier = Modifier.padding(start = 76.dp),
color = dividerColor,
thickness = 0.5.dp
)
}
}
}
}
// Нижний padding
Spacer(modifier = Modifier.height(16.dp))
}
}
}
/**
* Элемент диалога в списке выбора для Forward
*/
/** Элемент диалога в списке выбора для Forward */
@Composable
private fun ForwardDialogItem(
dialog: DialogUiModel,
isDarkTheme: Boolean,
isSavedMessages: Boolean = false,
onClick: () -> Unit
dialog: DialogUiModel,
isDarkTheme: Boolean,
isSavedMessages: Boolean = false,
onClick: () -> Unit
) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val avatarColors = remember(dialog.opponentKey, isDarkTheme) {
getAvatarColor(dialog.opponentKey, isDarkTheme)
}
val displayName = remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
when {
isSavedMessages -> "Saved Messages"
dialog.opponentTitle.isNotEmpty() -> dialog.opponentTitle
else -> dialog.opponentKey.take(8)
}
}
val initials = remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
when {
isSavedMessages -> "📁"
dialog.opponentTitle.isNotEmpty() -> {
dialog.opponentTitle
.split(" ")
.take(2)
.mapNotNull { it.firstOrNull()?.uppercase() }
.joinToString("")
val avatarColors =
remember(dialog.opponentKey, isDarkTheme) {
getAvatarColor(dialog.opponentKey, isDarkTheme)
}
else -> dialog.opponentKey.take(2).uppercase()
}
}
val displayName =
remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
when {
isSavedMessages -> "Saved Messages"
dialog.opponentTitle.isNotEmpty() -> dialog.opponentTitle
else -> dialog.opponentKey.take(8)
}
}
val initials =
remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
when {
isSavedMessages -> "📁"
dialog.opponentTitle.isNotEmpty() -> {
dialog.opponentTitle
.split(" ")
.take(2)
.mapNotNull { it.firstOrNull()?.uppercase() }
.joinToString("")
}
else -> dialog.opponentKey.take(2).uppercase()
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
modifier =
Modifier.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(
if (isSavedMessages) PrimaryBlue.copy(alpha = 0.15f)
else avatarColors.backgroundColor
),
contentAlignment = Alignment.Center
modifier =
Modifier.size(48.dp)
.clip(CircleShape)
.background(
if (isSavedMessages) PrimaryBlue.copy(alpha = 0.15f)
else avatarColors.backgroundColor
),
contentAlignment = Alignment.Center
) {
Text(
text = initials,
color = if (isSavedMessages) PrimaryBlue else avatarColors.textColor,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp
text = initials,
color = if (isSavedMessages) PrimaryBlue else avatarColors.textColor,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp
)
}
Spacer(modifier = Modifier.width(12.dp))
// Info
Column(
modifier = Modifier.weight(1f)
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = displayName,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
text = displayName,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = if (isSavedMessages) "Your personal notes" else dialog.lastMessage.ifEmpty { "No messages" },
fontSize = 14.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
text =
if (isSavedMessages) "Your personal notes"
else dialog.lastMessage.ifEmpty { "No messages" },
fontSize = 14.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
// Online indicator
if (!isSavedMessages && dialog.isOnline == 1) {
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(Color(0xFF34C759))
)
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(Color(0xFF34C759)))
}
}
}

View File

@@ -1,5 +1,7 @@
package com.rosetta.messenger.ui.chats
import android.content.Context
import android.view.inputmethod.InputMethodManager
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
@@ -21,8 +23,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalView
import android.content.Context
import android.view.inputmethod.InputMethodManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -36,260 +36,251 @@ 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
privateKeyHash: String,
currentUserPublicKey: String,
isDarkTheme: Boolean,
protocolState: ProtocolState,
onBackClick: () -> Unit,
onUserSelect: (SearchUser) -> Unit
) {
// Context и View для мгновенного закрытия клавиатуры
val context = LocalContext.current
val view = LocalView.current
val focusManager = LocalFocusManager.current
// 🔥 Функция мгновенного закрытия клавиатуры
val hideKeyboardInstantly: () -> Unit = {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
focusManager.clearFocus()
}
// Цвета ТОЧНО как в ChatsListScreen
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
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()
// Recent users (не текстовые запросы, а пользователи)
val recentUsers by RecentSearchesManager.recentUsers.collectAsState()
// Preload Lottie composition for search animation
val searchLottieComposition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.search))
val searchLottieComposition by
rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.search))
// Устанавливаем аккаунт для RecentSearchesManager
LaunchedEffect(currentUserPublicKey) {
if (currentUserPublicKey.isNotEmpty()) {
RecentSearchesManager.setAccount(currentUserPublicKey)
}
}
// Устанавливаем 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
) {
Row(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.height(64.dp)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Кнопка назад - с мгновенным закрытием клавиатуры
IconButton(onClick = {
hideKeyboardInstantly()
onBackClick()
}) {
Icon(
Icons.Default.ArrowBack,
contentDescription = "Back",
tint = textColor.copy(alpha = 0.6f)
)
}
// Поле ввода на всю оставшуюся ширину
Box(
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
topBar = {
// Кастомный header с полем ввода на всю ширину
Surface(modifier = Modifier.fillMaxWidth(), color = backgroundColor) {
Row(
modifier =
Modifier.fillMaxWidth()
.statusBarsPadding()
.height(64.dp)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
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() }) {
// Кнопка назад - с мгновенным закрытием клавиатуры
IconButton(
onClick = {
hideKeyboardInstantly()
onBackClick()
}
) {
Icon(
Icons.Default.Clear,
contentDescription = "Clear",
tint = secondaryTextColor.copy(alpha = 0.6f)
Icons.Default.ArrowBack,
contentDescription = "Back",
tint = textColor.copy(alpha = 0.6f)
)
}
// Поле ввода на всю оставшуюся ширину
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.copy(alpha = 0.6f)
)
}
}
}
}
}
},
containerColor = backgroundColor
},
containerColor = backgroundColor
) { paddingValues ->
// Контент - показываем recent users если поле пустое, иначе результаты
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
if (searchQuery.isEmpty() && recentUsers.isNotEmpty()) {
// Recent Users с аватарками
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(vertical = 8.dp)
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(vertical = 8.dp)
) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
modifier =
Modifier.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Recent",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = secondaryTextColor
"Recent",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = secondaryTextColor
)
TextButton(onClick = { RecentSearchesManager.clearAll() }) {
Text(
"Clear All",
fontSize = 13.sp,
color = PrimaryBlue
)
Text("Clear All", fontSize = 13.sp, color = PrimaryBlue)
}
}
}
items(recentUsers, key = { it.publicKey }) { user ->
RecentUserItem(
user = user,
isDarkTheme = isDarkTheme,
textColor = textColor,
secondaryTextColor = secondaryTextColor,
onClick = {
hideKeyboardInstantly()
RecentSearchesManager.addUser(user)
onUserSelect(user)
},
onRemove = {
RecentSearchesManager.removeUser(user.publicKey)
}
user = user,
isDarkTheme = isDarkTheme,
textColor = textColor,
secondaryTextColor = secondaryTextColor,
onClick = {
hideKeyboardInstantly()
RecentSearchesManager.addUser(user)
onUserSelect(user)
},
onRemove = { RecentSearchesManager.removeUser(user.publicKey) }
)
}
}
} else {
// Search Results
// Проверяем, не ищет ли пользователь сам себя (Saved Messages)
val isSavedMessagesSearch = searchQuery.trim().let { query ->
query.equals(currentUserPublicKey, ignoreCase = true) ||
query.equals(currentUserPublicKey.take(8), ignoreCase = true) ||
query.equals(currentUserPublicKey.takeLast(8), ignoreCase = true)
}
// Если ищем себя - показываем Saved Messages как первый результат
val resultsWithSavedMessages = if (isSavedMessagesSearch && searchResults.none { it.publicKey == currentUserPublicKey }) {
listOf(
SearchUser(
title = "Saved Messages",
username = "",
publicKey = currentUserPublicKey,
verified = 0,
online = 1
)
) + searchResults.filter { it.publicKey != currentUserPublicKey }
} else {
searchResults
}
SearchResultsList(
searchResults = resultsWithSavedMessages,
isSearching = isSearching,
currentUserPublicKey = currentUserPublicKey,
isDarkTheme = isDarkTheme,
preloadedComposition = searchLottieComposition,
onUserClick = { user ->
// Мгновенно закрываем клавиатуру
hideKeyboardInstantly()
// Сохраняем пользователя в историю (кроме Saved Messages)
if (user.publicKey != currentUserPublicKey) {
RecentSearchesManager.addUser(user)
val isSavedMessagesSearch =
searchQuery.trim().let { query ->
query.equals(currentUserPublicKey, ignoreCase = true) ||
query.equals(currentUserPublicKey.take(8), ignoreCase = true) ||
query.equals(
currentUserPublicKey.takeLast(8),
ignoreCase = true
)
}
// Если ищем себя - показываем Saved Messages как первый результат
val resultsWithSavedMessages =
if (isSavedMessagesSearch &&
searchResults.none { it.publicKey == currentUserPublicKey }
) {
listOf(
SearchUser(
title = "Saved Messages",
username = "",
publicKey = currentUserPublicKey,
verified = 0,
online = 1
)
) + searchResults.filter { it.publicKey != currentUserPublicKey }
} else {
searchResults
}
SearchResultsList(
searchResults = resultsWithSavedMessages,
isSearching = isSearching,
currentUserPublicKey = currentUserPublicKey,
isDarkTheme = isDarkTheme,
preloadedComposition = searchLottieComposition,
onUserClick = { user ->
// Мгновенно закрываем клавиатуру
hideKeyboardInstantly()
// Сохраняем пользователя в историю (кроме Saved Messages)
if (user.publicKey != currentUserPublicKey) {
RecentSearchesManager.addUser(user)
}
onUserSelect(user)
}
onUserSelect(user)
}
)
}
}
@@ -298,91 +289,85 @@ fun SearchScreen(
@Composable
private fun RecentUserItem(
user: SearchUser,
isDarkTheme: Boolean,
textColor: Color,
secondaryTextColor: Color,
onClick: () -> Unit,
onRemove: () -> Unit
user: SearchUser,
isDarkTheme: Boolean,
textColor: Color,
secondaryTextColor: Color,
onClick: () -> Unit,
onRemove: () -> Unit
) {
val displayName = user.title.ifEmpty {
user.username.ifEmpty {
user.publicKey.take(8) + "..."
}
}
val displayName =
user.title.ifEmpty { user.username.ifEmpty { user.publicKey.take(8) + "..." } }
// Используем getInitials из ChatsListScreen
val initials = getInitials(displayName)
// Используем getAvatarColor из ChatsListScreen для правильных цветов
val avatarColors = getAvatarColor(user.publicKey, isDarkTheme)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
modifier =
Modifier.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
modifier =
Modifier.size(48.dp)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
) {
Text(
text = initials,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = avatarColors.textColor
text = initials,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = avatarColors.textColor
)
}
Spacer(modifier = Modifier.width(12.dp))
// Name and username
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = displayName,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
text = displayName,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (user.verified != 0) {
Spacer(modifier = Modifier.width(4.dp))
Icon(
Icons.Default.Verified,
contentDescription = "Verified",
tint = PrimaryBlue,
modifier = Modifier.size(16.dp)
Icons.Default.Verified,
contentDescription = "Verified",
tint = PrimaryBlue,
modifier = Modifier.size(16.dp)
)
}
}
if (user.username.isNotEmpty()) {
Text(
text = "@${user.username}",
fontSize = 14.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
text = "@${user.username}",
fontSize = 14.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
// Remove button
IconButton(
onClick = onRemove,
modifier = Modifier.size(40.dp)
) {
IconButton(onClick = onRemove, modifier = Modifier.size(40.dp)) {
Icon(
Icons.Default.Close,
contentDescription = "Remove",
tint = secondaryTextColor.copy(alpha = 0.6f),
modifier = Modifier.size(20.dp)
Icons.Default.Close,
contentDescription = "Remove",
tint = secondaryTextColor.copy(alpha = 0.6f),
modifier = Modifier.size(20.dp)
)
}
}