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:
@@ -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
164
app/proguard-rules.pro
vendored
Normal 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.**
|
||||||
@@ -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)
|
||||||
@@ -33,13 +33,13 @@ class KeyboardTransitionCoordinator {
|
|||||||
// ============ Состояния переходов ============
|
// ============ Состояния переходов ============
|
||||||
|
|
||||||
enum class TransitionState {
|
enum class TransitionState {
|
||||||
IDLE, // Ничего не происходит
|
IDLE, // Ничего не происходит
|
||||||
KEYBOARD_TO_EMOJI, // Keyboard → Emoji
|
KEYBOARD_TO_EMOJI, // Keyboard → Emoji
|
||||||
EMOJI_TO_KEYBOARD, // Emoji → Keyboard
|
EMOJI_TO_KEYBOARD, // Emoji → Keyboard
|
||||||
KEYBOARD_OPENING, // Только keyboard открывается
|
KEYBOARD_OPENING, // Только keyboard открывается
|
||||||
EMOJI_OPENING, // Только emoji открывается
|
EMOJI_OPENING, // Только emoji открывается
|
||||||
KEYBOARD_CLOSING, // Только keyboard закрывается
|
KEYBOARD_CLOSING, // Только keyboard закрывается
|
||||||
EMOJI_CLOSING // Только emoji закрывается
|
EMOJI_CLOSING // Только emoji закрывается
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Основное состояние ============
|
// ============ Основное состояние ============
|
||||||
@@ -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())
|
||||||
try {
|
.postDelayed(
|
||||||
hideEmoji()
|
{
|
||||||
isEmojiVisible = false
|
try {
|
||||||
isKeyboardVisible = true
|
hideEmoji()
|
||||||
|
isEmojiVisible = false
|
||||||
|
isKeyboardVisible = true
|
||||||
|
|
||||||
// Через время анимации завершаем переход
|
// Через время анимации завершаем переход
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
Handler(Looper.getMainLooper())
|
||||||
currentState = TransitionState.IDLE
|
.postDelayed(
|
||||||
isTransitioning = false
|
{
|
||||||
}, TRANSITION_DURATION)
|
currentState = TransitionState.IDLE
|
||||||
} catch (e: Exception) {
|
isTransitioning = false
|
||||||
currentState = TransitionState.IDLE
|
},
|
||||||
isTransitioning = false
|
TRANSITION_DURATION
|
||||||
}
|
)
|
||||||
}, SHORT_DELAY)
|
} catch (e: Exception) {
|
||||||
|
currentState = TransitionState.IDLE
|
||||||
|
isTransitioning = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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())
|
||||||
currentState = TransitionState.IDLE
|
.postDelayed(
|
||||||
isTransitioning = false
|
{
|
||||||
}, TRANSITION_DURATION)
|
currentState = TransitionState.IDLE
|
||||||
|
isTransitioning = false
|
||||||
|
},
|
||||||
|
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())
|
||||||
currentState = TransitionState.IDLE
|
.postDelayed(
|
||||||
isTransitioning = false
|
{
|
||||||
}, TRANSITION_DURATION)
|
currentState = TransitionState.IDLE
|
||||||
|
isTransitioning = false
|
||||||
|
},
|
||||||
|
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())
|
||||||
currentState = TransitionState.IDLE
|
.postDelayed(
|
||||||
isTransitioning = false
|
{
|
||||||
}, TRANSITION_DURATION)
|
currentState = TransitionState.IDLE
|
||||||
|
isTransitioning = false
|
||||||
|
},
|
||||||
|
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() }
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.*
|
||||||
@@ -35,16 +34,33 @@ import kotlinx.coroutines.launch
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SetPasswordScreen(
|
fun SetPasswordScreen(
|
||||||
seedPhrase: List<String>,
|
seedPhrase: List<String>,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
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,101 +79,97 @@ 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 =
|
||||||
val rect = android.graphics.Rect()
|
android.view.ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
view.getWindowVisibleDisplayFrame(rect)
|
val rect = android.graphics.Rect()
|
||||||
val screenHeight = view.rootView.height
|
view.getWindowVisibleDisplayFrame(rect)
|
||||||
val keypadHeight = screenHeight - rect.bottom
|
val screenHeight = view.rootView.height
|
||||||
isKeyboardVisible = keypadHeight > screenHeight * 0.15
|
val keypadHeight = screenHeight - rect.bottom
|
||||||
}
|
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()
|
verticalAlignment = Alignment.CenterVertically
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onBack, enabled = !isCreating) {
|
IconButton(onClick = onBack, enabled = !isCreating) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ArrowBack,
|
imageVector = Icons.Default.ArrowBack,
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
tint = textColor.copy(alpha = 0.6f)
|
tint = textColor.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Text(
|
Text(
|
||||||
text = "Set Password",
|
text = "Set Password",
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Spacer(modifier = Modifier.width(48.dp))
|
Spacer(modifier = Modifier.width(48.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
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
|
||||||
targetValue = if (isKeyboardVisible) 48.dp else 80.dp,
|
animateDpAsState(
|
||||||
animationSpec = tween(300, easing = FastOutSlowInEasing)
|
targetValue = if (isKeyboardVisible) 48.dp else 80.dp,
|
||||||
)
|
animationSpec = tween(300, easing = FastOutSlowInEasing)
|
||||||
val iconInnerSize by animateDpAsState(
|
)
|
||||||
targetValue = if (isKeyboardVisible) 24.dp else 40.dp,
|
val iconInnerSize by
|
||||||
animationSpec = tween(300, easing = FastOutSlowInEasing)
|
animateDpAsState(
|
||||||
)
|
targetValue = if (isKeyboardVisible) 24.dp else 40.dp,
|
||||||
|
animationSpec = tween(300, easing = FastOutSlowInEasing)
|
||||||
|
)
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500)) + scaleIn(
|
enter =
|
||||||
initialScale = 0.5f,
|
fadeIn(tween(500)) +
|
||||||
animationSpec = tween(500, easing = FastOutSlowInEasing)
|
scaleIn(
|
||||||
)
|
initialScale = 0.5f,
|
||||||
|
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(
|
||||||
.background(PrimaryBlue.copy(alpha = 0.1f)),
|
RoundedCornerShape(
|
||||||
contentAlignment = Alignment.Center
|
if (isKeyboardVisible) 12.dp else 20.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.background(PrimaryBlue.copy(alpha = 0.1f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Lock,
|
Icons.Default.Lock,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = PrimaryBlue,
|
tint = PrimaryBlue,
|
||||||
modifier = Modifier.size(iconInnerSize)
|
modifier = Modifier.size(iconInnerSize)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,32 +177,35 @@ fun SetPasswordScreen(
|
|||||||
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 12.dp else 24.dp))
|
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 12.dp else 24.dp))
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500, delayMillis = 100)) + slideInVertically(
|
enter =
|
||||||
initialOffsetY = { -20 },
|
fadeIn(tween(500, delayMillis = 100)) +
|
||||||
animationSpec = tween(500, delayMillis = 100)
|
slideInVertically(
|
||||||
)
|
initialOffsetY = { -20 },
|
||||||
|
animationSpec = tween(500, delayMillis = 100)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Protect Your Account",
|
text = "Protect Your Account",
|
||||||
fontSize = if (isKeyboardVisible) 20.sp else 24.sp,
|
fontSize = if (isKeyboardVisible) 20.sp else 24.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 6.dp else 8.dp))
|
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 6.dp else 8.dp))
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
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 =
|
||||||
fontSize = if (isKeyboardVisible) 12.sp else 14.sp,
|
"This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.",
|
||||||
color = secondaryTextColor,
|
fontSize = if (isKeyboardVisible) 12.sp else 14.sp,
|
||||||
textAlign = TextAlign.Center,
|
color = secondaryTextColor,
|
||||||
lineHeight = if (isKeyboardVisible) 16.sp else 20.sp
|
textAlign = TextAlign.Center,
|
||||||
|
lineHeight = if (isKeyboardVisible) 16.sp else 20.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,110 +213,126 @@ fun SetPasswordScreen(
|
|||||||
|
|
||||||
// Password Field
|
// Password Field
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500, delayMillis = 300)) + slideInVertically(
|
enter =
|
||||||
initialOffsetY = { 50 },
|
fadeIn(tween(500, delayMillis = 300)) +
|
||||||
animationSpec = tween(500, delayMillis = 300)
|
slideInVertically(
|
||||||
)
|
initialOffsetY = { 50 },
|
||||||
|
animationSpec = tween(500, delayMillis = 300)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
password = it
|
password = it
|
||||||
error = null
|
error = null
|
||||||
},
|
},
|
||||||
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
|
||||||
trailingIcon = {
|
else PasswordVisualTransformation(),
|
||||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
trailingIcon = {
|
||||||
Icon(
|
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||||
imageVector = if (passwordVisible)
|
Icon(
|
||||||
Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
imageVector =
|
||||||
contentDescription = if (passwordVisible) "Hide" else "Show"
|
if (passwordVisible) Icons.Default.VisibilityOff
|
||||||
)
|
else Icons.Default.Visibility,
|
||||||
}
|
contentDescription =
|
||||||
},
|
if (passwordVisible) "Hide" else "Show"
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
)
|
||||||
focusedBorderColor = PrimaryBlue,
|
}
|
||||||
unfocusedBorderColor = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0),
|
},
|
||||||
focusedLabelColor = PrimaryBlue,
|
colors =
|
||||||
cursorColor = PrimaryBlue,
|
OutlinedTextFieldDefaults.colors(
|
||||||
focusedTextColor = textColor,
|
focusedBorderColor = PrimaryBlue,
|
||||||
unfocusedTextColor = textColor
|
unfocusedBorderColor =
|
||||||
),
|
if (isDarkTheme) Color(0xFF4A4A4A)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
else Color(0xFFD0D0D0),
|
||||||
shape = RoundedCornerShape(12.dp),
|
focusedLabelColor = PrimaryBlue,
|
||||||
keyboardOptions = KeyboardOptions(
|
cursorColor = PrimaryBlue,
|
||||||
keyboardType = KeyboardType.Password,
|
focusedTextColor = textColor,
|
||||||
imeAction = ImeAction.Next
|
unfocusedTextColor = textColor
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password strength indicator
|
// Password strength indicator
|
||||||
if (password.isNotEmpty()) {
|
if (password.isNotEmpty()) {
|
||||||
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 =
|
||||||
initialOffsetX = { -30 },
|
fadeIn(tween(400, delayMillis = 350)) +
|
||||||
animationSpec = tween(400, delayMillis = 350)
|
slideInHorizontally(
|
||||||
)
|
initialOffsetX = { -30 },
|
||||||
|
animationSpec = tween(400, delayMillis = 350)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val strength = when {
|
val strength =
|
||||||
password.length < 6 -> "Weak"
|
when {
|
||||||
password.length < 10 -> "Medium"
|
password.length < 6 -> "Weak"
|
||||||
else -> "Strong"
|
password.length < 10 -> "Medium"
|
||||||
}
|
else -> "Strong"
|
||||||
val strengthColor = when {
|
}
|
||||||
password.length < 6 -> Color(0xFFE53935)
|
val strengthColor =
|
||||||
password.length < 10 -> Color(0xFFFFA726)
|
when {
|
||||||
else -> Color(0xFF4CAF50)
|
password.length < 6 -> Color(0xFFE53935)
|
||||||
}
|
password.length < 10 -> Color(0xFFFFA726)
|
||||||
|
else -> Color(0xFF4CAF50)
|
||||||
|
}
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Shield,
|
imageVector = Icons.Default.Shield,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = strengthColor,
|
tint = strengthColor,
|
||||||
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 = "Password strength: $strength",
|
text = "Password strength: $strength",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = strengthColor
|
color = strengthColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Warning for weak passwords
|
// Warning for weak passwords
|
||||||
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(
|
||||||
.padding(8.dp),
|
Color(0xFFE53935).copy(alpha = 0.1f)
|
||||||
verticalAlignment = Alignment.Top
|
)
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Warning,
|
imageVector = Icons.Default.Warning,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color(0xFFE53935),
|
tint = Color(0xFFE53935),
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
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 =
|
||||||
fontSize = 11.sp,
|
"Your password is too weak. Consider using at least 6 characters for better security.",
|
||||||
color = Color(0xFFE53935),
|
fontSize = 11.sp,
|
||||||
lineHeight = 14.sp
|
color = Color(0xFFE53935),
|
||||||
|
lineHeight = 14.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,81 +344,97 @@ fun SetPasswordScreen(
|
|||||||
|
|
||||||
// Confirm Password Field
|
// Confirm Password Field
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500, delayMillis = 400)) + slideInVertically(
|
enter =
|
||||||
initialOffsetY = { 50 },
|
fadeIn(tween(500, delayMillis = 400)) +
|
||||||
animationSpec = tween(500, delayMillis = 400)
|
slideInVertically(
|
||||||
)
|
initialOffsetY = { 50 },
|
||||||
|
animationSpec = tween(500, delayMillis = 400)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = confirmPassword,
|
value = confirmPassword,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
confirmPassword = it
|
confirmPassword = it
|
||||||
error = null
|
error = null
|
||||||
},
|
},
|
||||||
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
|
||||||
trailingIcon = {
|
else PasswordVisualTransformation(),
|
||||||
IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) {
|
trailingIcon = {
|
||||||
Icon(
|
IconButton(
|
||||||
imageVector = if (confirmPasswordVisible)
|
onClick = {
|
||||||
Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
confirmPasswordVisible = !confirmPasswordVisible
|
||||||
contentDescription = if (confirmPasswordVisible) "Hide" else "Show"
|
}
|
||||||
)
|
) {
|
||||||
}
|
Icon(
|
||||||
},
|
imageVector =
|
||||||
isError = confirmPassword.isNotEmpty() && !passwordsMatch,
|
if (confirmPasswordVisible)
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
Icons.Default.VisibilityOff
|
||||||
focusedBorderColor = PrimaryBlue,
|
else Icons.Default.Visibility,
|
||||||
unfocusedBorderColor = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0),
|
contentDescription =
|
||||||
focusedLabelColor = PrimaryBlue,
|
if (confirmPasswordVisible) "Hide" else "Show"
|
||||||
cursorColor = PrimaryBlue,
|
)
|
||||||
focusedTextColor = textColor,
|
}
|
||||||
unfocusedTextColor = textColor
|
},
|
||||||
),
|
isError = confirmPassword.isNotEmpty() && !passwordsMatch,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
colors =
|
||||||
shape = RoundedCornerShape(12.dp),
|
OutlinedTextFieldDefaults.colors(
|
||||||
keyboardOptions = KeyboardOptions(
|
focusedBorderColor = PrimaryBlue,
|
||||||
keyboardType = KeyboardType.Password,
|
unfocusedBorderColor =
|
||||||
imeAction = ImeAction.Done
|
if (isDarkTheme) Color(0xFF4A4A4A)
|
||||||
|
else Color(0xFFD0D0D0),
|
||||||
|
focusedLabelColor = PrimaryBlue,
|
||||||
|
cursorColor = PrimaryBlue,
|
||||||
|
focusedTextColor = textColor,
|
||||||
|
unfocusedTextColor = textColor
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match indicator
|
// Match indicator
|
||||||
if (confirmPassword.isNotEmpty()) {
|
if (confirmPassword.isNotEmpty()) {
|
||||||
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 =
|
||||||
initialOffsetX = { -30 },
|
fadeIn(tween(400, delayMillis = 450)) +
|
||||||
animationSpec = tween(400, delayMillis = 450)
|
slideInHorizontally(
|
||||||
)
|
initialOffsetX = { -30 },
|
||||||
|
animationSpec = tween(400, delayMillis = 450)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
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,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = matchColor,
|
tint = matchColor,
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,14 +442,14 @@ fun SetPasswordScreen(
|
|||||||
error?.let { errorMsg ->
|
error?.let { errorMsg ->
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = true,
|
visible = true,
|
||||||
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f)
|
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = errorMsg,
|
text = errorMsg,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = Color(0xFFE53935),
|
color = Color(0xFFE53935),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,42 +458,43 @@ 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 =
|
||||||
initialOffsetY = { 30 },
|
fadeIn(tween(400)) +
|
||||||
animationSpec = tween(400)
|
slideInVertically(
|
||||||
) + scaleIn(
|
initialOffsetY = { 30 },
|
||||||
initialScale = 0.9f,
|
animationSpec = tween(400)
|
||||||
animationSpec = tween(400)
|
) +
|
||||||
),
|
scaleIn(initialScale = 0.9f, animationSpec = tween(400)),
|
||||||
exit = fadeOut(tween(300)) + slideOutVertically(
|
exit =
|
||||||
targetOffsetY = { 30 },
|
fadeOut(tween(300)) +
|
||||||
animationSpec = tween(300)
|
slideOutVertically(
|
||||||
) + scaleOut(
|
targetOffsetY = { 30 },
|
||||||
targetScale = 0.9f,
|
animationSpec = tween(300)
|
||||||
animationSpec = tween(300)
|
) +
|
||||||
)
|
scaleOut(targetScale = 0.9f, 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),
|
||||||
verticalAlignment = Alignment.Top
|
verticalAlignment = Alignment.Top
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Info,
|
imageVector = Icons.Default.Info,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = PrimaryBlue,
|
tint = PrimaryBlue,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
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 =
|
||||||
fontSize = 13.sp,
|
"Your password is never stored or sent anywhere. It's only used to encrypt your keys locally.",
|
||||||
color = secondaryTextColor,
|
fontSize = 13.sp,
|
||||||
lineHeight = 18.sp
|
color = secondaryTextColor,
|
||||||
|
lineHeight = 18.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,94 +503,110 @@ fun SetPasswordScreen(
|
|||||||
|
|
||||||
// Create Account Button
|
// Create Account Button
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500, delayMillis = 600)) + slideInVertically(
|
enter =
|
||||||
initialOffsetY = { 50 },
|
fadeIn(tween(500, delayMillis = 600)) +
|
||||||
animationSpec = tween(500, delayMillis = 600)
|
slideInVertically(
|
||||||
)
|
initialOffsetY = { 50 },
|
||||||
|
animationSpec = tween(500, delayMillis = 600)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!passwordsMatch) {
|
if (!passwordsMatch) {
|
||||||
error = "Passwords don't match"
|
error = "Passwords don't match"
|
||||||
return@Button
|
return@Button
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreating = true
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
// Generate keys from seed phrase
|
||||||
|
val keyPair =
|
||||||
|
CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
||||||
|
|
||||||
|
// Encrypt private key and seed phrase
|
||||||
|
val encryptedPrivateKey =
|
||||||
|
CryptoManager.encryptWithPassword(
|
||||||
|
keyPair.privateKey,
|
||||||
|
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(
|
||||||
|
publicKey = keyPair.publicKey,
|
||||||
|
encryptedPrivateKey = encryptedPrivateKey,
|
||||||
|
encryptedSeedPhrase = encryptedSeedPhrase,
|
||||||
|
name = truncatedKey
|
||||||
|
)
|
||||||
|
|
||||||
|
accountManager.saveAccount(account)
|
||||||
|
accountManager.setCurrentAccount(keyPair.publicKey)
|
||||||
|
|
||||||
|
// 🔌 Connect to server and authenticate
|
||||||
|
val privateKeyHash =
|
||||||
|
CryptoManager.generatePrivateKeyHash(
|
||||||
|
keyPair.privateKey
|
||||||
|
)
|
||||||
|
ProtocolManager.connect()
|
||||||
|
// Give WebSocket time to connect before authenticating
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
ProtocolManager.authenticate(
|
||||||
|
keyPair.publicKey,
|
||||||
|
privateKeyHash
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create DecryptedAccount to pass to callback
|
||||||
|
val decryptedAccount =
|
||||||
|
DecryptedAccount(
|
||||||
|
publicKey = keyPair.publicKey,
|
||||||
|
privateKey = keyPair.privateKey,
|
||||||
|
seedPhrase = seedPhrase,
|
||||||
|
privateKeyHash = privateKeyHash,
|
||||||
|
name = truncatedKey
|
||||||
|
)
|
||||||
|
|
||||||
|
onAccountCreated(decryptedAccount)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error = "Failed to create account: ${e.message}"
|
||||||
|
isCreating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = canContinue,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = PrimaryBlue,
|
||||||
|
contentColor = Color.White,
|
||||||
|
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
|
||||||
|
disabledContentColor = Color.White.copy(alpha = 0.5f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
if (isCreating) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
color = Color.White,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "Create Account",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
isCreating = true
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
// Generate keys from seed phrase
|
|
||||||
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
|
||||||
|
|
||||||
// Encrypt private key and seed phrase
|
|
||||||
val encryptedPrivateKey = CryptoManager.encryptWithPassword(
|
|
||||||
keyPair.privateKey, 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(
|
|
||||||
publicKey = keyPair.publicKey,
|
|
||||||
encryptedPrivateKey = encryptedPrivateKey,
|
|
||||||
encryptedSeedPhrase = encryptedSeedPhrase,
|
|
||||||
name = truncatedKey
|
|
||||||
)
|
|
||||||
|
|
||||||
accountManager.saveAccount(account)
|
|
||||||
accountManager.setCurrentAccount(keyPair.publicKey)
|
|
||||||
|
|
||||||
// 🔌 Connect to server and authenticate
|
|
||||||
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
|
|
||||||
ProtocolManager.connect()
|
|
||||||
// Give WebSocket time to connect before authenticating
|
|
||||||
kotlinx.coroutines.delay(500)
|
|
||||||
ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash)
|
|
||||||
|
|
||||||
// Create DecryptedAccount to pass to callback
|
|
||||||
val decryptedAccount = DecryptedAccount(
|
|
||||||
publicKey = keyPair.publicKey,
|
|
||||||
privateKey = keyPair.privateKey,
|
|
||||||
seedPhrase = seedPhrase,
|
|
||||||
privateKeyHash = privateKeyHash,
|
|
||||||
name = truncatedKey
|
|
||||||
)
|
|
||||||
|
|
||||||
onAccountCreated(decryptedAccount)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
error = "Failed to create account: ${e.message}"
|
|
||||||
isCreating = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = canContinue,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(56.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = PrimaryBlue,
|
|
||||||
contentColor = Color.White,
|
|
||||||
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
|
|
||||||
disabledContentColor = Color.White.copy(alpha = 0.5f)
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
if (isCreating) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
color = Color.White,
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "Create Account",
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} }
|
}
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||||
@@ -34,16 +31,29 @@ val AuthSurfaceLight = Color(0xFFF5F5F5)
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WelcomeScreen(
|
fun WelcomeScreen(
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
hasExistingAccount: Boolean = false,
|
hasExistingAccount: Boolean = false,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
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,61 +63,48 @@ 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"))
|
||||||
composition = lockComposition,
|
val lockProgress by
|
||||||
iterations = 1, // Play once
|
animateLottieCompositionAsState(
|
||||||
speed = 1f
|
composition = lockComposition,
|
||||||
)
|
iterations = 1, // Play once
|
||||||
|
speed = 1f
|
||||||
|
)
|
||||||
|
|
||||||
// Entry animation
|
// Entry animation
|
||||||
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",
|
||||||
tint = textColor.copy(alpha = 0.6f)
|
tint = textColor.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp).statusBarsPadding(),
|
||||||
.fillMaxSize()
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
.padding(horizontal = 24.dp)
|
|
||||||
.statusBarsPadding(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.weight(0.15f))
|
Spacer(modifier = Modifier.weight(0.15f))
|
||||||
|
|
||||||
// Animated Lock Icon
|
// Animated Lock Icon
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
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,
|
||||||
progress = { lockProgress },
|
progress = { lockProgress },
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,19 +114,21 @@ fun WelcomeScreen(
|
|||||||
|
|
||||||
// Title
|
// Title
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(600, delayMillis = 200)) + slideInVertically(
|
enter =
|
||||||
initialOffsetY = { 50 },
|
fadeIn(tween(600, delayMillis = 200)) +
|
||||||
animationSpec = tween(600, delayMillis = 200)
|
slideInVertically(
|
||||||
)
|
initialOffsetY = { 50 },
|
||||||
|
animationSpec = tween(600, delayMillis = 200)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Your Keys,\nYour Messages",
|
text = "Your Keys,\nYour Messages",
|
||||||
fontSize = 32.sp,
|
fontSize = 32.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
lineHeight = 40.sp
|
lineHeight = 40.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,50 +136,49 @@ fun WelcomeScreen(
|
|||||||
|
|
||||||
// Subtitle
|
// Subtitle
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(600, delayMillis = 300)) + slideInVertically(
|
enter =
|
||||||
initialOffsetY = { 50 },
|
fadeIn(tween(600, delayMillis = 300)) +
|
||||||
animationSpec = tween(600, delayMillis = 300)
|
slideInVertically(
|
||||||
)
|
initialOffsetY = { 50 },
|
||||||
|
animationSpec = tween(600, delayMillis = 300)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Secure messaging with\ncryptographic keys",
|
text = "Secure messaging with\ncryptographic keys",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = secondaryTextColor,
|
color = secondaryTextColor,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
) {
|
) {
|
||||||
CompactFeatureItem(
|
CompactFeatureItem(
|
||||||
icon = Icons.Default.Security,
|
icon = Icons.Default.Security,
|
||||||
text = "Encrypted",
|
text = "Encrypted",
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
textColor = textColor
|
textColor = textColor
|
||||||
)
|
)
|
||||||
CompactFeatureItem(
|
CompactFeatureItem(
|
||||||
icon = Icons.Default.NoAccounts,
|
icon = Icons.Default.NoAccounts,
|
||||||
text = "No Phone",
|
text = "No Phone",
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
textColor = textColor
|
textColor = textColor
|
||||||
)
|
)
|
||||||
CompactFeatureItem(
|
CompactFeatureItem(
|
||||||
icon = Icons.Default.Key,
|
icon = Icons.Default.Key,
|
||||||
text = "Your Keys",
|
text = "Your Keys",
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
textColor = textColor
|
textColor = textColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,37 +187,39 @@ fun WelcomeScreen(
|
|||||||
|
|
||||||
// Create Seed Button
|
// Create Seed Button
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically(
|
enter =
|
||||||
initialOffsetY = { 100 },
|
fadeIn(tween(600, delayMillis = 500)) +
|
||||||
animationSpec = tween(600, delayMillis = 500)
|
slideInVertically(
|
||||||
)
|
initialOffsetY = { 100 },
|
||||||
|
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 =
|
||||||
elevation = ButtonDefaults.buttonElevation(
|
ButtonDefaults.buttonElevation(
|
||||||
defaultElevation = 0.dp,
|
defaultElevation = 0.dp,
|
||||||
pressedElevation = 0.dp
|
pressedElevation = 0.dp
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Key,
|
imageVector = Icons.Default.Key,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Generate New Seed Phrase",
|
text = "Generate New Seed Phrase",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,31 +228,31 @@ fun WelcomeScreen(
|
|||||||
|
|
||||||
// Import Seed Button
|
// Import Seed Button
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically(
|
enter =
|
||||||
initialOffsetY = { 100 },
|
fadeIn(tween(600, delayMillis = 600)) +
|
||||||
animationSpec = tween(600, delayMillis = 600)
|
slideInVertically(
|
||||||
)
|
initialOffsetY = { 100 },
|
||||||
|
animationSpec = tween(600, delayMillis = 600)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onImportSeed,
|
onClick = onImportSeed,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
.fillMaxWidth()
|
shape = RoundedCornerShape(16.dp)
|
||||||
.height(56.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Download,
|
imageVector = Icons.Default.Download,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
tint = PrimaryBlue
|
tint = PrimaryBlue
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "I Already Have a Seed Phrase",
|
text = "I Already Have a Seed Phrase",
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = PrimaryBlue
|
color = PrimaryBlue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,70 +264,62 @@ fun WelcomeScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CompactFeatureItem(
|
private fun CompactFeatureItem(
|
||||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
text: String,
|
text: String,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
textColor: Color
|
textColor: Color
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
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
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = PrimaryBlue,
|
tint = PrimaryBlue,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
color = textColor.copy(alpha = 0.8f),
|
color = textColor.copy(alpha = 0.8f),
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FeatureItem(
|
private fun FeatureItem(
|
||||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
text: String,
|
text: String,
|
||||||
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
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = PrimaryBlue,
|
tint = PrimaryBlue,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -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 сообщений
|
||||||
@@ -38,15 +36,13 @@ import java.util.*
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ForwardChatPickerBottomSheet(
|
fun ForwardChatPickerBottomSheet(
|
||||||
dialogs: List<DialogUiModel>,
|
dialogs: List<DialogUiModel>,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
currentUserPublicKey: String,
|
currentUserPublicKey: String,
|
||||||
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
|
||||||
@@ -66,67 +62,60 @@ fun ForwardChatPickerBottomSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { dismissWithAnimation() },
|
onDismissRequest = { dismissWithAnimation() },
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
containerColor = backgroundColor,
|
containerColor = backgroundColor,
|
||||||
dragHandle = {
|
dragHandle = {
|
||||||
// Кастомный handle
|
// Кастомный handle
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
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))
|
||||||
},
|
}
|
||||||
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()
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
.padding(horizontal = 16.dp),
|
verticalAlignment = Alignment.CenterVertically
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
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 {
|
||||||
Text(
|
Text(
|
||||||
text = "Forward to",
|
text = "Forward to",
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "$messagesCount message${if (messagesCount > 1) "s" else ""} selected",
|
text =
|
||||||
fontSize = 14.sp,
|
"$messagesCount message${if (messagesCount > 1) "s" else ""} selected",
|
||||||
color = secondaryTextColor
|
fontSize = 14.sp,
|
||||||
|
color = secondaryTextColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,68 +123,62 @@ fun ForwardChatPickerBottomSheet(
|
|||||||
// Кнопка закрытия с анимацией
|
// Кнопка закрытия с анимацией
|
||||||
IconButton(onClick = { dismissWithAnimation() }) {
|
IconButton(onClick = { dismissWithAnimation() }) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Close,
|
Icons.Default.Close,
|
||||||
contentDescription = "Close",
|
contentDescription = "Close",
|
||||||
tint = secondaryTextColor.copy(alpha = 0.6f)
|
tint = secondaryTextColor.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
contentAlignment = Alignment.Center
|
||||||
.height(200.dp),
|
|
||||||
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,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = secondaryTextColor
|
color = secondaryTextColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Start a conversation first",
|
text = "Start a conversation first",
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = secondaryTextColor.copy(alpha = 0.7f)
|
color = secondaryTextColor.copy(alpha = 0.7f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Сепаратор между диалогами
|
// Сепаратор между диалогами
|
||||||
if (dialog != dialogs.last()) {
|
if (dialog != dialogs.last()) {
|
||||||
Divider(
|
Divider(
|
||||||
modifier = Modifier.padding(start = 76.dp),
|
modifier = Modifier.padding(start = 76.dp),
|
||||||
color = dividerColor,
|
color = dividerColor,
|
||||||
thickness = 0.5.dp
|
thickness = 0.5.dp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,105 +191,101 @@ fun ForwardChatPickerBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Элемент диалога в списке выбора для Forward */
|
||||||
* Элемент диалога в списке выбора для Forward
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ForwardDialogItem(
|
private fun ForwardDialogItem(
|
||||||
dialog: DialogUiModel,
|
dialog: DialogUiModel,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
isSavedMessages: Boolean = false,
|
isSavedMessages: Boolean = false,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
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 =
|
||||||
getAvatarColor(dialog.opponentKey, isDarkTheme)
|
remember(dialog.opponentKey, isDarkTheme) {
|
||||||
}
|
getAvatarColor(dialog.opponentKey, isDarkTheme)
|
||||||
|
}
|
||||||
val displayName = remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
|
|
||||||
when {
|
val displayName =
|
||||||
isSavedMessages -> "Saved Messages"
|
remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
|
||||||
dialog.opponentTitle.isNotEmpty() -> dialog.opponentTitle
|
when {
|
||||||
else -> dialog.opponentKey.take(8)
|
isSavedMessages -> "Saved Messages"
|
||||||
}
|
dialog.opponentTitle.isNotEmpty() -> dialog.opponentTitle
|
||||||
}
|
else -> dialog.opponentKey.take(8)
|
||||||
|
}
|
||||||
val initials = remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
|
}
|
||||||
when {
|
|
||||||
isSavedMessages -> "📁"
|
val initials =
|
||||||
dialog.opponentTitle.isNotEmpty() -> {
|
remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) {
|
||||||
dialog.opponentTitle
|
when {
|
||||||
.split(" ")
|
isSavedMessages -> "📁"
|
||||||
.take(2)
|
dialog.opponentTitle.isNotEmpty() -> {
|
||||||
.mapNotNull { it.firstOrNull()?.uppercase() }
|
dialog.opponentTitle
|
||||||
.joinToString("")
|
.split(" ")
|
||||||
|
.take(2)
|
||||||
|
.mapNotNull { it.firstOrNull()?.uppercase() }
|
||||||
|
.joinToString("")
|
||||||
|
}
|
||||||
|
else -> dialog.opponentKey.take(2).uppercase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> dialog.opponentKey.take(2).uppercase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
else avatarColors.backgroundColor
|
else avatarColors.backgroundColor
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = initials,
|
text = initials,
|
||||||
color = if (isSavedMessages) PrimaryBlue else avatarColors.textColor,
|
color = if (isSavedMessages) PrimaryBlue else avatarColors.textColor,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|
||||||
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 =
|
||||||
fontSize = 14.sp,
|
if (isSavedMessages) "Your personal notes"
|
||||||
color = secondaryTextColor,
|
else dialog.lastMessage.ifEmpty { "No messages" },
|
||||||
maxLines = 1,
|
fontSize = 14.sp,
|
||||||
overflow = TextOverflow.Ellipsis
|
color = secondaryTextColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,19 +36,16 @@ 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(
|
||||||
privateKeyHash: String,
|
privateKeyHash: String,
|
||||||
currentUserPublicKey: String,
|
currentUserPublicKey: String,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
protocolState: ProtocolState,
|
protocolState: ProtocolState,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onUserSelect: (SearchUser) -> Unit
|
onUserSelect: (SearchUser) -> Unit
|
||||||
) {
|
) {
|
||||||
// Context и View для мгновенного закрытия клавиатуры
|
// Context и View для мгновенного закрытия клавиатуры
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -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) {
|
||||||
@@ -104,192 +102,185 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
// Кастомный header с полем ввода на всю ширину
|
// Кастомный header с полем ввода на всю ширину
|
||||||
Surface(
|
Surface(modifier = Modifier.fillMaxWidth(), color = backgroundColor) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Row(
|
||||||
color = backgroundColor
|
modifier =
|
||||||
) {
|
Modifier.fillMaxWidth()
|
||||||
Row(
|
.statusBarsPadding()
|
||||||
modifier = Modifier
|
.height(64.dp)
|
||||||
.fillMaxWidth()
|
.padding(horizontal = 4.dp),
|
||||||
.statusBarsPadding()
|
verticalAlignment = Alignment.CenterVertically
|
||||||
.height(64.dp)
|
|
||||||
.padding(horizontal = 4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// Кнопка назад - с мгновенным закрытием клавиатуры
|
|
||||||
IconButton(onClick = {
|
|
||||||
hideKeyboardInstantly()
|
|
||||||
onBackClick()
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.ArrowBack,
|
|
||||||
contentDescription = "Back",
|
|
||||||
tint = textColor.copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Поле ввода на всю оставшуюся ширину
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
TextField(
|
// Кнопка назад - с мгновенным закрытием клавиатуры
|
||||||
value = searchQuery,
|
IconButton(
|
||||||
onValueChange = { searchViewModel.onSearchQueryChange(it) },
|
onClick = {
|
||||||
placeholder = {
|
hideKeyboardInstantly()
|
||||||
Text(
|
onBackClick()
|
||||||
"Search users...",
|
}
|
||||||
color = secondaryTextColor.copy(alpha = 0.7f),
|
) {
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = Color.Transparent,
|
|
||||||
unfocusedContainerColor = Color.Transparent,
|
|
||||||
focusedTextColor = textColor,
|
|
||||||
unfocusedTextColor = textColor,
|
|
||||||
cursorColor = PrimaryBlue,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent
|
|
||||||
),
|
|
||||||
textStyle = androidx.compose.ui.text.TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Normal
|
|
||||||
),
|
|
||||||
singleLine = true,
|
|
||||||
enabled = protocolState == ProtocolState.AUTHENTICATED,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Подчеркивание
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(2.dp)
|
|
||||||
.background(
|
|
||||||
PrimaryBlue.copy(alpha = 0.8f),
|
|
||||||
RoundedCornerShape(1.dp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Кнопка очистки
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = searchQuery.isNotEmpty(),
|
|
||||||
enter = fadeIn(tween(150)) + scaleIn(tween(150)),
|
|
||||||
exit = fadeOut(tween(150)) + scaleOut(tween(150))
|
|
||||||
) {
|
|
||||||
IconButton(onClick = { searchViewModel.clearSearchQuery() }) {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Clear,
|
Icons.Default.ArrowBack,
|
||||||
contentDescription = "Clear",
|
contentDescription = "Back",
|
||||||
tint = secondaryTextColor.copy(alpha = 0.6f)
|
tint = textColor.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Поле ввода на всю оставшуюся ширину
|
||||||
|
Box(modifier = Modifier.weight(1f).padding(end = 8.dp)) {
|
||||||
|
TextField(
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = { searchViewModel.onSearchQueryChange(it) },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
"Search users...",
|
||||||
|
color = secondaryTextColor.copy(alpha = 0.7f),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors =
|
||||||
|
TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
focusedTextColor = textColor,
|
||||||
|
unfocusedTextColor = textColor,
|
||||||
|
cursorColor = PrimaryBlue,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent
|
||||||
|
),
|
||||||
|
textStyle =
|
||||||
|
androidx.compose.ui.text.TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
enabled = protocolState == ProtocolState.AUTHENTICATED,
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth().focusRequester(focusRequester)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Подчеркивание
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(2.dp)
|
||||||
|
.background(
|
||||||
|
PrimaryBlue.copy(alpha = 0.8f),
|
||||||
|
RoundedCornerShape(1.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка очистки
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = searchQuery.isNotEmpty(),
|
||||||
|
enter = fadeIn(tween(150)) + scaleIn(tween(150)),
|
||||||
|
exit = fadeOut(tween(150)) + scaleOut(tween(150))
|
||||||
|
) {
|
||||||
|
IconButton(onClick = { searchViewModel.clearSearchQuery() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Clear,
|
||||||
|
contentDescription = "Clear",
|
||||||
|
tint = secondaryTextColor.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
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(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(vertical = 8.dp)
|
contentPadding = PaddingValues(vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
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
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Recent",
|
"Recent",
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(recentUsers, key = { it.publicKey }) { user ->
|
items(recentUsers, key = { it.publicKey }) { user ->
|
||||||
RecentUserItem(
|
RecentUserItem(
|
||||||
user = user,
|
user = user,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
secondaryTextColor = secondaryTextColor,
|
secondaryTextColor = secondaryTextColor,
|
||||||
onClick = {
|
onClick = {
|
||||||
hideKeyboardInstantly()
|
hideKeyboardInstantly()
|
||||||
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 =
|
||||||
query.equals(currentUserPublicKey, ignoreCase = true) ||
|
searchQuery.trim().let { query ->
|
||||||
query.equals(currentUserPublicKey.take(8), ignoreCase = true) ||
|
query.equals(currentUserPublicKey, ignoreCase = true) ||
|
||||||
query.equals(currentUserPublicKey.takeLast(8), ignoreCase = true)
|
query.equals(currentUserPublicKey.take(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 =
|
||||||
listOf(
|
if (isSavedMessagesSearch &&
|
||||||
SearchUser(
|
searchResults.none { it.publicKey == currentUserPublicKey }
|
||||||
title = "Saved Messages",
|
) {
|
||||||
username = "",
|
listOf(
|
||||||
publicKey = currentUserPublicKey,
|
SearchUser(
|
||||||
verified = 0,
|
title = "Saved Messages",
|
||||||
online = 1
|
username = "",
|
||||||
)
|
publicKey = currentUserPublicKey,
|
||||||
) + searchResults.filter { it.publicKey != currentUserPublicKey }
|
verified = 0,
|
||||||
} else {
|
online = 1
|
||||||
searchResults
|
)
|
||||||
}
|
) + searchResults.filter { it.publicKey != currentUserPublicKey }
|
||||||
|
} else {
|
||||||
|
searchResults
|
||||||
|
}
|
||||||
|
|
||||||
SearchResultsList(
|
SearchResultsList(
|
||||||
searchResults = resultsWithSavedMessages,
|
searchResults = resultsWithSavedMessages,
|
||||||
isSearching = isSearching,
|
isSearching = isSearching,
|
||||||
currentUserPublicKey = currentUserPublicKey,
|
currentUserPublicKey = currentUserPublicKey,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
preloadedComposition = searchLottieComposition,
|
preloadedComposition = searchLottieComposition,
|
||||||
onUserClick = { user ->
|
onUserClick = { user ->
|
||||||
// Мгновенно закрываем клавиатуру
|
// Мгновенно закрываем клавиатуру
|
||||||
hideKeyboardInstantly()
|
hideKeyboardInstantly()
|
||||||
// Сохраняем пользователя в историю (кроме Saved Messages)
|
// Сохраняем пользователя в историю (кроме Saved Messages)
|
||||||
if (user.publicKey != currentUserPublicKey) {
|
if (user.publicKey != currentUserPublicKey) {
|
||||||
RecentSearchesManager.addUser(user)
|
RecentSearchesManager.addUser(user)
|
||||||
|
}
|
||||||
|
onUserSelect(user)
|
||||||
}
|
}
|
||||||
onUserSelect(user)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,18 +289,15 @@ fun SearchScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RecentUserItem(
|
private fun RecentUserItem(
|
||||||
user: SearchUser,
|
user: SearchUser,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
textColor: Color,
|
textColor: Color,
|
||||||
secondaryTextColor: Color,
|
secondaryTextColor: Color,
|
||||||
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,25 +305,25 @@ 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
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = initials,
|
text = initials,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = avatarColors.textColor
|
color = avatarColors.textColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,44 +333,41 @@ private fun RecentUserItem(
|
|||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = displayName,
|
text = displayName,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
if (user.verified != 0) {
|
if (user.verified != 0) {
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Verified,
|
Icons.Default.Verified,
|
||||||
contentDescription = "Verified",
|
contentDescription = "Verified",
|
||||||
tint = PrimaryBlue,
|
tint = PrimaryBlue,
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (user.username.isNotEmpty()) {
|
if (user.username.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = "@${user.username}",
|
text = "@${user.username}",
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = secondaryTextColor,
|
color = secondaryTextColor,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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",
|
||||||
tint = secondaryTextColor.copy(alpha = 0.6f),
|
tint = secondaryTextColor.copy(alpha = 0.6f),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user