Files
forkmessager/client-mobile/chats/presentation/chat_detail/ChatDetailScreen.kt
T

238 lines
9.9 KiB
Kotlin

package chats.presentation.chat_detail
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
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.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chats.presentation.components.EmojiPicker
import chats.presentation.components.MessageBubble
import core.presentation.components.AppAvatar
import core.utils.VoiceRecorder
import core.utils.copyUriToFile
import java.io.File
import ru.knot.messager.R
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChatDetailScreen(
chatId: String,
chatName: String,
viewModel: ChatDetailViewModel,
onBack: () -> Unit
) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
val voiceRecorder = remember { VoiceRecorder(context) }
var textInput by remember { mutableStateOf("") }
var isEmojiPickerVisible by remember { mutableStateOf(false) }
var isRecording by remember { mutableStateOf(false) }
var autoPlayingMessageId by remember { mutableStateOf<String?>(null) }
var currentPlaybackSpeed by remember { mutableFloatStateOf(1.0f) }
val playNextVoiceMessage = { currentId: String, speed: Float ->
val currentIndex = state.messages.indexOfFirst { it.id == currentId }
if (currentIndex != -1 && currentIndex < state.messages.size - 1) {
val nextVoiceIndexInSublist = state.messages.subList(currentIndex + 1, state.messages.size)
.indexOfFirst { it.mediaType == chats.domain.model.MediaType.AUDIO && it.content.isNullOrEmpty() }
if (nextVoiceIndexInSublist != -1) {
val actualNextIndex = currentIndex + 1 + nextVoiceIndexInSublist
autoPlayingMessageId = state.messages[actualNextIndex].id
currentPlaybackSpeed = speed
// Прокручиваем к следующему сообщению, иначе оно не распарсится LazyColumn
scope.launch {
listState.animateScrollToItem(actualNextIndex)
}
} else {
autoPlayingMessageId = null
}
} else {
autoPlayingMessageId = null
}
}
// Пикер галереи
val galleryLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
val file = copyUriToFile(context, it)
file?.let { viewModel.uploadMedia(it) }
}
}
// Загрузка данных чата при входе
LaunchedEffect(chatId) {
viewModel.setChatId(chatId)
}
// Автопрокрутка к последнему сообщению
LaunchedEffect(state.messages.size) {
if (state.messages.isNotEmpty()) {
listState.animateScrollToItem(state.messages.size - 1)
}
}
Scaffold(
topBar = {
TopAppBar(
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
AppAvatar(
url = state.chatAvatar,
name = state.chatName ?: chatName,
size = 36.dp,
modifier = Modifier.padding(end = 8.dp)
)
Column {
Text(state.chatName ?: chatName, style = MaterialTheme.typography.titleMedium)
if (state.isTyping) {
Text(
stringResource(R.string.typing),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary
)
}
}
}
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(R.string.back))
}
},
actions = {
if (state.canCall) {
IconButton(onClick = { /* Вызов (WebRTC) */ }) {
Icon(Icons.Default.Call, contentDescription = stringResource(R.string.call))
}
IconButton(onClick = { /* Видеозвонок (WebRTC) */ }) {
Icon(Icons.Default.VideoCall, contentDescription = stringResource(R.string.video_call))
}
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Список сообщений
Box(
modifier = Modifier
.weight(1f)
.background(MaterialTheme.colorScheme.background)
) {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(state.messages) { message ->
MessageBubble(
message = message,
isCurrentUser = message.senderId == viewModel.getCurrentUserId(),
autoPlay = message.id == autoPlayingMessageId,
initialPlaybackSpeed = if (message.id == autoPlayingMessageId) currentPlaybackSpeed else 1.0f,
onVoiceFinished = { speed -> playNextVoiceMessage(message.id, speed) },
onReactionClick = { emoji -> viewModel.addReaction(message.id, emoji) }
)
}
}
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
// Панель ввода
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = { isEmojiPickerVisible = !isEmojiPickerVisible }) {
Icon(
Icons.Default.EmojiEmotions,
contentDescription = stringResource(R.string.emoji),
tint = if (isEmojiPickerVisible) MaterialTheme.colorScheme.primary else Color.Gray
)
}
IconButton(onClick = { galleryLauncher.launch("*/*") }) {
Icon(Icons.Default.AttachFile, contentDescription = stringResource(R.string.attach))
}
TextField(
value = textInput,
onValueChange = { textInput = it },
modifier = Modifier.weight(1f),
placeholder = { Text(stringResource(R.string.message_placeholder)) },
maxLines = 4,
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
)
if (textInput.isBlank()) {
IconButton(
onClick = {
if (!isRecording) {
val file = File(context.cacheDir, "voice_${System.currentTimeMillis()}.mp3")
voiceRecorder.startRecording(file)
isRecording = true
} else {
voiceRecorder.stopRecording()
isRecording = false
}
}
) {
Icon(
if (isRecording) Icons.Default.Stop else Icons.Default.Mic,
contentDescription = stringResource(R.string.voice_message),
tint = if (isRecording) Color.Red else Color.Gray
)
}
} else {
IconButton(onClick = {
viewModel.sendMessage(textInput)
textInput = ""
}) {
Icon(Icons.Default.Send, contentDescription = stringResource(R.string.send))
}
}
}
if (isEmojiPickerVisible) {
EmojiPicker(onEmojiSelected = { textInput += it })
}
}
}
}
}