feat: Implement pinch-to-zoom functionality in InAppCameraScreen and add zoom controls

This commit is contained in:
2026-02-23 00:49:29 +05:00
parent 290143dfc4
commit f4cb3f8253
10 changed files with 503 additions and 260 deletions

View File

@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// Rosetta versioning — bump here on each release // Rosetta versioning — bump here on each release
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
val rosettaVersionName = "1.0.4" val rosettaVersionName = "1.0.5"
val rosettaVersionCode = 4 // Increment on each release val rosettaVersionCode = 5 // Increment on each release
android { android {
namespace = "com.rosetta.messenger" namespace = "com.rosetta.messenger"

View File

@@ -17,13 +17,15 @@ object ReleaseNotes {
val RELEASE_NOTICE = """ val RELEASE_NOTICE = """
Update v$VERSION_PLACEHOLDER Update v$VERSION_PLACEHOLDER
- New attachment panel with tabs (photos, files, avatar) - Emoji keyboard in attachment panel with smooth transitions
- Message drafts — text is saved when leaving a chat - File browser — send any file from device storage
- Circular reveal animation for theme switching - Send gallery photos as files (without compression)
- Photo albums in media picker - Camera: pinch-to-zoom, zoom slider, transparent status bar
- Camera flash mode is now saved between sessions - Double checkmarks for read photos, files and avatars
- Swipe-back gesture fixes - Haptic feedback on swipe-to-reply
- UI and performance improvements - System accounts hidden from forward picker
- Smoother caption bar animations
- Bug fixes and performance improvements
""".trimIndent() """.trimIndent()
fun getNotice(version: String): String = fun getNotice(version: String): String =

View File

@@ -25,12 +25,15 @@ 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
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.rosetta.messenger.R
import com.rosetta.messenger.data.ForwardManager import com.rosetta.messenger.data.ForwardManager
import com.rosetta.messenger.data.MessageRepository
import com.rosetta.messenger.repository.AvatarRepository import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.components.AppleEmojiText import com.rosetta.messenger.ui.components.AppleEmojiText
import com.rosetta.messenger.ui.components.AvatarImage import com.rosetta.messenger.ui.components.AvatarImage
import com.rosetta.messenger.ui.components.VerifiedBadge import com.rosetta.messenger.ui.components.VerifiedBadge
import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import androidx.compose.ui.res.painterResource
import java.util.* import java.util.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -65,6 +68,11 @@ fun ForwardChatPickerBottomSheet(
val forwardMessages by ForwardManager.forwardMessages.collectAsState() val forwardMessages by ForwardManager.forwardMessages.collectAsState()
val messagesCount = forwardMessages.size val messagesCount = forwardMessages.size
// 🔥 Фильтруем системные аккаунты (Safe, Rosetta Updates)
val filteredDialogs = remember(dialogs) {
dialogs.filter { !MessageRepository.isSystemAccount(it.opponentKey) }
}
// Мультивыбор чатов // Мультивыбор чатов
var selectedChats by remember { mutableStateOf<Set<String>>(emptySet()) } var selectedChats by remember { mutableStateOf<Set<String>>(emptySet()) }
@@ -187,7 +195,7 @@ fun ForwardChatPickerBottomSheet(
Divider(color = dividerColor, thickness = 0.5.dp) Divider(color = dividerColor, thickness = 0.5.dp)
// Список диалогов // Список диалогов
if (dialogs.isEmpty()) { if (filteredDialogs.isEmpty()) {
// Empty state // Empty state
Box( Box(
modifier = Modifier.fillMaxWidth().height(200.dp), modifier = Modifier.fillMaxWidth().height(200.dp),
@@ -214,7 +222,7 @@ fun ForwardChatPickerBottomSheet(
Modifier.fillMaxWidth() Modifier.fillMaxWidth()
.weight(1f) .weight(1f)
) { ) {
items(dialogs, key = { it.opponentKey }) { dialog -> items(filteredDialogs, key = { it.opponentKey }) { dialog ->
val isSelected = dialog.opponentKey in selectedChats val isSelected = dialog.opponentKey in selectedChats
ForwardDialogItem( ForwardDialogItem(
dialog = dialog, dialog = dialog,
@@ -233,7 +241,7 @@ fun ForwardChatPickerBottomSheet(
) )
// Сепаратор между диалогами // Сепаратор между диалогами
if (dialog != dialogs.last()) { if (dialog != filteredDialogs.last()) {
Divider( Divider(
modifier = Modifier.padding(start = 76.dp), modifier = Modifier.padding(start = 76.dp),
color = dividerColor, color = dividerColor,
@@ -252,7 +260,7 @@ fun ForwardChatPickerBottomSheet(
val hasSelection = selectedChats.isNotEmpty() val hasSelection = selectedChats.isNotEmpty()
Button( Button(
onClick = { onClick = {
val selectedDialogs = dialogs.filter { it.opponentKey in selectedChats } val selectedDialogs = filteredDialogs.filter { it.opponentKey in selectedChats }
onChatsSelected(selectedDialogs) onChatsSelected(selectedDialogs)
}, },
enabled = hasSelection, enabled = hasSelection,
@@ -339,14 +347,14 @@ private fun ForwardDialogItem(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.clip(CircleShape) .clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.15f)), .background(PrimaryBlue),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Icon(
text = "📁", painter = painterResource(R.drawable.bookmark_outlined),
color = PrimaryBlue, contentDescription = null,
fontWeight = FontWeight.SemiBold, tint = Color.White,
fontSize = 16.sp modifier = Modifier.size(22.dp)
) )
} }
} else { } else {

View File

@@ -784,28 +784,40 @@ internal fun CaptionBar(
captionText: String, captionText: String,
onCaptionChange: (String) -> Unit, onCaptionChange: (String) -> Unit,
isCaptionKeyboardVisible: Boolean, isCaptionKeyboardVisible: Boolean,
isEmojiVisible: Boolean = false,
onToggleKeyboard: () -> Unit, onToggleKeyboard: () -> Unit,
onFocusCaption: () -> Unit, onFocusCaption: () -> Unit,
onViewCreated: (com.rosetta.messenger.ui.components.AppleEmojiEditTextView) -> Unit, onViewCreated: (com.rosetta.messenger.ui.components.AppleEmojiEditTextView) -> Unit,
isDarkTheme: Boolean, isDarkTheme: Boolean,
density: androidx.compose.ui.unit.Density density: androidx.compose.ui.unit.Density
) { ) {
val captionBarHeight = 52.dp * captionBarProgress // Telegram-style CaptionBar:
Box( // - AnimatedVisibility for show/hide (expandVertically + fadeIn/Out, 180ms)
modifier = Modifier // - animateContentSize for smooth multiline expansion (200ms CubicBezier,
.fillMaxWidth() // matching Telegram's captionEditTextTopOffset animation)
.height(captionBarHeight) val captionMultilineEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1f)
.graphicsLayer { clip = true } val captionLiftEasing = CubicBezierEasing(0f, 0f, 0.2f, 1f)
val isVisible = captionBarProgress > 0.001f
AnimatedVisibility(
visible = isVisible,
enter = expandVertically(
animationSpec = tween(180, easing = captionLiftEasing),
expandFrom = Alignment.Bottom
) + fadeIn(animationSpec = tween(180, easing = captionLiftEasing)),
exit = shrinkVertically(
animationSpec = tween(180, easing = captionLiftEasing),
shrinkTowards = Alignment.Bottom
) + fadeOut(animationSpec = tween(180, easing = captionLiftEasing))
) { ) {
if (captionBarProgress > 0.001f) {
val captionOffsetPx = with(density) { (1f - captionBarProgress) * 18.dp.toPx() }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.graphicsLayer { // animateContentSize: smooth multiline expansion/collapse
alpha = captionBarProgress // like Telegram's captionEditTextTopOffset (200ms CubicBezier)
translationY = captionOffsetPx .animateContentSize(
} animationSpec = tween(200, easing = captionMultilineEasing)
)
) { ) {
// Thin divider // Thin divider
Box( Box(
@@ -825,8 +837,11 @@ internal fun CaptionBar(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
// Emoji visible → show Keyboard icon (to switch back)
// Keyboard visible → show Smile icon (to switch to emoji)
// Neither → show Smile icon (to open keyboard on first tap)
val captionIconPainter = val captionIconPainter =
if (isCaptionKeyboardVisible) TelegramIcons.Keyboard if (isEmojiVisible) TelegramIcons.Keyboard
else TelegramIcons.Smile else TelegramIcons.Smile
Icon( Icon(
painter = captionIconPainter, painter = captionIconPainter,
@@ -862,8 +877,6 @@ internal fun CaptionBar(
onFocusChanged = { hasFocus -> onFocusChanged = { hasFocus ->
// When EditText gets native touch → gains focus → // When EditText gets native touch → gains focus →
// trigger FLAG_ALT_FOCUSABLE_IM management in ChatAttachAlert. // trigger FLAG_ALT_FOCUSABLE_IM management in ChatAttachAlert.
// This replaces the previous .clickable on parent Box which
// swallowed touch events and prevented keyboard from appearing.
android.util.Log.d("AttachAlert", "CaptionBar EditText onFocusChanged: hasFocus=$hasFocus") android.util.Log.d("AttachAlert", "CaptionBar EditText onFocusChanged: hasFocus=$hasFocus")
if (hasFocus) onFocusCaption() if (hasFocus) onFocusCaption()
} }
@@ -873,7 +886,6 @@ internal fun CaptionBar(
} }
} }
} }
}
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// Floating Send Button // Floating Send Button

View File

@@ -39,6 +39,11 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.rosetta.messenger.ui.chats.components.ImageEditorScreen import com.rosetta.messenger.ui.chats.components.ImageEditorScreen
import com.rosetta.messenger.ui.chats.components.PhotoPreviewWithCaptionScreen import com.rosetta.messenger.ui.chats.components.PhotoPreviewWithCaptionScreen
import com.rosetta.messenger.ui.chats.components.ThumbnailPosition import com.rosetta.messenger.ui.chats.components.ThumbnailPosition
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -103,10 +108,11 @@ private fun updatePopupImeFocusable(rootView: View, imeFocusable: Boolean) {
} else { } else {
params.flags = params.flags or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM params.flags = params.flags or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
} }
// Don't change softInputMode on the Popup window. // Telegram approach: SOFT_INPUT_ADJUST_NOTHING on the Popup window itself.
// Compose Popup + enableEdgeToEdge() handles keyboard insets via WindowInsets // This prevents the system from resizing or panning the Popup when keyboard
// (no physical window resize). Manually changing to ADJUST_RESIZE causes // opens. The sheet stays at the same height — we handle keyboard space
// double-subtraction: system shrinks window AND we subtract imeInsets. // internally via a bottom spacer (like Telegram's AdjustPanLayoutHelper).
params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
try { try {
wm.updateViewLayout(rootView, params) wm.updateViewLayout(rootView, params)
AttachAlertDebugLog.log("AttachAlert", "updatePopupImeFocusable($imeFocusable) — flags updated OK") AttachAlertDebugLog.log("AttachAlert", "updatePopupImeFocusable($imeFocusable) — flags updated OK")
@@ -166,6 +172,10 @@ fun ChatAttachAlert(
var pendingDismissAfterConfirm by remember { mutableStateOf<(() -> Unit)?>(null) } var pendingDismissAfterConfirm by remember { mutableStateOf<(() -> Unit)?>(null) }
var hadSelection by remember { mutableStateOf(false) } var hadSelection by remember { mutableStateOf(false) }
var isCaptionKeyboardVisible by remember { mutableStateOf(false) } var isCaptionKeyboardVisible by remember { mutableStateOf(false) }
var showEmojiPicker by remember { mutableStateOf(false) }
val coordinator = rememberKeyboardTransitionCoordinator()
var lastToggleTime by remember { mutableLongStateOf(0L) }
val toggleCooldownMs = 500L
var showAlbumMenu by remember { mutableStateOf(false) } var showAlbumMenu by remember { mutableStateOf(false) }
var imeBottomInsetPx by remember { mutableIntStateOf(0) } var imeBottomInsetPx by remember { mutableIntStateOf(0) }
@@ -189,6 +199,9 @@ fun ChatAttachAlert(
AttachAlertDebugLog.log("AttachAlert", "hideKeyboard() called, captionInputActive=$captionInputActive, shouldShow=$shouldShow, isClosing=$isClosing") AttachAlertDebugLog.log("AttachAlert", "hideKeyboard() called, captionInputActive=$captionInputActive, shouldShow=$shouldShow, isClosing=$isClosing")
pendingCaptionFocus = false pendingCaptionFocus = false
captionInputActive = false captionInputActive = false
showEmojiPicker = false
coordinator.isEmojiVisible = false
coordinator.isEmojiBoxVisible = false
focusManager.clearFocus(force = true) focusManager.clearFocus(force = true)
captionEditTextView?.clearFocus() captionEditTextView?.clearFocus()
@@ -218,6 +231,61 @@ fun ChatAttachAlert(
pendingCaptionFocus = true pendingCaptionFocus = true
} }
// Telegram-style emoji toggle using KeyboardTransitionCoordinator
fun toggleEmojiPicker() {
val currentTime = System.currentTimeMillis()
if (currentTime - lastToggleTime < toggleCooldownMs) return
lastToggleTime = currentTime
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (coordinator.isEmojiVisible) {
// EMOJI → KEYBOARD
coordinator.requestShowKeyboard(
showKeyboard = {
captionInputActive = true
popupRootView?.let { updatePopupImeFocusable(it, true) }
captionEditTextView?.let { editText ->
editText.requestFocus()
@Suppress("DEPRECATION")
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}
},
hideEmoji = { showEmojiPicker = false }
)
} else if (isCaptionKeyboardVisible || imeBottomInsetPx > 0) {
// KEYBOARD → EMOJI
coordinator.requestShowEmoji(
hideKeyboard = {
captionEditTextView?.windowToken?.let { token ->
imm.hideSoftInputFromWindow(token, 0)
}
},
showEmoji = { showEmojiPicker = true }
)
} else {
// Nothing visible → open emoji directly
if (coordinator.emojiHeight == 0.dp) {
// Use saved keyboard height minus nav bar (same as spacer)
val savedPx = KeyboardHeightProvider.getSavedKeyboardHeight(context)
val navBarPx = (context as? Activity)?.window?.decorView?.let { view ->
androidx.core.view.ViewCompat.getRootWindowInsets(view)
?.getInsets(androidx.core.view.WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
} ?: 0
val effectivePx = if (savedPx > 0) (savedPx - navBarPx).coerceAtLeast(0) else 0
coordinator.emojiHeight = if (effectivePx > 0) {
with(density) { effectivePx.toDp() }
} else {
260.dp
}
}
coordinator.isEmojiBoxVisible = true
coordinator.openEmojiOnly(
showEmoji = { showEmojiPicker = true }
)
}
}
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// Layout metrics // Layout metrics
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
@@ -233,7 +301,9 @@ fun ChatAttachAlert(
(screenHeightPx + statusBarInsetPx).coerceAtLeast(screenHeightPx * 0.95f) (screenHeightPx + statusBarInsetPx).coerceAtLeast(screenHeightPx * 0.95f)
val minHeightPx = screenHeightPx * 0.2f val minHeightPx = screenHeightPx * 0.2f
val captionLiftEasing = CubicBezierEasing(0f, 0f, 0.2f, 1f) val captionLiftEasing = CubicBezierEasing(0f, 0f, 0.2f, 1f)
val telegramButtonsEasing = CubicBezierEasing(0f, 0f, 0.58f, 1f) // Telegram uses DecelerateInterpolator for all showCommentTextView animations.
// CubicBezier(0,0,0.2,1) approximates DecelerateInterpolator — unify to one easing.
val telegramButtonsEasing = captionLiftEasing
val collapsedStateHeightPx = collapsedHeightPx.coerceAtMost(expandedHeightPx) val collapsedStateHeightPx = collapsedHeightPx.coerceAtMost(expandedHeightPx)
val selectionHeaderExtraPx = with(density) { 26.dp.toPx() } val selectionHeaderExtraPx = with(density) { 26.dp.toPx() }
@@ -607,14 +677,12 @@ fun ChatAttachAlert(
window.statusBarColor = if (dark) 0xFF1C1C1E.toInt() else 0xFFFFFFFF.toInt() window.statusBarColor = if (dark) 0xFF1C1C1E.toInt() else 0xFFFFFFFF.toInt()
insetsController?.isAppearanceLightStatusBars = !dark insetsController?.isAppearanceLightStatusBars = !dark
} else { } else {
window.statusBarColor = android.graphics.Color.argb( // Apply scrim to status bar so it matches the overlay darkness
(alpha * 255).toInt().coerceIn(0, 255), 0, 0, 0 val scrimInt = (alpha * 255).toInt().coerceIn(0, 255)
) window.statusBarColor = android.graphics.Color.argb(scrimInt, 0, 0, 0)
insetsController?.isAppearanceLightStatusBars = false insetsController?.isAppearanceLightStatusBars = false
} }
window.navigationBarColor = android.graphics.Color.argb( window.navigationBarColor = android.graphics.Color.TRANSPARENT
(alpha * 255).toInt().coerceIn(0, 255), 0, 0, 0
)
insetsController?.isAppearanceLightNavigationBars = alpha < 0.15f insetsController?.isAppearanceLightNavigationBars = alpha < 0.15f
} }
} }
@@ -686,26 +754,66 @@ fun ChatAttachAlert(
} }
} }
// Telegram approach: the sheet height NEVER changes for keyboard.
// We compute the keyboard height only for a bottom Spacer inside the Column.
val keyboardInsetPx = val keyboardInsetPx =
(imeBottomInsetPx.toFloat() - navigationBarInsetPx).coerceAtLeast(0f) (imeBottomInsetPx.toFloat() - navigationBarInsetPx).coerceAtLeast(0f)
// Only shrink the sheet for keyboard when caption input is active. val keyboardSpacerPxRaw =
// When FLAG_ALT_FOCUSABLE_IM is set, keyboard belongs to chat input behind picker.
val targetKeyboardInsetPx =
if (state.selectedItemOrder.isNotEmpty() && captionInputActive && !isClosing) keyboardInsetPx else 0f if (state.selectedItemOrder.isNotEmpty() && captionInputActive && !isClosing) keyboardInsetPx else 0f
// Smooth 250ms animation matching Telegram's AdjustPanLayoutHelper duration.
// This interpolates the keyboard offset so the sheet slides up/down smoothly
// instead of jumping instantly when the IME appears/disappears.
val animatedKeyboardInsetPx by animateFloatAsState(
targetValue = targetKeyboardInsetPx,
animationSpec = tween(250, easing = FastOutSlowInEasing),
label = "keyboard_inset_anim"
)
val navBarDp = with(density) { navigationBarInsetPx.toDp() }
// Log sheet geometry on every recomposition where values are interesting // Track whether emoji box was previously visible to detect the moment it disappears
if (imeBottomInsetPx > 0 || captionInputActive) { val prevEmojiBoxVisible = remember { mutableStateOf(false) }
AttachAlertDebugLog.log("AttachAlert", "GEOM: imePx=$imeBottomInsetPx, navBarPx=${navigationBarInsetPx.toInt()}, kbInsetPx=${keyboardInsetPx.toInt()}, targetKbPx=${targetKeyboardInsetPx.toInt()}, animatedKbPx=${animatedKeyboardInsetPx.toInt()}, sheetPx=${sheetHeightPx.value.toInt()}, captionActive=$captionInputActive, selected=${state.selectedItemOrder.size}, shouldShow=$shouldShow, isClosing=$isClosing") val emojiBoxCurrentlyVisible = coordinator.isEmojiBoxVisible
val justDismissedEmoji = prevEmojiBoxVisible.value && !emojiBoxCurrentlyVisible
// Update tracking ASAP
SideEffect { prevEmojiBoxVisible.value = emojiBoxCurrentlyVisible }
// Smooth spring animation on the spacer for a fluid keyboard transition
// BUT: snap instantly when emoji box disappears (to avoid input dropping)
val keyboardSpacerAnim = remember { Animatable(0f) }
LaunchedEffect(keyboardSpacerPxRaw, emojiBoxCurrentlyVisible, justDismissedEmoji) {
if (justDismissedEmoji) {
// Emoji box just disappeared → snap spacer to current keyboard height
keyboardSpacerAnim.snapTo(keyboardSpacerPxRaw)
} else if (!emojiBoxCurrentlyVisible) {
// Normal case: animate with spring
keyboardSpacerAnim.animateTo(
keyboardSpacerPxRaw,
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = 400f
)
)
} }
// When emoji box is visible, spacer is not rendered—no animation needed
}
val keyboardSpacerPx = keyboardSpacerAnim.value
val keyboardSpacerDp = with(density) { keyboardSpacerPx.toDp() }
// Feed coordinator the same nav-bar-subtracted keyboard height as the spacer.
// This way emoji picker height == spacer height == exact keyboard visual area.
LaunchedEffect(Unit) {
snapshotFlow {
with(density) {
(imeBottomInsetPx.toFloat() - navigationBarInsetPx).coerceAtLeast(0f).toDp()
}
}.collect { keyboardHeightDp ->
coordinator.updateKeyboardHeight(keyboardHeightDp)
if (keyboardHeightDp > 100.dp) {
coordinator.syncHeights()
}
}
}
// Compute bottom padding for send button: emoji panel height or keyboard spacer
val bottomInputPadding = if (coordinator.isEmojiBoxVisible) {
coordinator.emojiHeight
} else keyboardSpacerDp
// When keyboard or emoji is open, nav bar is behind — don't pad for it
val navBarDp = if (keyboardSpacerPx > 0f || coordinator.isEmojiBoxVisible) 0.dp
else with(density) { navigationBarInsetPx.toDp() }
Box( Box(
modifier = Modifier modifier = Modifier
@@ -718,8 +826,9 @@ fun ChatAttachAlert(
.padding(bottom = navBarDp), .padding(bottom = navBarDp),
contentAlignment = Alignment.BottomCenter contentAlignment = Alignment.BottomCenter
) { ) {
val visibleSheetHeightPx = // Sheet height stays constant — keyboard space is handled by
(sheetHeightPx.value - animatedKeyboardInsetPx).coerceAtLeast(minHeightPx) // internal Spacer, not by shrinking the container (Telegram approach).
val visibleSheetHeightPx = sheetHeightPx.value.coerceAtLeast(minHeightPx)
val currentHeightDp = with(density) { visibleSheetHeightPx.toDp() } val currentHeightDp = with(density) { visibleSheetHeightPx.toDp() }
val slideOffset = (visibleSheetHeightPx * animatedOffset).toInt() val slideOffset = (visibleSheetHeightPx * animatedOffset).toInt()
val expandProgress = val expandProgress =
@@ -936,15 +1045,39 @@ fun ChatAttachAlert(
captionText = state.captionText, captionText = state.captionText,
onCaptionChange = { viewModel.updateCaption(it) }, onCaptionChange = { viewModel.updateCaption(it) },
isCaptionKeyboardVisible = isCaptionKeyboardVisible, isCaptionKeyboardVisible = isCaptionKeyboardVisible,
onToggleKeyboard = { isEmojiVisible = showEmojiPicker,
if (isCaptionKeyboardVisible) hideKeyboard() onToggleKeyboard = { toggleEmojiPicker() },
else focusCaptionInput()
},
onFocusCaption = { focusCaptionInput() }, onFocusCaption = { focusCaptionInput() },
onViewCreated = { captionEditTextView = it }, onViewCreated = { captionEditTextView = it },
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
density = density density = density
) )
// ── Emoji Picker via AnimatedKeyboardTransition ──
AnimatedKeyboardTransition(
coordinator = coordinator,
showEmojiPicker = showEmojiPicker
) {
OptimizedEmojiPicker(
isVisible = true,
isDarkTheme = isDarkTheme,
onEmojiSelected = { emoji ->
viewModel.updateCaption(state.captionText + emoji)
captionEditTextView?.let { editText ->
val newText = state.captionText + emoji
editText.setText(newText)
editText.setSelection(newText.length)
}
},
onClose = { toggleEmojiPicker() },
modifier = Modifier.fillMaxSize()
)
}
// ── Keyboard spacer (Telegram approach, only when emoji box not shown) ──
if (!coordinator.isEmojiBoxVisible) {
Spacer(modifier = Modifier.height(keyboardSpacerDp))
}
} // end Column } // end Column
// ── Floating Send Button ── // ── Floating Send Button ──
@@ -952,7 +1085,7 @@ fun ChatAttachAlert(
val sendButtonVisible = state.selectedItemOrder.isNotEmpty() val sendButtonVisible = state.selectedItemOrder.isNotEmpty()
val sendButtonProgress by animateFloatAsState( val sendButtonProgress by animateFloatAsState(
targetValue = if (sendButtonVisible) 1f else 0f, targetValue = if (sendButtonVisible) 1f else 0f,
animationSpec = tween(180, easing = FastOutSlowInEasing), animationSpec = tween(180, easing = captionLiftEasing),
label = "send_button_progress" label = "send_button_progress"
) )
FloatingSendButton( FloatingSendButton(
@@ -966,120 +1099,11 @@ fun ChatAttachAlert(
onMediaSelected(selected, caption) onMediaSelected(selected, caption)
animatedClose() animatedClose()
}, },
modifier = Modifier.align(Alignment.BottomEnd) modifier = Modifier
.align(Alignment.BottomEnd)
.padding(bottom = bottomInputPadding)
) )
// ── Debug Log Button ──
var showDebugLogs by remember { mutableStateOf(false) }
// Small debug FAB in top-left corner
Box(
modifier = Modifier
.align(Alignment.TopStart)
.padding(start = 8.dp, top = 8.dp)
.size(28.dp)
.clip(RoundedCornerShape(6.dp))
.background(Color.Red.copy(alpha = 0.7f))
.clickable { showDebugLogs = !showDebugLogs },
contentAlignment = Alignment.Center
) {
Text("L", color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.Bold)
}
// Debug log overlay
if (showDebugLogs) {
val logEntries = remember { mutableStateOf(AttachAlertDebugLog.entries) }
// Refresh logs periodically
LaunchedEffect(showDebugLogs) {
while (true) {
logEntries.value = AttachAlertDebugLog.entries
kotlinx.coroutines.delay(500)
}
}
Box(
modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth(0.92f)
.fillMaxHeight(0.7f)
.clip(RoundedCornerShape(12.dp))
.background(Color.Black.copy(alpha = 0.92f))
.padding(8.dp)
) {
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"AttachAlert Debug (${logEntries.value.size})",
color = Color.Green,
fontSize = 13.sp,
fontWeight = FontWeight.Bold
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
"CLEAR",
color = Color.Yellow,
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.clickable {
AttachAlertDebugLog.clear()
logEntries.value = emptyList()
}
.padding(horizontal = 6.dp, vertical = 2.dp)
)
Text(
"",
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.clickable { showDebugLogs = false }
.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}
Spacer(Modifier.height(4.dp))
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(6.dp))
.background(Color(0xFF1A1A1A))
) {
val logListState = rememberLazyListState()
LaunchedEffect(logEntries.value.size) {
if (logEntries.value.isNotEmpty()) {
logListState.animateScrollToItem(logEntries.value.size - 1)
}
}
LazyColumn(
state = logListState,
modifier = Modifier.fillMaxSize().padding(6.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
items(logEntries.value) { entry ->
val entryColor = when {
entry.contains("[W/") -> Color(0xFFFF9800)
entry.contains("result=true") || entry.contains("OK") -> Color(0xFF4CAF50)
entry.contains("result=false") || entry.contains("failed") || entry.contains("NULL") -> Color(0xFFFF5252)
else -> Color(0xFFB0B0B0)
}
Text(
text = entry,
color = entryColor,
fontSize = 10.sp,
lineHeight = 13.sp,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
}
}
} // end Box sheet container } // end Box sheet container
} }
} }

View File

@@ -1267,12 +1267,20 @@ fun ImageAttachment(
) )
} }
MessageStatus.READ -> { MessageStatus.READ -> {
Box(modifier = Modifier.height(14.dp)) {
Icon( Icon(
painter = TelegramIcons.Done, painter = TelegramIcons.Done,
contentDescription = null, contentDescription = null,
tint = Color.White, tint = Color.White,
modifier = Modifier.size(14.dp) modifier = Modifier.size(14.dp)
) )
Icon(
painter = TelegramIcons.Done,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(14.dp).offset(x = 4.dp)
)
}
} }
else -> {} else -> {}
} }
@@ -1693,6 +1701,7 @@ fun FileAttachment(
) )
} }
MessageStatus.READ -> { MessageStatus.READ -> {
Box(modifier = Modifier.height(14.dp)) {
Icon( Icon(
painter = TelegramIcons.Done, painter = TelegramIcons.Done,
contentDescription = null, contentDescription = null,
@@ -1700,6 +1709,14 @@ fun FileAttachment(
if (isDarkTheme) Color.White else Color(0xFF4FC3F7), if (isDarkTheme) Color.White else Color(0xFF4FC3F7),
modifier = Modifier.size(14.dp) modifier = Modifier.size(14.dp)
) )
Icon(
painter = TelegramIcons.Done,
contentDescription = null,
tint =
if (isDarkTheme) Color.White else Color(0xFF4FC3F7),
modifier = Modifier.size(14.dp).offset(x = 4.dp)
)
}
} }
MessageStatus.ERROR -> { MessageStatus.ERROR -> {
Icon( Icon(
@@ -2089,12 +2106,20 @@ fun AvatarAttachment(
) )
} }
MessageStatus.READ -> { MessageStatus.READ -> {
Box(modifier = Modifier.height(14.dp)) {
Icon( Icon(
painter = TelegramIcons.Done, painter = TelegramIcons.Done,
contentDescription = "Read", contentDescription = "Read",
tint = Color.White, tint = Color.White,
modifier = Modifier.size(14.dp) modifier = Modifier.size(14.dp)
) )
Icon(
painter = TelegramIcons.Done,
contentDescription = "Read",
tint = Color.White,
modifier = Modifier.size(14.dp).offset(x = 4.dp)
)
}
} }
MessageStatus.ERROR -> { MessageStatus.ERROR -> {
Icon( Icon(

View File

@@ -34,6 +34,7 @@ import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Layout
@@ -41,6 +42,7 @@ import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -292,7 +294,9 @@ fun MessageBubble(
contextMenuContent: @Composable () -> Unit = {} contextMenuContent: @Composable () -> Unit = {}
) { ) {
// Swipe-to-reply state // Swipe-to-reply state
val hapticFeedback = LocalHapticFeedback.current
var swipeOffset by remember { mutableStateOf(0f) } var swipeOffset by remember { mutableStateOf(0f) }
var hapticTriggered by remember { mutableStateOf(false) }
val swipeThreshold = 80f val swipeThreshold = 80f
val maxSwipe = 120f val maxSwipe = 120f
@@ -402,8 +406,12 @@ fun MessageBubble(
onSwipeToReply() onSwipeToReply()
} }
swipeOffset = 0f swipeOffset = 0f
hapticTriggered = false
},
onDragCancel = {
swipeOffset = 0f
hapticTriggered = false
}, },
onDragCancel = { swipeOffset = 0f },
onHorizontalDrag = { change, dragAmount -> onHorizontalDrag = { change, dragAmount ->
// Только свайп влево (отрицательное значение) // Только свайп влево (отрицательное значение)
if (dragAmount < 0 || swipeOffset < 0) { if (dragAmount < 0 || swipeOffset < 0) {
@@ -411,6 +419,15 @@ fun MessageBubble(
val newOffset = swipeOffset + dragAmount val newOffset = swipeOffset + dragAmount
swipeOffset = swipeOffset =
newOffset.coerceIn(-maxSwipe, 0f) newOffset.coerceIn(-maxSwipe, 0f)
// Haptic при достижении порога
if (swipeOffset <= -swipeThreshold && !hapticTriggered) {
hapticTriggered = true
hapticFeedback.performHapticFeedback(
HapticFeedbackType.LongPress
)
} else if (swipeOffset > -swipeThreshold && hapticTriggered) {
hapticTriggered = false
}
} }
} }
) )

View File

@@ -3,10 +3,12 @@ package com.rosetta.messenger.ui.chats.components
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import android.view.ScaleGestureDetector
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException import androidx.camera.core.ImageCaptureException
@@ -28,6 +30,8 @@ import androidx.compose.material.icons.filled.FlipCameraAndroid
import androidx.compose.material.icons.filled.FlashOff import androidx.compose.material.icons.filled.FlashOff
import androidx.compose.material.icons.filled.FlashOn import androidx.compose.material.icons.filled.FlashOn
import androidx.compose.material.icons.filled.FlashAuto import androidx.compose.material.icons.filled.FlashAuto
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Remove
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
@@ -39,7 +43,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -90,6 +97,10 @@ fun InAppCameraScreen(
// Camera references // Camera references
var imageCapture by remember { mutableStateOf<ImageCapture?>(null) } var imageCapture by remember { mutableStateOf<ImageCapture?>(null) }
var cameraProvider by remember { mutableStateOf<ProcessCameraProvider?>(null) } var cameraProvider by remember { mutableStateOf<ProcessCameraProvider?>(null) }
var camera by remember { mutableStateOf<Camera?>(null) }
// Zoom state — linear 0..1 progress synced with camera
var zoomLinearProgress by remember { mutableFloatStateOf(0.5f) }
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// 🎬 FADE ANIMATION (как в ImageEditorScreen) // 🎬 FADE ANIMATION (как в ImageEditorScreen)
@@ -157,7 +168,7 @@ fun InAppCameraScreen(
val progress = animationProgress.value val progress = animationProgress.value
val currentStatusColor = androidx.core.graphics.ColorUtils.blendARGB( val currentStatusColor = androidx.core.graphics.ColorUtils.blendARGB(
originalStatusBarColor, android.graphics.Color.BLACK, progress originalStatusBarColor, android.graphics.Color.TRANSPARENT, progress
) )
val currentNavColor = androidx.core.graphics.ColorUtils.blendARGB( val currentNavColor = androidx.core.graphics.ColorUtils.blendARGB(
originalNavigationBarColor, android.graphics.Color.BLACK, progress originalNavigationBarColor, android.graphics.Color.BLACK, progress
@@ -262,12 +273,13 @@ fun InAppCameraScreen(
try { try {
provider.unbindAll() provider.unbindAll()
provider.bindToLifecycle( camera = provider.bindToLifecycle(
lifecycleOwner, lifecycleOwner,
cameraSelector, cameraSelector,
preview, preview,
capture capture
) )
camera?.cameraControl?.setLinearZoom(zoomLinearProgress)
} catch (e: Exception) { } catch (e: Exception) {
} }
} }
@@ -290,7 +302,7 @@ fun InAppCameraScreen(
.graphicsLayer { alpha = animationProgress.value } .graphicsLayer { alpha = animationProgress.value }
.background(Color.Black) .background(Color.Black)
) { ) {
// Camera Preview // Camera Preview with pinch-to-zoom
AndroidView( AndroidView(
factory = { ctx -> factory = { ctx ->
PreviewView(ctx).apply { PreviewView(ctx).apply {
@@ -301,6 +313,31 @@ fun InAppCameraScreen(
scaleType = PreviewView.ScaleType.FILL_CENTER scaleType = PreviewView.ScaleType.FILL_CENTER
implementationMode = PreviewView.ImplementationMode.COMPATIBLE implementationMode = PreviewView.ImplementationMode.COMPATIBLE
previewView = this previewView = this
// Pinch-to-zoom via ScaleGestureDetector
val scaleGestureDetector = ScaleGestureDetector(ctx,
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val cam = camera ?: return true
val zoomState = cam.cameraInfo.zoomState.value ?: return true
val currentZoom = zoomState.zoomRatio
val newZoom = currentZoom * detector.scaleFactor
cam.cameraControl.setZoomRatio(newZoom)
// Sync slider with pinch: convert ratio to linear 0..1
val minZoom = zoomState.minZoomRatio
val maxZoom = zoomState.maxZoomRatio
val clampedZoom = newZoom.coerceIn(minZoom, maxZoom)
zoomLinearProgress = if (maxZoom > minZoom)
((clampedZoom - minZoom) / (maxZoom - minZoom)).coerceIn(0f, 1f)
else 0f
return true
}
}
)
setOnTouchListener { _, event ->
scaleGestureDetector.onTouchEvent(event)
true
}
} }
}, },
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -357,6 +394,111 @@ fun InAppCameraScreen(
} }
} }
// ── Zoom slider with /+ buttons (Telegram-style) ──
val zoomState = camera?.cameraInfo?.zoomState?.value
val currentZoomRatio = zoomState?.zoomRatio ?: 1f
val minZoomRatio = zoomState?.minZoomRatio ?: 1f
val maxZoomRatio = zoomState?.maxZoomRatio ?: 1f
val zoomLabel = "%.1fx".format(currentZoomRatio)
// Only show slider if camera supports zoom range > 1
if (maxZoomRatio > minZoomRatio + 0.1f) {
Column(
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(bottom = 130.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Zoom ratio label (e.g. "2.3x")
Text(
text = zoomLabel,
color = Color.White,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center,
modifier = Modifier
.background(Color.Black.copy(alpha = 0.4f), RoundedCornerShape(10.dp))
.padding(horizontal = 8.dp, vertical = 2.dp)
)
Spacer(modifier = Modifier.height(6.dp))
// Slider row: [] ━━━━━━━━━ [+]
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Minus button
Box(
modifier = Modifier
.size(32.dp)
.background(Color.Black.copy(alpha = 0.4f), CircleShape)
.clip(CircleShape)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
val newProgress = (zoomLinearProgress - 0.1f).coerceIn(0f, 1f)
zoomLinearProgress = newProgress
camera?.cameraControl?.setLinearZoom(newProgress)
},
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Remove,
contentDescription = "Zoom out",
tint = Color.White,
modifier = Modifier.size(18.dp)
)
}
// Slider track
Slider(
value = zoomLinearProgress,
onValueChange = { newValue ->
zoomLinearProgress = newValue
camera?.cameraControl?.setLinearZoom(newValue)
},
modifier = Modifier
.weight(1f)
.padding(horizontal = 4.dp),
colors = SliderDefaults.colors(
thumbColor = Color.White,
activeTrackColor = Color.White,
inactiveTrackColor = Color.White.copy(alpha = 0.3f)
)
)
// Plus button
Box(
modifier = Modifier
.size(32.dp)
.background(Color.Black.copy(alpha = 0.4f), CircleShape)
.clip(CircleShape)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
val newProgress = (zoomLinearProgress + 0.1f).coerceIn(0f, 1f)
zoomLinearProgress = newProgress
camera?.cameraControl?.setLinearZoom(newProgress)
},
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Add,
contentDescription = "Zoom in",
tint = Color.White,
modifier = Modifier.size(18.dp)
)
}
}
}
}
// Bottom controls (Shutter + Flip) // Bottom controls (Shutter + Flip)
Row( Row(
modifier = Modifier modifier = Modifier

View File

@@ -43,7 +43,10 @@ class AppleEmojiEditTextView @JvmOverloads constructor(
) : EditText(context, attrs, defStyleAttr) { ) : EditText(context, attrs, defStyleAttr) {
var onTextChange: ((String) -> Unit)? = null var onTextChange: ((String) -> 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 private var isUpdating = false
private var lastMeasuredHeight: Int = 0
companion object { companion object {
// Regex для эмодзи - public для доступа из других компонентов // Regex для эмодзи - public для доступа из других компонентов
@@ -123,6 +126,14 @@ class AppleEmojiEditTextView @JvmOverloads constructor(
}) })
} }
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (oldh != 0 && h != oldh) {
onHeightChanged?.invoke(oldh, h)
}
lastMeasuredHeight = h
}
fun setTextWithEmojis(newText: String) { fun setTextWithEmojis(newText: String) {
if (newText == text.toString()) return if (newText == text.toString()) return
isUpdating = true isUpdating = true
@@ -269,7 +280,8 @@ fun AppleEmojiTextField(
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, onViewCreated: ((AppleEmojiEditTextView) -> Unit)? = null,
requestFocus: Boolean = false, requestFocus: Boolean = false,
onFocusChanged: ((Boolean) -> Unit)? = null onFocusChanged: ((Boolean) -> Unit)? = null,
onHeightChanged: ((oldHeight: Int, newHeight: Int) -> Unit)? = null
) { ) {
// Храним ссылку на view для управления фокусом // Храним ссылку на view для управления фокусом
var editTextView by remember { mutableStateOf<AppleEmojiEditTextView?>(null) } var editTextView by remember { mutableStateOf<AppleEmojiEditTextView?>(null) }
@@ -292,6 +304,7 @@ fun AppleEmojiTextField(
setHint(hint) setHint(hint)
setTextSize(textSize) setTextSize(textSize)
onTextChange = onValueChange onTextChange = onValueChange
this.onHeightChanged = onHeightChanged
// Убираем все возможные фоны у EditText // Убираем все возможные фоны у EditText
background = null background = null
setBackgroundColor(android.graphics.Color.TRANSPARENT) setBackgroundColor(android.graphics.Color.TRANSPARENT)

View File

@@ -130,7 +130,7 @@ private fun EmojiPickerContent(
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.height(keyboardHeight) .heightIn(max = keyboardHeight)
.background(panelBackground) .background(panelBackground)
) { ) {
// ============ КАТЕГОРИИ (синхронизированы с pager) ============ // ============ КАТЕГОРИИ (синхронизированы с pager) ============