feat: Enhance packet sending logic to handle connection issues and implement swipe-to-reply functionality in chat UI
This commit is contained in:
@@ -267,14 +267,24 @@ class Protocol(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send packet to server
|
* Send packet to server
|
||||||
* Packets are queued if handshake is not complete
|
* Packets are queued if handshake is not complete or if connection is down
|
||||||
|
* (как в Архиве - сохраняем пакеты при любых проблемах с соединением)
|
||||||
*/
|
*/
|
||||||
fun sendPacket(packet: Packet) {
|
fun sendPacket(packet: Packet) {
|
||||||
if (!handshakeComplete && packet !is PacketHandshake) {
|
// Проверяем состояние соединения
|
||||||
log("📦 Queueing packet: ${packet.getPacketId()}")
|
val socket = webSocket
|
||||||
|
val isConnected = _state.value == ProtocolState.AUTHENTICATED
|
||||||
|
|
||||||
|
// Добавляем в очередь если:
|
||||||
|
// 1. Handshake не завершён (кроме самого пакета handshake)
|
||||||
|
// 2. WebSocket не подключен или null
|
||||||
|
// 3. Не authenticated
|
||||||
|
if ((!handshakeComplete && packet !is PacketHandshake) || socket == null || !isConnected) {
|
||||||
|
log("📦 Queueing packet: ${packet.getPacketId()} (handshake=$handshakeComplete, socket=${socket != null}, state=${_state.value})")
|
||||||
packetQueue.add(packet)
|
packetQueue.add(packet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sendPacketDirect(packet)
|
sendPacketDirect(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +300,9 @@ class Protocol(
|
|||||||
|
|
||||||
val socket = webSocket
|
val socket = webSocket
|
||||||
if (socket == null) {
|
if (socket == null) {
|
||||||
log("❌ WebSocket is null, cannot send packet ${packet.getPacketId()}")
|
log("❌ WebSocket is null, re-queueing packet ${packet.getPacketId()}")
|
||||||
|
// Как в Архиве - возвращаем пакет в очередь при ошибке
|
||||||
|
packetQueue.add(packet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +315,9 @@ class Protocol(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log("❌ Exception sending packet ${packet.getPacketId()}: ${e.message}")
|
log("❌ Exception sending packet ${packet.getPacketId()}: ${e.message}")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
// Как в Архиве - возвращаем пакет в очередь при ошибке отправки
|
||||||
|
log("📦 Re-queueing packet ${packet.getPacketId()} due to send error")
|
||||||
|
packetQueue.add(packet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
@@ -44,6 +48,9 @@ import androidx.compose.ui.graphics.vector.path
|
|||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
@@ -230,14 +237,18 @@ fun ChatDetailScreen(
|
|||||||
val replyMessages by viewModel.replyMessages.collectAsState()
|
val replyMessages by viewModel.replyMessages.collectAsState()
|
||||||
val hasReply = replyMessages.isNotEmpty()
|
val hasReply = replyMessages.isNotEmpty()
|
||||||
|
|
||||||
|
// 🔥 FocusRequester для автофокуса на инпут при reply
|
||||||
|
val inputFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
// 🔥 Дополнительная высота для reply панели (~50dp)
|
// 🔥 Дополнительная высота для reply панели (~50dp)
|
||||||
val replyPanelHeight = if (hasReply) 50.dp else 0.dp
|
val replyPanelHeight = if (hasReply) 50.dp else 0.dp
|
||||||
|
|
||||||
// Динамический bottom padding для списка: инпут (~70dp) + reply (~50dp) + клавиатура/эмодзи
|
// Динамический bottom padding для списка: инпут (~70dp) + reply (~50dp) + клавиатура/эмодзи
|
||||||
|
// Одинаковый базовый отступ 70.dp для всех состояний
|
||||||
val listBottomPadding = when {
|
val listBottomPadding = when {
|
||||||
isKeyboardVisible -> 70.dp + replyPanelHeight + imeHeight
|
isKeyboardVisible -> 70.dp + replyPanelHeight + imeHeight
|
||||||
showEmojiPicker -> 70.dp + replyPanelHeight + emojiPanelHeight
|
showEmojiPicker -> 70.dp + replyPanelHeight + emojiPanelHeight
|
||||||
else -> 100.dp + replyPanelHeight
|
else -> 70.dp + replyPanelHeight // Было 100.dp, теперь одинаково для всех состояний
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telegram-style scroll tracking
|
// Telegram-style scroll tracking
|
||||||
@@ -861,6 +872,10 @@ fun ChatDetailScreen(
|
|||||||
selectedMessages + selectionKey
|
selectedMessages + selectionKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onSwipeToReply = {
|
||||||
|
// 🔥 Swipe-to-reply: добавляем это сообщение в reply
|
||||||
|
viewModel.setReplyMessages(listOf(message))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -994,7 +1009,9 @@ fun ChatDetailScreen(
|
|||||||
isBlocked = isBlocked,
|
isBlocked = isBlocked,
|
||||||
// Emoji picker state (поднят для KeyboardAvoidingView)
|
// Emoji picker state (поднят для KeyboardAvoidingView)
|
||||||
showEmojiPicker = showEmojiPicker,
|
showEmojiPicker = showEmojiPicker,
|
||||||
onToggleEmojiPicker = { showEmojiPicker = it }
|
onToggleEmojiPicker = { showEmojiPicker = it },
|
||||||
|
// Focus requester для автофокуса при reply
|
||||||
|
focusRequester = inputFocusRequester
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1007,7 +1024,7 @@ fun ChatDetailScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.imePadding()
|
.windowInsetsPadding(WindowInsets.ime.only(WindowInsetsSides.Bottom))
|
||||||
) {
|
) {
|
||||||
// Плоский контейнер как у инпута
|
// Плоский контейнер как у инпута
|
||||||
Column(
|
Column(
|
||||||
@@ -1360,8 +1377,24 @@ private fun MessageBubble(
|
|||||||
showTail: Boolean = true,
|
showTail: Boolean = true,
|
||||||
isSelected: Boolean = false,
|
isSelected: Boolean = false,
|
||||||
onLongClick: () -> Unit = {},
|
onLongClick: () -> Unit = {},
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {},
|
||||||
|
onSwipeToReply: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
// 🔥 Swipe-to-reply state (как в Telegram)
|
||||||
|
var swipeOffset by remember { mutableStateOf(0f) }
|
||||||
|
val swipeThreshold = 80f // dp порог для активации reply
|
||||||
|
val maxSwipe = 120f // Максимальный сдвиг
|
||||||
|
|
||||||
|
// Анимация возврата
|
||||||
|
val animatedOffset by animateFloatAsState(
|
||||||
|
targetValue = swipeOffset,
|
||||||
|
animationSpec = spring(dampingRatio = 0.6f, stiffness = 400f),
|
||||||
|
label = "swipeOffset"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Прогресс свайпа для иконки reply (0..1) - используем абсолютное значение т.к. свайп влево
|
||||||
|
val swipeProgress = (kotlin.math.abs(animatedOffset) / swipeThreshold).coerceIn(0f, 1f)
|
||||||
|
|
||||||
// ❌ УБРАЛИ: Telegram-style enter animation - она мешает при скролле
|
// ❌ УБРАЛИ: Telegram-style enter animation - она мешает при скролле
|
||||||
// val (alpha, translationY) = rememberMessageEnterAnimation(message.id)
|
// val (alpha, translationY) = rememberMessageEnterAnimation(message.id)
|
||||||
|
|
||||||
@@ -1407,10 +1440,69 @@ private fun MessageBubble(
|
|||||||
|
|
||||||
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
||||||
|
|
||||||
|
// 🔥 Swipe-to-reply wrapper
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectHorizontalDragGestures(
|
||||||
|
onDragEnd = {
|
||||||
|
// Если свайп достиг порога - активируем reply
|
||||||
|
if (swipeOffset <= -swipeThreshold) {
|
||||||
|
onSwipeToReply()
|
||||||
|
}
|
||||||
|
// Возвращаем на место
|
||||||
|
swipeOffset = 0f
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
swipeOffset = 0f
|
||||||
|
},
|
||||||
|
onHorizontalDrag = { _, dragAmount ->
|
||||||
|
// Только свайп влево (отрицательный dragAmount)
|
||||||
|
val newOffset = swipeOffset + dragAmount
|
||||||
|
swipeOffset = newOffset.coerceIn(-maxSwipe, 0f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// 🔥 Reply icon (появляется справа при свайпе влево)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = swipeProgress
|
||||||
|
scaleX = swipeProgress
|
||||||
|
scaleY = swipeProgress
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
if (swipeProgress >= 1f) PrimaryBlue
|
||||||
|
else if (isDarkTheme) Color(0xFF3A3A3A)
|
||||||
|
else Color(0xFFE0E0E0)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Reply,
|
||||||
|
contentDescription = "Reply",
|
||||||
|
tint = if (swipeProgress >= 1f) Color.White
|
||||||
|
else if (isDarkTheme) Color.White.copy(alpha = 0.7f)
|
||||||
|
else Color(0xFF666666),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 1.dp)
|
.padding(horizontal = 8.dp, vertical = 1.dp)
|
||||||
|
.offset { IntOffset(animatedOffset.toInt(), 0) }
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
// ❌ УБРАЛИ: alpha = alpha * selectionAlpha и translationY
|
// ❌ УБРАЛИ: alpha = alpha * selectionAlpha и translationY
|
||||||
// Оставляем только selection анимацию
|
// Оставляем только selection анимацию
|
||||||
@@ -1449,8 +1541,6 @@ private fun MessageBubble(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick
|
onLongClick = onLongClick
|
||||||
)
|
)
|
||||||
// Тень только для исходящих
|
|
||||||
.then(if (message.isOutgoing) Modifier.shadow(elevation = 1.dp, shape = bubbleShape, clip = false) else Modifier)
|
|
||||||
.clip(bubbleShape)
|
.clip(bubbleShape)
|
||||||
.background(bubbleColor)
|
.background(bubbleColor)
|
||||||
.padding(horizontal = 12.dp, vertical = 7.dp)
|
.padding(horizontal = 12.dp, vertical = 7.dp)
|
||||||
@@ -1488,6 +1578,7 @@ private fun MessageBubble(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // End of swipe Box wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1691,7 +1782,9 @@ private fun MessageInputBar(
|
|||||||
isBlocked: Boolean = false,
|
isBlocked: Boolean = false,
|
||||||
// Emoji picker state (поднят для KeyboardAvoidingView)
|
// Emoji picker state (поднят для KeyboardAvoidingView)
|
||||||
showEmojiPicker: Boolean = false,
|
showEmojiPicker: Boolean = false,
|
||||||
onToggleEmojiPicker: (Boolean) -> Unit = {}
|
onToggleEmojiPicker: (Boolean) -> Unit = {},
|
||||||
|
// Focus requester для автофокуса при reply
|
||||||
|
focusRequester: FocusRequester? = null
|
||||||
) {
|
) {
|
||||||
val hasReply = replyMessages.isNotEmpty()
|
val hasReply = replyMessages.isNotEmpty()
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
@@ -1704,6 +1797,23 @@ private fun MessageInputBar(
|
|||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
// 🔥 Ссылка на EditText для программного фокуса
|
||||||
|
var editTextView by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiEditTextView?>(null) }
|
||||||
|
|
||||||
|
// 🔥 Автофокус при открытии reply панели
|
||||||
|
LaunchedEffect(hasReply, editTextView) {
|
||||||
|
if (hasReply) {
|
||||||
|
// Даём время на создание view если ещё null
|
||||||
|
kotlinx.coroutines.delay(50)
|
||||||
|
editTextView?.let { editText ->
|
||||||
|
editText.requestFocus()
|
||||||
|
// Открываем клавиатуру
|
||||||
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔥 Отслеживаем высоту клавиатуры (Telegram-style)
|
// 🔥 Отслеживаем высоту клавиатуры (Telegram-style)
|
||||||
val imeInsets = WindowInsets.ime
|
val imeInsets = WindowInsets.ime
|
||||||
val imeHeight = with(density) { imeInsets.getBottom(density).toDp() }
|
val imeHeight = with(density) { imeInsets.getBottom(density).toDp() }
|
||||||
@@ -1739,13 +1849,10 @@ private fun MessageInputBar(
|
|||||||
// Закрываем клавиатуру через IMM
|
// Закрываем клавиатуру через IMM
|
||||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
// Небольшая задержка перед показом эмодзи для плавного перехода
|
// Показываем эмодзи сразу без задержки (эмодзи уже предзагружены)
|
||||||
scope.launch {
|
|
||||||
delay(50)
|
|
||||||
onToggleEmojiPicker(true)
|
onToggleEmojiPicker(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Функция отправки - НЕ закрывает клавиатуру (UX правило #6)
|
// Функция отправки - НЕ закрывает клавиатуру (UX правило #6)
|
||||||
fun handleSend() {
|
fun handleSend() {
|
||||||
@@ -1760,7 +1867,7 @@ private fun MessageInputBar(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.imePadding()
|
.windowInsetsPadding(WindowInsets.ime.only(WindowInsetsSides.Bottom))
|
||||||
) {
|
) {
|
||||||
// Если пользователь заблокирован - показываем BlockedChatFooter (плоский как инпут)
|
// Если пользователь заблокирован - показываем BlockedChatFooter (плоский как инпут)
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
@@ -1992,8 +2099,8 @@ private fun MessageInputBar(
|
|||||||
// Мгновенно когда клавиатура открывается
|
// Мгновенно когда клавиатура открывается
|
||||||
snap()
|
snap()
|
||||||
} else {
|
} else {
|
||||||
// Плавно когда открываем/закрываем эмодзи без клавиатуры
|
// Быстрая анимация для мгновенного отклика (как в Telegram)
|
||||||
tween(durationMillis = 200, easing = FastOutSlowInEasing)
|
tween(durationMillis = 150, easing = TelegramEasing)
|
||||||
},
|
},
|
||||||
label = "EmojiPanelHeight"
|
label = "EmojiPanelHeight"
|
||||||
)
|
)
|
||||||
@@ -2004,7 +2111,8 @@ private fun MessageInputBar(
|
|||||||
.height(animatedHeight)
|
.height(animatedHeight)
|
||||||
.clipToBounds()
|
.clipToBounds()
|
||||||
) {
|
) {
|
||||||
if (showEmojiPicker && !isKeyboardVisible) {
|
// 🚀 Рендерим панель только когда нужно
|
||||||
|
if (showEmojiPicker && !isKeyboardVisible && animatedHeight > 0.dp) {
|
||||||
AppleEmojiPickerPanel(
|
AppleEmojiPickerPanel(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onEmojiSelected = { emoji ->
|
onEmojiSelected = { emoji ->
|
||||||
|
|||||||
@@ -876,12 +876,11 @@ fun DialogItem(dialog: DialogEntity, isDarkTheme: Boolean, onClick: () -> Unit)
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
// 🔥 Используем AppleEmojiText для отображения эмодзи
|
||||||
|
AppleEmojiText(
|
||||||
text = dialog.lastMessage.ifEmpty { "No messages" },
|
text = dialog.lastMessage.ifEmpty { "No messages" },
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = secondaryTextColor,
|
color = secondaryTextColor,
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -206,7 +206,8 @@ fun AppleEmojiTextField(
|
|||||||
textColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.White,
|
textColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.White,
|
||||||
textSize: Float = 16f,
|
textSize: Float = 16f,
|
||||||
hint: String = "Message",
|
hint: String = "Message",
|
||||||
hintColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Gray
|
hintColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Gray,
|
||||||
|
onViewCreated: ((AppleEmojiEditTextView) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
@@ -219,6 +220,8 @@ fun AppleEmojiTextField(
|
|||||||
// Убираем все возможные фоны у EditText
|
// Убираем все возможные фоны у EditText
|
||||||
background = null
|
background = null
|
||||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||||
|
// Уведомляем о создании view
|
||||||
|
onViewCreated?.invoke(this)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { view ->
|
update = { view ->
|
||||||
|
|||||||
@@ -151,7 +151,10 @@ object EmojiCache {
|
|||||||
|
|
||||||
// Предзагрузка при старте приложения (вызывать из Application или MainActivity)
|
// Предзагрузка при старте приложения (вызывать из Application или MainActivity)
|
||||||
fun preload(context: Context) {
|
fun preload(context: Context) {
|
||||||
if (allEmojis != null) return
|
if (allEmojis != null) {
|
||||||
|
isLoaded = true
|
||||||
|
return
|
||||||
|
}
|
||||||
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
|
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
|
||||||
loadEmojisInternal(context)
|
loadEmojisInternal(context)
|
||||||
}
|
}
|
||||||
@@ -356,21 +359,25 @@ fun AppleEmojiPickerPanel(
|
|||||||
var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) }
|
var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) }
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
|
|
||||||
// Загружаем эмодзи если еще не загружены (без задержки если уже в кеше)
|
// Загружаем эмодзи если еще не загружены (синхронно из кеша если уже загружено)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (!EmojiCache.isLoaded) {
|
if (!EmojiCache.isLoaded) {
|
||||||
|
kotlinx.coroutines.launch(kotlinx.coroutines.Dispatchers.Main) {
|
||||||
EmojiCache.loadEmojis(context)
|
EmojiCache.loadEmojis(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Текущие эмодзи для выбранной категории
|
// Текущие эмодзи для выбранной категории - используем derivedStateOf для оптимизации
|
||||||
val currentEmojis = remember(selectedCategory.key, EmojiCache.isLoaded) {
|
val currentEmojis by remember {
|
||||||
|
derivedStateOf {
|
||||||
if (EmojiCache.isLoaded) {
|
if (EmojiCache.isLoaded) {
|
||||||
EmojiCache.getEmojisForCategory(selectedCategory.key)
|
EmojiCache.getEmojisForCategory(selectedCategory.key)
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Сбрасываем скролл при смене категории
|
// Сбрасываем скролл при смене категории
|
||||||
LaunchedEffect(selectedCategory) {
|
LaunchedEffect(selectedCategory) {
|
||||||
@@ -390,10 +397,10 @@ fun AppleEmojiPickerPanel(
|
|||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(categoryBarBackground)
|
.background(categoryBarBackground),
|
||||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp)
|
||||||
) {
|
) {
|
||||||
items(EMOJI_CATEGORIES) { category ->
|
items(EMOJI_CATEGORIES) { category ->
|
||||||
CategoryButton(
|
CategoryButton(
|
||||||
@@ -446,15 +453,15 @@ fun AppleEmojiPickerPanel(
|
|||||||
columns = GridCells.Fixed(8),
|
columns = GridCells.Fixed(8),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f),
|
||||||
.padding(horizontal = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(1.dp),
|
horizontalArrangement = Arrangement.spacedBy(1.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(1.dp),
|
verticalArrangement = Arrangement.spacedBy(1.dp),
|
||||||
contentPadding = PaddingValues(vertical = 4.dp)
|
contentPadding = PaddingValues(horizontal = 12.dp, top = 4.dp, bottom = 16.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = currentEmojis,
|
items = currentEmojis,
|
||||||
key = { it }
|
key = { emoji -> emoji },
|
||||||
|
contentType = { "emoji" }
|
||||||
) { unified ->
|
) { unified ->
|
||||||
EmojiButton(
|
EmojiButton(
|
||||||
unified = unified,
|
unified = unified,
|
||||||
|
|||||||
Reference in New Issue
Block a user