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.AccountManager
import com.rosetta.messenger.data.DecryptedAccount import com.rosetta.messenger.data.DecryptedAccount
import com.rosetta.messenger.data.PreferencesManager import com.rosetta.messenger.data.PreferencesManager
import com.rosetta.messenger.data.RecentSearchesManager
import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.ui.auth.AccountInfo import com.rosetta.messenger.ui.auth.AccountInfo
import com.rosetta.messenger.ui.auth.AuthFlow import com.rosetta.messenger.ui.auth.AuthFlow
@@ -46,6 +47,7 @@ class MainActivity : ComponentActivity() {
preferencesManager = PreferencesManager(this) preferencesManager = PreferencesManager(this)
accountManager = AccountManager(this) accountManager = AccountManager(this)
RecentSearchesManager.init(this)
setContent { setContent {
val scope = rememberCoroutineScope() 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.saveAccount(account)
accountManager.setCurrentAccount(keyPair.publicKey) accountManager.setCurrentAccount(keyPair.publicKey)
// Save as last logged account for next time
accountManager.setLastLoggedPublicKey(keyPair.publicKey)
// 🔌 Connect to server and authenticate // 🔌 Connect to server and authenticate
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey) val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)

View File

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

View File

@@ -4,6 +4,9 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* 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.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@@ -11,12 +14,15 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.rosetta.messenger.data.RecentSearchesManager
import com.rosetta.messenger.network.ProtocolState import com.rosetta.messenger.network.ProtocolState
import com.rosetta.messenger.network.SearchUser import com.rosetta.messenger.network.SearchUser
@@ -37,7 +43,8 @@ fun SearchScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onUserSelect: (SearchUser) -> 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 textColor = if (isDarkTheme) Color.White else Color(0xFF1a1a1a)
val secondaryTextColor = if (isDarkTheme) Color(0xFFB0B0B0) else Color(0xFF6c757d) val secondaryTextColor = if (isDarkTheme) Color(0xFFB0B0B0) else Color(0xFF6c757d)
val surfaceColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color.White val surfaceColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color.White
@@ -48,6 +55,9 @@ fun SearchScreen(
val searchResults by searchViewModel.searchResults.collectAsState() val searchResults by searchViewModel.searchResults.collectAsState()
val isSearching by searchViewModel.isSearching.collectAsState() val isSearching by searchViewModel.isSearching.collectAsState()
// Recent users (не текстовые запросы, а пользователи)
val recentUsers by RecentSearchesManager.recentUsers.collectAsState()
// Устанавливаем privateKeyHash // Устанавливаем privateKeyHash
LaunchedEffect(privateKeyHash) { LaunchedEffect(privateKeyHash) {
if (privateKeyHash.isNotEmpty()) { if (privateKeyHash.isNotEmpty()) {
@@ -69,8 +79,7 @@ fun SearchScreen(
// Кастомный header с полем ввода на всю ширину // Кастомный header с полем ввода на всю ширину
Surface( Surface(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
color = backgroundColor, color = backgroundColor
shadowElevation = 4.dp
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -157,20 +166,163 @@ fun SearchScreen(
}, },
containerColor = backgroundColor containerColor = backgroundColor
) { paddingValues -> ) { paddingValues ->
// Контент - результаты поиска // Контент - показываем recent users если поле пустое, иначе результаты
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
SearchResultsList( if (searchQuery.isEmpty() && recentUsers.isNotEmpty()) {
searchResults = searchResults, // Recent Users с аватарками
isSearching = isSearching, LazyColumn(
currentUserPublicKey = currentUserPublicKey, modifier = Modifier.fillMaxSize(),
isDarkTheme = isDarkTheme, contentPadding = PaddingValues(vertical = 8.dp)
onUserClick = { user -> ) {
onUserSelect(user) 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)
) )
} }
} }