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:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user