feat: Implement Recent Searches functionality in SearchScreen for improved user experience

This commit is contained in:
k1ngsterr1
2026-01-12 17:05:38 +05:00
parent 325c5ace4b
commit 67e99901be
5 changed files with 273 additions and 11 deletions

View File

@@ -24,6 +24,7 @@ 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.data.RecentSearchesManager
import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.ui.auth.AccountInfo
import com.rosetta.messenger.ui.auth.AuthFlow
@@ -46,6 +47,7 @@ class MainActivity : ComponentActivity() {
preferencesManager = PreferencesManager(this)
accountManager = AccountManager(this)
RecentSearchesManager.init(this)
setContent {
val scope = rememberCoroutineScope()

View File

@@ -0,0 +1,104 @@
package com.rosetta.messenger.data
import android.content.Context
import android.content.SharedPreferences
import com.rosetta.messenger.network.SearchUser
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.json.JSONArray
import org.json.JSONObject
/**
* Менеджер для хранения недавно открытых пользователей (до 15 записей)
*/
object RecentSearchesManager {
private const val PREFS_NAME = "recent_searches"
private const val KEY_USERS = "recent_users"
private const val MAX_USERS = 15
private lateinit var prefs: SharedPreferences
private val _recentUsers = MutableStateFlow<List<SearchUser>>(emptyList())
val recentUsers: StateFlow<List<SearchUser>> = _recentUsers.asStateFlow()
fun init(context: Context) {
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
loadUsers()
}
private fun loadUsers() {
val usersJson = prefs.getString(KEY_USERS, "[]") ?: "[]"
try {
val jsonArray = JSONArray(usersJson)
val users = mutableListOf<SearchUser>()
for (i in 0 until jsonArray.length()) {
val obj = jsonArray.getJSONObject(i)
users.add(
SearchUser(
publicKey = obj.getString("publicKey"),
username = obj.optString("username", ""),
title = obj.optString("title", ""),
verified = obj.optInt("verified", 0),
online = obj.optInt("online", 0)
)
)
}
_recentUsers.value = users
} catch (e: Exception) {
_recentUsers.value = emptyList()
}
}
/**
* Добавить пользователя в историю
*/
fun addUser(user: SearchUser) {
val currentUsers = _recentUsers.value.toMutableList()
// Удаляем если уже есть (чтобы переместить наверх)
currentUsers.removeAll { it.publicKey == user.publicKey }
// Добавляем в начало
currentUsers.add(0, user)
// Ограничиваем до MAX_USERS
val limitedUsers = currentUsers.take(MAX_USERS)
// Сохраняем
_recentUsers.value = limitedUsers
saveUsers(limitedUsers)
}
/**
* Удалить пользователя из истории
*/
fun removeUser(publicKey: String) {
val currentUsers = _recentUsers.value.toMutableList()
currentUsers.removeAll { it.publicKey == publicKey }
_recentUsers.value = currentUsers
saveUsers(currentUsers)
}
/**
* Очистить всю историю
*/
fun clearAll() {
_recentUsers.value = emptyList()
prefs.edit().remove(KEY_USERS).apply()
}
private fun saveUsers(users: List<SearchUser>) {
val jsonArray = JSONArray()
users.forEach { user ->
val obj = JSONObject().apply {
put("publicKey", user.publicKey)
put("username", user.username)
put("title", user.title)
put("verified", user.verified)
put("online", user.online)
}
jsonArray.put(obj)
}
prefs.edit().putString(KEY_USERS, jsonArray.toString()).apply()
}
}

View File

@@ -494,6 +494,8 @@ fun SetPasswordScreen(
accountManager.saveAccount(account)
accountManager.setCurrentAccount(keyPair.publicKey)
// Save as last logged account for next time
accountManager.setLastLoggedPublicKey(keyPair.publicKey)
// 🔌 Connect to server and authenticate
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)

View File

@@ -587,6 +587,8 @@ fun UnlockScreen(
ProtocolManager.authenticate(account.publicKey, privateKeyHash)
accountManager.setCurrentAccount(account.publicKey)
// Save as last logged account for next time
accountManager.setLastLoggedPublicKey(account.publicKey)
onUnlocked(decryptedAccount)
} catch (e: Exception) {

View File

@@ -4,6 +4,9 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
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.*
@@ -11,12 +14,15 @@ 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.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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.data.RecentSearchesManager
import com.rosetta.messenger.network.ProtocolState
import com.rosetta.messenger.network.SearchUser
@@ -37,7 +43,8 @@ fun SearchScreen(
onBackClick: () -> Unit,
onUserSelect: (SearchUser) -> Unit
) {
val backgroundColor = if (isDarkTheme) Color(0xFF121212) else Color(0xFFF8F9FA)
// Цвета ТОЧНО как в 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
@@ -48,6 +55,9 @@ fun SearchScreen(
val searchResults by searchViewModel.searchResults.collectAsState()
val isSearching by searchViewModel.isSearching.collectAsState()
// Recent users (не текстовые запросы, а пользователи)
val recentUsers by RecentSearchesManager.recentUsers.collectAsState()
// Устанавливаем privateKeyHash
LaunchedEffect(privateKeyHash) {
if (privateKeyHash.isNotEmpty()) {
@@ -69,8 +79,7 @@ fun SearchScreen(
// Кастомный header с полем ввода на всю ширину
Surface(
modifier = Modifier.fillMaxWidth(),
color = backgroundColor,
shadowElevation = 4.dp
color = backgroundColor
) {
Row(
modifier = Modifier
@@ -157,20 +166,163 @@ fun SearchScreen(
},
containerColor = backgroundColor
) { paddingValues ->
// Контент - результаты поиска
// Контент - показываем recent users если поле пустое, иначе результаты
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
SearchResultsList(
searchResults = searchResults,
isSearching = isSearching,
currentUserPublicKey = currentUserPublicKey,
isDarkTheme = isDarkTheme,
onUserClick = { user ->
onUserSelect(user)
if (searchQuery.isEmpty() && recentUsers.isNotEmpty()) {
// Recent Users с аватарками
LazyColumn(
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
) {
Text(
"Recent",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = secondaryTextColor
)
TextButton(onClick = { RecentSearchesManager.clearAll() }) {
Text(
"Clear All",
fontSize = 13.sp,
color = PrimaryBlue
)
}
}
}
items(recentUsers, key = { it.publicKey }) { user ->
RecentUserItem(
user = user,
isDarkTheme = isDarkTheme,
textColor = textColor,
secondaryTextColor = secondaryTextColor,
onClick = {
RecentSearchesManager.addUser(user)
onUserSelect(user)
},
onRemove = {
RecentSearchesManager.removeUser(user.publicKey)
}
)
}
}
} else {
// Search Results
SearchResultsList(
searchResults = searchResults,
isSearching = isSearching,
currentUserPublicKey = currentUserPublicKey,
isDarkTheme = isDarkTheme,
onUserClick = { user ->
// Сохраняем пользователя в историю
RecentSearchesManager.addUser(user)
onUserSelect(user)
}
)
}
}
}
}
@Composable
private fun RecentUserItem(
user: SearchUser,
isDarkTheme: Boolean,
textColor: Color,
secondaryTextColor: Color,
onClick: () -> Unit,
onRemove: () -> Unit
) {
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
) {
// Avatar
Box(
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
)
}
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
)
if (user.verified != 0) {
Spacer(modifier = Modifier.width(4.dp))
Icon(
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
)
}
}
// Remove button
IconButton(
onClick = onRemove,
modifier = Modifier.size(40.dp)
) {
Icon(
Icons.Default.Close,
contentDescription = "Remove",
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
)
}
}