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

View File

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

View File

@@ -5,7 +5,6 @@ import androidx.compose.animation.core.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@@ -40,11 +39,28 @@ fun SetPasswordScreen(
onBack: () -> Unit, onBack: () -> Unit,
onAccountCreated: (DecryptedAccount) -> Unit onAccountCreated: (DecryptedAccount) -> Unit
) { ) {
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f)) val themeAnimSpec =
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec) tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec) val backgroundColor by
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec) animateColorAsState(
val cardColor by animateColorAsState(if (isDarkTheme) AuthSurface else AuthSurfaceLight, animationSpec = themeAnimSpec) 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 context = LocalContext.current
val accountManager = remember { AccountManager(context) } val accountManager = remember { AccountManager(context) }
@@ -63,7 +79,8 @@ fun SetPasswordScreen(
var isKeyboardVisible by remember { mutableStateOf(false) } var isKeyboardVisible by remember { mutableStateOf(false) }
DisposableEffect(view) { DisposableEffect(view) {
val listener = android.view.ViewTreeObserver.OnGlobalLayoutListener { val listener =
android.view.ViewTreeObserver.OnGlobalLayoutListener {
val rect = android.graphics.Rect() val rect = android.graphics.Rect()
view.getWindowVisibleDisplayFrame(rect) view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height val screenHeight = view.rootView.height
@@ -71,34 +88,20 @@ fun SetPasswordScreen(
isKeyboardVisible = keypadHeight > screenHeight * 0.15 isKeyboardVisible = keypadHeight > screenHeight * 0.15
} }
view.viewTreeObserver.addOnGlobalLayoutListener(listener) view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose { onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) { visible = true }
visible = true
}
val passwordsMatch = password == confirmPassword && password.isNotEmpty() val passwordsMatch = password == confirmPassword && password.isNotEmpty()
val isPasswordWeak = password.isNotEmpty() && password.length < 6 val isPasswordWeak = password.isNotEmpty() && password.length < 6
val canContinue = passwordsMatch && !isCreating val canContinue = passwordsMatch && !isCreating
Box( Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
modifier = Modifier Column(modifier = Modifier.fillMaxSize().statusBarsPadding()) {
.fillMaxSize()
.background(backgroundColor)
) {
Column(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
) {
// Top Bar // Top Bar
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp),
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton(onClick = onBack, enabled = !isCreating) { IconButton(onClick = onBack, enabled = !isCreating) {
@@ -120,8 +123,8 @@ fun SetPasswordScreen(
} }
Column( Column(
modifier = Modifier modifier =
.fillMaxSize() Modifier.fillMaxSize()
.imePadding() .imePadding()
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
@@ -130,26 +133,35 @@ fun SetPasswordScreen(
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 8.dp else 16.dp)) Spacer(modifier = Modifier.height(if (isKeyboardVisible) 8.dp else 16.dp))
// Lock Icon - smaller when keyboard is visible // Lock Icon - smaller when keyboard is visible
val iconSize by animateDpAsState( val iconSize by
animateDpAsState(
targetValue = if (isKeyboardVisible) 48.dp else 80.dp, targetValue = if (isKeyboardVisible) 48.dp else 80.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing) animationSpec = tween(300, easing = FastOutSlowInEasing)
) )
val iconInnerSize by animateDpAsState( val iconInnerSize by
animateDpAsState(
targetValue = if (isKeyboardVisible) 24.dp else 40.dp, targetValue = if (isKeyboardVisible) 24.dp else 40.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing) animationSpec = tween(300, easing = FastOutSlowInEasing)
) )
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(500)) + scaleIn( enter =
fadeIn(tween(500)) +
scaleIn(
initialScale = 0.5f, initialScale = 0.5f,
animationSpec = tween(500, easing = FastOutSlowInEasing) animationSpec =
tween(500, easing = FastOutSlowInEasing)
) )
) { ) {
Box( Box(
modifier = Modifier modifier =
.size(iconSize) Modifier.size(iconSize)
.clip(RoundedCornerShape(if (isKeyboardVisible) 12.dp else 20.dp)) .clip(
RoundedCornerShape(
if (isKeyboardVisible) 12.dp else 20.dp
)
)
.background(PrimaryBlue.copy(alpha = 0.1f)), .background(PrimaryBlue.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -166,7 +178,9 @@ fun SetPasswordScreen(
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(500, delayMillis = 100)) + slideInVertically( enter =
fadeIn(tween(500, delayMillis = 100)) +
slideInVertically(
initialOffsetY = { -20 }, initialOffsetY = { -20 },
animationSpec = tween(500, delayMillis = 100) animationSpec = tween(500, delayMillis = 100)
) )
@@ -186,7 +200,8 @@ fun SetPasswordScreen(
enter = fadeIn(tween(500, delayMillis = 200)) enter = fadeIn(tween(500, delayMillis = 200))
) { ) {
Text( 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, fontSize = if (isKeyboardVisible) 12.sp else 14.sp,
color = secondaryTextColor, color = secondaryTextColor,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@@ -199,7 +214,9 @@ fun SetPasswordScreen(
// Password Field // Password Field
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(500, delayMillis = 300)) + slideInVertically( enter =
fadeIn(tween(500, delayMillis = 300)) +
slideInVertically(
initialOffsetY = { 50 }, initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 300) animationSpec = tween(500, delayMillis = 300)
) )
@@ -213,20 +230,26 @@ fun SetPasswordScreen(
label = { Text("Password") }, label = { Text("Password") },
placeholder = { Text("Enter password") }, placeholder = { Text("Enter password") },
singleLine = true, singleLine = true,
visualTransformation = if (passwordVisible) visualTransformation =
VisualTransformation.None else PasswordVisualTransformation(), if (passwordVisible) VisualTransformation.None
else PasswordVisualTransformation(),
trailingIcon = { trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) { IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon( Icon(
imageVector = if (passwordVisible) imageVector =
Icons.Default.VisibilityOff else Icons.Default.Visibility, if (passwordVisible) Icons.Default.VisibilityOff
contentDescription = if (passwordVisible) "Hide" else "Show" else Icons.Default.Visibility,
contentDescription =
if (passwordVisible) "Hide" else "Show"
) )
} }
}, },
colors = OutlinedTextFieldDefaults.colors( colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue, focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0), unfocusedBorderColor =
if (isDarkTheme) Color(0xFF4A4A4A)
else Color(0xFFD0D0D0),
focusedLabelColor = PrimaryBlue, focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue, cursorColor = PrimaryBlue,
focusedTextColor = textColor, focusedTextColor = textColor,
@@ -234,7 +257,8 @@ fun SetPasswordScreen(
), ),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions( keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Password, keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next imeAction = ImeAction.Next
) )
@@ -246,7 +270,9 @@ fun SetPasswordScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(400, delayMillis = 350)) + slideInHorizontally( enter =
fadeIn(tween(400, delayMillis = 350)) +
slideInHorizontally(
initialOffsetX = { -30 }, initialOffsetX = { -30 },
animationSpec = tween(400, delayMillis = 350) animationSpec = tween(400, delayMillis = 350)
) )
@@ -256,12 +282,14 @@ fun SetPasswordScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
val strength = when { val strength =
when {
password.length < 6 -> "Weak" password.length < 6 -> "Weak"
password.length < 10 -> "Medium" password.length < 10 -> "Medium"
else -> "Strong" else -> "Strong"
} }
val strengthColor = when { val strengthColor =
when {
password.length < 6 -> Color(0xFFE53935) password.length < 6 -> Color(0xFFE53935)
password.length < 10 -> Color(0xFFFFA726) password.length < 10 -> Color(0xFFFFA726)
else -> Color(0xFF4CAF50) else -> Color(0xFF4CAF50)
@@ -283,10 +311,12 @@ fun SetPasswordScreen(
if (isPasswordWeak) { if (isPasswordWeak) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Row( Row(
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth()
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(Color(0xFFE53935).copy(alpha = 0.1f)) .background(
Color(0xFFE53935).copy(alpha = 0.1f)
)
.padding(8.dp), .padding(8.dp),
verticalAlignment = Alignment.Top verticalAlignment = Alignment.Top
) { ) {
@@ -298,7 +328,8 @@ fun SetPasswordScreen(
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( 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, fontSize = 11.sp,
color = Color(0xFFE53935), color = Color(0xFFE53935),
lineHeight = 14.sp lineHeight = 14.sp
@@ -314,7 +345,9 @@ fun SetPasswordScreen(
// Confirm Password Field // Confirm Password Field
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(500, delayMillis = 400)) + slideInVertically( enter =
fadeIn(tween(500, delayMillis = 400)) +
slideInVertically(
initialOffsetY = { 50 }, initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 400) animationSpec = tween(500, delayMillis = 400)
) )
@@ -328,21 +361,32 @@ fun SetPasswordScreen(
label = { Text("Confirm Password") }, label = { Text("Confirm Password") },
placeholder = { Text("Re-enter password") }, placeholder = { Text("Re-enter password") },
singleLine = true, singleLine = true,
visualTransformation = if (confirmPasswordVisible) visualTransformation =
VisualTransformation.None else PasswordVisualTransformation(), if (confirmPasswordVisible) VisualTransformation.None
else PasswordVisualTransformation(),
trailingIcon = { trailingIcon = {
IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) { IconButton(
onClick = {
confirmPasswordVisible = !confirmPasswordVisible
}
) {
Icon( Icon(
imageVector = if (confirmPasswordVisible) imageVector =
Icons.Default.VisibilityOff else Icons.Default.Visibility, if (confirmPasswordVisible)
contentDescription = if (confirmPasswordVisible) "Hide" else "Show" Icons.Default.VisibilityOff
else Icons.Default.Visibility,
contentDescription =
if (confirmPasswordVisible) "Hide" else "Show"
) )
} }
}, },
isError = confirmPassword.isNotEmpty() && !passwordsMatch, isError = confirmPassword.isNotEmpty() && !passwordsMatch,
colors = OutlinedTextFieldDefaults.colors( colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue, focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0), unfocusedBorderColor =
if (isDarkTheme) Color(0xFF4A4A4A)
else Color(0xFFD0D0D0),
focusedLabelColor = PrimaryBlue, focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue, cursorColor = PrimaryBlue,
focusedTextColor = textColor, focusedTextColor = textColor,
@@ -350,7 +394,8 @@ fun SetPasswordScreen(
), ),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions( keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Password, keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done imeAction = ImeAction.Done
) )
@@ -362,7 +407,9 @@ fun SetPasswordScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(400, delayMillis = 450)) + slideInHorizontally( enter =
fadeIn(tween(400, delayMillis = 450)) +
slideInHorizontally(
initialOffsetX = { -30 }, initialOffsetX = { -30 },
animationSpec = tween(400, delayMillis = 450) animationSpec = tween(400, delayMillis = 450)
) )
@@ -371,9 +418,13 @@ fun SetPasswordScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
val matchIcon = if (passwordsMatch) Icons.Default.Check else Icons.Default.Close val matchIcon =
val matchColor = if (passwordsMatch) Color(0xFF4CAF50) else Color(0xFFE53935) if (passwordsMatch) Icons.Default.Check else Icons.Default.Close
val matchText = if (passwordsMatch) "Passwords match" else "Passwords don't match" val matchColor =
if (passwordsMatch) Color(0xFF4CAF50) else Color(0xFFE53935)
val matchText =
if (passwordsMatch) "Passwords match"
else "Passwords don't match"
Icon( Icon(
imageVector = matchIcon, imageVector = matchIcon,
@@ -382,11 +433,7 @@ fun SetPasswordScreen(
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text( Text(text = matchText, fontSize = 12.sp, color = matchColor)
text = matchText,
fontSize = 12.sp,
color = matchColor
)
} }
} }
} }
@@ -412,24 +459,24 @@ fun SetPasswordScreen(
// Info - hide when keyboard is visible // Info - hide when keyboard is visible
AnimatedVisibility( AnimatedVisibility(
visible = visible && !isKeyboardVisible, visible = visible && !isKeyboardVisible,
enter = fadeIn(tween(400)) + slideInVertically( enter =
fadeIn(tween(400)) +
slideInVertically(
initialOffsetY = { 30 }, initialOffsetY = { 30 },
animationSpec = tween(400) animationSpec = tween(400)
) + scaleIn( ) +
initialScale = 0.9f, scaleIn(initialScale = 0.9f, animationSpec = tween(400)),
animationSpec = tween(400) exit =
), fadeOut(tween(300)) +
exit = fadeOut(tween(300)) + slideOutVertically( slideOutVertically(
targetOffsetY = { 30 }, targetOffsetY = { 30 },
animationSpec = tween(300) animationSpec = tween(300)
) + scaleOut( ) +
targetScale = 0.9f, scaleOut(targetScale = 0.9f, animationSpec = tween(300))
animationSpec = tween(300)
)
) { ) {
Row( Row(
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth()
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.background(cardColor) .background(cardColor)
.padding(16.dp), .padding(16.dp),
@@ -443,7 +490,8 @@ fun SetPasswordScreen(
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Text( 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, fontSize = 13.sp,
color = secondaryTextColor, color = secondaryTextColor,
lineHeight = 18.sp lineHeight = 18.sp
@@ -456,7 +504,9 @@ fun SetPasswordScreen(
// Create Account Button // Create Account Button
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(500, delayMillis = 600)) + slideInVertically( enter =
fadeIn(tween(500, delayMillis = 600)) +
slideInVertically(
initialOffsetY = { 50 }, initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 600) animationSpec = tween(500, delayMillis = 600)
) )
@@ -472,19 +522,26 @@ fun SetPasswordScreen(
scope.launch { scope.launch {
try { try {
// Generate keys from seed phrase // Generate keys from seed phrase
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase) val keyPair =
CryptoManager.generateKeyPairFromSeed(seedPhrase)
// Encrypt private key and seed phrase // Encrypt private key and seed phrase
val encryptedPrivateKey = CryptoManager.encryptWithPassword( val encryptedPrivateKey =
keyPair.privateKey, password CryptoManager.encryptWithPassword(
keyPair.privateKey,
password
) )
val encryptedSeedPhrase = CryptoManager.encryptWithPassword( val encryptedSeedPhrase =
seedPhrase.joinToString(" "), password CryptoManager.encryptWithPassword(
seedPhrase.joinToString(" "),
password
) )
// Save account with truncated public key as name // Save account with truncated public key as name
val truncatedKey = "${keyPair.publicKey.take(6)}...${keyPair.publicKey.takeLast(4)}" val truncatedKey =
val account = EncryptedAccount( "${keyPair.publicKey.take(6)}...${keyPair.publicKey.takeLast(4)}"
val account =
EncryptedAccount(
publicKey = keyPair.publicKey, publicKey = keyPair.publicKey,
encryptedPrivateKey = encryptedPrivateKey, encryptedPrivateKey = encryptedPrivateKey,
encryptedSeedPhrase = encryptedSeedPhrase, encryptedSeedPhrase = encryptedSeedPhrase,
@@ -495,14 +552,21 @@ fun SetPasswordScreen(
accountManager.setCurrentAccount(keyPair.publicKey) accountManager.setCurrentAccount(keyPair.publicKey)
// 🔌 Connect to server and authenticate // 🔌 Connect to server and authenticate
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey) val privateKeyHash =
CryptoManager.generatePrivateKeyHash(
keyPair.privateKey
)
ProtocolManager.connect() ProtocolManager.connect()
// Give WebSocket time to connect before authenticating // Give WebSocket time to connect before authenticating
kotlinx.coroutines.delay(500) kotlinx.coroutines.delay(500)
ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash) ProtocolManager.authenticate(
keyPair.publicKey,
privateKeyHash
)
// Create DecryptedAccount to pass to callback // Create DecryptedAccount to pass to callback
val decryptedAccount = DecryptedAccount( val decryptedAccount =
DecryptedAccount(
publicKey = keyPair.publicKey, publicKey = keyPair.publicKey,
privateKey = keyPair.privateKey, privateKey = keyPair.privateKey,
seedPhrase = seedPhrase, seedPhrase = seedPhrase,
@@ -518,10 +582,9 @@ fun SetPasswordScreen(
} }
}, },
enabled = canContinue, enabled = canContinue,
modifier = Modifier modifier = Modifier.fillMaxWidth().height(56.dp),
.fillMaxWidth() colors =
.height(56.dp), ButtonDefaults.buttonColors(
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue, containerColor = PrimaryBlue,
contentColor = Color.White, contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f), disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
@@ -542,7 +605,8 @@ fun SetPasswordScreen(
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
} } }
}
Spacer(modifier = Modifier.height(32.dp)) 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.DecryptedAccount
import com.rosetta.messenger.data.EncryptedAccount import com.rosetta.messenger.data.EncryptedAccount
import com.rosetta.messenger.network.ProtocolManager 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.getAvatarColor
import com.rosetta.messenger.ui.chats.getAvatarText import com.rosetta.messenger.ui.chats.getAvatarText
import kotlinx.coroutines.flow.first import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// Account model for dropdown // Account model for dropdown
@@ -59,11 +58,28 @@ fun UnlockScreen(
onUnlocked: (DecryptedAccount) -> Unit, onUnlocked: (DecryptedAccount) -> Unit,
onSwitchAccount: () -> Unit = {} onSwitchAccount: () -> Unit = {}
) { ) {
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f)) val themeAnimSpec =
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec) tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec) val backgroundColor by
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec) animateColorAsState(
val cardBackground by animateColorAsState(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5), animationSpec = themeAnimSpec) 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 context = LocalContext.current
val accountManager = remember { AccountManager(context) } val accountManager = remember { AccountManager(context) }
@@ -87,16 +103,14 @@ fun UnlockScreen(
val lastLoggedKey = accountManager.getLastLoggedPublicKey() val lastLoggedKey = accountManager.getLastLoggedPublicKey()
val allAccounts = accountManager.getAllAccounts() val allAccounts = accountManager.getAllAccounts()
accounts = allAccounts.map { acc -> accounts =
AccountItem( allAccounts.map { acc ->
publicKey = acc.publicKey, AccountItem(publicKey = acc.publicKey, name = acc.name, encryptedAccount = acc)
name = acc.name,
encryptedAccount = acc
)
} }
// Find the target account - приоритет: selectedAccountId > lastLoggedKey > первый // Find the target account - приоритет: selectedAccountId > lastLoggedKey > первый
val targetAccount = when { val targetAccount =
when {
!selectedAccountId.isNullOrEmpty() -> { !selectedAccountId.isNullOrEmpty() -> {
accounts.find { it.publicKey == selectedAccountId } accounts.find { it.publicKey == selectedAccountId }
} }
@@ -112,9 +126,11 @@ fun UnlockScreen(
} }
// Filter accounts by search // Filter accounts by search
val filteredAccounts = remember(searchQuery, accounts) { val filteredAccounts =
remember(searchQuery, accounts) {
if (searchQuery.isEmpty()) accounts if (searchQuery.isEmpty()) accounts
else accounts.filter { else
accounts.filter {
it.name.contains(searchQuery, ignoreCase = true) || it.name.contains(searchQuery, ignoreCase = true) ||
it.publicKey.contains(searchQuery, ignoreCase = true) it.publicKey.contains(searchQuery, ignoreCase = true)
} }
@@ -125,9 +141,11 @@ fun UnlockScreen(
LaunchedEffect(Unit) { visible = true } LaunchedEffect(Unit) { visible = true }
// Dropdown animation // Dropdown animation
val dropdownProgress by animateFloatAsState( val dropdownProgress by
animateFloatAsState(
targetValue = if (isDropdownExpanded) 1f else 0f, targetValue = if (isDropdownExpanded) 1f else 0f,
animationSpec = spring( animationSpec =
spring(
dampingRatio = Spring.DampingRatioMediumBouncy, dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow stiffness = Spring.StiffnessLow
), ),
@@ -148,14 +166,10 @@ fun UnlockScreen(
} }
} }
Box( Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
Column( Column(
modifier = Modifier modifier =
.fillMaxSize() Modifier.fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.imePadding() .imePadding()
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
@@ -172,9 +186,7 @@ fun UnlockScreen(
Image( Image(
painter = painterResource(id = R.drawable.rosetta_icon), painter = painterResource(id = R.drawable.rosetta_icon),
contentDescription = "Rosetta", contentDescription = "Rosetta",
modifier = Modifier modifier = Modifier.size(100.dp).clip(CircleShape)
.size(100.dp)
.clip(CircleShape)
) )
} }
@@ -183,7 +195,9 @@ fun UnlockScreen(
// Title // Title
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(600, delayMillis = 200)) + slideInVertically( enter =
fadeIn(tween(600, delayMillis = 200)) +
slideInVertically(
initialOffsetY = { 30 }, initialOffsetY = { 30 },
animationSpec = tween(600, delayMillis = 200) animationSpec = tween(600, delayMillis = 200)
) )
@@ -198,10 +212,7 @@ fun UnlockScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility( AnimatedVisibility(visible = visible, enter = fadeIn(tween(600, delayMillis = 300))) {
visible = visible,
enter = fadeIn(tween(600, delayMillis = 300))
) {
Text( Text(
text = "Select your account and enter password", text = "Select your account and enter password",
fontSize = 16.sp, fontSize = 16.sp,
@@ -215,7 +226,9 @@ fun UnlockScreen(
// Account Selector Card // Account Selector Card
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(600, delayMillis = 350)) + slideInVertically( enter =
fadeIn(tween(600, delayMillis = 350)) +
slideInVertically(
initialOffsetY = { 50 }, initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 350) animationSpec = tween(600, delayMillis = 350)
) )
@@ -223,8 +236,8 @@ fun UnlockScreen(
Column { Column {
// Account selector dropdown // Account selector dropdown
Card( Card(
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth()
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.clickable(enabled = accounts.size > 1) { .clickable(enabled = accounts.size > 1) {
isDropdownExpanded = !isDropdownExpanded isDropdownExpanded = !isDropdownExpanded
@@ -233,17 +246,16 @@ fun UnlockScreen(
shape = RoundedCornerShape(16.dp) shape = RoundedCornerShape(16.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(16.dp),
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Avatar // Avatar
if (selectedAccount != null) { if (selectedAccount != null) {
val avatarColors = getAvatarColor(selectedAccount!!.publicKey, isDarkTheme) val avatarColors =
getAvatarColor(selectedAccount!!.publicKey, isDarkTheme)
Box( Box(
modifier = Modifier modifier =
.size(48.dp) Modifier.size(48.dp)
.clip(CircleShape) .clip(CircleShape)
.background(avatarColors.backgroundColor), .background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -286,9 +298,8 @@ fun UnlockScreen(
imageVector = Icons.Default.KeyboardArrowDown, imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null, contentDescription = null,
tint = secondaryTextColor.copy(alpha = 0.6f), tint = secondaryTextColor.copy(alpha = 0.6f),
modifier = Modifier modifier =
.size(24.dp) Modifier.size(24.dp).graphicsLayer {
.graphicsLayer {
rotationZ = 180f * dropdownProgress rotationZ = 180f * dropdownProgress
} }
) )
@@ -299,20 +310,29 @@ fun UnlockScreen(
// Dropdown list with animation // Dropdown list with animation
AnimatedVisibility( AnimatedVisibility(
visible = isDropdownExpanded && accounts.size > 1, visible = isDropdownExpanded && accounts.size > 1,
enter = fadeIn(tween(150)) + expandVertically( enter =
fadeIn(tween(150)) +
expandVertically(
expandFrom = Alignment.Top, 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, shrinkTowards = Alignment.Top,
animationSpec = tween(150) animationSpec = tween(150)
) )
) { ) {
Card( Card(
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth()
.padding(top = 8.dp) .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), colors = CardDefaults.cardColors(containerColor = cardBackground),
shape = RoundedCornerShape(16.dp) shape = RoundedCornerShape(16.dp)
) { ) {
@@ -325,7 +345,10 @@ fun UnlockScreen(
placeholder = { placeholder = {
Text( Text(
"Search accounts...", "Search accounts...",
color = secondaryTextColor.copy(alpha = 0.6f) color =
secondaryTextColor.copy(
alpha = 0.6f
)
) )
}, },
leadingIcon = { leadingIcon = {
@@ -335,45 +358,60 @@ fun UnlockScreen(
tint = secondaryTextColor.copy(alpha = 0.6f) tint = secondaryTextColor.copy(alpha = 0.6f)
) )
}, },
colors = OutlinedTextFieldDefaults.colors( colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue, focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = Color.Transparent, unfocusedBorderColor =
focusedContainerColor = Color.Transparent, Color.Transparent,
unfocusedContainerColor = Color.Transparent, focusedContainerColor =
Color.Transparent,
unfocusedContainerColor =
Color.Transparent,
focusedTextColor = textColor, focusedTextColor = textColor,
unfocusedTextColor = textColor, unfocusedTextColor = textColor,
cursorColor = PrimaryBlue cursorColor = PrimaryBlue
), ),
singleLine = true, singleLine = true,
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp) .padding(
horizontal = 12.dp,
vertical = 8.dp
)
.focusRequester(searchFocusRequester), .focusRequester(searchFocusRequester),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) )
Divider( Divider(
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0), color =
if (isDarkTheme) Color(0xFF3A3A3A)
else Color(0xFFE0E0E0),
thickness = 0.5.dp thickness = 0.5.dp
) )
} }
// Account list // Account list
LazyColumn( LazyColumn(
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth()
.padding(vertical = if (accounts.size <= 3) 8.dp else 0.dp) .padding(
vertical =
if (accounts.size <= 3) 8.dp
else 0.dp
)
) { ) {
items(filteredAccounts, key = { it.publicKey }) { account -> items(filteredAccounts, key = { it.publicKey }) { account ->
val isSelected = account.publicKey == selectedAccount?.publicKey val isSelected =
val itemScale by animateFloatAsState( account.publicKey == selectedAccount?.publicKey
val itemScale by
animateFloatAsState(
targetValue = if (isSelected) 1f else 0.98f, targetValue = if (isSelected) 1f else 0.98f,
label = "itemScale" label = "itemScale"
) )
Row( Row(
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth()
.scale(itemScale) .scale(itemScale)
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.clickable { .clickable {
@@ -383,19 +421,29 @@ fun UnlockScreen(
error = null error = null
} }
.background( .background(
if (isSelected) PrimaryBlue.copy(alpha = 0.1f) if (isSelected)
PrimaryBlue.copy(
alpha = 0.1f
)
else Color.Transparent else Color.Transparent
) )
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(
horizontal = 16.dp,
vertical = 12.dp
),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Avatar // Avatar
val avatarColors = getAvatarColor(account.publicKey, isDarkTheme) val avatarColors =
getAvatarColor(account.publicKey, isDarkTheme)
Box( Box(
modifier = Modifier modifier =
.size(40.dp) Modifier.size(40.dp)
.clip(CircleShape) .clip(CircleShape)
.background(avatarColors.backgroundColor), .background(
avatarColors
.backgroundColor
),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
@@ -412,7 +460,9 @@ fun UnlockScreen(
Text( Text(
text = account.name, text = account.name,
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, fontWeight =
if (isSelected) FontWeight.SemiBold
else FontWeight.Normal,
color = textColor, color = textColor,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@@ -443,9 +493,8 @@ fun UnlockScreen(
text = "No accounts found", text = "No accounts found",
color = secondaryTextColor, color = secondaryTextColor,
fontSize = 14.sp, fontSize = 14.sp,
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth().padding(24.dp),
.padding(24.dp),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
@@ -462,7 +511,9 @@ fun UnlockScreen(
// Password Field // Password Field
AnimatedVisibility( AnimatedVisibility(
visible = visible && !isDropdownExpanded, visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(600, delayMillis = 400)) + slideInVertically( enter =
fadeIn(tween(600, delayMillis = 400)) +
slideInVertically(
initialOffsetY = { 50 }, initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 400) animationSpec = tween(600, delayMillis = 400)
) )
@@ -476,21 +527,26 @@ fun UnlockScreen(
label = { Text("Password") }, label = { Text("Password") },
placeholder = { Text("Enter your password") }, placeholder = { Text("Enter your password") },
singleLine = true, singleLine = true,
visualTransformation = if (passwordVisible) visualTransformation =
VisualTransformation.None else PasswordVisualTransformation(), if (passwordVisible) VisualTransformation.None
else PasswordVisualTransformation(),
trailingIcon = { trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) { IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon( Icon(
imageVector = if (passwordVisible) imageVector =
Icons.Default.VisibilityOff else Icons.Default.Visibility, if (passwordVisible) Icons.Default.VisibilityOff
else Icons.Default.Visibility,
contentDescription = if (passwordVisible) "Hide" else "Show" contentDescription = if (passwordVisible) "Hide" else "Show"
) )
} }
}, },
isError = error != null, isError = error != null,
colors = OutlinedTextFieldDefaults.colors( colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue, focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0), unfocusedBorderColor =
if (isDarkTheme) Color(0xFF4A4A4A)
else Color(0xFFD0D0D0),
focusedLabelColor = PrimaryBlue, focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue, cursorColor = PrimaryBlue,
focusedTextColor = textColor, focusedTextColor = textColor,
@@ -499,7 +555,8 @@ fun UnlockScreen(
), ),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions( keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Password, keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done imeAction = ImeAction.Done
) )
@@ -513,11 +570,7 @@ fun UnlockScreen(
exit = fadeOut() exit = fadeOut()
) { ) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(text = error ?: "", fontSize = 14.sp, color = Color(0xFFE53935))
text = error ?: "",
fontSize = 14.sp,
color = Color(0xFFE53935)
)
} }
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@@ -525,7 +578,9 @@ fun UnlockScreen(
// Unlock Button // Unlock Button
AnimatedVisibility( AnimatedVisibility(
visible = visible && !isDropdownExpanded, visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically( enter =
fadeIn(tween(600, delayMillis = 500)) +
slideInVertically(
initialOffsetY = { 50 }, initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 500) animationSpec = tween(600, delayMillis = 500)
) )
@@ -547,8 +602,10 @@ fun UnlockScreen(
val account = selectedAccount!!.encryptedAccount val account = selectedAccount!!.encryptedAccount
// Try to decrypt // Try to decrypt
val decryptedPrivateKey = CryptoManager.decryptWithPassword( val decryptedPrivateKey =
account.encryptedPrivateKey, password CryptoManager.decryptWithPassword(
account.encryptedPrivateKey,
password
) )
if (decryptedPrivateKey == null) { if (decryptedPrivateKey == null) {
@@ -557,13 +614,21 @@ fun UnlockScreen(
return@launch return@launch
} }
val decryptedSeedPhrase = CryptoManager.decryptWithPassword( val decryptedSeedPhrase =
account.encryptedSeedPhrase, password CryptoManager.decryptWithPassword(
)?.split(" ") ?: emptyList() account.encryptedSeedPhrase,
password
)
?.split(" ")
?: emptyList()
val privateKeyHash = CryptoManager.generatePrivateKeyHash(decryptedPrivateKey) val privateKeyHash =
CryptoManager.generatePrivateKeyHash(
decryptedPrivateKey
)
val decryptedAccount = DecryptedAccount( val decryptedAccount =
DecryptedAccount(
publicKey = account.publicKey, publicKey = account.publicKey,
privateKey = decryptedPrivateKey, privateKey = decryptedPrivateKey,
seedPhrase = decryptedSeedPhrase, seedPhrase = decryptedSeedPhrase,
@@ -579,7 +644,6 @@ fun UnlockScreen(
accountManager.setCurrentAccount(account.publicKey) accountManager.setCurrentAccount(account.publicKey)
onUnlocked(decryptedAccount) onUnlocked(decryptedAccount)
} catch (e: Exception) { } catch (e: Exception) {
error = "Failed to unlock: \${e.message}" error = "Failed to unlock: \${e.message}"
isUnlocking = false isUnlocking = false
@@ -587,10 +651,9 @@ fun UnlockScreen(
} }
}, },
enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking, enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking,
modifier = Modifier modifier = Modifier.fillMaxWidth().height(56.dp),
.fillMaxWidth() colors =
.height(56.dp), ButtonDefaults.buttonColors(
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue, containerColor = PrimaryBlue,
contentColor = Color.White, contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f), disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
@@ -611,11 +674,7 @@ fun UnlockScreen(
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(text = "Unlock", fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
text = "Unlock",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
} }
} }
} }
@@ -625,14 +684,14 @@ fun UnlockScreen(
// Create New Account button // Create New Account button
AnimatedVisibility( AnimatedVisibility(
visible = visible && !isDropdownExpanded, visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically( enter =
fadeIn(tween(600, delayMillis = 600)) +
slideInVertically(
initialOffsetY = { 50 }, initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 600) animationSpec = tween(600, delayMillis = 600)
) )
) { ) {
TextButton( TextButton(onClick = onSwitchAccount) {
onClick = onSwitchAccount
) {
Icon( Icon(
imageVector = Icons.Default.PersonAdd, imageVector = Icons.Default.PersonAdd,
contentDescription = null, contentDescription = null,
@@ -640,11 +699,7 @@ fun UnlockScreen(
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(text = "Create New Account", color = PrimaryBlue, fontSize = 15.sp)
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.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -14,7 +13,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
@@ -24,7 +22,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.airbnb.lottie.compose.* import com.airbnb.lottie.compose.*
import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
// Auth colors // Auth colors
val AuthBackground = Color(0xFF1B1B1B) val AuthBackground = Color(0xFF1B1B1B)
@@ -40,10 +37,23 @@ fun WelcomeScreen(
onCreateSeed: () -> Unit, onCreateSeed: () -> Unit,
onImportSeed: () -> Unit onImportSeed: () -> Unit
) { ) {
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f)) val themeAnimSpec =
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec) tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec) val backgroundColor by
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec) 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 // Sync navigation bar color with background
val view = LocalView.current val view = LocalView.current
@@ -53,8 +63,10 @@ fun WelcomeScreen(
} }
// Animation for Lottie // Animation for Lottie
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json")) val lockComposition by
val lockProgress by animateLottieCompositionAsState( rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
val lockProgress by
animateLottieCompositionAsState(
composition = lockComposition, composition = lockComposition,
iterations = 1, // Play once iterations = 1, // Play once
speed = 1f speed = 1f
@@ -64,19 +76,10 @@ fun WelcomeScreen(
var visible by remember { mutableStateOf(false) } var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { visible = true } LaunchedEffect(Unit) { visible = true }
Box( Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
// Back button when coming from UnlockScreen // Back button when coming from UnlockScreen
if (hasExistingAccount) { if (hasExistingAccount) {
IconButton( IconButton(onClick = onBack, modifier = Modifier.statusBarsPadding().padding(4.dp)) {
onClick = onBack,
modifier = Modifier
.statusBarsPadding()
.padding(4.dp)
) {
Icon( Icon(
Icons.Default.ArrowBack, Icons.Default.ArrowBack,
contentDescription = "Back", contentDescription = "Back",
@@ -86,10 +89,7 @@ fun WelcomeScreen(
} }
Column( Column(
modifier = Modifier modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp).statusBarsPadding(),
.fillMaxSize()
.padding(horizontal = 24.dp)
.statusBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.weight(0.15f)) Spacer(modifier = Modifier.weight(0.15f))
@@ -99,10 +99,7 @@ fun WelcomeScreen(
visible = visible, visible = visible,
enter = fadeIn(tween(600)) + scaleIn(tween(600, easing = FastOutSlowInEasing)) enter = fadeIn(tween(600)) + scaleIn(tween(600, easing = FastOutSlowInEasing))
) { ) {
Box( Box(modifier = Modifier.size(180.dp), contentAlignment = Alignment.Center) {
modifier = Modifier.size(180.dp),
contentAlignment = Alignment.Center
) {
lockComposition?.let { comp -> lockComposition?.let { comp ->
LottieAnimation( LottieAnimation(
composition = comp, composition = comp,
@@ -118,7 +115,9 @@ fun WelcomeScreen(
// Title // Title
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(600, delayMillis = 200)) + slideInVertically( enter =
fadeIn(tween(600, delayMillis = 200)) +
slideInVertically(
initialOffsetY = { 50 }, initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 200) animationSpec = tween(600, delayMillis = 200)
) )
@@ -138,7 +137,9 @@ fun WelcomeScreen(
// Subtitle // Subtitle
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(600, delayMillis = 300)) + slideInVertically( enter =
fadeIn(tween(600, delayMillis = 300)) +
slideInVertically(
initialOffsetY = { 50 }, initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 300) animationSpec = tween(600, delayMillis = 300)
) )
@@ -156,10 +157,7 @@ fun WelcomeScreen(
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Features list with icons - placed above buttons // Features list with icons - placed above buttons
AnimatedVisibility( AnimatedVisibility(visible = visible, enter = fadeIn(tween(600, delayMillis = 400))) {
visible = visible,
enter = fadeIn(tween(600, delayMillis = 400))
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
@@ -190,22 +188,24 @@ fun WelcomeScreen(
// Create Seed Button // Create Seed Button
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically( enter =
fadeIn(tween(600, delayMillis = 500)) +
slideInVertically(
initialOffsetY = { 100 }, initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 500) animationSpec = tween(600, delayMillis = 500)
) )
) { ) {
Button( Button(
onClick = onCreateSeed, onClick = onCreateSeed,
modifier = Modifier modifier = Modifier.fillMaxWidth().height(56.dp),
.fillMaxWidth() colors =
.height(56.dp), ButtonDefaults.buttonColors(
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue, containerColor = PrimaryBlue,
contentColor = Color.White contentColor = Color.White
), ),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
elevation = ButtonDefaults.buttonElevation( elevation =
ButtonDefaults.buttonElevation(
defaultElevation = 0.dp, defaultElevation = 0.dp,
pressedElevation = 0.dp pressedElevation = 0.dp
) )
@@ -229,16 +229,16 @@ fun WelcomeScreen(
// Import Seed Button // Import Seed Button
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically( enter =
fadeIn(tween(600, delayMillis = 600)) +
slideInVertically(
initialOffsetY = { 100 }, initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 600) animationSpec = tween(600, delayMillis = 600)
) )
) { ) {
TextButton( TextButton(
onClick = onImportSeed, onClick = onImportSeed,
modifier = Modifier modifier = Modifier.fillMaxWidth().height(56.dp),
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(16.dp) shape = RoundedCornerShape(16.dp)
) { ) {
Icon( Icon(
@@ -274,8 +274,8 @@ private fun CompactFeatureItem(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Box( Box(
modifier = Modifier modifier =
.size(48.dp) Modifier.size(48.dp)
.clip(CircleShape) .clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.12f)), .background(PrimaryBlue.copy(alpha = 0.12f)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -304,13 +304,10 @@ private fun FeatureItem(
isDarkTheme: Boolean, isDarkTheme: Boolean,
textColor: Color textColor: Color
) { ) {
Row( Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Box( Box(
modifier = Modifier modifier =
.size(40.dp) Modifier.size(40.dp)
.clip(CircleShape) .clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.15f)), .background(PrimaryBlue.copy(alpha = 0.15f)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -323,11 +320,6 @@ private fun FeatureItem(
) )
} }
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Text( Text(text = text, fontSize = 15.sp, color = textColor, fontWeight = FontWeight.Medium)
text = text,
fontSize = 15.sp,
color = textColor,
fontWeight = FontWeight.Medium
)
} }
} }

View File

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

View File

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