Ответы

This commit is contained in:
Халимов Рустам
2026-04-17 21:59:53 +03:00
parent 8c00d1376d
commit bef30c2c86
13 changed files with 227 additions and 33 deletions
@@ -56,6 +56,11 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter
import chats.presentation.components.SwipeableMessageItem
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@OptIn(ExperimentalMaterial3Api::class, androidx.compose.ui.ExperimentalComposeUiApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable
fun ChatDetailScreen(
@@ -98,6 +103,14 @@ fun ChatDetailScreen(
var selectedMediaList by remember { mutableStateOf<List<chats.domain.model.Media>?>(null) }
var initialMediaIndex by remember { mutableIntStateOf(0) }
val focusRequester = remember { FocusRequester() }
// Автоматический фокус при ответе
LaunchedEffect(state.replyingMessage) {
if (state.replyingMessage != null) {
focusRequester.requestFocus()
}
}
val playNextVoiceMessage = { currentId: String, speed: Float ->
val currentIndex = state.messages.indexOfFirst { it.id == currentId }
@@ -319,30 +332,45 @@ fun ChatDetailScreen(
}) { item ->
when(item) {
is MessageListItem.MessageItem -> {
MessageBubble(
message = item.message,
isCurrentUser = item.message.senderId == viewModel.getCurrentUserId(),
autoPlay = item.message.id == autoPlayingMessageId,
initialPlaybackSpeed = if (item.message.id == autoPlayingMessageId) currentPlaybackSpeed else 1.0f,
onVoiceFinished = { speed -> playNextVoiceMessage(item.message.id, speed) },
onReactionClick = { emoji -> viewModel.addReaction(item.message.id, emoji) },
onMediaClick = { clickedMedia ->
val isMedia = clickedMedia.type.startsWith("image") ||
clickedMedia.type.startsWith("video") ||
clickedMedia.type.contains("gif")
if (isMedia) {
val initialIndex = allChatMedia.indexOfFirst { it.first.url == clickedMedia.url }
selectedMediaList = allChatMedia.map { it.first }
initialMediaIndex = if (initialIndex != -1) initialIndex else 0
} else {
try {
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(clickedMedia.url))
context.startActivity(intent)
} catch (e: Exception) { }
}
SwipeableMessageItem(
onReply = {
viewModel.onReply(item.message)
}
)
) {
MessageBubble(
message = item.message,
isCurrentUser = item.message.senderId == viewModel.getCurrentUserId(),
autoPlay = item.message.id == autoPlayingMessageId,
initialPlaybackSpeed = if (item.message.id == autoPlayingMessageId) currentPlaybackSpeed else 1.0f,
onVoiceFinished = { speed -> playNextVoiceMessage(item.message.id, speed) },
onReactionClick = { emoji -> viewModel.addReaction(item.message.id, emoji) },
onReplyClick = { reply ->
// TODO: Scroll to reply
val index = listItems.indexOfFirst {
it is MessageListItem.MessageItem && it.message.id == reply.id
}
if (index != -1) {
scope.launch { listState.animateScrollToItem(index) }
}
},
onMediaClick = { clickedMedia ->
val isMedia = clickedMedia.type.startsWith("image") ||
clickedMedia.type.startsWith("video") ||
clickedMedia.type.contains("gif")
if (isMedia) {
val initialIndex = allChatMedia.indexOfFirst { it.first.url == clickedMedia.url }
selectedMediaList = allChatMedia.map { it.first }
initialMediaIndex = if (initialIndex != -1) initialIndex else 0
} else {
try {
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(clickedMedia.url))
context.startActivity(intent)
} catch (e: Exception) { }
}
}
)
}
}
is MessageListItem.DateHeader -> {
Box(
@@ -586,6 +614,51 @@ fun ChatDetailScreen(
}
}
}
// Reply Preview Bar
state.replyingMessage?.let { replyMsg ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.fillMaxHeight()
.width(2.dp)
.background(MaterialTheme.colorScheme.primary)
)
Icon(
imageVector = Icons.Default.Reply,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 8.dp).size(20.dp)
)
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp, vertical = 2.dp)
) {
Text(
text = "Ответ " + (if (replyMsg.senderId == viewModel.getCurrentUserId()) "самом себе" else replyMsg.senderName),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
Text(
text = replyMsg.content ?: if (replyMsg.media.isNotEmpty()) "Медиа" else "...",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
IconButton(onClick = { viewModel.cancelReply() }) {
Icon(Icons.Default.Close, contentDescription = "Cancel", modifier = Modifier.size(20.dp), tint = Color.Gray)
}
}
}
Row(
modifier = Modifier
@@ -628,11 +701,12 @@ fun ChatDetailScreen(
)
}
BasicTextField(
BasicTextField(
value = state.inputText,
onValueChange = { viewModel.onInputTextChanged(it) },
modifier = Modifier
.weight(1f)
.focusRequester(focusRequester)
.onFocusChanged {
if (it.isFocused) {
isEmojiPickerVisible = false