Improve message forwarding picker

This commit is contained in:
sevenhill
2026-07-09 15:21:02 +03:00
parent 0a42e6f860
commit 82f241df13
2 changed files with 152 additions and 25 deletions
@@ -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<ChatDto>,
selectedChatId: String?,
session: QMaxSession?,
cachedAvatarPaths: Map<String, String>,
onCacheAvatar: (String) -> Unit,
onSelect: (ChatDto) -> Unit,
onSend: (List<ChatDto>) -> 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<String>()) }
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<String, String>,
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)
}
}
}
}
}
@@ -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<ChatDto>) = 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)