diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 7afb04a..ebeb3ca 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -42,6 +42,7 @@ import com.rosetta.messenger.network.SearchUser import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.components.VerifiedBadge import com.rosetta.messenger.ui.components.AppleEmojiPickerPanel +import com.rosetta.messenger.ui.components.AppleEmojiTextField import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.* @@ -518,35 +519,21 @@ private fun MessageInputBar( .padding(horizontal = 14.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { - // Text field + // Apple Emoji Text Field (с PNG эмодзи) Box( modifier = Modifier .weight(1f) .padding(top = 8.dp, bottom = 8.dp, end = 70.dp), // место для emoji + send contentAlignment = Alignment.CenterStart ) { - BasicTextField( + AppleEmojiTextField( value = value, onValueChange = onValueChange, - textStyle = androidx.compose.ui.text.TextStyle( - color = textColor, - fontSize = 16.sp - ), - cursorBrush = SolidColor(PrimaryBlue), - modifier = Modifier.fillMaxWidth(), - maxLines = 5, - decorationBox = { innerTextField -> - Box(contentAlignment = Alignment.CenterStart) { - if (value.isEmpty()) { - Text( - text = "Message", - color = placeholderColor.copy(alpha = 0.6f), - fontSize = 16.sp - ) - } - innerTextField() - } - } + textColor = textColor, + textSize = 16f, + hint = "Message", + hintColor = placeholderColor.copy(alpha = 0.6f), + modifier = Modifier.fillMaxWidth() ) } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt b/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt new file mode 100644 index 0000000..4f9d95a --- /dev/null +++ b/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt @@ -0,0 +1,219 @@ +package com.rosetta.messenger.ui.components + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.text.Editable +import android.text.SpannableStringBuilder +import android.text.TextWatcher +import android.text.style.ImageSpan +import android.util.AttributeSet +import android.util.LruCache +import android.view.Gravity +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.viewinterop.AndroidView +import java.util.regex.Pattern + +/** + * Apple Emoji EditText - кастомный EditText с PNG эмодзи + * Заменяет системные эмодзи на Apple PNG изображения из assets + */ +class AppleEmojiEditTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.editTextStyle +) : EditText(context, attrs, defStyleAttr) { + + var onTextChange: ((String) -> Unit)? = null + private var isUpdating = false + + companion object { + // Regex для эмодзи + private val EMOJI_PATTERN = Pattern.compile( + "[\\x{1F600}-\\x{1F64F}]|" + // Emoticons + "[\\x{1F300}-\\x{1F5FF}]|" + // Misc Symbols and Pictographs + "[\\x{1F680}-\\x{1F6FF}]|" + // Transport and Map + "[\\x{1F1E0}-\\x{1F1FF}]|" + // Flags + "[\\x{2600}-\\x{26FF}]|" + // Misc symbols + "[\\x{2700}-\\x{27BF}]|" + // Dingbats + "[\\x{FE00}-\\x{FE0F}]|" + // Variation Selectors + "[\\x{1F900}-\\x{1F9FF}]|" + // Supplemental Symbols + "[\\x{1FA00}-\\x{1FA6F}]|" + // Chess Symbols + "[\\x{1FA70}-\\x{1FAFF}]|" + // Symbols Extended-A + "[\\x{231A}-\\x{231B}]|" + // Watch, Hourglass + "[\\x{23E9}-\\x{23F3}]|" + // Media controls + "[\\x{23F8}-\\x{23FA}]|" + // Media controls + "[\\x{25AA}-\\x{25AB}]|" + // Squares + "[\\x{25B6}]|[\\x{25C0}]|" + // Play buttons + "[\\x{25FB}-\\x{25FE}]|" + // Squares + "[\\x{2614}-\\x{2615}]|" + // Umbrella, Hot beverage + "[\\x{2648}-\\x{2653}]|" + // Zodiac + "[\\x{267F}]|[\\x{2693}]|" + // Wheelchair, Anchor + "[\\x{26A1}]|[\\x{26AA}-\\x{26AB}]|" + + "[\\x{26BD}-\\x{26BE}]|" + // Sports + "[\\x{26C4}-\\x{26C5}]|" + // Weather + "[\\x{26CE}]|[\\x{26D4}]|" + + "[\\x{26EA}]|[\\x{26F2}-\\x{26F3}]|" + + "[\\x{26F5}]|[\\x{26FA}]|[\\x{26FD}]|" + + "[\\x{2702}]|[\\x{2705}]|" + + "[\\x{2708}-\\x{270D}]|[\\x{270F}]|" + + "[\\x{2712}]|[\\x{2714}]|[\\x{2716}]|" + + "[\\x{271D}]|[\\x{2721}]|" + + "[\\x{2728}]|[\\x{2733}-\\x{2734}]|" + + "[\\x{2744}]|[\\x{2747}]|" + + "[\\x{274C}]|[\\x{274E}]|" + + "[\\x{2753}-\\x{2755}]|[\\x{2757}]|" + + "[\\x{2763}-\\x{2764}]|" + + "[\\x{2795}-\\x{2797}]|[\\x{27A1}]|" + + "[\\x{27B0}]|[\\x{27BF}]|" + + "[\\x{2934}-\\x{2935}]|" + + "[\\x{2B05}-\\x{2B07}]|" + + "[\\x{2B1B}-\\x{2B1C}]|" + + "[\\x{2B50}]|[\\x{2B55}]|" + + "[\\x{3030}]|[\\x{303D}]|" + + "[\\x{3297}]|[\\x{3299}]" + ) + + // Кэш для bitmap и drawable + private val bitmapCache = LruCache(500) + private val drawableCache = LruCache(500) + } + + init { + // Настраиваем EditText + background = null + setPadding(0, 0, 0, 0) + gravity = Gravity.CENTER_VERTICAL or Gravity.START + isSingleLine = false + maxLines = 5 + imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI + + addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + if (!isUpdating && s != null) { + replaceEmojisWithImages(s) + // Отправляем plain text без spans + onTextChange?.invoke(s.toString()) + } + } + }) + } + + fun setTextWithEmojis(newText: String) { + if (newText == text.toString()) return + isUpdating = true + setText(newText) + isUpdating = false + replaceEmojisWithImages(editableText) + } + + private fun replaceEmojisWithImages(editable: Editable) { + if (isUpdating) return + isUpdating = true + + try { + val textStr = editable.toString() + val matcher = EMOJI_PATTERN.matcher(textStr) + val cursorPosition = selectionStart + + while (matcher.find()) { + val emoji = matcher.group() + val start = matcher.start() + val end = matcher.end() + + // Проверяем, есть ли уже ImageSpan + val existingSpans = editable.getSpans(start, end, ImageSpan::class.java) + if (existingSpans.isNotEmpty()) continue + + val unified = emojiToUnified(emoji) + var drawable = drawableCache.get(unified) + + if (drawable == null) { + var bitmap = bitmapCache.get(unified) + if (bitmap == null) { + bitmap = loadFromAssets(unified) + if (bitmap != null) { + bitmapCache.put(unified, bitmap) + } + } + + if (bitmap != null) { + drawable = BitmapDrawable(getContext().resources, bitmap) + val size = (textSize * 1.15).toInt() + drawable.setBounds(0, 0, size, size) + drawableCache.put(unified, drawable) + } + } + + if (drawable != null && start < editable.length && end <= editable.length) { + val imageSpan = ImageSpan(drawable, ImageSpan.ALIGN_CENTER) + editable.setSpan(imageSpan, start, end, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + if (cursorPosition >= 0 && cursorPosition <= editable.length) { + setSelection(cursorPosition) + } + } finally { + isUpdating = false + } + } + + private fun loadFromAssets(unified: String): Bitmap? { + return try { + val inputStream = getContext().assets.open("emoji/$unified.png") + val bitmap = BitmapFactory.decodeStream(inputStream) + inputStream.close() + bitmap + } catch (e: Exception) { + null + } + } + + private fun emojiToUnified(emoji: String): String { + return emoji.codePoints() + .filter { it != 0xFE0F } + .mapToObj { String.format("%04x", it) } + .toList() + .joinToString("-") + } +} + +/** + * Compose обёртка для AppleEmojiEditText + */ +@Composable +fun AppleEmojiTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + textColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.White, + textSize: Float = 16f, + hint: String = "Message", + hintColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Gray +) { + AndroidView( + factory = { ctx -> + AppleEmojiEditTextView(ctx).apply { + setTextColor(textColor.toArgb()) + setHintTextColor(hintColor.toArgb()) + setHint(hint) + setTextSize(textSize) + onTextChange = onValueChange + } + }, + update = { view -> + if (view.text.toString() != value) { + view.setTextWithEmojis(value) + } + }, + modifier = modifier + ) +}