feat: Enhance search functionality and user experience

- Added local account metadata handling in SearchScreen for improved "Saved Messages" search fallback.
- Updated search logic to include username and account name checks when searching for the user.
- Introduced search logging in SearchUsersViewModel for better debugging and tracking of search queries.
- Refactored image download process in AttachmentComponents to include detailed logging for debugging.
- Created AttachmentDownloadDebugLogger to manage and display download logs.
- Improved DeviceVerificationBanner UI for better user engagement during device verification.
- Adjusted OtherProfileScreen layout to enhance information visibility and user interaction.
- Updated network security configuration to include new Let's Encrypt certificate for CDN.
This commit is contained in:
2026-02-19 17:34:16 +05:00
parent cacd6dc029
commit 53d0e44ef8
26 changed files with 972 additions and 613 deletions

View File

@@ -26,8 +26,10 @@ import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
import com.rosetta.messenger.data.AccountManager
import com.rosetta.messenger.data.DecryptedAccount
import com.rosetta.messenger.data.EncryptedAccount
import com.rosetta.messenger.data.PreferencesManager
import com.rosetta.messenger.data.RecentSearchesManager
import com.rosetta.messenger.data.resolveAccountDisplayName
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.PacketPushNotification
import com.rosetta.messenger.network.ProtocolManager
@@ -158,31 +160,7 @@ class MainActivity : FragmentActivity() {
accountManager.logout() // Always start logged out
val accounts = accountManager.getAllAccounts()
hasExistingAccount = accounts.isNotEmpty()
accountInfoList =
accounts.map { account ->
val shortKey = account.publicKey.take(7)
val displayName = account.name ?: shortKey
val initials =
displayName
.trim()
.split(Regex("\\s+"))
.filter { it.isNotEmpty() }
.let { words ->
when {
words.isEmpty() -> "??"
words.size == 1 -> words[0].take(2).uppercase()
else ->
"${words[0].first()}${words[1].first()}".uppercase()
}
}
AccountInfo(
id = account.publicKey,
name = displayName,
username = account.username ?: "",
initials = initials,
publicKey = account.publicKey
)
}
accountInfoList = accounts.map { it.toAccountInfo() }
}
// Wait for initial load
@@ -197,6 +175,15 @@ class MainActivity : FragmentActivity() {
return@setContent
}
// Ensure push token subscription is sent whenever protocol reaches AUTHENTICATED.
// This recovers token binding after reconnects and delayed handshakes.
LaunchedEffect(protocolState, currentAccount?.publicKey) {
currentAccount ?: return@LaunchedEffect
if (protocolState == ProtocolState.AUTHENTICATED) {
sendFcmTokenToServer()
}
}
RosettaAndroidTheme(darkTheme = isDarkTheme, animated = true) {
Surface(
modifier = Modifier.fillMaxSize(),
@@ -212,6 +199,10 @@ class MainActivity : FragmentActivity() {
"auth_new"
isLoggedIn != true && hasExistingAccount == true ->
"auth_unlock"
isLoggedIn == true &&
currentAccount == null &&
hasExistingAccount == true ->
"auth_unlock"
protocolState ==
ProtocolState.DEVICE_VERIFICATION_REQUIRED ->
"device_confirm"
@@ -261,45 +252,12 @@ class MainActivity : FragmentActivity() {
// 📤 Отправляем FCM токен на сервер после успешной
// аутентификации
account?.let { sendFcmTokenToServer(it) }
account?.let { sendFcmTokenToServer() }
// Reload accounts list
scope.launch {
val accounts = accountManager.getAllAccounts()
accountInfoList =
accounts.map { acc ->
val shortKey = acc.publicKey.take(7)
val displayName = acc.name ?: shortKey
val initials =
displayName
.trim()
.split(Regex("\\s+"))
.filter {
it.isNotEmpty()
}
.let { words ->
when {
words.isEmpty() ->
"??"
words.size ==
1 ->
words[0]
.take(
2
)
.uppercase()
else ->
"${words[0].first()}${words[1].first()}".uppercase()
}
}
AccountInfo(
id = acc.publicKey,
name = displayName,
username = acc.username ?: "",
initials = initials,
publicKey = acc.publicKey
)
}
accountInfoList = accounts.map { it.toAccountInfo() }
}
},
onLogout = {
@@ -359,27 +317,7 @@ class MainActivity : FragmentActivity() {
accountManager.deleteAccount(publicKey)
// 8. Refresh accounts list
val accounts = accountManager.getAllAccounts()
accountInfoList = accounts.map { acc ->
val shortKey = acc.publicKey.take(7)
val displayName = acc.name ?: shortKey
val initials = displayName.trim()
.split(Regex("\\s+"))
.filter { it.isNotEmpty() }
.let { words ->
when {
words.isEmpty() -> "??"
words.size == 1 -> words[0].take(2).uppercase()
else -> "${words[0].first()}${words[1].first()}".uppercase()
}
}
AccountInfo(
id = acc.publicKey,
name = displayName,
username = acc.username ?: "",
initials = initials,
publicKey = acc.publicKey
)
}
accountInfoList = accounts.map { it.toAccountInfo() }
hasExistingAccount = accounts.isNotEmpty()
// 8. Navigate away last
currentAccount = null
@@ -391,37 +329,7 @@ class MainActivity : FragmentActivity() {
onAccountInfoUpdated = {
// Reload account list when profile is updated
val accounts = accountManager.getAllAccounts()
accountInfoList =
accounts.map { acc ->
val shortKey = acc.publicKey.take(7)
val displayName = acc.name ?: shortKey
val initials =
displayName
.trim()
.split(Regex("\\s+"))
.filter { it.isNotEmpty() }
.let { words ->
when {
words.isEmpty() ->
"??"
words.size == 1 ->
words[0]
.take(
2
)
.uppercase()
else ->
"${words[0].first()}${words[1].first()}".uppercase()
}
}
AccountInfo(
id = acc.publicKey,
name = displayName,
username = acc.username ?: "",
initials = initials,
publicKey = acc.publicKey
)
}
accountInfoList = accounts.map { it.toAccountInfo() }
},
onSwitchAccount = { targetPublicKey ->
// Switch to another account: logout current, then auto-login target
@@ -491,6 +399,11 @@ class MainActivity : FragmentActivity() {
// Сохраняем токен локально
saveFcmToken(token)
addFcmLog("💾 Токен сохранен локально")
if (ProtocolManager.state.value == ProtocolState.AUTHENTICATED) {
addFcmLog("🔁 Протокол уже AUTHENTICATED, отправляем токен сразу")
sendFcmTokenToServer()
}
} else {
addFcmLog("⚠️ Токен пустой")
}
@@ -514,7 +427,7 @@ class MainActivity : FragmentActivity() {
* Отправить FCM токен на сервер Вызывается после успешной аутентификации, когда аккаунт уже
* расшифрован
*/
private fun sendFcmTokenToServer(account: DecryptedAccount) {
private fun sendFcmTokenToServer() {
lifecycleScope.launch {
try {
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
@@ -558,6 +471,30 @@ class MainActivity : FragmentActivity() {
}
}
private fun buildInitials(displayName: String): String =
displayName
.trim()
.split(Regex("\\s+"))
.filter { it.isNotEmpty() }
.let { words ->
when {
words.isEmpty() -> "??"
words.size == 1 -> words[0].take(2).uppercase()
else -> "${words[0].first()}${words[1].first()}".uppercase()
}
}
private fun EncryptedAccount.toAccountInfo(): AccountInfo {
val displayName = resolveAccountDisplayName(publicKey, name, username)
return AccountInfo(
id = publicKey,
name = displayName,
username = username ?: "",
initials = buildInitials(displayName),
publicKey = publicKey
)
}
/**
* Navigation sealed class — replaces ~15 boolean flags with a type-safe navigation stack. ChatsList
* is always the base layer and is not part of the stack. Each SwipeBackContainer reads a
@@ -592,14 +529,17 @@ fun MainScreen(
onAccountInfoUpdated: suspend () -> Unit = {},
onSwitchAccount: (String) -> Unit = {}
) {
val accountPublicKey = account?.publicKey.orEmpty()
// Reactive state for account name and username
var accountName by remember { mutableStateOf(account?.name ?: "Account") }
var accountName by remember(accountPublicKey) {
mutableStateOf(resolveAccountDisplayName(accountPublicKey, account?.name, null))
}
val accountPhone =
account?.publicKey?.take(16)?.let {
"+${it.take(1)} ${it.substring(1, 4)} ${it.substring(4, 7)}${it.substring(7)}"
}
?: "+7 775 9932587"
val accountPublicKey = account?.publicKey ?: "04c266b98ae5"
.orEmpty()
val accountPrivateKey = account?.privateKey ?: ""
val privateKeyHash = account?.privateKeyHash ?: ""
@@ -611,11 +551,17 @@ fun MainScreen(
// Load username AND name from AccountManager (persisted in DataStore)
val context = LocalContext.current
LaunchedEffect(accountPublicKey, reloadTrigger) {
if (accountPublicKey.isNotBlank() && accountPublicKey != "04c266b98ae5") {
if (accountPublicKey.isNotBlank()) {
val accountManager = AccountManager(context)
val encryptedAccount = accountManager.getAccount(accountPublicKey)
accountUsername = encryptedAccount?.username ?: ""
accountName = encryptedAccount?.name ?: accountName
val username = encryptedAccount?.username
accountUsername = username.orEmpty()
accountName =
resolveAccountDisplayName(
accountPublicKey,
encryptedAccount?.name ?: accountName,
username
)
}
}
@@ -625,14 +571,17 @@ fun MainScreen(
// Реактивно обновляем username/name когда сервер отвечает на fetchOwnProfile()
val ownProfileUpdated by ProtocolManager.ownProfileUpdated.collectAsState()
LaunchedEffect(ownProfileUpdated) {
if (ownProfileUpdated > 0L &&
accountPublicKey.isNotBlank() &&
accountPublicKey != "04c266b98ae5"
) {
if (ownProfileUpdated > 0L && accountPublicKey.isNotBlank()) {
val accountManager = AccountManager(context)
val encryptedAccount = accountManager.getAccount(accountPublicKey)
accountUsername = encryptedAccount?.username ?: ""
accountName = encryptedAccount?.name ?: accountName
val username = encryptedAccount?.username
accountUsername = username.orEmpty()
accountName =
resolveAccountDisplayName(
accountPublicKey,
encryptedAccount?.name ?: accountName,
username
)
}
}
@@ -715,7 +664,7 @@ fun MainScreen(
// AvatarRepository для работы с аватарами
val avatarRepository =
remember(accountPublicKey) {
if (accountPublicKey.isNotBlank() && accountPublicKey != "04c266b98ae5") {
if (accountPublicKey.isNotBlank()) {
val database = RosettaDatabase.getDatabase(context)
AvatarRepository(
context = context,