193 lines
7.7 KiB
Kotlin
193 lines
7.7 KiB
Kotlin
package chats.presentation.chat_detail
|
|
|
|
import android.Manifest
|
|
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.utils.VoiceRecorder
|
|
import core.utils.copyUriToFile
|
|
import kotlinx.coroutines.launch
|
|
import java.io.File
|
|
import ru.knot.messager.R
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun ChatDetailScreen(
|
|
chatName: String,
|
|
viewModel: ChatDetailViewModel,
|
|
onBack: () -> Unit
|
|
) {
|
|
val state by viewModel.state.collectAsState()
|
|
val context = LocalContext.current
|
|
val listState = rememberLazyListState()
|
|
val voiceRecorder = remember { VoiceRecorder(context) }
|
|
var textInput by remember { mutableStateOf("") }
|
|
var isEmojiPickerVisible by remember { mutableStateOf(false) }
|
|
var isRecording by remember { mutableStateOf(false) }
|
|
|
|
// Пикер галереи
|
|
val galleryLauncher = rememberLauncherForActivityResult(
|
|
contract = ActivityResultContracts.GetContent()
|
|
) { uri: Uri? ->
|
|
uri?.let {
|
|
val file = copyUriToFile(context, it)
|
|
file?.let { viewModel.uploadMedia(it) }
|
|
}
|
|
}
|
|
|
|
// Автопрокрутка к последнему сообщению
|
|
LaunchedEffect(state.messages.size) {
|
|
if (state.messages.isNotEmpty()) {
|
|
listState.animateScrollToItem(state.messages.size - 1)
|
|
}
|
|
}
|
|
|
|
Scaffold(
|
|
topBar = {
|
|
TopAppBar(
|
|
title = {
|
|
Column {
|
|
Text(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 == "CURRENT_USER_ID" // TODO: Get from Auth
|
|
)
|
|
}
|
|
}
|
|
|
|
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
|
|
// TODO: Send voice file
|
|
}
|
|
}
|
|
) {
|
|
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 })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|