Реакции, но с багом
This commit is contained in:
@@ -270,10 +270,14 @@ fun ChatDetailScreen(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { /* TODO: Forward */ }) {
|
||||
IconButton(onClick = {
|
||||
viewModel.onForwardSelectedMessages()
|
||||
}) {
|
||||
Icon(Icons.Default.Forward, contentDescription = "Forward")
|
||||
}
|
||||
IconButton(onClick = { /* TODO: Delete */ }) {
|
||||
IconButton(onClick = {
|
||||
// TODO: Bulk Delete
|
||||
}) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Delete")
|
||||
}
|
||||
}
|
||||
@@ -363,6 +367,7 @@ fun ChatDetailScreen(
|
||||
initialPlaybackSpeed = if (item.message.id == autoPlayingMessageId) currentPlaybackSpeed else 1.0f,
|
||||
onVoiceFinished = { speed -> playNextVoiceMessage(item.message.id, speed) },
|
||||
onReactionClick = { emoji -> viewModel.addReaction(item.message.id, emoji) },
|
||||
onReply = { msg -> viewModel.onReply(msg) },
|
||||
onReplyClick = { reply ->
|
||||
val index = listItems.indexOfFirst {
|
||||
it is MessageListItem.MessageItem && it.message.id == reply.id
|
||||
@@ -372,6 +377,10 @@ fun ChatDetailScreen(
|
||||
}
|
||||
},
|
||||
onSelect = { viewModel.toggleSelection(it) },
|
||||
onForward = { viewModel.onForward(it) },
|
||||
onPin = { viewModel.onPin(it) },
|
||||
onEdit = { viewModel.onEdit(it) },
|
||||
onDelete = { msg, forEveryone -> viewModel.deleteMessage(msg, forEveryone) },
|
||||
isSelected = state.selectedMessageIds.contains(item.message.id),
|
||||
isSelectionMode = state.selectedMessageIds.isNotEmpty(),
|
||||
onMediaClick = { clickedMedia ->
|
||||
@@ -725,6 +734,52 @@ fun ChatDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Edit Preview Bar
|
||||
state.editingMessage?.let { editMsg ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(2.dp)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 8.dp).size(20.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Редактирование",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = editMsg.content ?: "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { viewModel.cancelEdit() }) {
|
||||
Icon(Icons.Default.Close, contentDescription = "Cancel", modifier = Modifier.size(20.dp), tint = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -929,6 +984,52 @@ fun ChatDetailScreen(
|
||||
onClose = { selectedMediaList = null }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.forwardingMessages.isNotEmpty()) {
|
||||
ForwardChatSelectionDialog(
|
||||
chats = state.availableChatsToForward,
|
||||
onChatSelected = { targetChatId ->
|
||||
viewModel.forwardMessages(targetChatId, state.forwardingMessages)
|
||||
viewModel.cancelForwarding()
|
||||
viewModel.clearSelection()
|
||||
},
|
||||
onDismiss = { viewModel.cancelForwarding() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ForwardChatSelectionDialog(
|
||||
chats: List<chats.domain.model.Chat>,
|
||||
onChatSelected: (String) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Выберите чат для пересылки") },
|
||||
text = {
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 400.dp)) {
|
||||
items(chats) { chat ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onChatSelected(chat.id) }
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppAvatar(url = chat.avatar, name = chat.name, size = 40.dp)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(chat.name, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
Divider(color = Color.Gray.copy(alpha = 0.2f))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Отмена") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
sealed class MessageListItem {
|
||||
|
||||
@@ -43,7 +43,11 @@ data class ChatDetailState(
|
||||
val isCompressionEnabled: Boolean = true,
|
||||
val inputText: String = "",
|
||||
val replyingMessage: Message? = null,
|
||||
val selectedMessageIds: Set<String> = emptySet()
|
||||
val editingMessage: Message? = null,
|
||||
val forwardingMessages: List<Message> = emptyList(),
|
||||
val availableChatsToForward: List<chats.domain.model.Chat> = emptyList(),
|
||||
val selectedMessageIds: Set<String> = emptySet(),
|
||||
val pinnedMessages: List<Message> = emptyList()
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
@@ -187,6 +191,25 @@ class ChatDetailViewModel @Inject constructor(
|
||||
s.copy(messages = updatedMessages)
|
||||
}
|
||||
}
|
||||
|
||||
// For handling reaction removed event
|
||||
private fun removeMessageReaction(messageId: String, userId: String, emoji: String) {
|
||||
_state.update { s ->
|
||||
val updatedMessages = s.messages.map { msg ->
|
||||
if (msg.id == messageId) {
|
||||
val currentReactions = msg.reactions.toMutableMap()
|
||||
val count = currentReactions[emoji] ?: 0
|
||||
if (count > 1) {
|
||||
currentReactions[emoji] = count - 1
|
||||
} else {
|
||||
currentReactions.remove(emoji)
|
||||
}
|
||||
msg.copy(reactions = currentReactions)
|
||||
} else msg
|
||||
}
|
||||
s.copy(messages = updatedMessages)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeSignalREvents(chatId: String) {
|
||||
signalrEventsJob?.cancel()
|
||||
@@ -222,7 +245,24 @@ class ChatDetailViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
is ChatEvent.ReactionUpdated -> {
|
||||
updateMessageReaction(event.messageId, event.userId, event.emoji)
|
||||
if (event.isRemoved) {
|
||||
removeMessageReaction(event.messageId, event.userId, event.emoji)
|
||||
} else {
|
||||
updateMessageReaction(event.messageId, event.userId, event.emoji)
|
||||
}
|
||||
}
|
||||
is ChatEvent.MessageDeleted -> {
|
||||
_state.update { currentState ->
|
||||
currentState.copy(messages = currentState.messages.filter { it.id != event.messageId })
|
||||
}
|
||||
}
|
||||
is ChatEvent.MessageEdited -> {
|
||||
_state.update { currentState ->
|
||||
val updated = currentState.messages.map { msg ->
|
||||
if (msg.id == event.messageId) msg.copy(content = event.content) else msg
|
||||
}
|
||||
currentState.copy(messages = updated)
|
||||
}
|
||||
}
|
||||
is ChatEvent.UserTyping -> {
|
||||
_state.update { it.copy(isTyping = true) }
|
||||
@@ -245,6 +285,18 @@ class ChatDetailViewModel @Inject constructor(
|
||||
currentState.copy(messages = updatedMessages)
|
||||
}
|
||||
}
|
||||
is ChatEvent.MessagePinned -> {
|
||||
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
||||
_state.update { s ->
|
||||
val domainMsg = event.message.toDomain(getCurrentUserId(), baseUrl)
|
||||
s.copy(pinnedMessages = (s.pinnedMessages + domainMsg).distinctBy { it.id })
|
||||
}
|
||||
}
|
||||
is ChatEvent.MessageUnpinned -> {
|
||||
_state.update { s ->
|
||||
s.copy(pinnedMessages = s.pinnedMessages.filter { it.id != event.messageId })
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
@@ -313,6 +365,46 @@ class ChatDetailViewModel @Inject constructor(
|
||||
_state.update { it.copy(replyingMessage = message) }
|
||||
}
|
||||
|
||||
fun forwardMessages(targetChatId: String, messages: List<Message>) {
|
||||
viewModelScope.launch {
|
||||
messages.forEach { msg ->
|
||||
repository.sendMessage(
|
||||
chatId = targetChatId,
|
||||
content = msg.content,
|
||||
type = msg.mediaType.name.lowercase(),
|
||||
forwardedFromId = msg.senderId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onForward(message: Message) {
|
||||
_state.update { it.copy(forwardingMessages = listOf(message)) }
|
||||
loadChatsForForwarding()
|
||||
}
|
||||
|
||||
fun loadChatsForForwarding() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val chats = repository.getChats()
|
||||
_state.update { it.copy(availableChatsToForward = chats) }
|
||||
} catch (e: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelForwarding() {
|
||||
_state.update { it.copy(forwardingMessages = emptyList(), availableChatsToForward = emptyList()) }
|
||||
}
|
||||
|
||||
fun onForwardSelectedMessages() {
|
||||
val selectedIds = _state.value.selectedMessageIds
|
||||
val messages = _state.value.messages.filter { selectedIds.contains(it.id) }
|
||||
_state.update { it.copy(forwardingMessages = messages) }
|
||||
loadChatsForForwarding()
|
||||
}
|
||||
|
||||
fun toggleSelection(messageId: String) {
|
||||
_state.update { s ->
|
||||
val newSelection = if (s.selectedMessageIds.contains(messageId)) {
|
||||
@@ -332,13 +424,70 @@ class ChatDetailViewModel @Inject constructor(
|
||||
_state.update { it.copy(replyingMessage = null) }
|
||||
}
|
||||
|
||||
fun cancelEdit() {
|
||||
_state.update { it.copy(editingMessage = null, inputText = "") }
|
||||
}
|
||||
|
||||
fun deleteMessage(message: Message, forEveryone: Boolean) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.deleteMessage(message.id, forEveryone)
|
||||
// Local update if needed (will also come via SignalR for everyone, but forMe might need local only update)
|
||||
_state.update { currentState ->
|
||||
currentState.copy(messages = currentState.messages.filter { it.id != message.id })
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.update { it.copy(error = "Delete failed: ${e.localizedMessage}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onEdit(message: Message) {
|
||||
_state.update { it.copy(editingMessage = message, inputText = message.content ?: "", replyingMessage = null) }
|
||||
}
|
||||
|
||||
fun pinMessage(messageId: String) {
|
||||
val chatId = currentChatId ?: return
|
||||
signalrClient.pinMessage(messageId, chatId)
|
||||
}
|
||||
|
||||
fun unpinMessage(messageId: String) {
|
||||
val chatId = currentChatId ?: return
|
||||
signalrClient.unpinMessage(messageId, chatId)
|
||||
}
|
||||
|
||||
fun onPin(message: Message) {
|
||||
pinMessage(message.id)
|
||||
}
|
||||
|
||||
fun sendMessage(onFail: (String) -> Unit = {}) {
|
||||
val chatId = currentChatId ?: return
|
||||
val text = _state.value.inputText
|
||||
val pending = _state.value.pendingAttachments
|
||||
val replyToId = _state.value.replyingMessage?.id
|
||||
val editingMsg = _state.value.editingMessage
|
||||
if (text.isBlank() && pending.isEmpty()) return
|
||||
|
||||
// Handle Edit
|
||||
if (editingMsg != null) {
|
||||
_state.update { it.copy(inputText = "", editingMessage = null) }
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val updated = repository.editMessage(editingMsg.id, text)
|
||||
_state.update { currentState ->
|
||||
val updatedList = currentState.messages.map {
|
||||
if (it.id == updated.id) updated else it
|
||||
}
|
||||
currentState.copy(messages = updatedList)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.update { it.copy(error = "Edit failed: ${e.localizedMessage}") }
|
||||
onFail(text)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Clear input immediately to avoid double clicks and ensure UI experience
|
||||
_state.update { it.copy(inputText = "", replyingMessage = null) }
|
||||
|
||||
@@ -417,13 +566,11 @@ class ChatDetailViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun addReaction(messageId: String, emoji: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.addReaction(messageId, emoji)
|
||||
} catch (e: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
val chatId = currentChatId ?: return
|
||||
|
||||
// We rely on SignalR event to update the count to avoid double counting
|
||||
// especially since domain Message doesn't track user IDs for reactions yet.
|
||||
signalrClient.addReaction(messageId, chatId, emoji)
|
||||
}
|
||||
|
||||
fun sendVoiceMessage(file: File) {
|
||||
|
||||
Reference in New Issue
Block a user