fix: Update Java home path in gradle.properties for consistency
This commit is contained in:
@@ -191,6 +191,8 @@ fun ChatDetailScreen(
|
|||||||
// Telegram-style scroll tracking
|
// Telegram-style scroll tracking
|
||||||
var wasManualScroll by remember { mutableStateOf(false) }
|
var wasManualScroll by remember { mutableStateOf(false) }
|
||||||
var isAtBottom by remember { mutableStateOf(true) }
|
var isAtBottom by remember { mutableStateOf(true) }
|
||||||
|
// Флаг для скрытия кнопки scroll при отправке (чтобы не мигала)
|
||||||
|
var isSendingMessage by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Track if user is at bottom of list
|
// Track if user is at bottom of list
|
||||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||||
@@ -232,19 +234,29 @@ fun ChatDetailScreen(
|
|||||||
val isOnline by viewModel.opponentOnline.collectAsState()
|
val isOnline by viewModel.opponentOnline.collectAsState()
|
||||||
|
|
||||||
// 🔥 Добавляем информацию о датах к сообщениям
|
// 🔥 Добавляем информацию о датах к сообщениям
|
||||||
|
// В reversed layout (новые внизу) - показываем дату ПОСЛЕ сообщения (визуально сверху)
|
||||||
val messagesWithDates =
|
val messagesWithDates =
|
||||||
remember(messages) {
|
remember(messages) {
|
||||||
val result = mutableListOf<Pair<ChatMessage, Boolean>>() // message, showDateHeader
|
val result = mutableListOf<Pair<ChatMessage, Boolean>>() // message, showDateHeader
|
||||||
var lastDateString = ""
|
var lastDateString = ""
|
||||||
|
|
||||||
// Сортируем по времени (старые -> новые)
|
// Сортируем по времени (новые -> старые) для reversed layout
|
||||||
val sortedMessages = messages.sortedBy { it.timestamp.time }
|
val sortedMessages = messages.sortedByDescending { it.timestamp.time }
|
||||||
|
|
||||||
for (message in sortedMessages) {
|
for (i in sortedMessages.indices) {
|
||||||
|
val message = sortedMessages[i]
|
||||||
val dateString =
|
val dateString =
|
||||||
SimpleDateFormat("yyyyMMdd", Locale.getDefault())
|
SimpleDateFormat("yyyyMMdd", Locale.getDefault())
|
||||||
.format(message.timestamp)
|
.format(message.timestamp)
|
||||||
val showDate = dateString != lastDateString
|
|
||||||
|
// Показываем дату если это последнее сообщение за день
|
||||||
|
// (следующее сообщение - другой день или нет следующего)
|
||||||
|
val nextMessage = sortedMessages.getOrNull(i + 1)
|
||||||
|
val nextDateString = nextMessage?.let {
|
||||||
|
SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(it.timestamp)
|
||||||
|
}
|
||||||
|
val showDate = nextDateString == null || nextDateString != dateString
|
||||||
|
|
||||||
result.add(message to showDate)
|
result.add(message to showDate)
|
||||||
lastDateString = dateString
|
lastDateString = dateString
|
||||||
}
|
}
|
||||||
@@ -464,10 +476,15 @@ fun ChatDetailScreen(
|
|||||||
},
|
},
|
||||||
containerColor = Color.Transparent
|
containerColor = Color.Transparent
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
// Используем Box для overlay - инпут поверх контента
|
// 🔥 Column с imePadding - весь контент поднимается с клавиатурой
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
|
Column(
|
||||||
// Список сообщений - занимает всё пространство
|
modifier = Modifier
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.imePadding() // Контент поднимается над клавиатурой
|
||||||
|
) {
|
||||||
|
// Список сообщений - занимает всё доступное пространство
|
||||||
|
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||||
if (messages.isEmpty()) {
|
if (messages.isEmpty()) {
|
||||||
// Пустое состояние
|
// Пустое состояние
|
||||||
Column(
|
Column(
|
||||||
@@ -527,68 +544,115 @@ fun ChatDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
// Добавляем padding сверху и снизу для скролла под floating input
|
// padding для контента списка
|
||||||
contentPadding =
|
contentPadding =
|
||||||
PaddingValues(
|
PaddingValues(
|
||||||
start = 8.dp,
|
start = 8.dp,
|
||||||
end = 8.dp,
|
end = 8.dp,
|
||||||
top = 8.dp,
|
top = 8.dp,
|
||||||
bottom = 80.dp // Отступ для floating input
|
bottom = 8.dp
|
||||||
),
|
),
|
||||||
reverseLayout = true
|
reverseLayout = true
|
||||||
) {
|
) {
|
||||||
// Для inverted FlatList: идём от новых к старым
|
// Reversed layout: item 0 = самое новое сообщение (внизу экрана)
|
||||||
val reversedMessages = messagesWithDates.reversed()
|
// messagesWithDates уже отсортирован новые->старые
|
||||||
itemsIndexed(reversedMessages, key = { _, item -> item.first.id }) {
|
itemsIndexed(messagesWithDates, key = { _, item -> item.first.id }) {
|
||||||
index,
|
index,
|
||||||
(message, showDate) ->
|
(message, showDate) ->
|
||||||
// В inverted списке дата показывается ПЕРЕД сообщением (визуально
|
|
||||||
// ПОСЛЕ)
|
|
||||||
Column {
|
Column {
|
||||||
MessageBubble(
|
// В reversed layout: дата показывается ПОСЛЕ сообщения
|
||||||
message = message,
|
// (визуально СВЕРХУ группы сообщений)
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
index = index
|
|
||||||
)
|
|
||||||
// Разделитель даты
|
|
||||||
if (showDate) {
|
if (showDate) {
|
||||||
DateHeader(
|
DateHeader(
|
||||||
dateText = getDateText(message.timestamp.time),
|
dateText = getDateText(message.timestamp.time),
|
||||||
secondaryTextColor = secondaryTextColor
|
secondaryTextColor = secondaryTextColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
MessageBubble(
|
||||||
|
message = message,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
index = index
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telegram-style "Scroll to Bottom" кнопка
|
// Telegram-style "Scroll to Bottom" кнопка - Liquid Glass стиль
|
||||||
if (!isAtBottom && messages.isNotEmpty()) {
|
// Не показываем при отправке сообщения (чтобы не мигала)
|
||||||
FloatingActionButton(
|
if (!isAtBottom && messages.isNotEmpty() && !isSendingMessage) {
|
||||||
onClick = {
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 16.dp, bottom = 16.dp)
|
||||||
|
.size(44.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 8.dp,
|
||||||
|
shape = CircleShape,
|
||||||
|
clip = false,
|
||||||
|
ambientColor = Color.Black.copy(alpha = 0.3f),
|
||||||
|
spotColor = Color.Black.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
brush =
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors =
|
||||||
|
if (isDarkTheme) {
|
||||||
|
listOf(
|
||||||
|
Color(0xFF2D2D2F)
|
||||||
|
.copy(alpha = 0.92f),
|
||||||
|
Color(0xFF1C1C1E)
|
||||||
|
.copy(alpha = 0.96f)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
Color(0xFFF2F2F7)
|
||||||
|
.copy(alpha = 0.94f),
|
||||||
|
Color(0xFFE5E5EA)
|
||||||
|
.copy(alpha = 0.97f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
brush =
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors =
|
||||||
|
if (isDarkTheme) {
|
||||||
|
listOf(
|
||||||
|
Color.White.copy(alpha = 0.18f),
|
||||||
|
Color.White.copy(alpha = 0.06f)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
Color.White.copy(alpha = 0.9f),
|
||||||
|
Color.Black.copy(alpha = 0.05f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
wasManualScroll = false
|
wasManualScroll = false
|
||||||
listState.animateScrollToItem(0)
|
listState.animateScrollToItem(0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
contentAlignment = Alignment.Center
|
||||||
Modifier.align(Alignment.BottomEnd)
|
|
||||||
.padding(end = 16.dp, bottom = 16.dp)
|
|
||||||
.size(48.dp),
|
|
||||||
containerColor = PrimaryBlue,
|
|
||||||
elevation = FloatingActionButtonDefaults.elevation(6.dp)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.KeyboardArrowDown,
|
Icons.Default.KeyboardArrowDown,
|
||||||
contentDescription = "Scroll to bottom",
|
contentDescription = "Scroll to bottom",
|
||||||
tint = Color.White
|
tint = if (isDarkTheme) Color.White.copy(alpha = 0.85f) else Color.Black.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 FLOATING INPUT - поверх контента внизу экрана
|
// 🔥 INPUT BAR - внизу Column, автоматически над клавиатурой
|
||||||
Box(modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth()) {
|
|
||||||
MessageInputBar(
|
MessageInputBar(
|
||||||
value = inputText,
|
value = inputText,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
@@ -599,8 +663,16 @@ fun ChatDetailScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSend = {
|
onSend = {
|
||||||
|
// Скрываем кнопку scroll на время отправки
|
||||||
|
isSendingMessage = true
|
||||||
viewModel.sendMessage()
|
viewModel.sendMessage()
|
||||||
// ProtocolManager.addLog("📤 Sending message...")
|
// Скроллим к новому сообщению
|
||||||
|
scope.launch {
|
||||||
|
delay(100)
|
||||||
|
listState.animateScrollToItem(0)
|
||||||
|
delay(300) // Ждём завершения анимации
|
||||||
|
isSendingMessage = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
backgroundColor = inputBackgroundColor,
|
backgroundColor = inputBackgroundColor,
|
||||||
@@ -609,7 +681,6 @@ fun ChatDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} // Закрытие Box с fade-in
|
} // Закрытие Box с fade-in
|
||||||
|
|
||||||
// Диалог логов
|
// Диалог логов
|
||||||
@@ -808,8 +879,14 @@ private fun DateHeader(dateText: String, secondaryTextColor: Color) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Панель ввода сообщения 1:1 как в React Native Оптимизированная версия с правильным
|
* Панель ввода сообщения - Telegram UX канон
|
||||||
* позиционированием
|
*
|
||||||
|
* Золотые правила:
|
||||||
|
* 1. Инпут всегда связан с клавиатурой (imePadding)
|
||||||
|
* 2. Последнее сообщение всегда видно
|
||||||
|
* 3. Никаких прыжков layout'а
|
||||||
|
* 4. После отправки: инпут очищается, клавиатура НЕ закрывается
|
||||||
|
* 5. Инпут растёт вверх при многострочном тексте (до 6 строк)
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -827,80 +904,30 @@ private fun MessageInputBar(
|
|||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
// Цвета - Telegram liquid glass style
|
|
||||||
val circleBackground =
|
|
||||||
if (isDarkTheme) Color(0xFF1F1F1F).copy(alpha = 0.75f)
|
|
||||||
else Color(0xFFF0F0F0).copy(alpha = 0.85f)
|
|
||||||
val circleBorder =
|
|
||||||
if (isDarkTheme) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.08f)
|
|
||||||
val circleIcon = if (isDarkTheme) Color.White.copy(alpha = 0.85f) else Color(0xFF333333)
|
|
||||||
|
|
||||||
// Liquid glass input - темнее и с эффектом размытия
|
|
||||||
val glassBackground =
|
|
||||||
if (isDarkTheme) Color(0xFF1A1A1A).copy(alpha = 0.88f)
|
|
||||||
else Color(0xFFF0F0F0).copy(alpha = 0.92f)
|
|
||||||
val glassBorder =
|
|
||||||
if (isDarkTheme) Color.White.copy(alpha = 0.12f) else Color.Black.copy(alpha = 0.08f)
|
|
||||||
val emojiIconColor =
|
|
||||||
if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.5f)
|
|
||||||
val panelBackground =
|
|
||||||
if (isDarkTheme) Color(0xFF0E0E0E).copy(alpha = 0.95f)
|
|
||||||
else Color(0xFFF5F5F5).copy(alpha = 0.95f)
|
|
||||||
|
|
||||||
// Состояние отправки
|
// Состояние отправки
|
||||||
val canSend = remember(value) { value.isNotBlank() }
|
val canSend = remember(value) { value.isNotBlank() }
|
||||||
|
|
||||||
// Easing
|
// Easing анимации
|
||||||
val backEasing = CubicBezierEasing(0.34f, 1.56f, 0.64f, 1f)
|
val backEasing = CubicBezierEasing(0.34f, 1.56f, 0.64f, 1f)
|
||||||
val smoothEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1f)
|
|
||||||
|
|
||||||
// Анимации Send
|
|
||||||
val sendScale by
|
|
||||||
animateFloatAsState(
|
|
||||||
targetValue = if (canSend) 1f else 0f,
|
|
||||||
animationSpec = tween(220, easing = backEasing),
|
|
||||||
label = "sendScale"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Анимации Mic
|
|
||||||
val micOpacity by
|
|
||||||
animateFloatAsState(
|
|
||||||
targetValue = if (canSend) 0f else 1f,
|
|
||||||
animationSpec = tween(200, easing = smoothEasing),
|
|
||||||
label = "micOpacity"
|
|
||||||
)
|
|
||||||
val micTranslateX by
|
|
||||||
animateFloatAsState(
|
|
||||||
targetValue = if (canSend) 80f else 0f,
|
|
||||||
animationSpec = tween(250, easing = smoothEasing),
|
|
||||||
label = "micTranslateX"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Input margin - для правильного позиционирования как на скриншоте
|
|
||||||
val inputEndMargin by
|
|
||||||
animateDpAsState(
|
|
||||||
targetValue = if (canSend) 0.dp else 44.dp,
|
|
||||||
animationSpec = tween(220, easing = smoothEasing),
|
|
||||||
label = "inputEndMargin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Функция переключения emoji picker
|
// Функция переключения emoji picker
|
||||||
fun toggleEmojiPicker() {
|
fun toggleEmojiPicker() {
|
||||||
if (showEmojiPicker) {
|
if (showEmojiPicker) {
|
||||||
showEmojiPicker = false
|
showEmojiPicker = false
|
||||||
} else {
|
} else {
|
||||||
// Скрываем клавиатуру и убираем фокус
|
// Сначала скрываем клавиатуру, затем показываем emoji picker
|
||||||
|
focusManager.clearFocus(force = true)
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
focusManager.clearFocus()
|
// Небольшая задержка чтобы клавиатура успела закрыться
|
||||||
showEmojiPicker = true
|
showEmojiPicker = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция отправки
|
// Функция отправки - НЕ закрывает клавиатуру (UX правило #6)
|
||||||
fun handleSend() {
|
fun handleSend() {
|
||||||
if (value.isNotBlank()) {
|
if (value.isNotBlank()) {
|
||||||
onSend()
|
onSend()
|
||||||
onValueChange("")
|
// Очищаем инпут, но клавиатура остаётся открытой
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,14 +935,14 @@ private fun MessageInputBar(
|
|||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.background(Color.Transparent)
|
.background(Color.Transparent)
|
||||||
.then(if (!showEmojiPicker) Modifier.imePadding() else Modifier)
|
// imePadding уже на родительском Column
|
||||||
) {
|
) {
|
||||||
// 🔥 TELEGRAM-STYLE FLOATING LIQUID GLASS INPUT
|
// 🔥 TELEGRAM-STYLE FLOATING LIQUID GLASS INPUT
|
||||||
// Единый liquid glass контейнер без фона
|
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 16.dp)
|
.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 16.dp)
|
||||||
|
// Инпут растёт вверх до 6 строк (~140dp)
|
||||||
.heightIn(min = 44.dp, max = 140.dp)
|
.heightIn(min = 44.dp, max = 140.dp)
|
||||||
.shadow(
|
.shadow(
|
||||||
elevation = 4.dp,
|
elevation = 4.dp,
|
||||||
@@ -926,36 +953,22 @@ private fun MessageInputBar(
|
|||||||
)
|
)
|
||||||
.clip(RoundedCornerShape(22.dp))
|
.clip(RoundedCornerShape(22.dp))
|
||||||
.background(
|
.background(
|
||||||
// Telegram glass effect - достаточно плотный но с эффектом
|
|
||||||
// стекла
|
|
||||||
brush =
|
brush =
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
colors =
|
colors =
|
||||||
if (isDarkTheme) {
|
if (isDarkTheme) {
|
||||||
listOf(
|
listOf(
|
||||||
Color(0xFF2D2D2F)
|
Color(0xFF2D2D2F)
|
||||||
.copy(
|
.copy(alpha = 0.92f),
|
||||||
alpha =
|
|
||||||
0.92f
|
|
||||||
),
|
|
||||||
Color(0xFF1C1C1E)
|
Color(0xFF1C1C1E)
|
||||||
.copy(
|
.copy(alpha = 0.96f)
|
||||||
alpha =
|
|
||||||
0.96f
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
listOf(
|
listOf(
|
||||||
Color(0xFFF2F2F7)
|
Color(0xFFF2F2F7)
|
||||||
.copy(
|
.copy(alpha = 0.94f),
|
||||||
alpha =
|
|
||||||
0.94f
|
|
||||||
),
|
|
||||||
Color(0xFFE5E5EA)
|
Color(0xFFE5E5EA)
|
||||||
.copy(
|
.copy(alpha = 0.97f)
|
||||||
alpha =
|
|
||||||
0.97f
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -967,21 +980,13 @@ private fun MessageInputBar(
|
|||||||
colors =
|
colors =
|
||||||
if (isDarkTheme) {
|
if (isDarkTheme) {
|
||||||
listOf(
|
listOf(
|
||||||
Color.White.copy(
|
Color.White.copy(alpha = 0.18f),
|
||||||
alpha = 0.18f
|
Color.White.copy(alpha = 0.06f)
|
||||||
),
|
|
||||||
Color.White.copy(
|
|
||||||
alpha = 0.06f
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
listOf(
|
listOf(
|
||||||
Color.White.copy(
|
Color.White.copy(alpha = 0.9f),
|
||||||
alpha = 0.9f
|
Color.Black.copy(alpha = 0.05f)
|
||||||
),
|
|
||||||
Color.Black.copy(
|
|
||||||
alpha = 0.05f
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -990,7 +995,7 @@ private fun MessageInputBar(
|
|||||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||||
verticalAlignment = Alignment.Bottom
|
verticalAlignment = Alignment.Bottom
|
||||||
) {
|
) {
|
||||||
// EMOJI BUTTON - выравнивается по низу
|
// EMOJI BUTTON
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.align(Alignment.Bottom)
|
Modifier.align(Alignment.Bottom)
|
||||||
@@ -1017,7 +1022,7 @@ private fun MessageInputBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TEXT INPUT - растягивается и центрируется вертикально
|
// TEXT INPUT
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.weight(1f)
|
Modifier.weight(1f)
|
||||||
@@ -1038,7 +1043,7 @@ private fun MessageInputBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ATTACH BUTTON - выравнивается по низу
|
// ATTACH BUTTON
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.align(Alignment.Bottom)
|
Modifier.align(Alignment.Bottom)
|
||||||
@@ -1062,7 +1067,7 @@ private fun MessageInputBar(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.width(2.dp))
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
|
||||||
// MIC / SEND BUTTON - Mic когда пусто, Send когда есть текст (Telegram style)
|
// MIC / SEND BUTTON
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.align(Alignment.Bottom)
|
Modifier.align(Alignment.Bottom)
|
||||||
@@ -1080,9 +1085,7 @@ private fun MessageInputBar(
|
|||||||
indication = null,
|
indication = null,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (canSend) handleSend()
|
if (canSend) handleSend()
|
||||||
else {
|
else { /* TODO: Voice recording */ }
|
||||||
/* TODO: Start voice recording */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -1093,7 +1096,6 @@ private fun MessageInputBar(
|
|||||||
label = "iconCrossfade"
|
label = "iconCrossfade"
|
||||||
) { showSend ->
|
) { showSend ->
|
||||||
if (showSend) {
|
if (showSend) {
|
||||||
// Telegram Send icon - кастомная SVG
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = TelegramSendIcon,
|
imageVector = TelegramSendIcon,
|
||||||
contentDescription = "Send",
|
contentDescription = "Send",
|
||||||
@@ -1101,7 +1103,6 @@ private fun MessageInputBar(
|
|||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Mic icon
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Mic,
|
Icons.Default.Mic,
|
||||||
contentDescription = "Voice",
|
contentDescription = "Voice",
|
||||||
|
|||||||
@@ -236,11 +236,15 @@ fun AppleEmojiText(
|
|||||||
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
|
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
|
||||||
else fontSize.value
|
else fontSize.value
|
||||||
|
|
||||||
|
// Минимальная высота для корректного отображения emoji
|
||||||
|
val minHeight = (fontSizeValue * 1.5).toInt()
|
||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
AppleEmojiTextView(ctx).apply {
|
AppleEmojiTextView(ctx).apply {
|
||||||
setTextColor(color.toArgb())
|
setTextColor(color.toArgb())
|
||||||
setTextSize(fontSizeValue)
|
setTextSize(fontSizeValue)
|
||||||
|
minimumHeight = (minHeight * ctx.resources.displayMetrics.density).toInt()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { view ->
|
update = { view ->
|
||||||
@@ -265,6 +269,11 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
|||||||
private val bitmapCache = LruCache<String, Bitmap>(100)
|
private val bitmapCache = LruCache<String, Bitmap>(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Отключаем лишние отступы шрифта для корректного отображения emoji
|
||||||
|
includeFontPadding = false
|
||||||
|
}
|
||||||
|
|
||||||
fun setTextWithEmojis(text: String) {
|
fun setTextWithEmojis(text: String) {
|
||||||
val spannable = SpannableStringBuilder(text)
|
val spannable = SpannableStringBuilder(text)
|
||||||
val matcher = EMOJI_PATTERN.matcher(text)
|
val matcher = EMOJI_PATTERN.matcher(text)
|
||||||
@@ -275,12 +284,13 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
|||||||
val bitmap = loadEmojiBitmap(unified)
|
val bitmap = loadEmojiBitmap(unified)
|
||||||
|
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
val size = (textSize * 1.2).toInt()
|
val size = (textSize * 1.3).toInt() // Увеличиваем размер emoji
|
||||||
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, size, size, true)
|
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, size, size, true)
|
||||||
val drawable = BitmapDrawable(resources, scaledBitmap)
|
val drawable = BitmapDrawable(resources, scaledBitmap)
|
||||||
drawable.setBounds(0, 0, size, size)
|
drawable.setBounds(0, 0, size, size)
|
||||||
|
|
||||||
val span = ImageSpan(drawable, ImageSpan.ALIGN_BASELINE)
|
// ALIGN_BOTTOM лучше работает с emoji - не обрезает сверху
|
||||||
|
val span = ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM)
|
||||||
spannable.setSpan(span, matcher.start(), matcher.end(),
|
spannable.setSpan(span, matcher.start(), matcher.end(),
|
||||||
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ android.useAndroidX=true
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|
||||||
# Use Java 17 for build
|
# Use Java 17 for build
|
||||||
org.gradle.java.home=/opt/homebrew/Cellar/openjdk@17/17.0.14/libexec/openjdk.jdk/Contents/Home
|
org.gradle.java.home=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
|
||||||
|
|
||||||
# Increase heap size for Gradle
|
# Increase heap size for Gradle
|
||||||
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||||
|
|||||||
Reference in New Issue
Block a user