diff --git a/android/app/src/main/java/xyz/kusoft/qmax/MainActivity.kt b/android/app/src/main/java/xyz/kusoft/qmax/MainActivity.kt index b1d78c2..860ea20 100644 --- a/android/app/src/main/java/xyz/kusoft/qmax/MainActivity.kt +++ b/android/app/src/main/java/xyz/kusoft/qmax/MainActivity.kt @@ -2088,14 +2088,16 @@ private fun ChatScreen(vm: QMaxViewModel) { onClose = { materialsOpen = false } ) } - if (state.forwardTarget != null) { + val forwardTarget = state.forwardTarget + if (forwardTarget != null) { ForwardMessageDialog( + message = forwardTarget, chats = state.chats, selectedChatId = chat.id, session = session, cachedAvatarPaths = state.cachedAvatarPaths, onCacheAvatar = vm::cacheAvatar, - onSelect = vm::forwardTo, + onSend = vm::forwardTo, onClose = vm::closeForwardPicker ) } @@ -2103,12 +2105,13 @@ private fun ChatScreen(vm: QMaxViewModel) { @Composable private fun ForwardMessageDialog( + message: MessageDto, chats: List, selectedChatId: String?, session: QMaxSession?, cachedAvatarPaths: Map, onCacheAvatar: (String) -> Unit, - onSelect: (ChatDto) -> Unit, + onSend: (List) -> Unit, onClose: () -> Unit ) { val sortedChats = remember(chats) { @@ -2117,30 +2120,70 @@ private fun ForwardMessageDialog( .thenByDescending { it.lastMessageAt.orEmpty() } ) } + var query by rememberSaveable { mutableStateOf("") } + var selectedChatIds by remember { mutableStateOf(emptySet()) } + val filteredChats = remember(sortedChats, query) { + val term = query.trim() + if (term.isBlank()) { + sortedChats + } else { + sortedChats.filter { chat -> + chat.title.contains(term, ignoreCase = true) || + chat.lastMessagePreview?.contains(term, ignoreCase = true) == true + } + } + } + val selectedChats = remember(sortedChats, selectedChatIds) { + sortedChats.filter { it.id in selectedChatIds } + } AlertDialog( onDismissRequest = onClose, confirmButton = { - TextButton(onClick = onClose) { - Text("\u041e\u0442\u043c\u0435\u043d\u0430") + Button( + onClick = { onSend(selectedChats) }, + enabled = selectedChats.isNotEmpty() + ) { + Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null) + Spacer(Modifier.width(6.dp)) + Text(if (selectedChats.size <= 1) "Отправить" else "Отправить (${selectedChats.size})") } }, - title = { Text("\u041f\u0435\u0440\u0435\u0441\u043b\u0430\u0442\u044c") }, + dismissButton = { + TextButton(onClick = onClose) { + Text("Отмена") + } + }, + title = { Text("Переслать") }, text = { - if (sortedChats.isEmpty()) { - EmptyState("\u0427\u0430\u0442\u043e\u0432 \u043f\u043e\u043a\u0430 \u043d\u0435\u0442", modifier = Modifier.height(220.dp)) - } else { - LazyColumn(Modifier.fillMaxWidth().height(430.dp)) { - items(sortedChats, key = { it.id }) { chat -> - ForwardChatRow( - chat = chat, - isCurrent = chat.id == selectedChatId, - session = session, - cachedAvatarPaths = cachedAvatarPaths, - onCacheAvatar = onCacheAvatar, - onClick = { onSelect(chat) } - ) - HorizontalDivider(color = Color(0xFFE9EEF2), thickness = 0.6.dp, modifier = Modifier.padding(start = 56.dp)) + Column(Modifier.fillMaxWidth()) { + ForwardMessagePreview(message) + Spacer(Modifier.height(10.dp)) + ForwardSearchField(query = query, onQuery = { query = it }) + Spacer(Modifier.height(8.dp)) + when { + sortedChats.isEmpty() -> EmptyState("Чатов пока нет", modifier = Modifier.height(220.dp)) + filteredChats.isEmpty() -> EmptyState("Ничего не найдено", modifier = Modifier.height(220.dp)) + else -> LazyColumn(Modifier.fillMaxWidth().height(430.dp)) { + items(filteredChats, key = { it.id }) { chat -> + val selected = chat.id in selectedChatIds + ForwardChatRow( + chat = chat, + isCurrent = chat.id == selectedChatId, + selected = selected, + session = session, + cachedAvatarPaths = cachedAvatarPaths, + onCacheAvatar = onCacheAvatar, + onClick = { + selectedChatIds = if (selected) { + selectedChatIds - chat.id + } else { + selectedChatIds + chat.id + } + } + ) + HorizontalDivider(color = Color(0xFFE9EEF2), thickness = 0.6.dp, modifier = Modifier.padding(start = 56.dp)) + } } } } @@ -2152,6 +2195,7 @@ private fun ForwardMessageDialog( private fun ForwardChatRow( chat: ChatDto, isCurrent: Boolean, + selected: Boolean, session: QMaxSession?, cachedAvatarPaths: Map, onCacheAvatar: (String) -> Unit, @@ -2201,7 +2245,82 @@ private fun ForwardChatRow( ) } Spacer(Modifier.width(8.dp)) - Text(formatChatTime(chat.lastMessageAt), color = QMaxMuted, style = MaterialTheme.typography.labelSmall) + if (selected) { + Surface(color = QMaxBlue, shape = CircleShape, modifier = Modifier.size(26.dp)) { + Box(contentAlignment = Alignment.Center) { + Icon(Icons.Filled.Check, contentDescription = "Выбрано", tint = Color.White, modifier = Modifier.size(18.dp)) + } + } + } else { + Text(formatChatTime(chat.lastMessageAt), color = QMaxMuted, style = MaterialTheme.typography.labelSmall) + } + } +} + +@Composable +private fun ForwardMessagePreview(message: MessageDto) { + Surface(color = Color(0xFFE7F1FA), shape = RoundedCornerShape(10.dp)) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + Modifier + .width(3.dp) + .height(38.dp) + .background(QMaxBlue) + ) + Spacer(Modifier.width(9.dp)) + Column(Modifier.weight(1f)) { + Text( + replyPreviewAuthor(message), + color = QMaxBlue, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + replyPreviewBody(message), + color = QMaxMuted, + style = MaterialTheme.typography.bodyMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} + +@Composable +private fun ForwardSearchField(query: String, onQuery: (String) -> Unit) { + Surface(color = Color(0xFFF1F1F3), shape = RoundedCornerShape(24.dp)) { + Row( + modifier = Modifier.fillMaxWidth().height(46.dp).padding(start = 14.dp, end = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Filled.Search, contentDescription = null, tint = QMaxMuted, modifier = Modifier.size(23.dp)) + TextField( + value = query, + onValueChange = onQuery, + placeholder = { Text("Кому переслать", color = Color(0xFF8A8E93)) }, + singleLine = true, + colors = TextFieldDefaults.colors( + focusedTextColor = QMaxText, + unfocusedTextColor = QMaxText, + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + cursorColor = QMaxBlue + ), + modifier = Modifier.weight(1f) + ) + if (query.isNotBlank()) { + IconButton(onClick = { onQuery("") }) { + Icon(Icons.Filled.Close, contentDescription = "Очистить поиск", tint = QMaxMuted) + } + } + } } } diff --git a/android/app/src/main/java/xyz/kusoft/qmax/ui/QMaxViewModel.kt b/android/app/src/main/java/xyz/kusoft/qmax/ui/QMaxViewModel.kt index 68a207a..5cdccb5 100644 --- a/android/app/src/main/java/xyz/kusoft/qmax/ui/QMaxViewModel.kt +++ b/android/app/src/main/java/xyz/kusoft/qmax/ui/QMaxViewModel.kt @@ -894,15 +894,23 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() { state.value = state.value.copy(forwardTarget = null) } - fun forwardTo(chat: ChatDto) = launchLoading(false) { + fun forwardTo(targetChats: List) = launchLoading(false) { val session = requireSession() val source = state.value.forwardTarget ?: return@launchLoading - val forwarded = repository.forwardMessage(session, source.chatId, source.id, chat.id) + val targets = targetChats.distinctBy { it.id } + if (targets.isEmpty()) return@launchLoading + val forwardedMessages = targets.map { chat -> + repository.forwardMessage(session, source.chatId, source.id, chat.id) + } val current = state.value - state.value = if (current.selectedChat?.id == chat.id) { + val openedChatId = current.selectedChat?.id + val openedChatForwardedMessages = forwardedMessages.filter { it.chatId == openedChatId } + state.value = if (openedChatForwardedMessages.isNotEmpty()) { current.copy( forwardTarget = null, - messages = (current.messages.filterNot { it.id == forwarded.id } + forwarded).sortedBy { it.sentAt } + messages = (current.messages.filterNot { existing -> + openedChatForwardedMessages.any { it.id == existing.id } + } + openedChatForwardedMessages).sortedBy { it.sentAt } ) } else { current.copy(forwardTarget = null)