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:
@@ -0,0 +1,31 @@
|
||||
package com.rosetta.messenger.ui.auth
|
||||
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
import com.rosetta.messenger.network.ProtocolState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
internal suspend fun awaitAuthHandshakeState(
|
||||
publicKey: String,
|
||||
privateKeyHash: String,
|
||||
attempts: Int = 2,
|
||||
timeoutMs: Long = 25_000L
|
||||
): ProtocolState? {
|
||||
repeat(attempts) {
|
||||
ProtocolManager.disconnect()
|
||||
delay(200)
|
||||
ProtocolManager.authenticate(publicKey, privateKeyHash)
|
||||
|
||||
val state = withTimeoutOrNull(timeoutMs) {
|
||||
ProtocolManager.state.first {
|
||||
it == ProtocolState.AUTHENTICATED ||
|
||||
it == ProtocolState.DEVICE_VERIFICATION_REQUIRED
|
||||
}
|
||||
}
|
||||
if (state != null) {
|
||||
return state
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.rosetta.messenger.ui.auth
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -15,6 +20,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
@@ -29,6 +35,8 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -54,11 +62,16 @@ fun DeviceConfirmScreen(
|
||||
isDarkTheme: Boolean,
|
||||
onExit: () -> Unit
|
||||
) {
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
|
||||
val cardColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White
|
||||
val cardBorderColor = if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFE8E8ED)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val backgroundTop = if (isDarkTheme) Color(0xFF17181D) else Color(0xFFF4F7FC)
|
||||
val backgroundBottom = if (isDarkTheme) Color(0xFF121316) else Color(0xFFE9EEF7)
|
||||
val cardColor = if (isDarkTheme) Color(0xFF23252B) else Color.White
|
||||
val cardBorderColor = if (isDarkTheme) Color(0xFF343844) else Color(0xFFDCE4F0)
|
||||
val textColor = if (isDarkTheme) Color(0xFFF2F3F5) else Color(0xFF1B1C1F)
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFFB2B5BD) else Color(0xFF6F7480)
|
||||
val accentColor = if (isDarkTheme) Color(0xFF4A9FFF) else PrimaryBlue
|
||||
val deviceCardColor = if (isDarkTheme) Color(0xFF1A1C22) else Color(0xFFF5F8FD)
|
||||
val exitButtonColor = if (isDarkTheme) Color(0xFF3D2227) else Color(0xFFFFEAED)
|
||||
val exitButtonTextColor = Color(0xFFFF5E61)
|
||||
val onExitState by rememberUpdatedState(onExit)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -92,7 +105,12 @@ fun DeviceConfirmScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
.background(
|
||||
brush =
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(backgroundTop, backgroundBottom)
|
||||
)
|
||||
)
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 22.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -102,98 +120,160 @@ fun DeviceConfirmScreen(
|
||||
.fillMaxWidth()
|
||||
.widthIn(max = 400.dp),
|
||||
color = cardColor,
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
border = BorderStroke(1.dp, cardBorderColor)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp),
|
||||
modifier = Modifier.padding(horizontal = 22.dp, vertical = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
LottieAnimation(
|
||||
composition = composition,
|
||||
progress = { progress },
|
||||
modifier = Modifier.size(128.dp)
|
||||
)
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(118.dp)
|
||||
.clip(CircleShape)
|
||||
.background(accentColor.copy(alpha = if (isDarkTheme) 0.16f else 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
LottieAnimation(
|
||||
composition = composition,
|
||||
progress = { progress },
|
||||
modifier = Modifier.size(96.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.DeviceMobile,
|
||||
contentDescription = null,
|
||||
tint = PrimaryBlue
|
||||
tint = accentColor
|
||||
)
|
||||
Spacer(modifier = Modifier.size(6.dp))
|
||||
Text(
|
||||
text = "NEW DEVICE REQUEST",
|
||||
color = PrimaryBlue,
|
||||
color = accentColor,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Text(
|
||||
text = "Waiting for approval",
|
||||
color = textColor,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Text(
|
||||
text = "Open Rosetta on your first device and approve this login request.",
|
||||
color = secondaryTextColor,
|
||||
fontSize = 14.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
|
||||
Text(
|
||||
text = "\"$localDeviceName\" is waiting for approval",
|
||||
color = textColor.copy(alpha = 0.9f),
|
||||
fontSize = 13.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
Text(
|
||||
text = "If you didn't request this login, tap Exit.",
|
||||
color = secondaryTextColor,
|
||||
fontSize = 12.sp,
|
||||
text = "Waiting for approval",
|
||||
color = textColor,
|
||||
fontSize = 34.sp,
|
||||
lineHeight = 38.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Button(
|
||||
onClick = onExitState,
|
||||
modifier = Modifier.height(42.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFFF3B30),
|
||||
contentColor = Color.White
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
Text(
|
||||
text = "Open Rosetta on your first device and approve this login request.",
|
||||
color = secondaryTextColor,
|
||||
fontSize = 15.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = deviceCardColor,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
border = BorderStroke(1.dp, cardBorderColor.copy(alpha = if (isDarkTheme) 0.7f else 1f))
|
||||
) {
|
||||
Text("Exit")
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
text = "Device waiting for approval",
|
||||
color = secondaryTextColor,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = localDeviceName,
|
||||
color = textColor,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Waiting for confirmation...",
|
||||
color = secondaryTextColor,
|
||||
fontSize = 11.sp
|
||||
)
|
||||
Button(
|
||||
onClick = onExitState,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(46.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = exitButtonColor,
|
||||
contentColor = exitButtonTextColor
|
||||
),
|
||||
border = BorderStroke(1.dp, exitButtonTextColor.copy(alpha = 0.35f)),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Exit",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Waiting for confirmation",
|
||||
color = secondaryTextColor,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
WaitingDots(color = secondaryTextColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WaitingDots(color: Color) {
|
||||
val transition = rememberInfiniteTransition(label = "waiting-dots")
|
||||
val progress by transition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec =
|
||||
infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1000, easing = FastOutSlowInEasing)
|
||||
),
|
||||
label = "waiting-dots-progress"
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
repeat(3) { index ->
|
||||
val shifted = (progress + index * 0.22f) % 1f
|
||||
val alpha = 0.3f + (1f - shifted) * 0.7f
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(5.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color.copy(alpha = alpha))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import com.rosetta.messenger.crypto.CryptoManager
|
||||
import com.rosetta.messenger.data.AccountManager
|
||||
import com.rosetta.messenger.data.DecryptedAccount
|
||||
import com.rosetta.messenger.data.EncryptedAccount
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -520,20 +519,26 @@ fun SetPasswordScreen(
|
||||
)
|
||||
|
||||
accountManager.saveAccount(account)
|
||||
accountManager.setCurrentAccount(keyPair.publicKey)
|
||||
|
||||
// 🔌 Connect to server and authenticate
|
||||
val privateKeyHash =
|
||||
CryptoManager.generatePrivateKeyHash(
|
||||
keyPair.privateKey
|
||||
)
|
||||
ProtocolManager.connect()
|
||||
// Give WebSocket time to connect before authenticating
|
||||
kotlinx.coroutines.delay(500)
|
||||
ProtocolManager.authenticate(
|
||||
keyPair.publicKey,
|
||||
privateKeyHash
|
||||
)
|
||||
|
||||
val handshakeState =
|
||||
awaitAuthHandshakeState(
|
||||
keyPair.publicKey,
|
||||
privateKeyHash
|
||||
)
|
||||
if (handshakeState == null) {
|
||||
error =
|
||||
"Failed to connect to server. Please try again."
|
||||
isCreating = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
accountManager.setCurrentAccount(keyPair.publicKey)
|
||||
|
||||
// Create DecryptedAccount to pass to callback
|
||||
val decryptedAccount =
|
||||
|
||||
@@ -43,19 +43,16 @@ import com.rosetta.messenger.crypto.CryptoManager
|
||||
import com.rosetta.messenger.data.AccountManager
|
||||
import com.rosetta.messenger.data.DecryptedAccount
|
||||
import com.rosetta.messenger.data.EncryptedAccount
|
||||
import com.rosetta.messenger.data.resolveAccountDisplayName
|
||||
import com.rosetta.messenger.database.RosettaDatabase
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
import com.rosetta.messenger.network.ProtocolState
|
||||
import com.rosetta.messenger.repository.AvatarRepository
|
||||
import com.rosetta.messenger.ui.components.AvatarImage
|
||||
import com.rosetta.messenger.ui.chats.getAvatarColor
|
||||
import com.rosetta.messenger.ui.chats.getAvatarText
|
||||
import com.rosetta.messenger.ui.chats.utils.getInitials
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// Account model for dropdown
|
||||
data class AccountItem(
|
||||
@@ -116,33 +113,17 @@ val decryptedPrivateKey = CryptoManager.decryptWithPassword(
|
||||
privateKey = decryptedPrivateKey,
|
||||
seedPhrase = decryptedSeedPhrase,
|
||||
privateKeyHash = privateKeyHash,
|
||||
name = account.name
|
||||
name = selectedAccount.name
|
||||
)
|
||||
|
||||
// Connect to server
|
||||
val connectStart = System.currentTimeMillis()
|
||||
ProtocolManager.connect()
|
||||
|
||||
// Wait for websocket connection
|
||||
val connected = withTimeoutOrNull(5000) {
|
||||
ProtocolManager.state.first { it != ProtocolState.DISCONNECTED }
|
||||
}
|
||||
val connectTime = System.currentTimeMillis() - connectStart
|
||||
if (connected == null) {
|
||||
val handshakeState = awaitAuthHandshakeState(account.publicKey, privateKeyHash)
|
||||
if (handshakeState == null) {
|
||||
onError("Failed to connect to server")
|
||||
onUnlocking(false)
|
||||
return
|
||||
}
|
||||
|
||||
kotlinx.coroutines.delay(300)
|
||||
|
||||
// Authenticate
|
||||
val authStart = System.currentTimeMillis()
|
||||
ProtocolManager.authenticate(account.publicKey, privateKeyHash)
|
||||
val authTime = System.currentTimeMillis() - authStart
|
||||
|
||||
accountManager.setCurrentAccount(account.publicKey)
|
||||
|
||||
val totalTime = System.currentTimeMillis() - totalStart
|
||||
onSuccess(decryptedAccount)
|
||||
} catch (e: Exception) {
|
||||
onError("Failed to unlock: ${e.message}")
|
||||
@@ -216,7 +197,11 @@ fun UnlockScreen(
|
||||
val allAccounts = accountManager.getAllAccounts()
|
||||
accounts =
|
||||
allAccounts.map { acc ->
|
||||
AccountItem(publicKey = acc.publicKey, name = acc.name, encryptedAccount = acc)
|
||||
AccountItem(
|
||||
publicKey = acc.publicKey,
|
||||
name = resolveAccountDisplayName(acc.publicKey, acc.name, acc.username),
|
||||
encryptedAccount = acc
|
||||
)
|
||||
}
|
||||
|
||||
// Find the target account - приоритет: selectedAccountId > lastLoggedKey > первый
|
||||
@@ -359,6 +344,7 @@ fun UnlockScreen(
|
||||
avatarRepository = avatarRepository,
|
||||
size = 120.dp,
|
||||
isDarkTheme = isDarkTheme,
|
||||
displayName = selectedAccount!!.name,
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
)
|
||||
} else {
|
||||
@@ -479,7 +465,8 @@ fun UnlockScreen(
|
||||
publicKey = account.publicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
size = 40.dp,
|
||||
isDarkTheme = isDarkTheme
|
||||
isDarkTheme = isDarkTheme,
|
||||
displayName = account.name
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Reference in New Issue
Block a user