Чат, правки

This commit is contained in:
Халимов Рустам
2026-04-14 12:52:03 +03:00
parent d7e75797ef
commit 2d0bc0d75c
12 changed files with 89 additions and 34 deletions

View File

@@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp
import chats.presentation.components.EmojiPicker
import chats.presentation.components.MessageBubble
import core.presentation.components.AppAvatar
import core.presentation.components.AppMediaLightbox
import core.utils.VoiceRecorder
import core.utils.copyUriToFile
import java.io.File
@@ -46,6 +47,9 @@ fun ChatDetailScreen(
var autoPlayingMessageId by remember { mutableStateOf<String?>(null) }
var currentPlaybackSpeed by remember { mutableFloatStateOf(1.0f) }
var selectedMediaList by remember { mutableStateOf<List<chats.domain.model.Media>?>(null) }
var initialMediaIndex by remember { mutableIntStateOf(0) }
val playNextVoiceMessage = { currentId: String, speed: Float ->
val currentIndex = state.messages.indexOfFirst { it.id == currentId }
@@ -150,14 +154,27 @@ fun ChatDetailScreen(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(state.messages) { message ->
val allChatMedia = state.messages.flatMap { msg ->
msg.media.filter {
it.type.startsWith("image") ||
it.type.startsWith("video") ||
it.filename?.endsWith(".gif", true) == true
}.map { it to msg.id }
}.reversed()
items(state.messages, key = { it.id }) { 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) }
onReactionClick = { emoji -> viewModel.addReaction(message.id, emoji) },
onMediaClick = { clickedMedia ->
val initialIndex = allChatMedia.indexOfFirst { it.first.url == clickedMedia.url }
selectedMediaList = allChatMedia.map { it.first }
initialMediaIndex = if (initialIndex != -1) initialIndex else 0
}
)
}
@@ -233,5 +250,14 @@ fun ChatDetailScreen(
}
}
}
// Просмотрщик медиа
selectedMediaList?.let { list ->
AppMediaLightbox(
mediaList = list,
initialIndex = initialMediaIndex,
onClose = { selectedMediaList = null }
)
}
}
}

View File

@@ -13,6 +13,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import ru.knot.messager.R
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -46,7 +48,8 @@ fun MessageBubble(
onReactionClick: (String) -> Unit = {},
autoPlay: Boolean = false,
initialPlaybackSpeed: Float = 1.0f,
onVoiceFinished: (Float) -> Unit = {}
onVoiceFinished: (Float) -> Unit = {},
onMediaClick: (chats.domain.model.Media) -> Unit = {}
) {
val context = LocalContext.current
var showReactionPicker by remember { mutableStateOf(false) }
@@ -145,34 +148,40 @@ fun MessageBubble(
if (mediaCount == 1) {
val mediaItem = message.media[0]
when {
mediaItem.type.startsWith("image") || message.mediaType == MediaType.IMAGE -> {
mediaItem.type.startsWith("image") || message.mediaType == MediaType.IMAGE || mediaItem.type.startsWith("image") -> {
AsyncImage(
model = mediaItem.url,
imageLoader = core.utils.CoilUtils.getGifImageLoader(context),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.clickable { lightboxMediaIndex = 0 },
contentScale = ContentScale.FillWidth
.heightIn(max = 300.dp)
.clip(RoundedCornerShape(8.dp))
.clickable { onMediaClick(mediaItem) },
contentScale = ContentScale.Crop
)
}
mediaItem.type.startsWith("video") || message.mediaType == MediaType.VIDEO -> {
message.mediaType == MediaType.VIDEO || mediaItem.type.startsWith("video") -> {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.Black)
.clickable { lightboxMediaIndex = 0 },
contentAlignment = Alignment.Center
.height(240.dp)
.clip(RoundedCornerShape(8.dp))
) {
AppVideoPlayer(
url = mediaItem.url,
modifier = Modifier.fillMaxSize(),
useController = false,
autoPlay = true,
isMuted = true
isMuted = true,
autoPlay = true
)
// Прозрачный слой поверх видео для клика
Box(modifier = Modifier
.fillMaxSize()
.background(Color.Transparent)
.clickable { onMediaClick(mediaItem) }
)
Icon(
imageVector = Icons.Default.VolumeOff,
contentDescription = null,
@@ -181,7 +190,7 @@ fun MessageBubble(
)
}
}
mediaItem.type.startsWith("audio") || mediaItem.filename?.endsWith(".mp3", true) == true || message.mediaType == MediaType.AUDIO -> {
mediaItem.type.startsWith("audio") || mediaItem.filename?.endsWith(".mp2", true) == true || mediaItem.filename?.endsWith(".mp3", true) == true || message.mediaType == MediaType.AUDIO -> {
AppAudioPlayer(
url = mediaItem.url,
name = mediaItem.filename,
@@ -201,7 +210,7 @@ fun MessageBubble(
// Multi-media grid
PhotoGrid(
message.media,
onMediaClick = { index -> lightboxMediaIndex = index },
onMediaClick = { index -> onMediaClick(message.media[index]) },
modifier = Modifier.fillMaxWidth().height(300.dp).clip(RoundedCornerShape(12.dp))
)
}

View File

@@ -4,6 +4,8 @@ import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
@@ -18,6 +20,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@@ -60,11 +63,12 @@ fun AppMediaLightbox(
contentAlignment = Alignment.Center
) {
if (media.type.startsWith("video")) {
val isCurrentPage = pagerState.currentPage == page
AppVideoPlayer(
url = media.url,
modifier = Modifier.fillMaxSize(),
useController = true,
autoPlay = true
autoPlay = isCurrentPage // Только если страница активна
)
} else {
var scale by remember { mutableFloatStateOf(1f) }
@@ -72,21 +76,10 @@ fun AppMediaLightbox(
AsyncImage(
model = media.url,
imageLoader = core.utils.CoilUtils.getGifImageLoader(LocalContext.current),
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { _, pan, zoom, _ ->
scale = (scale * zoom).coerceIn(1f, 5f)
if (scale > 1f) {
offset += pan
} else {
offset = androidx.compose.ui.geometry.Offset.Zero
}
}
)
}
.graphicsLayer(
scaleX = scale,
scaleY = scale,

View File

@@ -89,21 +89,22 @@ fun AppVideoPlayer(
factory = {
PlayerView(it).apply {
player = exoPlayer
this.useController = false // Use our custom UI
this.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
this.useController = false // We use our own UI if needed
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
setBackgroundColor(android.graphics.Color.BLACK)
}
},
modifier = Modifier.fillMaxSize().clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
indication = null,
enabled = useController // Only clickable if we have controls
) {
isControlsVisible = !isControlsVisible
}
)
AnimatedVisibility(
visible = isControlsVisible,
visible = isControlsVisible && useController,
enter = fadeIn(),
exit = fadeOut()
) {

View File

@@ -0,0 +1,26 @@
package core.utils
import android.content.Context
import android.os.Build
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
object CoilUtils {
private var gifImageLoader: ImageLoader? = null
fun getGifImageLoader(context: Context): ImageLoader {
if (gifImageLoader == null) {
gifImageLoader = ImageLoader.Builder(context)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.build()
}
return gifImageLoader!!
}
}