Правка чата

This commit is contained in:
Халимов Рустам
2026-04-14 11:31:20 +03:00
parent d8b0d86534
commit 3310a3c4a4
18 changed files with 665 additions and 104 deletions
@@ -58,13 +58,19 @@ class ChatListViewModel @Inject constructor(
_state.update { it.copy(isLoading = true) }
try {
val chats = repository.getChats()
_state.update { it.copy(chats = chats, isLoading = false) }
_state.update { it.copy(chats = sortChats(chats), isLoading = false) }
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.message) }
}
}
}
private fun sortChats(chats: List<Chat>): List<Chat> {
return chats.sortedWith(compareByDescending<Chat> {
it.name.equals("Избранное", ignoreCase = true) || it.name.equals("Saved Messages", ignoreCase = true)
}.thenByDescending { it.lastMessage?.createdAt })
}
private fun observeSignalREvents() {
signalrClient.events
.onEach { event ->
@@ -94,9 +100,9 @@ class ChatListViewModel @Inject constructor(
unreadCount = chat.unreadCount + 1
)
} else chat
}.sortedByDescending { it.lastMessage?.createdAt }
}
currentState.copy(chats = updatedChats)
currentState.copy(chats = sortChats(updatedChats))
}
}
}
@@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment
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.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -18,8 +19,9 @@ import androidx.compose.ui.unit.sp
import chats.domain.model.Chat
import androidx.compose.foundation.shape.RoundedCornerShape
import coil.compose.AsyncImage
import androidx.compose.ui.layout.ContentScale
import core.presentation.components.AppAvatar
import chats.domain.model.MediaType
import androidx.compose.runtime.remember
@Composable
fun ChatItem(
@@ -33,28 +35,11 @@ fun ChatItem(
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Аватар (мягкий квадрат)
Box(
modifier = Modifier
.size(50.dp)
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
if (chat.avatar != null) {
AsyncImage(
model = chat.avatar,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else {
Text(
text = chat.name.take(1).uppercase(),
style = MaterialTheme.typography.titleMedium
)
}
}
AppAvatar(
url = chat.avatar,
name = chat.name,
size = 50.dp
)
Spacer(modifier = Modifier.width(12.dp))
@@ -70,8 +55,15 @@ fun ChatItem(
overflow = TextOverflow.Ellipsis
)
chat.lastMessage?.let {
val formattedTime = remember(it.createdAt) {
try {
it.createdAt.substringAfter('T').take(5)
} catch (e: Exception) {
""
}
}
Text(
text = it.createdAt.takeLast(5), // Упрощенный формат времени
text = formattedTime,
style = MaterialTheme.typography.labelSmall,
color = Color.Gray
)
@@ -82,8 +74,27 @@ fun ChatItem(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
val previewText = remember(chat.lastMessage) {
val msg = chat.lastMessage
if (msg == null) return@remember "No messages yet"
if (!msg.content.isNullOrBlank()) {
msg.content
} else if (msg.mediaType == MediaType.AUDIO) {
"Голосовое сообщение"
} else if (msg.media.isNotEmpty()) {
when (msg.mediaType) {
MediaType.IMAGE -> "Фото"
MediaType.VIDEO -> "Видео"
MediaType.FILE -> "Файл"
else -> "Медиа"
}
} else {
"Сообщение"
}
}
Text(
text = chat.lastMessage?.content ?: "No messages yet",
text = previewText,
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray,
maxLines = 1,
@@ -35,6 +35,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.material3.Icon
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.VolumeOff
import core.presentation.components.AppMediaLightbox
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable
@@ -46,11 +49,12 @@ fun MessageBubble(
) {
val context = LocalContext.current
var showReactionPicker by remember { mutableStateOf(false) }
var lightboxMedia by remember { mutableStateOf<Pair<String, String>?>(null) }
val backgroundColor = if (isCurrentUser) {
Color(0xFF3390EC) // Telegram Blue
Color(0xFF3096E5) // Updated Blue
} else {
Color(0xFF2B2B2B) // Dark Grey
Color(0xFF212121) // Updated Dark Grey
}
val contentColor = Color.White
@@ -119,13 +123,14 @@ fun MessageBubble(
modifier = Modifier
.fillMaxHeight()
.width(2.dp)
.background(if (isCurrentUser) Color.White else Color(0xFF3390EC))
.background(if (isCurrentUser) Color.White else Color(0xFF3096E5))
)
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
val accentColor = if (isCurrentUser) Color.White else Color(0xFF3096E5)
Text(
text = reply.senderName,
style = MaterialTheme.typography.labelMedium,
color = if (isCurrentUser) Color.White else Color(0xFF3390EC),
color = accentColor,
maxLines = 1,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
)
@@ -153,22 +158,41 @@ fun MessageBubble(
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.clickable {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(mediaUrl))
context.startActivity(intent)
lightboxMedia = mediaUrl to "image"
},
contentScale = ContentScale.FillWidth
)
}
MediaType.VIDEO -> {
AppVideoPlayer(
url = mediaUrl,
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(12.dp)),
useController = true,
autoPlay = false
)
.clip(RoundedCornerShape(12.dp))
.background(Color.Black)
.clickable {
lightboxMedia = mediaUrl to "video"
},
contentAlignment = Alignment.Center
) {
AppVideoPlayer(
url = mediaUrl,
modifier = Modifier.fillMaxSize(),
useController = false,
autoPlay = true,
isMuted = true
)
// Volume Off icon in top right
Icon(
imageVector = Icons.Default.VolumeOff,
contentDescription = null,
tint = Color.White.copy(alpha = 0.7f),
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
.size(20.dp)
)
}
}
MediaType.AUDIO -> {
AppAudioPlayer(
@@ -186,6 +210,7 @@ fun MessageBubble(
// Photo Grid for multiple images
PhotoGrid(
urls = message.media,
onMediaClick = { lightboxMedia = it to "image" },
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
@@ -218,11 +243,20 @@ fun MessageBubble(
modifier = Modifier.padding(end = 4.dp)
)
}
val formattedTime = remember(message.createdAt) {
try {
// Handle ISO 8601 strings like 2024-05-14T10:49:02.12Z
val timePart = message.createdAt.substringAfter('T').take(5)
if (timePart.contains(':')) timePart else "00:00"
} catch (e: Exception) {
"00:00"
}
}
Text(
text = message.createdAt.takeLast(5),
text = formattedTime,
style = MaterialTheme.typography.labelSmall,
color = contentColor.copy(alpha = 0.6f),
fontSize = 10.sp
fontSize = 11.sp
)
if (isCurrentUser) {
Spacer(modifier = Modifier.width(2.dp))
@@ -236,6 +270,14 @@ fun MessageBubble(
}
}
}
lightboxMedia?.let { (url, type) ->
AppMediaLightbox(
url = url,
type = type,
onClose = { lightboxMedia = null }
)
}
}
@Composable
@@ -331,7 +373,7 @@ fun FileItem(mediaUrl: String, isCurrentUser: Boolean, contentColor: Color) {
}
@Composable
fun PhotoGrid(urls: List<String>, modifier: Modifier = Modifier) {
fun PhotoGrid(urls: List<String>, onMediaClick: (String) -> Unit, modifier: Modifier = Modifier) {
val items = urls.take(4)
Column(modifier = modifier) {
val rows = (items.size + 1) / 2
@@ -344,7 +386,8 @@ fun PhotoGrid(urls: List<String>, modifier: Modifier = Modifier) {
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(1.dp),
.padding(1.dp)
.clickable { onMediaClick(items[firstIndex]) },
contentScale = ContentScale.Crop
)
if (firstIndex + 1 < items.size) {
@@ -354,7 +397,8 @@ fun PhotoGrid(urls: List<String>, modifier: Modifier = Modifier) {
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(1.dp),
.padding(1.dp)
.clickable { onMediaClick(items[firstIndex + 1]) },
contentScale = ContentScale.Crop
)
} else if (rows > 1) {