fix: Update Java home path in gradle.properties for consistency

This commit is contained in:
k1ngsterr1
2026-01-12 01:44:37 +05:00
parent 1e2f8cfed5
commit a75dfaab98
3 changed files with 168 additions and 157 deletions

View File

@@ -191,6 +191,8 @@ fun ChatDetailScreen(
// Telegram-style scroll tracking
var wasManualScroll by remember { mutableStateOf(false) }
var isAtBottom by remember { mutableStateOf(true) }
// Флаг для скрытия кнопки scroll при отправке (чтобы не мигала)
var isSendingMessage by remember { mutableStateOf(false) }
// Track if user is at bottom of list
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
@@ -232,19 +234,29 @@ fun ChatDetailScreen(
val isOnline by viewModel.opponentOnline.collectAsState()
// 🔥 Добавляем информацию о датах к сообщениям
// В reversed layout (новые внизу) - показываем дату ПОСЛЕ сообщения (визуально сверху)
val messagesWithDates =
remember(messages) {
val result = mutableListOf<Pair<ChatMessage, Boolean>>() // message, showDateHeader
var lastDateString = ""
// Сортируем по времени (старые -> новые)
val sortedMessages = messages.sortedBy { it.timestamp.time }
// Сортируем по времени (новые -> старые) для reversed layout
val sortedMessages = messages.sortedByDescending { it.timestamp.time }
for (message in sortedMessages) {
for (i in sortedMessages.indices) {
val message = sortedMessages[i]
val dateString =
SimpleDateFormat("yyyyMMdd", Locale.getDefault())
.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)
lastDateString = dateString
}
@@ -464,10 +476,15 @@ fun ChatDetailScreen(
},
containerColor = Color.Transparent
) { paddingValues ->
// Используем Box для overlay - инпут поверх контента
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
// Список сообщений - занимает всё пространство
Box(modifier = Modifier.fillMaxSize()) {
// 🔥 Column с imePadding - весь контент поднимается с клавиатурой
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.imePadding() // Контент поднимается над клавиатурой
) {
// Список сообщений - занимает всё доступное пространство
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
if (messages.isEmpty()) {
// Пустое состояние
Column(
@@ -527,68 +544,115 @@ fun ChatDetailScreen(
}
}
),
// Добавляем padding сверху и снизу для скролла под floating input
// padding для контента списка
contentPadding =
PaddingValues(
start = 8.dp,
end = 8.dp,
top = 8.dp,
bottom = 80.dp // Отступ для floating input
bottom = 8.dp
),
reverseLayout = true
) {
// Для inverted FlatList: идём от новых к старым
val reversedMessages = messagesWithDates.reversed()
itemsIndexed(reversedMessages, key = { _, item -> item.first.id }) {
// Reversed layout: item 0 = самое новое сообщение (внизу экрана)
// messagesWithDates уже отсортирован новые->старые
itemsIndexed(messagesWithDates, key = { _, item -> item.first.id }) {
index,
(message, showDate) ->
// В inverted списке дата показывается ПЕРЕД сообщением (визуально
// ПОСЛЕ)
Column {
MessageBubble(
message = message,
isDarkTheme = isDarkTheme,
index = index
)
// Разделитель даты
// В reversed layout: дата показывается ПОСЛЕ сообщения
// (визуально СВЕРХУ группы сообщений)
if (showDate) {
DateHeader(
dateText = getDateText(message.timestamp.time),
secondaryTextColor = secondaryTextColor
)
}
MessageBubble(
message = message,
isDarkTheme = isDarkTheme,
index = index
)
}
}
}
}
// Telegram-style "Scroll to Bottom" кнопка
if (!isAtBottom && messages.isNotEmpty()) {
FloatingActionButton(
onClick = {
// Telegram-style "Scroll to Bottom" кнопка - Liquid Glass стиль
// Не показываем при отправке сообщения (чтобы не мигала)
if (!isAtBottom && messages.isNotEmpty() && !isSendingMessage) {
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 {
wasManualScroll = false
listState.animateScrollToItem(0)
}
},
modifier =
Modifier.align(Alignment.BottomEnd)
.padding(end = 16.dp, bottom = 16.dp)
.size(48.dp),
containerColor = PrimaryBlue,
elevation = FloatingActionButtonDefaults.elevation(6.dp)
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.KeyboardArrowDown,
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 - поверх контента внизу экрана
Box(modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth()) {
// 🔥 INPUT BAR - внизу Column, автоматически над клавиатурой
MessageInputBar(
value = inputText,
onValueChange = {
@@ -599,8 +663,16 @@ fun ChatDetailScreen(
}
},
onSend = {
// Скрываем кнопку scroll на время отправки
isSendingMessage = true
viewModel.sendMessage()
// ProtocolManager.addLog("📤 Sending message...")
// Скроллим к новому сообщению
scope.launch {
delay(100)
listState.animateScrollToItem(0)
delay(300) // Ждём завершения анимации
isSendingMessage = false
}
},
isDarkTheme = isDarkTheme,
backgroundColor = inputBackgroundColor,
@@ -609,7 +681,6 @@ fun ChatDetailScreen(
)
}
}
}
} // Закрытие 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)
@Composable
@@ -827,80 +904,30 @@ private fun MessageInputBar(
val focusManager = LocalFocusManager.current
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() }
// Easing
// Easing анимации
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
fun toggleEmojiPicker() {
if (showEmojiPicker) {
showEmojiPicker = false
} else {
// Скрываем клавиатуру и убираем фокус
// Сначала скрываем клавиатуру, затем показываем emoji picker
focusManager.clearFocus(force = true)
keyboardController?.hide()
focusManager.clearFocus()
// Небольшая задержка чтобы клавиатура успела закрыться
showEmojiPicker = true
}
}
// Функция отправки
// Функция отправки - НЕ закрывает клавиатуру (UX правило #6)
fun handleSend() {
if (value.isNotBlank()) {
onSend()
onValueChange("")
// Очищаем инпут, но клавиатура остаётся открытой
}
}
@@ -908,14 +935,14 @@ private fun MessageInputBar(
modifier =
Modifier.fillMaxWidth()
.background(Color.Transparent)
.then(if (!showEmojiPicker) Modifier.imePadding() else Modifier)
// imePadding уже на родительском Column
) {
// 🔥 TELEGRAM-STYLE FLOATING LIQUID GLASS INPUT
// Единый liquid glass контейнер без фона
Row(
modifier =
Modifier.fillMaxWidth()
.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 16.dp)
// Инпут растёт вверх до 6 строк (~140dp)
.heightIn(min = 44.dp, max = 140.dp)
.shadow(
elevation = 4.dp,
@@ -926,36 +953,22 @@ private fun MessageInputBar(
)
.clip(RoundedCornerShape(22.dp))
.background(
// Telegram glass effect - достаточно плотный но с эффектом
// стекла
brush =
Brush.verticalGradient(
colors =
if (isDarkTheme) {
listOf(
Color(0xFF2D2D2F)
.copy(
alpha =
0.92f
),
.copy(alpha = 0.92f),
Color(0xFF1C1C1E)
.copy(
alpha =
0.96f
)
.copy(alpha = 0.96f)
)
} else {
listOf(
Color(0xFFF2F2F7)
.copy(
alpha =
0.94f
),
.copy(alpha = 0.94f),
Color(0xFFE5E5EA)
.copy(
alpha =
0.97f
)
.copy(alpha = 0.97f)
)
}
)
@@ -967,21 +980,13 @@ private fun MessageInputBar(
colors =
if (isDarkTheme) {
listOf(
Color.White.copy(
alpha = 0.18f
),
Color.White.copy(
alpha = 0.06f
)
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
)
Color.White.copy(alpha = 0.9f),
Color.Black.copy(alpha = 0.05f)
)
}
),
@@ -990,7 +995,7 @@ private fun MessageInputBar(
.padding(horizontal = 6.dp, vertical = 4.dp),
verticalAlignment = Alignment.Bottom
) {
// EMOJI BUTTON - выравнивается по низу
// EMOJI BUTTON
Box(
modifier =
Modifier.align(Alignment.Bottom)
@@ -1017,7 +1022,7 @@ private fun MessageInputBar(
)
}
// TEXT INPUT - растягивается и центрируется вертикально
// TEXT INPUT
Box(
modifier =
Modifier.weight(1f)
@@ -1038,7 +1043,7 @@ private fun MessageInputBar(
)
}
// ATTACH BUTTON - выравнивается по низу
// ATTACH BUTTON
Box(
modifier =
Modifier.align(Alignment.Bottom)
@@ -1062,7 +1067,7 @@ private fun MessageInputBar(
Spacer(modifier = Modifier.width(2.dp))
// MIC / SEND BUTTON - Mic когда пусто, Send когда есть текст (Telegram style)
// MIC / SEND BUTTON
Box(
modifier =
Modifier.align(Alignment.Bottom)
@@ -1080,9 +1085,7 @@ private fun MessageInputBar(
indication = null,
onClick = {
if (canSend) handleSend()
else {
/* TODO: Start voice recording */
}
else { /* TODO: Voice recording */ }
}
),
contentAlignment = Alignment.Center
@@ -1093,7 +1096,6 @@ private fun MessageInputBar(
label = "iconCrossfade"
) { showSend ->
if (showSend) {
// Telegram Send icon - кастомная SVG
Icon(
imageVector = TelegramSendIcon,
contentDescription = "Send",
@@ -1101,7 +1103,6 @@ private fun MessageInputBar(
modifier = Modifier.size(20.dp)
)
} else {
// Mic icon
Icon(
Icons.Default.Mic,
contentDescription = "Voice",

View File

@@ -236,11 +236,15 @@ fun AppleEmojiText(
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
else fontSize.value
// Минимальная высота для корректного отображения emoji
val minHeight = (fontSizeValue * 1.5).toInt()
AndroidView(
factory = { ctx ->
AppleEmojiTextView(ctx).apply {
setTextColor(color.toArgb())
setTextSize(fontSizeValue)
minimumHeight = (minHeight * ctx.resources.displayMetrics.density).toInt()
}
},
update = { view ->
@@ -265,6 +269,11 @@ class AppleEmojiTextView @JvmOverloads constructor(
private val bitmapCache = LruCache<String, Bitmap>(100)
}
init {
// Отключаем лишние отступы шрифта для корректного отображения emoji
includeFontPadding = false
}
fun setTextWithEmojis(text: String) {
val spannable = SpannableStringBuilder(text)
val matcher = EMOJI_PATTERN.matcher(text)
@@ -275,12 +284,13 @@ class AppleEmojiTextView @JvmOverloads constructor(
val bitmap = loadEmojiBitmap(unified)
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 drawable = BitmapDrawable(resources, scaledBitmap)
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(),
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}

View File

@@ -7,7 +7,7 @@ android.useAndroidX=true
kotlin.code.style=official
# 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
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