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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user