From 67e99901be3b16d7bfdcc98e4553f83f16899089 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Mon, 12 Jan 2026 17:05:38 +0500 Subject: [PATCH] feat: Implement Recent Searches functionality in SearchScreen for improved user experience --- .../com/rosetta/messenger/MainActivity.kt | 2 + .../messenger/data/RecentSearchesManager.kt | 104 +++++++++++ .../messenger/ui/auth/SetPasswordScreen.kt | 2 + .../rosetta/messenger/ui/auth/UnlockScreen.kt | 2 + .../messenger/ui/chats/SearchScreen.kt | 174 ++++++++++++++++-- 5 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/rosetta/messenger/data/RecentSearchesManager.kt diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 3019a02..e4bb6cb 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -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() diff --git a/app/src/main/java/com/rosetta/messenger/data/RecentSearchesManager.kt b/app/src/main/java/com/rosetta/messenger/data/RecentSearchesManager.kt new file mode 100644 index 0000000..60ae8c8 --- /dev/null +++ b/app/src/main/java/com/rosetta/messenger/data/RecentSearchesManager.kt @@ -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>(emptyList()) + val recentUsers: StateFlow> = _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() + 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) { + 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() + } +} diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt index 352d285..ee002ab 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt @@ -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) diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt index 50c87fd..95cb06b 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt @@ -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) { diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt index 3c949cb..420de9f 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt @@ -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) ) } }