feat: Refactor RecentSearchesManager to support multiple accounts and improve user management
This commit is contained in:
@@ -11,23 +11,38 @@ import org.json.JSONObject
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Менеджер для хранения недавно открытых пользователей (до 15 записей)
|
* Менеджер для хранения недавно открытых пользователей (до 15 записей)
|
||||||
|
* Привязан к аккаунту пользователя
|
||||||
*/
|
*/
|
||||||
object RecentSearchesManager {
|
object RecentSearchesManager {
|
||||||
private const val PREFS_NAME = "recent_searches"
|
private const val PREFS_NAME = "recent_searches"
|
||||||
private const val KEY_USERS = "recent_users"
|
private const val KEY_USERS_PREFIX = "recent_users_" // + accountPublicKey
|
||||||
private const val MAX_USERS = 15
|
private const val MAX_USERS = 15
|
||||||
|
|
||||||
private lateinit var prefs: SharedPreferences
|
private lateinit var prefs: SharedPreferences
|
||||||
|
private var currentAccount: String = ""
|
||||||
private val _recentUsers = MutableStateFlow<List<SearchUser>>(emptyList())
|
private val _recentUsers = MutableStateFlow<List<SearchUser>>(emptyList())
|
||||||
val recentUsers: StateFlow<List<SearchUser>> = _recentUsers.asStateFlow()
|
val recentUsers: StateFlow<List<SearchUser>> = _recentUsers.asStateFlow()
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Установить текущий аккаунт и загрузить его историю
|
||||||
|
*/
|
||||||
|
fun setAccount(accountPublicKey: String) {
|
||||||
|
currentAccount = accountPublicKey
|
||||||
loadUsers()
|
loadUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUsersKey(): String = KEY_USERS_PREFIX + currentAccount
|
||||||
|
|
||||||
private fun loadUsers() {
|
private fun loadUsers() {
|
||||||
val usersJson = prefs.getString(KEY_USERS, "[]") ?: "[]"
|
if (currentAccount.isEmpty()) {
|
||||||
|
_recentUsers.value = emptyList()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val usersJson = prefs.getString(getUsersKey(), "[]") ?: "[]"
|
||||||
try {
|
try {
|
||||||
val jsonArray = JSONArray(usersJson)
|
val jsonArray = JSONArray(usersJson)
|
||||||
val users = mutableListOf<SearchUser>()
|
val users = mutableListOf<SearchUser>()
|
||||||
@@ -53,6 +68,8 @@ object RecentSearchesManager {
|
|||||||
* Добавить пользователя в историю
|
* Добавить пользователя в историю
|
||||||
*/
|
*/
|
||||||
fun addUser(user: SearchUser) {
|
fun addUser(user: SearchUser) {
|
||||||
|
if (currentAccount.isEmpty()) return
|
||||||
|
|
||||||
val currentUsers = _recentUsers.value.toMutableList()
|
val currentUsers = _recentUsers.value.toMutableList()
|
||||||
|
|
||||||
// Удаляем если уже есть (чтобы переместить наверх)
|
// Удаляем если уже есть (чтобы переместить наверх)
|
||||||
@@ -73,6 +90,8 @@ object RecentSearchesManager {
|
|||||||
* Удалить пользователя из истории
|
* Удалить пользователя из истории
|
||||||
*/
|
*/
|
||||||
fun removeUser(publicKey: String) {
|
fun removeUser(publicKey: String) {
|
||||||
|
if (currentAccount.isEmpty()) return
|
||||||
|
|
||||||
val currentUsers = _recentUsers.value.toMutableList()
|
val currentUsers = _recentUsers.value.toMutableList()
|
||||||
currentUsers.removeAll { it.publicKey == publicKey }
|
currentUsers.removeAll { it.publicKey == publicKey }
|
||||||
_recentUsers.value = currentUsers
|
_recentUsers.value = currentUsers
|
||||||
@@ -80,14 +99,18 @@ object RecentSearchesManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Очистить всю историю
|
* Очистить всю историю текущего аккаунта
|
||||||
*/
|
*/
|
||||||
fun clearAll() {
|
fun clearAll() {
|
||||||
_recentUsers.value = emptyList()
|
_recentUsers.value = emptyList()
|
||||||
prefs.edit().remove(KEY_USERS).apply()
|
if (currentAccount.isNotEmpty()) {
|
||||||
|
prefs.edit().remove(getUsersKey()).apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveUsers(users: List<SearchUser>) {
|
private fun saveUsers(users: List<SearchUser>) {
|
||||||
|
if (currentAccount.isEmpty()) return
|
||||||
|
|
||||||
val jsonArray = JSONArray()
|
val jsonArray = JSONArray()
|
||||||
users.forEach { user ->
|
users.forEach { user ->
|
||||||
val obj = JSONObject().apply {
|
val obj = JSONObject().apply {
|
||||||
@@ -99,6 +122,6 @@ object RecentSearchesManager {
|
|||||||
}
|
}
|
||||||
jsonArray.put(obj)
|
jsonArray.put(obj)
|
||||||
}
|
}
|
||||||
prefs.edit().putString(KEY_USERS, jsonArray.toString()).apply()
|
prefs.edit().putString(getUsersKey(), jsonArray.toString()).apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,8 +110,9 @@ class Protocol(
|
|||||||
private fun startHeartbeat(intervalSeconds: Int) {
|
private fun startHeartbeat(intervalSeconds: Int) {
|
||||||
heartbeatJob?.cancel()
|
heartbeatJob?.cancel()
|
||||||
|
|
||||||
val intervalMs = (intervalSeconds * 1000L) / 2 // Send at half the interval
|
// Отправляем чаще - каждые 1/3 интервала (чтобы не терять соединение)
|
||||||
log("💓 Starting heartbeat with interval: ${intervalSeconds}s (sending every ${intervalMs}ms)")
|
val intervalMs = (intervalSeconds * 1000L) / 3
|
||||||
|
log("💓 Starting heartbeat with server interval: ${intervalSeconds}s (sending every ${intervalMs}ms = ${intervalMs/1000}s)")
|
||||||
|
|
||||||
heartbeatJob = scope.launch {
|
heartbeatJob = scope.launch {
|
||||||
// ⚡ СРАЗУ отправляем первый heartbeat (как в Архиве)
|
// ⚡ СРАЗУ отправляем первый heartbeat (как в Архиве)
|
||||||
@@ -200,16 +201,17 @@ class Protocol(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
log("⚠️ WebSocket closing: code=$code reason='$reason'")
|
// Code 3887 - кастомный код сервера, просто делаем reconnect тихо
|
||||||
log("⚠️ Stack trace at closing:")
|
if (code != 3887) {
|
||||||
Thread.currentThread().stackTrace.take(10).forEach {
|
log("⚠️ WebSocket closing: code=$code reason='$reason'")
|
||||||
log(" at $it")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
log("❌ WebSocket closed: code=$code reason='$reason'")
|
// Для кода 3887 не логируем - это частое закрытие сервером
|
||||||
log("❌ State was: ${_state.value}")
|
if (code != 3887) {
|
||||||
|
log("❌ WebSocket closed: code=$code reason='$reason'")
|
||||||
|
}
|
||||||
handleDisconnect()
|
handleDisconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,20 +360,13 @@ class Protocol(
|
|||||||
|
|
||||||
// Автоматический reconnect (простая логика как в Архиве - без счётчиков)
|
// Автоматический reconnect (простая логика как в Архиве - без счётчиков)
|
||||||
if (!isManuallyClosed) {
|
if (!isManuallyClosed) {
|
||||||
log("🔄 Connection lost from $previousState, attempting to reconnect...")
|
log("🔄 Reconnecting silently...")
|
||||||
log("🔄 Reconnecting in ${RECONNECT_INTERVAL}ms")
|
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(RECONNECT_INTERVAL)
|
// Быстрый reconnect - 1 секунда
|
||||||
|
delay(1000L)
|
||||||
if (!isManuallyClosed) {
|
if (!isManuallyClosed) {
|
||||||
connect()
|
connect()
|
||||||
// В Desktop проверяется socket?.readyState == WebSocket.OPEN
|
|
||||||
// В Android аналог - проверка state
|
|
||||||
if (_state.value == ProtocolState.CONNECTED ||
|
|
||||||
_state.value == ProtocolState.AUTHENTICATED) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
// В Desktop emit('reconnect') происходит здесь
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.airbnb.lottie.compose.*
|
import com.airbnb.lottie.compose.*
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
|
import com.rosetta.messenger.data.RecentSearchesManager
|
||||||
import com.rosetta.messenger.database.DialogEntity
|
import com.rosetta.messenger.database.DialogEntity
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
@@ -183,6 +184,8 @@ fun ChatsListScreen(
|
|||||||
LaunchedEffect(accountPublicKey) {
|
LaunchedEffect(accountPublicKey) {
|
||||||
if (accountPublicKey.isNotEmpty()) {
|
if (accountPublicKey.isNotEmpty()) {
|
||||||
chatsViewModel.setAccount(accountPublicKey)
|
chatsViewModel.setAccount(accountPublicKey)
|
||||||
|
// Устанавливаем аккаунт для RecentSearchesManager
|
||||||
|
RecentSearchesManager.setAccount(accountPublicKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ fun SearchScreen(
|
|||||||
// Recent users (не текстовые запросы, а пользователи)
|
// Recent users (не текстовые запросы, а пользователи)
|
||||||
val recentUsers by RecentSearchesManager.recentUsers.collectAsState()
|
val recentUsers by RecentSearchesManager.recentUsers.collectAsState()
|
||||||
|
|
||||||
|
// Устанавливаем аккаунт для RecentSearchesManager
|
||||||
|
LaunchedEffect(currentUserPublicKey) {
|
||||||
|
if (currentUserPublicKey.isNotEmpty()) {
|
||||||
|
RecentSearchesManager.setAccount(currentUserPublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Устанавливаем privateKeyHash
|
// Устанавливаем privateKeyHash
|
||||||
LaunchedEffect(privateKeyHash) {
|
LaunchedEffect(privateKeyHash) {
|
||||||
if (privateKeyHash.isNotEmpty()) {
|
if (privateKeyHash.isNotEmpty()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user