Refactor UI components in ChatsListScreen, ForwardChatPickerBottomSheet, and SearchScreen for improved readability and maintainability; adjust text color alpha values, streamline imports, and enhance keyboard handling functionality.

This commit is contained in:
k1ngsterr1
2026-01-17 21:09:47 +05:00
parent c9136ed499
commit a3810af4a0
11 changed files with 3763 additions and 3226 deletions

View File

@@ -129,3 +129,4 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
}

164
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,164 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# ============================================================
# Firebase Cloud Messaging
# ============================================================
-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }
-dontwarn com.google.firebase.**
-dontwarn com.google.android.gms.**
# Keep FirebaseMessagingService
-keep class * extends com.google.firebase.messaging.FirebaseMessagingService {
*;
}
-keep class com.rosetta.messenger.push.RosettaFirebaseMessagingService { *; }
# Keep notification data models
-keepclassmembers class * {
@com.google.firebase.messaging.RemoteMessage$MessageNotificationKeys <fields>;
}
# ============================================================
# Kotlin & Coroutines
# ============================================================
-keep class kotlinx.coroutines.** { *; }
-dontwarn kotlinx.coroutines.**
# Keep Kotlin metadata
-keep class kotlin.Metadata { *; }
# ============================================================
# Room Database
# ============================================================
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
-keepclassmembers class * {
@androidx.room.* <methods>;
}
-keep class com.rosetta.messenger.database.** { *; }
# ============================================================
# Crypto & Security
# ============================================================
# BouncyCastle
-keep class org.bouncycastle.** { *; }
-dontwarn org.bouncycastle.**
# Tink (XChaCha20-Poly1305)
-keep class com.google.crypto.tink.** { *; }
-dontwarn com.google.crypto.tink.**
# BitcoinJ
-keep class org.bitcoinj.** { *; }
-dontwarn org.bitcoinj.**
# Security Crypto
-keep class androidx.security.crypto.** { *; }
-dontwarn androidx.security.crypto.**
# Keep crypto classes
-keep class com.rosetta.messenger.crypto.** { *; }
# ============================================================
# Compose
# ============================================================
-keep class androidx.compose.** { *; }
-dontwarn androidx.compose.**
# Keep @Composable functions
-keepclassmembers class * {
@androidx.compose.runtime.Composable <methods>;
}
# ============================================================
# Data Models
# ============================================================
# Keep all data classes
-keep class com.rosetta.messenger.data.** { *; }
-keep class com.rosetta.messenger.network.** { *; }
# Keep Parcelable
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# ============================================================
# Serialization
# ============================================================
# Keep serialization info
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses
-keepattributes EnclosingMethod
# Gson
-keep class com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# ============================================================
# AndroidX & Material
# ============================================================
-keep class com.google.android.material.** { *; }
-dontwarn com.google.android.material.**
-keep class androidx.** { *; }
-dontwarn androidx.**
# ============================================================
# Reflection & Native
# ============================================================
-keepattributes RuntimeVisibleAnnotations
-keepattributes RuntimeInvisibleAnnotations
-keepattributes RuntimeVisibleParameterAnnotations
-keepattributes RuntimeInvisibleParameterAnnotations
# Keep native methods
-keepclasseswithmembernames class * {
native <methods>;
}
# ============================================================
# Debugging (remove in production)
# ============================================================
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
# ============================================================
# WebSocket (if using OkHttp)
# ============================================================
-dontwarn okhttp3.**
-dontwarn okio.**
-keep class okhttp3.** { *; }
-keep class okio.** { *; }
# ============================================================
# Lottie Animations
# ============================================================
-keep class com.airbnb.lottie.** { *; }
-dontwarn com.airbnb.lottie.**
# ============================================================
# Coil Image Loading
# ============================================================
-keep class coil.** { *; }
-dontwarn coil.**

View File

@@ -13,8 +13,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Координатор переходов между системной клавиатурой и emoji панелью.
* Реализует Telegram-style плавные анимации.
* Координатор переходов между системной клавиатурой и emoji панелью. Реализует Telegram-style
* плавные анимации.
*
* Ключевые принципы:
* - 250ms duration (как в Telegram AdjustPanLayoutHelper)
@@ -78,13 +78,10 @@ class KeyboardTransitionCoordinator {
/**
* Переход от системной клавиатуры к emoji панели.
*
* 🔥 КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: Показываем emoji СРАЗУ!
* Не ждем закрытия клавиатуры - emoji начинает выезжать синхронно.
* 🔥 КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: Показываем emoji СРАЗУ! Не ждем закрытия клавиатуры - emoji начинает
* выезжать синхронно.
*/
fun requestShowEmoji(
hideKeyboard: () -> Unit,
showEmoji: () -> Unit
) {
fun requestShowEmoji(hideKeyboard: () -> Unit, showEmoji: () -> Unit) {
currentState = TransitionState.KEYBOARD_TO_EMOJI
isTransitioning = true
@@ -95,14 +92,14 @@ class KeyboardTransitionCoordinator {
// 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры
showEmoji()
isEmojiVisible = true // 🔥 ВАЖНО: Устанавливаем флаг видимости emoji!
// Теперь скрываем клавиатуру (она будет закрываться синхронно с появлением emoji)
Log.d(TAG, " ⌨️ Hiding keyboard...")
try {
hideKeyboard()
Log.d(TAG, " ✅ hideKeyboard() completed")
} catch (e: Exception) {
}
} catch (e: Exception) {}
isKeyboardVisible = false
currentState = TransitionState.IDLE
@@ -115,13 +112,10 @@ class KeyboardTransitionCoordinator {
// ============ Главный метод: Emoji → Keyboard ============
/**
* Переход от emoji панели к системной клавиатуре.
* Telegram паттерн: показать клавиатуру и плавно скрыть emoji.
* Переход от emoji панели к системной клавиатуре. Telegram паттерн: показать клавиатуру и
* плавно скрыть emoji.
*/
fun requestShowKeyboard(
showKeyboard: () -> Unit,
hideEmoji: () -> Unit
) {
fun requestShowKeyboard(showKeyboard: () -> Unit, hideEmoji: () -> Unit) {
// 🔥 Отменяем pending emoji callback если он есть (предотвращаем конфликт)
if (pendingShowEmojiCallback != null) {
pendingShowEmojiCallback = null
@@ -133,33 +127,38 @@ class KeyboardTransitionCoordinator {
// Шаг 1: Показать системную клавиатуру
try {
showKeyboard()
} catch (e: Exception) {
}
} catch (e: Exception) {}
// Шаг 2: Через небольшую задержку скрыть emoji
Handler(Looper.getMainLooper()).postDelayed({
Handler(Looper.getMainLooper())
.postDelayed(
{
try {
hideEmoji()
isEmojiVisible = false
isKeyboardVisible = true
// Через время анимации завершаем переход
Handler(Looper.getMainLooper()).postDelayed({
Handler(Looper.getMainLooper())
.postDelayed(
{
currentState = TransitionState.IDLE
isTransitioning = false
}, TRANSITION_DURATION)
},
TRANSITION_DURATION
)
} catch (e: Exception) {
currentState = TransitionState.IDLE
isTransitioning = false
}
}, SHORT_DELAY)
},
SHORT_DELAY
)
}
// ============ Простые переходы ============
/**
* Открыть только emoji панель (без клавиатуры).
*/
/** Открыть только emoji панель (без клавиатуры). */
fun openEmojiOnly(showEmoji: () -> Unit) {
currentState = TransitionState.EMOJI_OPENING
isTransitioning = true
@@ -172,15 +171,17 @@ class KeyboardTransitionCoordinator {
showEmoji()
isEmojiVisible = true
Handler(Looper.getMainLooper()).postDelayed({
Handler(Looper.getMainLooper())
.postDelayed(
{
currentState = TransitionState.IDLE
isTransitioning = false
}, TRANSITION_DURATION)
},
TRANSITION_DURATION
)
}
/**
* Закрыть emoji панель.
*/
/** Закрыть emoji панель. */
fun closeEmoji(hideEmoji: () -> Unit) {
currentState = TransitionState.EMOJI_CLOSING
isTransitioning = true
@@ -188,15 +189,17 @@ class KeyboardTransitionCoordinator {
hideEmoji()
isEmojiVisible = false
Handler(Looper.getMainLooper()).postDelayed({
Handler(Looper.getMainLooper())
.postDelayed(
{
currentState = TransitionState.IDLE
isTransitioning = false
}, TRANSITION_DURATION)
},
TRANSITION_DURATION
)
}
/**
* Закрыть системную клавиатуру.
*/
/** Закрыть системную клавиатуру. */
fun closeKeyboard(hideKeyboard: () -> Unit) {
currentState = TransitionState.KEYBOARD_CLOSING
isTransitioning = true
@@ -204,17 +207,19 @@ class KeyboardTransitionCoordinator {
hideKeyboard()
isKeyboardVisible = false
Handler(Looper.getMainLooper()).postDelayed({
Handler(Looper.getMainLooper())
.postDelayed(
{
currentState = TransitionState.IDLE
isTransitioning = false
}, TRANSITION_DURATION)
},
TRANSITION_DURATION
)
}
// ============ Вспомогательные методы ============
/**
* Обновить высоту клавиатуры из IME.
*/
/** Обновить высоту клавиатуры из IME. */
fun updateKeyboardHeight(height: Dp) {
val now = System.currentTimeMillis()
val heightChanged = kotlin.math.abs(height.value - lastLoggedHeight) > 5f
@@ -252,9 +257,7 @@ class KeyboardTransitionCoordinator {
// 🔥 УБРАН pending callback - теперь emoji показывается сразу в requestShowEmoji()
}
/**
* Обновить высоту emoji панели.
*/
/** Обновить высоту emoji панели. */
fun updateEmojiHeight(height: Dp) {
if (height > 0.dp && height != emojiHeight) {
emojiHeight = height
@@ -264,8 +267,8 @@ class KeyboardTransitionCoordinator {
/**
* Синхронизировать высоты (emoji = keyboard).
*
* 🔥 ВАЖНО: Синхронизируем ТОЛЬКО когда клавиатура ОТКРЫВАЕТСЯ!
* При закрытии клавиатуры emojiHeight должна оставаться фиксированной!
* 🔥 ВАЖНО: Синхронизируем ТОЛЬКО когда клавиатура ОТКРЫВАЕТСЯ! При закрытии клавиатуры
* emojiHeight должна оставаться фиксированной!
*/
fun syncHeights() {
// 🔥 Синхронизируем ТОЛЬКО если клавиатура ОТКРЫТА и высота больше текущей emoji
@@ -275,8 +278,8 @@ class KeyboardTransitionCoordinator {
}
/**
* Инициализация высоты emoji панели (для pre-rendered подхода).
* Должна быть вызвана при старте для избежания 0dp высоты.
* Инициализация высоты emoji панели (для pre-rendered подхода). Должна быть вызвана при старте
* для избежания 0dp высоты.
*/
fun initializeEmojiHeight(height: Dp) {
if (emojiHeight == 0.dp && height > 0.dp) {
@@ -286,8 +289,8 @@ class KeyboardTransitionCoordinator {
}
/**
* Получить текущую высоту для резервирования места.
* Telegram паттерн: всегда резервировать максимум из двух.
* Получить текущую высоту для резервирования места. Telegram паттерн: всегда резервировать
* максимум из двух.
*/
fun getReservedHeight(): Dp {
return when {
@@ -298,16 +301,12 @@ class KeyboardTransitionCoordinator {
}
}
/**
* Проверка, можно ли начать новый переход.
*/
/** Проверка, можно ли начать новый переход. */
fun canStartTransition(): Boolean {
return !isTransitioning
}
/**
* Сброс состояния (для отладки).
*/
/** Сброс состояния (для отладки). */
fun reset() {
currentState = TransitionState.IDLE
isTransitioning = false
@@ -316,16 +315,11 @@ class KeyboardTransitionCoordinator {
transitionProgress = 0f
}
/**
* Логирование текущего состояния.
*/
fun logState() {
}
/** Логирование текущего состояния. */
fun logState() {}
}
/**
* Composable для создания и запоминания coordinator'а.
*/
/** Composable для создания и запоминания coordinator'а. */
@Composable
fun rememberKeyboardTransitionCoordinator(): KeyboardTransitionCoordinator {
return remember { KeyboardTransitionCoordinator() }

View File

@@ -350,7 +350,7 @@ fun MainScreen(
onToggleTheme: () -> Unit = {},
onLogout: () -> Unit = {}
) {
val accountName = account?.publicKey ?: "04c266b98ae5"
val accountName = account?.name ?: "Account"
val accountPhone = account?.publicKey?.take(16)?.let {
"+${it.take(1)} ${it.substring(1, 4)} ${it.substring(4, 7)}${it.substring(7)}"
} ?: "+7 775 9932587"

View File

@@ -5,7 +5,6 @@ import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
@@ -40,11 +39,28 @@ fun SetPasswordScreen(
onBack: () -> Unit,
onAccountCreated: (DecryptedAccount) -> Unit
) {
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec)
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec)
val cardColor by animateColorAsState(if (isDarkTheme) AuthSurface else AuthSurfaceLight, animationSpec = themeAnimSpec)
val themeAnimSpec =
tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val backgroundColor by
animateColorAsState(
if (isDarkTheme) AuthBackground else AuthBackgroundLight,
animationSpec = themeAnimSpec
)
val textColor by
animateColorAsState(
if (isDarkTheme) Color.White else Color.Black,
animationSpec = themeAnimSpec
)
val secondaryTextColor by
animateColorAsState(
if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
animationSpec = themeAnimSpec
)
val cardColor by
animateColorAsState(
if (isDarkTheme) AuthSurface else AuthSurfaceLight,
animationSpec = themeAnimSpec
)
val context = LocalContext.current
val accountManager = remember { AccountManager(context) }
@@ -63,7 +79,8 @@ fun SetPasswordScreen(
var isKeyboardVisible by remember { mutableStateOf(false) }
DisposableEffect(view) {
val listener = android.view.ViewTreeObserver.OnGlobalLayoutListener {
val listener =
android.view.ViewTreeObserver.OnGlobalLayoutListener {
val rect = android.graphics.Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
@@ -71,34 +88,20 @@ fun SetPasswordScreen(
isKeyboardVisible = keypadHeight > screenHeight * 0.15
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
LaunchedEffect(Unit) {
visible = true
}
LaunchedEffect(Unit) { visible = true }
val passwordsMatch = password == confirmPassword && password.isNotEmpty()
val isPasswordWeak = password.isNotEmpty() && password.length < 6
val canContinue = passwordsMatch && !isCreating
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
Column(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
) {
Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
Column(modifier = Modifier.fillMaxSize().statusBarsPadding()) {
// Top Bar
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp),
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack, enabled = !isCreating) {
@@ -120,8 +123,8 @@ fun SetPasswordScreen(
}
Column(
modifier = Modifier
.fillMaxSize()
modifier =
Modifier.fillMaxSize()
.imePadding()
.padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState()),
@@ -130,26 +133,35 @@ fun SetPasswordScreen(
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 8.dp else 16.dp))
// Lock Icon - smaller when keyboard is visible
val iconSize by animateDpAsState(
val iconSize by
animateDpAsState(
targetValue = if (isKeyboardVisible) 48.dp else 80.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
val iconInnerSize by animateDpAsState(
val iconInnerSize by
animateDpAsState(
targetValue = if (isKeyboardVisible) 24.dp else 40.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500)) + scaleIn(
enter =
fadeIn(tween(500)) +
scaleIn(
initialScale = 0.5f,
animationSpec = tween(500, easing = FastOutSlowInEasing)
animationSpec =
tween(500, easing = FastOutSlowInEasing)
)
) {
Box(
modifier = Modifier
.size(iconSize)
.clip(RoundedCornerShape(if (isKeyboardVisible) 12.dp else 20.dp))
modifier =
Modifier.size(iconSize)
.clip(
RoundedCornerShape(
if (isKeyboardVisible) 12.dp else 20.dp
)
)
.background(PrimaryBlue.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
@@ -166,7 +178,9 @@ fun SetPasswordScreen(
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 100)) + slideInVertically(
enter =
fadeIn(tween(500, delayMillis = 100)) +
slideInVertically(
initialOffsetY = { -20 },
animationSpec = tween(500, delayMillis = 100)
)
@@ -186,7 +200,8 @@ fun SetPasswordScreen(
enter = fadeIn(tween(500, delayMillis = 200))
) {
Text(
text = "This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.",
text =
"This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.",
fontSize = if (isKeyboardVisible) 12.sp else 14.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center,
@@ -199,7 +214,9 @@ fun SetPasswordScreen(
// Password Field
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 300)) + slideInVertically(
enter =
fadeIn(tween(500, delayMillis = 300)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 300)
)
@@ -213,20 +230,26 @@ fun SetPasswordScreen(
label = { Text("Password") },
placeholder = { Text("Enter password") },
singleLine = true,
visualTransformation = if (passwordVisible)
VisualTransformation.None else PasswordVisualTransformation(),
visualTransformation =
if (passwordVisible) VisualTransformation.None
else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
imageVector = if (passwordVisible)
Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (passwordVisible) "Hide" else "Show"
imageVector =
if (passwordVisible) Icons.Default.VisibilityOff
else Icons.Default.Visibility,
contentDescription =
if (passwordVisible) "Hide" else "Show"
)
}
},
colors = OutlinedTextFieldDefaults.colors(
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0),
unfocusedBorderColor =
if (isDarkTheme) Color(0xFF4A4A4A)
else Color(0xFFD0D0D0),
focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue,
focusedTextColor = textColor,
@@ -234,7 +257,8 @@ fun SetPasswordScreen(
),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
)
@@ -246,7 +270,9 @@ fun SetPasswordScreen(
Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 350)) + slideInHorizontally(
enter =
fadeIn(tween(400, delayMillis = 350)) +
slideInHorizontally(
initialOffsetX = { -30 },
animationSpec = tween(400, delayMillis = 350)
)
@@ -256,12 +282,14 @@ fun SetPasswordScreen(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val strength = when {
val strength =
when {
password.length < 6 -> "Weak"
password.length < 10 -> "Medium"
else -> "Strong"
}
val strengthColor = when {
val strengthColor =
when {
password.length < 6 -> Color(0xFFE53935)
password.length < 10 -> Color(0xFFFFA726)
else -> Color(0xFF4CAF50)
@@ -283,10 +311,12 @@ fun SetPasswordScreen(
if (isPasswordWeak) {
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
modifier =
Modifier.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFFE53935).copy(alpha = 0.1f))
.background(
Color(0xFFE53935).copy(alpha = 0.1f)
)
.padding(8.dp),
verticalAlignment = Alignment.Top
) {
@@ -298,7 +328,8 @@ fun SetPasswordScreen(
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Your password is too weak. Consider using at least 6 characters for better security.",
text =
"Your password is too weak. Consider using at least 6 characters for better security.",
fontSize = 11.sp,
color = Color(0xFFE53935),
lineHeight = 14.sp
@@ -314,7 +345,9 @@ fun SetPasswordScreen(
// Confirm Password Field
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 400)) + slideInVertically(
enter =
fadeIn(tween(500, delayMillis = 400)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 400)
)
@@ -328,21 +361,32 @@ fun SetPasswordScreen(
label = { Text("Confirm Password") },
placeholder = { Text("Re-enter password") },
singleLine = true,
visualTransformation = if (confirmPasswordVisible)
VisualTransformation.None else PasswordVisualTransformation(),
visualTransformation =
if (confirmPasswordVisible) VisualTransformation.None
else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) {
IconButton(
onClick = {
confirmPasswordVisible = !confirmPasswordVisible
}
) {
Icon(
imageVector = if (confirmPasswordVisible)
Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (confirmPasswordVisible) "Hide" else "Show"
imageVector =
if (confirmPasswordVisible)
Icons.Default.VisibilityOff
else Icons.Default.Visibility,
contentDescription =
if (confirmPasswordVisible) "Hide" else "Show"
)
}
},
isError = confirmPassword.isNotEmpty() && !passwordsMatch,
colors = OutlinedTextFieldDefaults.colors(
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0),
unfocusedBorderColor =
if (isDarkTheme) Color(0xFF4A4A4A)
else Color(0xFFD0D0D0),
focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue,
focusedTextColor = textColor,
@@ -350,7 +394,8 @@ fun SetPasswordScreen(
),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
)
@@ -362,7 +407,9 @@ fun SetPasswordScreen(
Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 450)) + slideInHorizontally(
enter =
fadeIn(tween(400, delayMillis = 450)) +
slideInHorizontally(
initialOffsetX = { -30 },
animationSpec = tween(400, delayMillis = 450)
)
@@ -371,9 +418,13 @@ fun SetPasswordScreen(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val matchIcon = if (passwordsMatch) Icons.Default.Check else Icons.Default.Close
val matchColor = if (passwordsMatch) Color(0xFF4CAF50) else Color(0xFFE53935)
val matchText = if (passwordsMatch) "Passwords match" else "Passwords don't match"
val matchIcon =
if (passwordsMatch) Icons.Default.Check else Icons.Default.Close
val matchColor =
if (passwordsMatch) Color(0xFF4CAF50) else Color(0xFFE53935)
val matchText =
if (passwordsMatch) "Passwords match"
else "Passwords don't match"
Icon(
imageVector = matchIcon,
@@ -382,11 +433,7 @@ fun SetPasswordScreen(
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = matchText,
fontSize = 12.sp,
color = matchColor
)
Text(text = matchText, fontSize = 12.sp, color = matchColor)
}
}
}
@@ -412,24 +459,24 @@ fun SetPasswordScreen(
// Info - hide when keyboard is visible
AnimatedVisibility(
visible = visible && !isKeyboardVisible,
enter = fadeIn(tween(400)) + slideInVertically(
enter =
fadeIn(tween(400)) +
slideInVertically(
initialOffsetY = { 30 },
animationSpec = tween(400)
) + scaleIn(
initialScale = 0.9f,
animationSpec = tween(400)
),
exit = fadeOut(tween(300)) + slideOutVertically(
) +
scaleIn(initialScale = 0.9f, animationSpec = tween(400)),
exit =
fadeOut(tween(300)) +
slideOutVertically(
targetOffsetY = { 30 },
animationSpec = tween(300)
) + scaleOut(
targetScale = 0.9f,
animationSpec = tween(300)
)
) +
scaleOut(targetScale = 0.9f, animationSpec = tween(300))
) {
Row(
modifier = Modifier
.fillMaxWidth()
modifier =
Modifier.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(cardColor)
.padding(16.dp),
@@ -443,7 +490,8 @@ fun SetPasswordScreen(
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Your password is never stored or sent anywhere. It's only used to encrypt your keys locally.",
text =
"Your password is never stored or sent anywhere. It's only used to encrypt your keys locally.",
fontSize = 13.sp,
color = secondaryTextColor,
lineHeight = 18.sp
@@ -456,7 +504,9 @@ fun SetPasswordScreen(
// Create Account Button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 600)) + slideInVertically(
enter =
fadeIn(tween(500, delayMillis = 600)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 600)
)
@@ -472,19 +522,26 @@ fun SetPasswordScreen(
scope.launch {
try {
// Generate keys from seed phrase
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
val keyPair =
CryptoManager.generateKeyPairFromSeed(seedPhrase)
// Encrypt private key and seed phrase
val encryptedPrivateKey = CryptoManager.encryptWithPassword(
keyPair.privateKey, password
val encryptedPrivateKey =
CryptoManager.encryptWithPassword(
keyPair.privateKey,
password
)
val encryptedSeedPhrase = CryptoManager.encryptWithPassword(
seedPhrase.joinToString(" "), password
val encryptedSeedPhrase =
CryptoManager.encryptWithPassword(
seedPhrase.joinToString(" "),
password
)
// Save account with truncated public key as name
val truncatedKey = "${keyPair.publicKey.take(6)}...${keyPair.publicKey.takeLast(4)}"
val account = EncryptedAccount(
val truncatedKey =
"${keyPair.publicKey.take(6)}...${keyPair.publicKey.takeLast(4)}"
val account =
EncryptedAccount(
publicKey = keyPair.publicKey,
encryptedPrivateKey = encryptedPrivateKey,
encryptedSeedPhrase = encryptedSeedPhrase,
@@ -495,14 +552,21 @@ fun SetPasswordScreen(
accountManager.setCurrentAccount(keyPair.publicKey)
// 🔌 Connect to server and authenticate
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
val privateKeyHash =
CryptoManager.generatePrivateKeyHash(
keyPair.privateKey
)
ProtocolManager.connect()
// Give WebSocket time to connect before authenticating
kotlinx.coroutines.delay(500)
ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash)
ProtocolManager.authenticate(
keyPair.publicKey,
privateKeyHash
)
// Create DecryptedAccount to pass to callback
val decryptedAccount = DecryptedAccount(
val decryptedAccount =
DecryptedAccount(
publicKey = keyPair.publicKey,
privateKey = keyPair.privateKey,
seedPhrase = seedPhrase,
@@ -518,10 +582,9 @@ fun SetPasswordScreen(
}
},
enabled = canContinue,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
modifier = Modifier.fillMaxWidth().height(56.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
@@ -542,7 +605,8 @@ fun SetPasswordScreen(
fontWeight = FontWeight.SemiBold
)
}
} }
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}

View File

@@ -38,10 +38,9 @@ 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 com.rosetta.messenger.ui.chats.getAvatarColor
import com.rosetta.messenger.ui.chats.getAvatarText
import kotlinx.coroutines.flow.first
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import kotlinx.coroutines.launch
// Account model for dropdown
@@ -59,11 +58,28 @@ fun UnlockScreen(
onUnlocked: (DecryptedAccount) -> Unit,
onSwitchAccount: () -> Unit = {}
) {
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec)
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec)
val cardBackground by animateColorAsState(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5), animationSpec = themeAnimSpec)
val themeAnimSpec =
tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val backgroundColor by
animateColorAsState(
if (isDarkTheme) AuthBackground else AuthBackgroundLight,
animationSpec = themeAnimSpec
)
val textColor by
animateColorAsState(
if (isDarkTheme) Color.White else Color.Black,
animationSpec = themeAnimSpec
)
val secondaryTextColor by
animateColorAsState(
if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
animationSpec = themeAnimSpec
)
val cardBackground by
animateColorAsState(
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5),
animationSpec = themeAnimSpec
)
val context = LocalContext.current
val accountManager = remember { AccountManager(context) }
@@ -87,16 +103,14 @@ fun UnlockScreen(
val lastLoggedKey = accountManager.getLastLoggedPublicKey()
val allAccounts = accountManager.getAllAccounts()
accounts = allAccounts.map { acc ->
AccountItem(
publicKey = acc.publicKey,
name = acc.name,
encryptedAccount = acc
)
accounts =
allAccounts.map { acc ->
AccountItem(publicKey = acc.publicKey, name = acc.name, encryptedAccount = acc)
}
// Find the target account - приоритет: selectedAccountId > lastLoggedKey > первый
val targetAccount = when {
val targetAccount =
when {
!selectedAccountId.isNullOrEmpty() -> {
accounts.find { it.publicKey == selectedAccountId }
}
@@ -112,9 +126,11 @@ fun UnlockScreen(
}
// Filter accounts by search
val filteredAccounts = remember(searchQuery, accounts) {
val filteredAccounts =
remember(searchQuery, accounts) {
if (searchQuery.isEmpty()) accounts
else accounts.filter {
else
accounts.filter {
it.name.contains(searchQuery, ignoreCase = true) ||
it.publicKey.contains(searchQuery, ignoreCase = true)
}
@@ -125,9 +141,11 @@ fun UnlockScreen(
LaunchedEffect(Unit) { visible = true }
// Dropdown animation
val dropdownProgress by animateFloatAsState(
val dropdownProgress by
animateFloatAsState(
targetValue = if (isDropdownExpanded) 1f else 0f,
animationSpec = spring(
animationSpec =
spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
@@ -148,14 +166,10 @@ fun UnlockScreen(
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
Column(
modifier = Modifier
.fillMaxSize()
modifier =
Modifier.fillMaxSize()
.verticalScroll(rememberScrollState())
.imePadding()
.padding(horizontal = 24.dp)
@@ -172,9 +186,7 @@ fun UnlockScreen(
Image(
painter = painterResource(id = R.drawable.rosetta_icon),
contentDescription = "Rosetta",
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
modifier = Modifier.size(100.dp).clip(CircleShape)
)
}
@@ -183,7 +195,9 @@ fun UnlockScreen(
// Title
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 200)) + slideInVertically(
enter =
fadeIn(tween(600, delayMillis = 200)) +
slideInVertically(
initialOffsetY = { 30 },
animationSpec = tween(600, delayMillis = 200)
)
@@ -198,10 +212,7 @@ fun UnlockScreen(
Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 300))
) {
AnimatedVisibility(visible = visible, enter = fadeIn(tween(600, delayMillis = 300))) {
Text(
text = "Select your account and enter password",
fontSize = 16.sp,
@@ -215,7 +226,9 @@ fun UnlockScreen(
// Account Selector Card
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 350)) + slideInVertically(
enter =
fadeIn(tween(600, delayMillis = 350)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 350)
)
@@ -223,8 +236,8 @@ fun UnlockScreen(
Column {
// Account selector dropdown
Card(
modifier = Modifier
.fillMaxWidth()
modifier =
Modifier.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.clickable(enabled = accounts.size > 1) {
isDropdownExpanded = !isDropdownExpanded
@@ -233,17 +246,16 @@ fun UnlockScreen(
shape = RoundedCornerShape(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
if (selectedAccount != null) {
val avatarColors = getAvatarColor(selectedAccount!!.publicKey, isDarkTheme)
val avatarColors =
getAvatarColor(selectedAccount!!.publicKey, isDarkTheme)
Box(
modifier = Modifier
.size(48.dp)
modifier =
Modifier.size(48.dp)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
@@ -286,9 +298,8 @@ fun UnlockScreen(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null,
tint = secondaryTextColor.copy(alpha = 0.6f),
modifier = Modifier
.size(24.dp)
.graphicsLayer {
modifier =
Modifier.size(24.dp).graphicsLayer {
rotationZ = 180f * dropdownProgress
}
)
@@ -299,20 +310,29 @@ fun UnlockScreen(
// Dropdown list with animation
AnimatedVisibility(
visible = isDropdownExpanded && accounts.size > 1,
enter = fadeIn(tween(150)) + expandVertically(
enter =
fadeIn(tween(150)) +
expandVertically(
expandFrom = Alignment.Top,
animationSpec = tween(200, easing = FastOutSlowInEasing)
animationSpec =
tween(200, easing = FastOutSlowInEasing)
),
exit = fadeOut(tween(100)) + shrinkVertically(
exit =
fadeOut(tween(100)) +
shrinkVertically(
shrinkTowards = Alignment.Top,
animationSpec = tween(150)
)
) {
Card(
modifier = Modifier
.fillMaxWidth()
modifier =
Modifier.fillMaxWidth()
.padding(top = 8.dp)
.heightIn(max = if (accounts.size > 5) 350.dp else ((accounts.size * 64 + 70).dp)),
.heightIn(
max =
if (accounts.size > 5) 350.dp
else ((accounts.size * 64 + 70).dp)
),
colors = CardDefaults.cardColors(containerColor = cardBackground),
shape = RoundedCornerShape(16.dp)
) {
@@ -325,7 +345,10 @@ fun UnlockScreen(
placeholder = {
Text(
"Search accounts...",
color = secondaryTextColor.copy(alpha = 0.6f)
color =
secondaryTextColor.copy(
alpha = 0.6f
)
)
},
leadingIcon = {
@@ -335,45 +358,60 @@ fun UnlockScreen(
tint = secondaryTextColor.copy(alpha = 0.6f)
)
},
colors = OutlinedTextFieldDefaults.colors(
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
unfocusedBorderColor =
Color.Transparent,
focusedContainerColor =
Color.Transparent,
unfocusedContainerColor =
Color.Transparent,
focusedTextColor = textColor,
unfocusedTextColor = textColor,
cursorColor = PrimaryBlue
),
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp)
modifier =
Modifier.fillMaxWidth()
.padding(
horizontal = 12.dp,
vertical = 8.dp
)
.focusRequester(searchFocusRequester),
shape = RoundedCornerShape(12.dp)
)
Divider(
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0),
color =
if (isDarkTheme) Color(0xFF3A3A3A)
else Color(0xFFE0E0E0),
thickness = 0.5.dp
)
}
// Account list
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = if (accounts.size <= 3) 8.dp else 0.dp)
modifier =
Modifier.fillMaxWidth()
.padding(
vertical =
if (accounts.size <= 3) 8.dp
else 0.dp
)
) {
items(filteredAccounts, key = { it.publicKey }) { account ->
val isSelected = account.publicKey == selectedAccount?.publicKey
val itemScale by animateFloatAsState(
val isSelected =
account.publicKey == selectedAccount?.publicKey
val itemScale by
animateFloatAsState(
targetValue = if (isSelected) 1f else 0.98f,
label = "itemScale"
)
Row(
modifier = Modifier
.fillMaxWidth()
modifier =
Modifier.fillMaxWidth()
.scale(itemScale)
.clip(RoundedCornerShape(12.dp))
.clickable {
@@ -383,19 +421,29 @@ fun UnlockScreen(
error = null
}
.background(
if (isSelected) PrimaryBlue.copy(alpha = 0.1f)
if (isSelected)
PrimaryBlue.copy(
alpha = 0.1f
)
else Color.Transparent
)
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(
horizontal = 16.dp,
vertical = 12.dp
),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
val avatarColors = getAvatarColor(account.publicKey, isDarkTheme)
val avatarColors =
getAvatarColor(account.publicKey, isDarkTheme)
Box(
modifier = Modifier
.size(40.dp)
modifier =
Modifier.size(40.dp)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
.background(
avatarColors
.backgroundColor
),
contentAlignment = Alignment.Center
) {
Text(
@@ -412,7 +460,9 @@ fun UnlockScreen(
Text(
text = account.name,
fontSize = 15.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
fontWeight =
if (isSelected) FontWeight.SemiBold
else FontWeight.Normal,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -443,9 +493,8 @@ fun UnlockScreen(
text = "No accounts found",
color = secondaryTextColor,
fontSize = 14.sp,
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
modifier =
Modifier.fillMaxWidth().padding(24.dp),
textAlign = TextAlign.Center
)
}
@@ -462,7 +511,9 @@ fun UnlockScreen(
// Password Field
AnimatedVisibility(
visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(600, delayMillis = 400)) + slideInVertically(
enter =
fadeIn(tween(600, delayMillis = 400)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 400)
)
@@ -476,21 +527,26 @@ fun UnlockScreen(
label = { Text("Password") },
placeholder = { Text("Enter your password") },
singleLine = true,
visualTransformation = if (passwordVisible)
VisualTransformation.None else PasswordVisualTransformation(),
visualTransformation =
if (passwordVisible) VisualTransformation.None
else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
imageVector = if (passwordVisible)
Icons.Default.VisibilityOff else Icons.Default.Visibility,
imageVector =
if (passwordVisible) Icons.Default.VisibilityOff
else Icons.Default.Visibility,
contentDescription = if (passwordVisible) "Hide" else "Show"
)
}
},
isError = error != null,
colors = OutlinedTextFieldDefaults.colors(
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0),
unfocusedBorderColor =
if (isDarkTheme) Color(0xFF4A4A4A)
else Color(0xFFD0D0D0),
focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue,
focusedTextColor = textColor,
@@ -499,7 +555,8 @@ fun UnlockScreen(
),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
)
@@ -513,11 +570,7 @@ fun UnlockScreen(
exit = fadeOut()
) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = error ?: "",
fontSize = 14.sp,
color = Color(0xFFE53935)
)
Text(text = error ?: "", fontSize = 14.sp, color = Color(0xFFE53935))
}
Spacer(modifier = Modifier.height(24.dp))
@@ -525,7 +578,9 @@ fun UnlockScreen(
// Unlock Button
AnimatedVisibility(
visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically(
enter =
fadeIn(tween(600, delayMillis = 500)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 500)
)
@@ -547,8 +602,10 @@ fun UnlockScreen(
val account = selectedAccount!!.encryptedAccount
// Try to decrypt
val decryptedPrivateKey = CryptoManager.decryptWithPassword(
account.encryptedPrivateKey, password
val decryptedPrivateKey =
CryptoManager.decryptWithPassword(
account.encryptedPrivateKey,
password
)
if (decryptedPrivateKey == null) {
@@ -557,13 +614,21 @@ fun UnlockScreen(
return@launch
}
val decryptedSeedPhrase = CryptoManager.decryptWithPassword(
account.encryptedSeedPhrase, password
)?.split(" ") ?: emptyList()
val decryptedSeedPhrase =
CryptoManager.decryptWithPassword(
account.encryptedSeedPhrase,
password
)
?.split(" ")
?: emptyList()
val privateKeyHash = CryptoManager.generatePrivateKeyHash(decryptedPrivateKey)
val privateKeyHash =
CryptoManager.generatePrivateKeyHash(
decryptedPrivateKey
)
val decryptedAccount = DecryptedAccount(
val decryptedAccount =
DecryptedAccount(
publicKey = account.publicKey,
privateKey = decryptedPrivateKey,
seedPhrase = decryptedSeedPhrase,
@@ -579,7 +644,6 @@ fun UnlockScreen(
accountManager.setCurrentAccount(account.publicKey)
onUnlocked(decryptedAccount)
} catch (e: Exception) {
error = "Failed to unlock: \${e.message}"
isUnlocking = false
@@ -587,10 +651,9 @@ fun UnlockScreen(
}
},
enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
modifier = Modifier.fillMaxWidth().height(56.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
@@ -611,11 +674,7 @@ fun UnlockScreen(
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Unlock",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
Text(text = "Unlock", fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
}
}
}
@@ -625,14 +684,14 @@ fun UnlockScreen(
// Create New Account button
AnimatedVisibility(
visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically(
enter =
fadeIn(tween(600, delayMillis = 600)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 600)
)
) {
TextButton(
onClick = onSwitchAccount
) {
TextButton(onClick = onSwitchAccount) {
Icon(
imageVector = Icons.Default.PersonAdd,
contentDescription = null,
@@ -640,11 +699,7 @@ fun UnlockScreen(
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Create New Account",
color = PrimaryBlue,
fontSize = 15.sp
)
Text(text = "Create New Account", color = PrimaryBlue, fontSize = 15.sp)
}
}

View File

@@ -3,7 +3,6 @@ package com.rosetta.messenger.ui.auth
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -14,7 +13,6 @@ import androidx.compose.runtime.*
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.graphics.toArgb
import androidx.compose.ui.platform.LocalView
@@ -24,7 +22,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.lottie.compose.*
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
// Auth colors
val AuthBackground = Color(0xFF1B1B1B)
@@ -40,10 +37,23 @@ fun WelcomeScreen(
onCreateSeed: () -> Unit,
onImportSeed: () -> Unit
) {
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec)
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec)
val themeAnimSpec =
tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val backgroundColor by
animateColorAsState(
if (isDarkTheme) AuthBackground else AuthBackgroundLight,
animationSpec = themeAnimSpec
)
val textColor by
animateColorAsState(
if (isDarkTheme) Color.White else Color.Black,
animationSpec = themeAnimSpec
)
val secondaryTextColor by
animateColorAsState(
if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
animationSpec = themeAnimSpec
)
// Sync navigation bar color with background
val view = LocalView.current
@@ -53,8 +63,10 @@ fun WelcomeScreen(
}
// Animation for Lottie
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
val lockProgress by animateLottieCompositionAsState(
val lockComposition by
rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
val lockProgress by
animateLottieCompositionAsState(
composition = lockComposition,
iterations = 1, // Play once
speed = 1f
@@ -64,19 +76,10 @@ fun WelcomeScreen(
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { visible = true }
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
// Back button when coming from UnlockScreen
if (hasExistingAccount) {
IconButton(
onClick = onBack,
modifier = Modifier
.statusBarsPadding()
.padding(4.dp)
) {
IconButton(onClick = onBack, modifier = Modifier.statusBarsPadding().padding(4.dp)) {
Icon(
Icons.Default.ArrowBack,
contentDescription = "Back",
@@ -86,10 +89,7 @@ fun WelcomeScreen(
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp)
.statusBarsPadding(),
modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp).statusBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(0.15f))
@@ -99,10 +99,7 @@ fun WelcomeScreen(
visible = visible,
enter = fadeIn(tween(600)) + scaleIn(tween(600, easing = FastOutSlowInEasing))
) {
Box(
modifier = Modifier.size(180.dp),
contentAlignment = Alignment.Center
) {
Box(modifier = Modifier.size(180.dp), contentAlignment = Alignment.Center) {
lockComposition?.let { comp ->
LottieAnimation(
composition = comp,
@@ -118,7 +115,9 @@ fun WelcomeScreen(
// Title
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 200)) + slideInVertically(
enter =
fadeIn(tween(600, delayMillis = 200)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 200)
)
@@ -138,7 +137,9 @@ fun WelcomeScreen(
// Subtitle
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 300)) + slideInVertically(
enter =
fadeIn(tween(600, delayMillis = 300)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 300)
)
@@ -156,10 +157,7 @@ fun WelcomeScreen(
Spacer(modifier = Modifier.height(24.dp))
// Features list with icons - placed above buttons
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 400))
) {
AnimatedVisibility(visible = visible, enter = fadeIn(tween(600, delayMillis = 400))) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
@@ -190,22 +188,24 @@ fun WelcomeScreen(
// Create Seed Button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically(
enter =
fadeIn(tween(600, delayMillis = 500)) +
slideInVertically(
initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 500)
)
) {
Button(
onClick = onCreateSeed,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
modifier = Modifier.fillMaxWidth().height(56.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White
),
shape = RoundedCornerShape(16.dp),
elevation = ButtonDefaults.buttonElevation(
elevation =
ButtonDefaults.buttonElevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp
)
@@ -229,16 +229,16 @@ fun WelcomeScreen(
// Import Seed Button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically(
enter =
fadeIn(tween(600, delayMillis = 600)) +
slideInVertically(
initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 600)
)
) {
TextButton(
onClick = onImportSeed,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
modifier = Modifier.fillMaxWidth().height(56.dp),
shape = RoundedCornerShape(16.dp)
) {
Icon(
@@ -274,8 +274,8 @@ private fun CompactFeatureItem(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.size(48.dp)
modifier =
Modifier.size(48.dp)
.clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.12f)),
contentAlignment = Alignment.Center
@@ -304,13 +304,10 @@ private fun FeatureItem(
isDarkTheme: Boolean,
textColor: Color
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(40.dp)
modifier =
Modifier.size(40.dp)
.clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.15f)),
contentAlignment = Alignment.Center
@@ -323,11 +320,6 @@ private fun FeatureItem(
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = text,
fontSize = 15.sp,
color = textColor,
fontWeight = FontWeight.Medium
)
Text(text = text, fontSize = 15.sp, color = textColor, fontWeight = FontWeight.Medium)
}
}

View File

@@ -534,38 +534,10 @@ fun ChatsListScreen(
Spacer(modifier = Modifier.height(6.dp))
// Connection status indicator
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.clickable { showStatusDialog = true }
) {
val statusColor =
when (protocolState) {
ProtocolState.AUTHENTICATED -> Color(0xFF4ADE80)
ProtocolState.CONNECTING,
ProtocolState.CONNECTED,
ProtocolState.HANDSHAKING -> Color(0xFFFBBF24)
else -> Color(0xFFF87171)
}
val statusText =
when (protocolState) {
ProtocolState.AUTHENTICATED -> "Online"
ProtocolState.CONNECTING,
ProtocolState.CONNECTED,
ProtocolState.HANDSHAKING -> "Connecting..."
else -> "Offline"
}
Box(
modifier =
Modifier.size(8.dp)
.clip(CircleShape)
.background(statusColor)
)
Spacer(modifier = Modifier.width(6.dp))
// Username display
if (accountName.isNotEmpty()) {
Text(
text = statusText,
text = "@$accountName",
fontSize = 13.sp,
color = Color.White.copy(alpha = 0.85f)
)
@@ -864,7 +836,9 @@ fun ChatsListScreen(
ProtocolState
.AUTHENTICATED
)
textColor.copy(alpha = 0.6f)
textColor.copy(
alpha = 0.6f
)
else
textColor.copy(
alpha = 0.5f

View File

@@ -9,14 +9,13 @@ 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.Close
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Close
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.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@@ -24,9 +23,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.data.ForwardManager
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import kotlinx.coroutines.launch
/**
* 📨 BottomSheet для выбора чата при Forward сообщений
@@ -44,9 +42,7 @@ fun ForwardChatPickerBottomSheet(
onDismiss: () -> Unit,
onChatSelected: (String) -> Unit
) {
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = false
)
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
val scope = rememberCoroutineScope()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White
@@ -77,12 +73,13 @@ fun ForwardChatPickerBottomSheet(
) {
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.width(36.dp)
modifier =
Modifier.width(36.dp)
.height(5.dp)
.clip(RoundedCornerShape(2.5.dp))
.background(
if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD1D1D6)
if (isDarkTheme) Color(0xFF4A4A4A)
else Color(0xFFD1D1D6)
)
)
Spacer(modifier = Modifier.height(16.dp))
@@ -90,30 +87,21 @@ fun ForwardChatPickerBottomSheet(
},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
) {
Column(modifier = Modifier.fillMaxWidth().navigationBarsPadding()) {
// Header
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Иконка и заголовок
Row(
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// 🔥 Красивая иконка Forward
Icon(
Icons.Filled.ArrowForward,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier
.size(24.dp)
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
@@ -124,7 +112,8 @@ fun ForwardChatPickerBottomSheet(
color = textColor
)
Text(
text = "$messagesCount message${if (messagesCount > 1) "s" else ""} selected",
text =
"$messagesCount message${if (messagesCount > 1) "s" else ""} selected",
fontSize = 14.sp,
color = secondaryTextColor
)
@@ -143,23 +132,16 @@ fun ForwardChatPickerBottomSheet(
Spacer(modifier = Modifier.height(12.dp))
Divider(
color = dividerColor,
thickness = 0.5.dp
)
Divider(color = dividerColor, thickness = 0.5.dp)
// Список диалогов
if (dialogs.isEmpty()) {
// Empty state
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
modifier = Modifier.fillMaxWidth().height(200.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "No chats yet",
fontSize = 16.sp,
@@ -176,18 +158,19 @@ fun ForwardChatPickerBottomSheet(
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 300.dp, max = 400.dp) // 🔥 Минимальная высота для лучшего UX
modifier =
Modifier.fillMaxWidth()
.heightIn(
min = 300.dp,
max = 400.dp
) // 🔥 Минимальная высота для лучшего UX
) {
items(dialogs, key = { it.opponentKey }) { dialog ->
ForwardDialogItem(
dialog = dialog,
isDarkTheme = isDarkTheme,
isSavedMessages = dialog.opponentKey == currentUserPublicKey,
onClick = {
onChatSelected(dialog.opponentKey)
}
onClick = { onChatSelected(dialog.opponentKey) }
)
// Сепаратор между диалогами
@@ -208,9 +191,7 @@ fun ForwardChatPickerBottomSheet(
}
}
/**
* Элемент диалога в списке выбора для Forward
*/
/** Элемент диалога в списке выбора для Forward */
@Composable
private fun ForwardDialogItem(
dialog: DialogUiModel,
@@ -221,11 +202,13 @@ private fun ForwardDialogItem(
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val avatarColors = remember(dialog.opponentKey, isDarkTheme) {
val avatarColors =
remember(dialog.opponentKey, isDarkTheme) {
getAvatarColor(dialog.opponentKey, isDarkTheme)
}
val displayName = remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
val displayName =
remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
when {
isSavedMessages -> "Saved Messages"
dialog.opponentTitle.isNotEmpty() -> dialog.opponentTitle
@@ -233,7 +216,8 @@ private fun ForwardDialogItem(
}
}
val initials = remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
val initials =
remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
when {
isSavedMessages -> "📁"
dialog.opponentTitle.isNotEmpty() -> {
@@ -248,16 +232,16 @@ private fun ForwardDialogItem(
}
Row(
modifier = Modifier
.fillMaxWidth()
modifier =
Modifier.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
Box(
modifier = Modifier
.size(48.dp)
modifier =
Modifier.size(48.dp)
.clip(CircleShape)
.background(
if (isSavedMessages) PrimaryBlue.copy(alpha = 0.15f)
@@ -276,9 +260,7 @@ private fun ForwardDialogItem(
Spacer(modifier = Modifier.width(12.dp))
// Info
Column(
modifier = Modifier.weight(1f)
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = displayName,
fontWeight = FontWeight.SemiBold,
@@ -291,7 +273,9 @@ private fun ForwardDialogItem(
Spacer(modifier = Modifier.height(2.dp))
Text(
text = if (isSavedMessages) "Your personal notes" else dialog.lastMessage.ifEmpty { "No messages" },
text =
if (isSavedMessages) "Your personal notes"
else dialog.lastMessage.ifEmpty { "No messages" },
fontSize = 14.sp,
color = secondaryTextColor,
maxLines = 1,
@@ -301,12 +285,7 @@ private fun ForwardDialogItem(
// Online indicator
if (!isSavedMessages && dialog.isOnline == 1) {
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(Color(0xFF34C759))
)
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(Color(0xFF34C759)))
}
}
}

View File

@@ -1,5 +1,7 @@
package com.rosetta.messenger.ui.chats
import android.content.Context
import android.view.inputmethod.InputMethodManager
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
@@ -21,8 +23,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalView
import android.content.Context
import android.view.inputmethod.InputMethodManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -36,10 +36,7 @@ import com.rosetta.messenger.network.SearchUser
// Primary Blue color
private val PrimaryBlue = Color(0xFF54A9EB)
/**
* Отдельная страница поиска пользователей
* Хедер на всю ширину с полем ввода
*/
/** Отдельная страница поиска пользователей Хедер на всю ширину с полем ввода */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchScreen(
@@ -78,7 +75,8 @@ fun SearchScreen(
val recentUsers by RecentSearchesManager.recentUsers.collectAsState()
// Preload Lottie composition for search animation
val searchLottieComposition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.search))
val searchLottieComposition by
rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.search))
// Устанавливаем аккаунт для RecentSearchesManager
LaunchedEffect(currentUserPublicKey) {
@@ -106,23 +104,22 @@ fun SearchScreen(
Scaffold(
topBar = {
// Кастомный header с полем ввода на всю ширину
Surface(
modifier = Modifier.fillMaxWidth(),
color = backgroundColor
) {
Surface(modifier = Modifier.fillMaxWidth(), color = backgroundColor) {
Row(
modifier = Modifier
.fillMaxWidth()
modifier =
Modifier.fillMaxWidth()
.statusBarsPadding()
.height(64.dp)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Кнопка назад - с мгновенным закрытием клавиатуры
IconButton(onClick = {
IconButton(
onClick = {
hideKeyboardInstantly()
onBackClick()
}) {
}
) {
Icon(
Icons.Default.ArrowBack,
contentDescription = "Back",
@@ -131,11 +128,7 @@ fun SearchScreen(
}
// Поле ввода на всю оставшуюся ширину
Box(
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
) {
Box(modifier = Modifier.weight(1f).padding(end = 8.dp)) {
TextField(
value = searchQuery,
onValueChange = { searchViewModel.onSearchQueryChange(it) },
@@ -146,7 +139,8 @@ fun SearchScreen(
fontSize = 16.sp
)
},
colors = TextFieldDefaults.colors(
colors =
TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedTextColor = textColor,
@@ -155,21 +149,21 @@ fun SearchScreen(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
textStyle = androidx.compose.ui.text.TextStyle(
textStyle =
androidx.compose.ui.text.TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal
),
singleLine = true,
enabled = protocolState == ProtocolState.AUTHENTICATED,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester)
modifier =
Modifier.fillMaxWidth().focusRequester(focusRequester)
)
// Подчеркивание
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
modifier =
Modifier.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(2.dp)
.background(
@@ -199,11 +193,7 @@ fun SearchScreen(
containerColor = backgroundColor
) { paddingValues ->
// Контент - показываем recent users если поле пустое, иначе результаты
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
if (searchQuery.isEmpty() && recentUsers.isNotEmpty()) {
// Recent Users с аватарками
LazyColumn(
@@ -212,8 +202,8 @@ fun SearchScreen(
) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
modifier =
Modifier.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
@@ -225,11 +215,7 @@ fun SearchScreen(
color = secondaryTextColor
)
TextButton(onClick = { RecentSearchesManager.clearAll() }) {
Text(
"Clear All",
fontSize = 13.sp,
color = PrimaryBlue
)
Text("Clear All", fontSize = 13.sp, color = PrimaryBlue)
}
}
}
@@ -245,23 +231,28 @@ fun SearchScreen(
RecentSearchesManager.addUser(user)
onUserSelect(user)
},
onRemove = {
RecentSearchesManager.removeUser(user.publicKey)
}
onRemove = { RecentSearchesManager.removeUser(user.publicKey) }
)
}
}
} else {
// Search Results
// Проверяем, не ищет ли пользователь сам себя (Saved Messages)
val isSavedMessagesSearch = searchQuery.trim().let { query ->
val isSavedMessagesSearch =
searchQuery.trim().let { query ->
query.equals(currentUserPublicKey, ignoreCase = true) ||
query.equals(currentUserPublicKey.take(8), ignoreCase = true) ||
query.equals(currentUserPublicKey.takeLast(8), ignoreCase = true)
query.equals(
currentUserPublicKey.takeLast(8),
ignoreCase = true
)
}
// Если ищем себя - показываем Saved Messages как первый результат
val resultsWithSavedMessages = if (isSavedMessagesSearch && searchResults.none { it.publicKey == currentUserPublicKey }) {
val resultsWithSavedMessages =
if (isSavedMessagesSearch &&
searchResults.none { it.publicKey == currentUserPublicKey }
) {
listOf(
SearchUser(
title = "Saved Messages",
@@ -305,11 +296,8 @@ private fun RecentUserItem(
onClick: () -> Unit,
onRemove: () -> Unit
) {
val displayName = user.title.ifEmpty {
user.username.ifEmpty {
user.publicKey.take(8) + "..."
}
}
val displayName =
user.title.ifEmpty { user.username.ifEmpty { user.publicKey.take(8) + "..." } }
// Используем getInitials из ChatsListScreen
val initials = getInitials(displayName)
@@ -317,16 +305,16 @@ private fun RecentUserItem(
val avatarColors = getAvatarColor(user.publicKey, isDarkTheme)
Row(
modifier = Modifier
.fillMaxWidth()
modifier =
Modifier.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
Box(
modifier = Modifier
.size(48.dp)
modifier =
Modifier.size(48.dp)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
@@ -374,10 +362,7 @@ private fun RecentUserItem(
}
// Remove button
IconButton(
onClick = onRemove,
modifier = Modifier.size(40.dp)
) {
IconButton(onClick = onRemove, modifier = Modifier.size(40.dp)) {
Icon(
Icons.Default.Close,
contentDescription = "Remove",