Compare commits

...

2 Commits

2 changed files with 68 additions and 7 deletions

View File

@@ -8,6 +8,7 @@ import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
@@ -721,6 +722,15 @@ fun ChatDetailScreen(
.maxWithOrNull(compareBy<ChatMessage>({ it.timestamp.time }, { it.id })) .maxWithOrNull(compareBy<ChatMessage>({ it.timestamp.time }, { it.id }))
?.id ?.id
var lastNewestMessageId by remember { mutableStateOf<String?>(null) } var lastNewestMessageId by remember { mutableStateOf<String?>(null) }
val isAtBottom by remember(listState) {
derivedStateOf {
listState.firstVisibleItemIndex == 0 &&
listState.firstVisibleItemScrollOffset <= 12
}
}
val showScrollToBottomButton by remember(messagesWithDates, isAtBottom) {
derivedStateOf { messagesWithDates.isNotEmpty() && !isAtBottom }
}
// Telegram-style: Прокрутка ТОЛЬКО при новых сообщениях (не при пагинации) // Telegram-style: Прокрутка ТОЛЬКО при новых сообщениях (не при пагинации)
// 🔥 Скроллим только если изменился ID самого нового сообщения // 🔥 Скроллим только если изменился ID самого нового сообщения
@@ -730,10 +740,15 @@ fun ChatDetailScreen(
lastNewestMessageId != null && lastNewestMessageId != null &&
newestMessageId != lastNewestMessageId newestMessageId != lastNewestMessageId
) { ) {
// Новое сообщение пришло - скроллим вниз val newestMessage = messages.firstOrNull { it.id == newestMessageId }
delay(50) // Debounce - ждём стабилизации val isOwnOutgoingMessage = newestMessage?.isOutgoing == true
listState.animateScrollToItem(0) val shouldAutoScroll = isAtBottom || isOwnOutgoingMessage
wasManualScroll = false
if (shouldAutoScroll) {
delay(50) // Debounce - ждём стабилизации
listState.animateScrollToItem(0)
wasManualScroll = false
}
} }
lastNewestMessageId = newestMessageId lastNewestMessageId = newestMessageId
} }
@@ -1141,7 +1156,8 @@ fun ChatDetailScreen(
color = Color.White, color = Color.White,
maxLines = 1, maxLines = 1,
overflow = android.text.TextUtils.TruncateAt.END, overflow = android.text.TextUtils.TruncateAt.END,
enableLinks = false enableLinks = false,
minHeightMultiplier = 1.1f
) )
if (!isSavedMessages && if (!isSavedMessages &&
!isGroupChat && !isGroupChat &&
@@ -2465,6 +2481,50 @@ fun ChatDetailScreen(
} }
} // Конец Column внутри Scaffold content } // Конец Column внутри Scaffold content
AnimatedVisibility(
visible = showScrollToBottomButton && !isLoading && !isSelectionMode,
enter = fadeIn(animationSpec = tween(140)) + expandVertically(expandFrom = Alignment.Bottom),
exit = fadeOut(animationSpec = tween(120)) + shrinkVertically(shrinkTowards = Alignment.Bottom),
modifier =
Modifier.align(Alignment.BottomEnd)
.padding(
end = 14.dp,
bottom = if (isSystemAccount) 24.dp else 86.dp
)
) {
Box(
modifier =
Modifier.size(38.dp)
.clip(CircleShape)
.background(
if (isDarkTheme) Color(0xFF2D2E31)
else Color.White
)
.clickable(
indication = null,
interactionSource =
remember {
MutableInteractionSource()
}
) {
scope.launch {
listState.animateScrollToItem(0)
wasManualScroll = false
}
},
contentAlignment = Alignment.Center
) {
Icon(
imageVector = TablerIcons.ChevronDown,
contentDescription = "Scroll to bottom",
tint =
if (isDarkTheme) Color(0xFFF2F2F3)
else Color(0xFF2D3138),
modifier = Modifier.size(21.dp)
)
}
}
// 📎 Media Picker — new tab-based ChatAttachAlert (Telegram-style) // 📎 Media Picker — new tab-based ChatAttachAlert (Telegram-style)
// Feature flag: set USE_NEW_ATTACH_ALERT to false to use old MediaPickerBottomSheet // Feature flag: set USE_NEW_ATTACH_ALERT to false to use old MediaPickerBottomSheet
val USE_NEW_ATTACH_ALERT = true val USE_NEW_ATTACH_ALERT = true

View File

@@ -358,13 +358,14 @@ fun AppleEmojiText(
enableMentions: Boolean = false, enableMentions: Boolean = false,
onMentionClick: ((String) -> Unit)? = null, onMentionClick: ((String) -> Unit)? = null,
onClick: (() -> Unit)? = null, // 🔥 Обычный tap (selection mode в MessageBubble) onClick: (() -> Unit)? = null, // 🔥 Обычный tap (selection mode в MessageBubble)
onLongClick: (() -> Unit)? = null // 🔥 Callback для long press (selection в MessageBubble) onLongClick: (() -> Unit)? = null, // 🔥 Callback для long press (selection в MessageBubble)
minHeightMultiplier: Float = 1.5f
) { ) {
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 // Минимальная высота для корректного отображения emoji
val minHeight = (fontSizeValue * 1.5).toInt() val minHeight = (fontSizeValue * minHeightMultiplier).toInt()
// Преобразуем FontWeight в Android typeface style // Преобразуем FontWeight в Android typeface style
val typefaceStyle = when (fontWeight) { val typefaceStyle = when (fontWeight) {