Релиз 1.2.6: sync-статусы, emoji-подсказки и UI-фиксы
Some checks failed
Android Kernel Build / build (push) Failing after 27m7s
Some checks failed
Android Kernel Build / build (push) Failing after 27m7s
This commit is contained in:
@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Rosetta versioning — bump here on each release
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
val rosettaVersionName = "1.2.5"
|
||||
val rosettaVersionCode = 27 // Increment on each release
|
||||
val rosettaVersionName = "1.2.6"
|
||||
val rosettaVersionCode = 28 // Increment on each release
|
||||
|
||||
android {
|
||||
namespace = "com.rosetta.messenger"
|
||||
|
||||
@@ -806,14 +806,10 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
packet.chachaKey
|
||||
}
|
||||
|
||||
val isSelfDialog = packet.toPublicKey.trim() == account
|
||||
// Для исходящих сообщений статус доставки меняется ТОЛЬКО по PacketDelivery.
|
||||
val initialDeliveredStatus =
|
||||
if (isOwnMessage && !isSelfDialog) {
|
||||
DeliveryStatus.WAITING.value
|
||||
} else {
|
||||
DeliveryStatus.DELIVERED.value
|
||||
}
|
||||
// Desktop parity (useSynchronize.ts):
|
||||
// own messages received via PacketMessage sync are inserted as DELIVERED immediately.
|
||||
// WAITING is used only for messages created locally on this device before PacketDelivery.
|
||||
val initialDeliveredStatus = DeliveryStatus.DELIVERED.value
|
||||
|
||||
// Создаем entity для кэша и возможной вставки
|
||||
val entity =
|
||||
@@ -1131,6 +1127,23 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
val privateKey = currentPrivateKey ?: return
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
// Desktop parity recovery:
|
||||
// historically, own synced direct messages ("sync:*" chacha_key) could be saved as WAITING/ERROR
|
||||
// on Android and then incorrectly shown with failed status.
|
||||
// Desktop stores them as DELIVERED from the beginning.
|
||||
val syncedOpponentsWithWrongStatus =
|
||||
messageDao.getSyncedOwnMessageOpponentsWithNonDeliveredStatus(account)
|
||||
val normalizedSyncedCount = messageDao.markSyncedOwnMessagesAsDelivered(account)
|
||||
if (normalizedSyncedCount > 0) {
|
||||
syncedOpponentsWithWrongStatus.forEach { opponentKey ->
|
||||
runCatching { dialogDao.updateDialogFromMessages(account, opponentKey) }
|
||||
}
|
||||
android.util.Log.i(
|
||||
"MessageRepository",
|
||||
"✅ Normalized $normalizedSyncedCount synced own messages to DELIVERED"
|
||||
)
|
||||
}
|
||||
|
||||
// Mark expired messages as ERROR (older than 80 seconds)
|
||||
val expiredCount = messageDao.markExpiredWaitingAsError(account, now - MESSAGE_MAX_TIME_TO_DELIVERED_MS)
|
||||
if (expiredCount > 0) {
|
||||
|
||||
@@ -17,17 +17,18 @@ object ReleaseNotes {
|
||||
val RELEASE_NOTICE = """
|
||||
Update v$VERSION_PLACEHOLDER
|
||||
|
||||
Что обновлено после версии 1.2.4
|
||||
Синхронизация статусов (desktop parity)
|
||||
- Исправлено ложное «ошибка отправки» для сообщений, пришедших в sync с других устройств
|
||||
- Синхронизированные исходящие теперь сразу помечаются как доставленные, как в desktop
|
||||
- Добавлена авто-нормализация старых sync-сообщений со статусами WAITING/ERROR в DELIVERED
|
||||
|
||||
Статусы отправки и прочтения
|
||||
- Исправлены ложные двойные галочки у фото и медиа-сообщений до фактической доставки
|
||||
- Исходящие сообщения теперь остаются в ожидании до реального PacketDelivery с сервера
|
||||
- PacketRead больше не переводит недоставленные сообщения в «прочитано»
|
||||
- Синхронизация статусов между БД, кэшем и UI стала стабильнее в личных чатах и группах
|
||||
Emoji-подсказки в поле ввода
|
||||
- Переработан UI подсказок: отдельный плавающий пузырёк над инпутом без изменения его высоты
|
||||
- Исправлен клиппинг/обрезание пузырька внизу экрана
|
||||
- Рендер эмодзи в подсказках приведён к Apple-like отображению
|
||||
|
||||
Надёжность отображения
|
||||
- Убраны ложные переходы статусов при быстрых событиях синхронизации
|
||||
- Логика read receipts приведена к более корректному серверному подтверждению
|
||||
Полировка интерфейса
|
||||
- В списке чатов выровнен текст после «Draft:» по одной line-height линии
|
||||
""".trimIndent()
|
||||
|
||||
fun getNotice(version: String): String =
|
||||
|
||||
@@ -482,6 +482,35 @@ interface MessageDao {
|
||||
)
|
||||
suspend fun markExpiredWaitingAsError(account: String, maxTimestamp: Long): Int
|
||||
|
||||
/**
|
||||
* Desktop parity recovery:
|
||||
* own direct messages synced from another device are stored with chacha_key "sync:*"
|
||||
* and should always be DELIVERED (never WAITING/ERROR).
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
UPDATE messages
|
||||
SET delivered = 1
|
||||
WHERE account = :account
|
||||
AND from_me = 1
|
||||
AND delivered != 1
|
||||
AND chacha_key LIKE 'sync:%'
|
||||
"""
|
||||
)
|
||||
suspend fun markSyncedOwnMessagesAsDelivered(account: String): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT DISTINCT to_public_key
|
||||
FROM messages
|
||||
WHERE account = :account
|
||||
AND from_me = 1
|
||||
AND delivered != 1
|
||||
AND chacha_key LIKE 'sync:%'
|
||||
"""
|
||||
)
|
||||
suspend fun getSyncedOwnMessageOpponentsWithNonDeliveredStatus(account: String): List<String>
|
||||
|
||||
/**
|
||||
* Update delivery status AND timestamp on delivery confirmation.
|
||||
* Desktop parity: useDialogFiber.ts sets timestamp = Date.now() on PacketDelivery.
|
||||
|
||||
@@ -4327,7 +4327,7 @@ fun DialogItemContent(
|
||||
if (showTyping) {
|
||||
TypingIndicatorSmall()
|
||||
} else if (!dialog.draftText.isNullOrEmpty()) {
|
||||
Row {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "Draft: ",
|
||||
fontSize = 14.sp,
|
||||
@@ -4343,7 +4343,8 @@ fun DialogItemContent(
|
||||
fontWeight = FontWeight.Normal,
|
||||
maxLines = 1,
|
||||
overflow = android.text.TextUtils.TruncateAt.END,
|
||||
enableLinks = false
|
||||
enableLinks = false,
|
||||
minHeightMultiplier = 1f
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -36,6 +37,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
||||
@@ -135,6 +137,8 @@ fun MessageInputBar(
|
||||
val density = LocalDensity.current
|
||||
|
||||
var editTextView by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiEditTextView?>(null) }
|
||||
var selectionStart by remember { mutableIntStateOf(value.length) }
|
||||
var selectionEnd by remember { mutableIntStateOf(value.length) }
|
||||
|
||||
// Auto-focus when reply panel opens
|
||||
LaunchedEffect(hasReply, editTextView) {
|
||||
@@ -269,6 +273,25 @@ fun MessageInputBar(
|
||||
}
|
||||
}
|
||||
|
||||
val emojiWordMatch =
|
||||
remember(value, selectionStart, selectionEnd) {
|
||||
EmojiKeywordSuggester.findWordAtCursor(value, selectionStart, selectionEnd)
|
||||
}
|
||||
|
||||
val emojiSuggestions =
|
||||
remember(emojiWordMatch) {
|
||||
emojiWordMatch?.let { EmojiKeywordSuggester.suggest(it) }.orEmpty()
|
||||
}
|
||||
|
||||
val shouldShowEmojiSuggestions =
|
||||
remember(emojiSuggestions, mentionSuggestions, isGroupChat, suppressKeyboard, showEmojiPicker) {
|
||||
emojiSuggestions.isNotEmpty() &&
|
||||
mentionSuggestions.isEmpty() &&
|
||||
!(isGroupChat && shouldShowMentionSuggestions) &&
|
||||
!showEmojiPicker &&
|
||||
!suppressKeyboard
|
||||
}
|
||||
|
||||
// Close keyboard when user is blocked
|
||||
LaunchedEffect(isBlocked) {
|
||||
if (isBlocked) {
|
||||
@@ -357,6 +380,14 @@ fun MessageInputBar(
|
||||
editTextView?.requestFocus()
|
||||
}
|
||||
|
||||
fun onSelectEmojiSuggestion(emoji: String) {
|
||||
val updated = EmojiKeywordSuggester.applySuggestion(value, selectionStart, selectionEnd, emoji)
|
||||
if (updated != value) {
|
||||
onValueChange(updated)
|
||||
}
|
||||
editTextView?.requestFocus()
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth().graphicsLayer { clip = false }) {
|
||||
if (isBlocked) {
|
||||
// BLOCKED CHAT FOOTER
|
||||
@@ -695,7 +726,94 @@ fun MessageInputBar(
|
||||
}
|
||||
}
|
||||
|
||||
// INPUT ROW
|
||||
// INPUT ROW + Telegram-like flat emoji suggestion strip overlay
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.graphicsLayer { clip = false }
|
||||
) {
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = shouldShowEmojiSuggestions,
|
||||
enter = fadeIn(animationSpec = tween(120)) +
|
||||
slideInVertically(animationSpec = tween(120), initialOffsetY = { it / 2 }),
|
||||
exit = fadeOut(animationSpec = tween(90)) +
|
||||
slideOutVertically(animationSpec = tween(90), targetOffsetY = { it / 3 }),
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.offset(y = (-46).dp)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp)
|
||||
.zIndex(4f)
|
||||
) {
|
||||
val barBackground = if (isDarkTheme) Color(0xFF303A48).copy(alpha = 0.97f) else Color(0xFFE9EEF6)
|
||||
val barBorder = if (isDarkTheme) Color.White.copy(alpha = 0.12f) else Color.Black.copy(alpha = 0.1f)
|
||||
val leadingIconBg =
|
||||
if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.06f)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = barBackground,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, barBorder),
|
||||
shadowElevation = if (isDarkTheme) 6.dp else 3.dp,
|
||||
tonalElevation = 0.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(38.dp)
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.clip(CircleShape)
|
||||
.background(leadingIconBg),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = TelegramIcons.Smile,
|
||||
contentDescription = null,
|
||||
tint = if (isDarkTheme) Color.White.copy(alpha = 0.92f) else Color(0xFF31415A),
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
LazyRow(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(emojiSuggestions, key = { it }) { emoji ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) { onSelectEmojiSuggestion(emoji) }
|
||||
.padding(horizontal = 1.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AppleEmojiText(
|
||||
text = emoji,
|
||||
fontSize = 26.sp,
|
||||
color = if (isDarkTheme) Color.White else Color.Black,
|
||||
maxLines = 1,
|
||||
enableLinks = false,
|
||||
minHeightMultiplier = 1f,
|
||||
onClick = { onSelectEmojiSuggestion(emoji) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -740,6 +858,10 @@ fun MessageInputBar(
|
||||
if (hasFocus && showEmojiPicker) {
|
||||
onToggleEmojiPicker(false)
|
||||
}
|
||||
},
|
||||
onSelectionChanged = { start, end ->
|
||||
selectionStart = start
|
||||
selectionEnd = end
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -783,6 +905,7 @@ fun MessageInputBar(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showForwardCancelDialog) {
|
||||
val titleText =
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.rosetta.messenger.ui.chats.input
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
data class EmojiWordMatch(
|
||||
val normalizedWord: String,
|
||||
val start: Int,
|
||||
val end: Int
|
||||
)
|
||||
|
||||
object EmojiKeywordSuggester {
|
||||
|
||||
private const val MIN_WORD_LENGTH = 2
|
||||
private const val MAX_WORD_LENGTH = 32
|
||||
|
||||
// test
|
||||
|
||||
private val keywordToEmoji: Map<String, List<String>> =
|
||||
linkedMapOf(
|
||||
"спасибо" to listOf("🙏", "😊", "❤️", "🤝"),
|
||||
"спс" to listOf("🙏", "😊"),
|
||||
"благодарю" to listOf("🙏", "🤝"),
|
||||
"пожалуйста" to listOf("🙏", "🙂"),
|
||||
"привет" to listOf("👋", "🙂", "😊"),
|
||||
"здравствуйте" to listOf("👋", "🙂"),
|
||||
"пока" to listOf("👋", "🫡"),
|
||||
"доброеутро" to listOf("☀️", "🌤️", "🙂"),
|
||||
"добрыйдень" to listOf("☀️", "🙂"),
|
||||
"добрыйвечер" to listOf("🌇", "🙂"),
|
||||
"ок" to listOf("👌", "👍"),
|
||||
"хорошо" to listOf("👌", "👍", "✅"),
|
||||
"круто" to listOf("🔥", "😎", "🤘"),
|
||||
"класс" to listOf("🔥", "😎", "✨"),
|
||||
"да" to listOf("✅", "👍"),
|
||||
"нет" to listOf("❌", "🙅"),
|
||||
"люблю" to listOf("❤️", "🥰", "😘"),
|
||||
"любовь" to listOf("❤️", "😍", "🥰"),
|
||||
"скучаю" to listOf("🥺", "❤️"),
|
||||
"смешно" to listOf("😂", "🤣", "😄"),
|
||||
"лол" to listOf("😂", "🤣"),
|
||||
"печаль" to listOf("😢", "🥺", "💔"),
|
||||
"грустно" to listOf("😢", "🥺"),
|
||||
"злюсь" to listOf("😡", "🤬"),
|
||||
"злой" to listOf("😡", "🤬"),
|
||||
"sorry" to listOf("🙏", "😔"),
|
||||
"thanks" to listOf("🙏", "😊", "❤️"),
|
||||
"thankyou" to listOf("🙏", "😊"),
|
||||
"hello" to listOf("👋", "🙂", "😊"),
|
||||
"hi" to listOf("👋", "🙂"),
|
||||
"bye" to listOf("👋", "🫡"),
|
||||
"goodmorning" to listOf("☀️", "🌤️", "🙂"),
|
||||
"goodevening" to listOf("🌇", "🙂"),
|
||||
"okey" to listOf("👌", "👍"),
|
||||
"okay" to listOf("👌", "👍"),
|
||||
"yes" to listOf("✅", "👍"),
|
||||
"no" to listOf("❌", "🙅"),
|
||||
"love" to listOf("❤️", "🥰", "😍"),
|
||||
"missyou" to listOf("🥺", "❤️"),
|
||||
"funny" to listOf("😂", "🤣", "😄"),
|
||||
"sad" to listOf("😢", "🥺", "💔"),
|
||||
"angry" to listOf("😡", "🤬"),
|
||||
"spasibo" to listOf("🙏", "😊", "❤️"),
|
||||
"privet" to listOf("👋", "🙂", "😊"),
|
||||
"poka" to listOf("👋", "🫡"),
|
||||
"lyublyu" to listOf("❤️", "🥰", "😘")
|
||||
)
|
||||
|
||||
fun findWordAtCursor(
|
||||
text: String,
|
||||
selectionStart: Int,
|
||||
selectionEnd: Int
|
||||
): EmojiWordMatch? {
|
||||
if (selectionStart != selectionEnd) return null
|
||||
if (text.isEmpty()) return null
|
||||
|
||||
val cursor = selectionEnd.coerceIn(0, text.length)
|
||||
var wordStart = cursor
|
||||
var wordEnd = cursor
|
||||
|
||||
while (wordStart > 0 && isWordChar(text[wordStart - 1])) {
|
||||
wordStart--
|
||||
}
|
||||
while (wordEnd < text.length && isWordChar(text[wordEnd])) {
|
||||
wordEnd++
|
||||
}
|
||||
|
||||
if (wordStart == wordEnd) return null
|
||||
|
||||
val rawWord = text.substring(wordStart, wordEnd)
|
||||
val normalized = normalizeWord(rawWord)
|
||||
if (normalized.length !in MIN_WORD_LENGTH..MAX_WORD_LENGTH) return null
|
||||
|
||||
return EmojiWordMatch(
|
||||
normalizedWord = normalized,
|
||||
start = wordStart,
|
||||
end = wordEnd
|
||||
)
|
||||
}
|
||||
|
||||
fun suggest(match: EmojiWordMatch, maxCount: Int = 8): List<String> {
|
||||
if (maxCount <= 0) return emptyList()
|
||||
|
||||
val exact = keywordToEmoji[match.normalizedWord].orEmpty()
|
||||
val prefix =
|
||||
keywordToEmoji
|
||||
.asSequence()
|
||||
.filter { (keyword, _) ->
|
||||
keyword != match.normalizedWord && keyword.startsWith(match.normalizedWord)
|
||||
}
|
||||
.sortedBy { (keyword, _) -> keyword.length }
|
||||
.flatMap { (_, emoji) -> emoji.asSequence() }
|
||||
.toList()
|
||||
|
||||
return buildList {
|
||||
val dedupe = LinkedHashSet<String>()
|
||||
(exact + prefix).forEach { emoji ->
|
||||
if (dedupe.add(emoji)) {
|
||||
add(emoji)
|
||||
if (size >= maxCount) return@buildList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applySuggestion(
|
||||
text: String,
|
||||
selectionStart: Int,
|
||||
selectionEnd: Int,
|
||||
emoji: String
|
||||
): String {
|
||||
val match = findWordAtCursor(text, selectionStart, selectionEnd) ?: return text
|
||||
val prefix = text.substring(0, match.start)
|
||||
val suffix = text.substring(match.end)
|
||||
|
||||
val shouldAddSpace =
|
||||
suffix.isEmpty() || (!suffix.first().isWhitespace() && suffix.first() !in setOf(',', '.', '!', '?', ':', ';'))
|
||||
val trailing = if (shouldAddSpace) " " else ""
|
||||
|
||||
return prefix + emoji + trailing + suffix
|
||||
}
|
||||
|
||||
private fun isWordChar(char: Char): Boolean {
|
||||
return char.isLetterOrDigit() || char == '_'
|
||||
}
|
||||
|
||||
private fun normalizeWord(word: String): String {
|
||||
return buildString(word.length) {
|
||||
word.forEach { char ->
|
||||
if (isWordChar(char)) append(char.lowercaseChar())
|
||||
}
|
||||
}.lowercase(Locale.ROOT)
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,7 @@ class AppleEmojiEditTextView @JvmOverloads constructor(
|
||||
) : EditText(context, attrs, defStyleAttr) {
|
||||
|
||||
var onTextChange: ((String) -> Unit)? = null
|
||||
var onSelectionRangeChange: ((Int, Int) -> Unit)? = null
|
||||
/** Called when the view's measured height changes (for multiline expansion animation). */
|
||||
var onHeightChanged: ((oldHeight: Int, newHeight: Int) -> Unit)? = null
|
||||
private var isUpdating = false
|
||||
@@ -173,6 +174,11 @@ class AppleEmojiEditTextView @JvmOverloads constructor(
|
||||
lastMeasuredHeight = h
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
||||
super.onSelectionChanged(selStart, selEnd)
|
||||
onSelectionRangeChange?.invoke(selStart, selEnd)
|
||||
}
|
||||
|
||||
fun setTextWithEmojis(newText: String) {
|
||||
if (newText == text.toString()) return
|
||||
isUpdating = true
|
||||
@@ -310,7 +316,8 @@ fun AppleEmojiTextField(
|
||||
onViewCreated: ((AppleEmojiEditTextView) -> Unit)? = null,
|
||||
requestFocus: Boolean = false,
|
||||
onFocusChanged: ((Boolean) -> Unit)? = null,
|
||||
onHeightChanged: ((oldHeight: Int, newHeight: Int) -> Unit)? = null
|
||||
onHeightChanged: ((oldHeight: Int, newHeight: Int) -> Unit)? = null,
|
||||
onSelectionChanged: ((Int, Int) -> Unit)? = null
|
||||
) {
|
||||
// Храним ссылку на view для управления фокусом
|
||||
var editTextView by remember { mutableStateOf<AppleEmojiEditTextView?>(null) }
|
||||
@@ -333,6 +340,7 @@ fun AppleEmojiTextField(
|
||||
setHint(hint)
|
||||
setTextSize(textSize)
|
||||
onTextChange = onValueChange
|
||||
onSelectionRangeChange = onSelectionChanged
|
||||
this.onHeightChanged = onHeightChanged
|
||||
// Убираем все возможные фоны у EditText
|
||||
background = null
|
||||
@@ -351,6 +359,7 @@ fun AppleEmojiTextField(
|
||||
// Always update the callback to prevent stale lambda references
|
||||
// after recomposition (e.g., after sending a photo)
|
||||
view.onTextChange = onValueChange
|
||||
view.onSelectionRangeChange = onSelectionChanged
|
||||
|
||||
if (view.text.toString() != value) {
|
||||
view.setTextWithEmojis(value)
|
||||
|
||||
Reference in New Issue
Block a user