feat: Remove TelegramInputBar component to streamline chat interface
This commit is contained in:
@@ -1,506 +0,0 @@
|
|||||||
package com.rosetta.messenger.ui.chats
|
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.animation.core.*
|
|
||||||
import androidx.compose.foundation.*
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.rosetta.messenger.ui.components.AppleEmojiPickerPanel
|
|
||||||
import com.rosetta.messenger.ui.components.AppleEmojiTextField
|
|
||||||
// Using TelegramEasing from ChatDetailScreen.kt
|
|
||||||
|
|
||||||
// Attach menu items
|
|
||||||
data class AttachMenuItem(
|
|
||||||
val icon: @Composable () -> Unit,
|
|
||||||
val label: String,
|
|
||||||
val color: Color,
|
|
||||||
val onClick: () -> Unit
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Telegram-style input bar - exact 1:1 replica
|
|
||||||
* Based on ChatActivityEnterView.java from Telegram Android source
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
|
||||||
fun TelegramInputBar(
|
|
||||||
value: String,
|
|
||||||
onValueChange: (String) -> Unit,
|
|
||||||
onSend: () -> Unit,
|
|
||||||
isDarkTheme: Boolean,
|
|
||||||
onAttachPhoto: () -> Unit = {},
|
|
||||||
onAttachFile: () -> Unit = {},
|
|
||||||
onAttachLocation: () -> Unit = {},
|
|
||||||
onAttachContact: () -> Unit = {},
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
|
|
||||||
// Focus & keyboard management
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
|
|
||||||
|
|
||||||
// States
|
|
||||||
var showEmojiPicker by remember { mutableStateOf(false) }
|
|
||||||
var showAttachMenu by remember { mutableStateOf(false) }
|
|
||||||
var isKeyboardVisible by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// Can send message
|
|
||||||
val canSend = value.isNotBlank()
|
|
||||||
|
|
||||||
// Send button animation (250ms CubicBezier like Telegram)
|
|
||||||
val sendScale by animateFloatAsState(
|
|
||||||
targetValue = if (canSend) 1f else 0.1f,
|
|
||||||
animationSpec = tween(durationMillis = 250, easing = TelegramEasing),
|
|
||||||
label = "send scale"
|
|
||||||
)
|
|
||||||
|
|
||||||
val sendAlpha by animateFloatAsState(
|
|
||||||
targetValue = if (canSend) 1f else 0f,
|
|
||||||
animationSpec = tween(durationMillis = 250, easing = TelegramEasing),
|
|
||||||
label = "send alpha"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Mic button animation
|
|
||||||
val micScale by animateFloatAsState(
|
|
||||||
targetValue = if (canSend) 0.1f else 1f,
|
|
||||||
animationSpec = tween(durationMillis = 250, easing = TelegramEasing),
|
|
||||||
label = "mic scale"
|
|
||||||
)
|
|
||||||
|
|
||||||
val micAlpha by animateFloatAsState(
|
|
||||||
targetValue = if (canSend) 0f else 1f,
|
|
||||||
animationSpec = tween(durationMillis = 250, easing = TelegramEasing),
|
|
||||||
label = "mic alpha"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Attach button rotation animation (like Telegram)
|
|
||||||
val attachRotation by animateFloatAsState(
|
|
||||||
targetValue = if (showAttachMenu) 45f else 0f,
|
|
||||||
animationSpec = tween(durationMillis = 200, easing = TelegramEasing),
|
|
||||||
label = "attach rotation"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Our custom colors (not Telegram's gray)
|
|
||||||
val primaryBlue = Color(0xFF007AFF)
|
|
||||||
val inputPanelBackground = if (isDarkTheme) Color(0xFF212121) else Color(0xFFFFFFFF)
|
|
||||||
val iconColor = if (isDarkTheme) Color.White.copy(alpha = 0.7f) else Color(0xFF8E8E93)
|
|
||||||
val activeIconColor = primaryBlue
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val hintColor = if (isDarkTheme) Color.White.copy(alpha = 0.4f) else Color(0xFF999999)
|
|
||||||
val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.12f) else Color.Black.copy(alpha = 0.08f)
|
|
||||||
|
|
||||||
// Attach menu items with colors
|
|
||||||
val attachMenuItems = remember {
|
|
||||||
listOf(
|
|
||||||
AttachMenuItem(
|
|
||||||
icon = { Icon(Icons.Default.Image, contentDescription = null, tint = Color.White, modifier = Modifier.size(24.dp)) },
|
|
||||||
label = "Photo",
|
|
||||||
color = Color(0xFF007AFF),
|
|
||||||
onClick = onAttachPhoto
|
|
||||||
),
|
|
||||||
AttachMenuItem(
|
|
||||||
icon = { Icon(Icons.Default.InsertDriveFile, contentDescription = null, tint = Color.White, modifier = Modifier.size(24.dp)) },
|
|
||||||
label = "File",
|
|
||||||
color = Color(0xFF34C759),
|
|
||||||
onClick = onAttachFile
|
|
||||||
),
|
|
||||||
AttachMenuItem(
|
|
||||||
icon = { Icon(Icons.Default.LocationOn, contentDescription = null, tint = Color.White, modifier = Modifier.size(24.dp)) },
|
|
||||||
label = "Location",
|
|
||||||
color = Color(0xFFFF9500),
|
|
||||||
onClick = onAttachLocation
|
|
||||||
),
|
|
||||||
AttachMenuItem(
|
|
||||||
icon = { Icon(Icons.Default.Person, contentDescription = null, tint = Color.White, modifier = Modifier.size(24.dp)) },
|
|
||||||
label = "Contact",
|
|
||||||
color = Color(0xFFAF52DE),
|
|
||||||
onClick = onAttachContact
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle emoji picker (Telegram behavior)
|
|
||||||
fun toggleEmojiPicker() {
|
|
||||||
if (showEmojiPicker) {
|
|
||||||
// Closing emoji - show keyboard
|
|
||||||
showEmojiPicker = false
|
|
||||||
isKeyboardVisible = true
|
|
||||||
} else {
|
|
||||||
// Opening emoji - hide keyboard first
|
|
||||||
keyboardController?.hide()
|
|
||||||
focusManager.clearFocus()
|
|
||||||
showAttachMenu = false
|
|
||||||
showEmojiPicker = true
|
|
||||||
isKeyboardVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open keyboard (when tapping on text field or closing emoji)
|
|
||||||
fun openKeyboard() {
|
|
||||||
showEmojiPicker = false
|
|
||||||
showAttachMenu = false
|
|
||||||
isKeyboardVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle attach menu
|
|
||||||
fun toggleAttachMenu() {
|
|
||||||
if (showAttachMenu) {
|
|
||||||
showAttachMenu = false
|
|
||||||
} else {
|
|
||||||
keyboardController?.hide()
|
|
||||||
focusManager.clearFocus()
|
|
||||||
showEmojiPicker = false
|
|
||||||
showAttachMenu = true
|
|
||||||
isKeyboardVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleSend() {
|
|
||||||
if (value.isNotBlank()) {
|
|
||||||
onSend()
|
|
||||||
onValueChange("")
|
|
||||||
showEmojiPicker = false
|
|
||||||
showAttachMenu = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.then(if (!showEmojiPicker && !showAttachMenu) Modifier.imePadding() else Modifier)
|
|
||||||
) {
|
|
||||||
// Attach menu (bottom sheet style like Telegram)
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = showAttachMenu,
|
|
||||||
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(tween(200)),
|
|
||||||
exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + fadeOut(tween(150))
|
|
||||||
) {
|
|
||||||
AttachMenuPanel(
|
|
||||||
items = attachMenuItems,
|
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
onDismiss = { showAttachMenu = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Telegram input panel container
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(inputPanelBackground)
|
|
||||||
) {
|
|
||||||
// Top divider (как в Telegram)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopCenter)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(0.5.dp)
|
|
||||||
.background(dividerColor)
|
|
||||||
)
|
|
||||||
|
|
||||||
// textFieldContainer - padding(0, dp(1), 0, 0)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 1.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(min = 48.dp, max = 192.dp) // DEFAULT_HEIGHT = 48dp, max 6 lines
|
|
||||||
.padding(start = 3.dp, end = 0.dp, bottom = 0.dp),
|
|
||||||
verticalAlignment = Alignment.Bottom
|
|
||||||
) {
|
|
||||||
// EMOJI BUTTON - 48dp, toggles between emoji/keyboard icon
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null,
|
|
||||||
onClick = { toggleEmojiPicker() }
|
|
||||||
)
|
|
||||||
.padding(7.5.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
// Crossfade animation between emoji and keyboard icons
|
|
||||||
Crossfade(
|
|
||||||
targetState = showEmojiPicker,
|
|
||||||
animationSpec = tween(200),
|
|
||||||
label = "emoji icon"
|
|
||||||
) { isEmojiOpen ->
|
|
||||||
Icon(
|
|
||||||
imageVector = if (isEmojiOpen) Icons.Default.Keyboard else Icons.Default.EmojiEmotions,
|
|
||||||
contentDescription = if (isEmojiOpen) "Keyboard" else "Emoji",
|
|
||||||
tint = if (isEmojiOpen) activeIconColor else iconColor,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MESSAGE EDIT TEXT CONTAINER - weight(1), margin right 50dp for attachLayout
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.heightIn(min = 48.dp, max = 192.dp)
|
|
||||||
.padding(end = 50.dp)
|
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null
|
|
||||||
) {
|
|
||||||
// Tap on text field - close emoji/attach, open keyboard
|
|
||||||
if (showEmojiPicker || showAttachMenu) {
|
|
||||||
openKeyboard()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
) {
|
|
||||||
// EditText - setTextSize(18sp), setPadding(0, dp(9), 0, dp(10))
|
|
||||||
AppleEmojiTextField(
|
|
||||||
value = value,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
textColor = textColor,
|
|
||||||
textSize = 18f, // EXACT 18sp from Telegram
|
|
||||||
hint = "Message",
|
|
||||||
hintColor = hintColor,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 0.dp, end = 0.dp, top = 9.dp, bottom = 10.dp) // EXACT padding from Telegram
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ATTACH LAYOUT (positioned absolutely at right) - contains attachButton
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.width(50.dp)
|
|
||||||
.height(48.dp)
|
|
||||||
.padding(end = 0.dp)
|
|
||||||
) {
|
|
||||||
// ATTACH BUTTON - 48dp with rotation animation
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null,
|
|
||||||
onClick = { toggleAttachMenu() }
|
|
||||||
)
|
|
||||||
.padding(7.5.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.AttachFile,
|
|
||||||
contentDescription = "Attach",
|
|
||||||
tint = if (showAttachMenu) activeIconColor else iconColor,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.graphicsLayer {
|
|
||||||
rotationZ = 45f + attachRotation // Base 45° + animation when open
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SEND BUTTON CONTAINER - 100dp width, right position (overlay over attachLayout)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.width(100.dp)
|
|
||||||
.height(48.dp)
|
|
||||||
) {
|
|
||||||
// AUDIO/VIDEO BUTTON (microphone) - 48dp
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterEnd)
|
|
||||||
.size(48.dp)
|
|
||||||
.graphicsLayer {
|
|
||||||
scaleX = micScale
|
|
||||||
scaleY = micScale
|
|
||||||
alpha = micAlpha
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null,
|
|
||||||
enabled = !canSend
|
|
||||||
) { /* TODO: voice recording */ }
|
|
||||||
.padding(7.5.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Mic,
|
|
||||||
contentDescription = "Voice",
|
|
||||||
tint = iconColor,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SEND BUTTON - 48dp with scale/alpha animation (250ms TelegramEasing)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterEnd)
|
|
||||||
.size(48.dp)
|
|
||||||
.graphicsLayer {
|
|
||||||
scaleX = sendScale
|
|
||||||
scaleY = sendScale
|
|
||||||
alpha = sendAlpha
|
|
||||||
}
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(primaryBlue) // Our blue color
|
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null,
|
|
||||||
enabled = canSend,
|
|
||||||
onClick = { handleSend() }
|
|
||||||
)
|
|
||||||
.padding(7.5.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Send,
|
|
||||||
contentDescription = "Send",
|
|
||||||
tint = Color.White,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emoji picker with animation
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = showEmojiPicker,
|
|
||||||
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(),
|
|
||||||
exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + fadeOut()
|
|
||||||
) {
|
|
||||||
AppleEmojiPickerPanel(
|
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
onEmojiSelected = { emoji ->
|
|
||||||
onValueChange(value + emoji)
|
|
||||||
},
|
|
||||||
onClose = { showEmojiPicker = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!showEmojiPicker && !showAttachMenu) {
|
|
||||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach Menu Panel - Telegram style bottom sheet with action buttons
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AttachMenuPanel(
|
|
||||||
items: List<AttachMenuItem>,
|
|
||||||
isDarkTheme: Boolean,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val panelBackground = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF8F8FA)
|
|
||||||
val labelColor = if (isDarkTheme) Color.White.copy(alpha = 0.9f) else Color.Black.copy(alpha = 0.8f)
|
|
||||||
val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.12f) else Color.Black.copy(alpha = 0.08f)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(panelBackground)
|
|
||||||
) {
|
|
||||||
// Top divider
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(0.5.dp)
|
|
||||||
.background(dividerColor)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Buttons grid
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 20.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
items.forEach { item ->
|
|
||||||
AttachMenuButton(
|
|
||||||
item = item,
|
|
||||||
labelColor = labelColor,
|
|
||||||
onDismiss = onDismiss
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Single attach menu button with icon and label
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AttachMenuButton(
|
|
||||||
item: AttachMenuItem,
|
|
||||||
labelColor: Color,
|
|
||||||
onDismiss: () -> Unit
|
|
||||||
) {
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null
|
|
||||||
) {
|
|
||||||
item.onClick()
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
// Colored circle with icon
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(56.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(item.color)
|
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null
|
|
||||||
) {
|
|
||||||
item.onClick()
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
item.icon()
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Label
|
|
||||||
Text(
|
|
||||||
text = item.label,
|
|
||||||
color = labelColor,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user