Правка чата
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user