feat(ui): add CreateGroupChatDialog for multi-bot room creation (#982)

This commit is contained in:
M57 authored and GitHub committed 2026-08-27 01:52:34 +03:00
1 parent d4d6a04afc
commit 274120cc86
7 files changed
+273

No files matched your search

@@ -21,6 +21,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.GroupAdd
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
@@ -71,6 +72,7 @@ fun BotsScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle()
var isSearchActive by remember { mutableStateOf(false) }
var showCreateDialog by remember { mutableStateOf(false) }
var showCreateGroupDialog by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
if (showCreateDialog) {
@@ -84,6 +86,18 @@ fun BotsScreen(
)
}
if (showCreateGroupDialog) {
CreateGroupChatDialog(
availableBots = state.profiles,
onDismiss = { showCreateGroupDialog = false },
onCreateGroup = { groupName, botNames ->
viewModel.createGroupChat(groupName, botNames) {
showCreateGroupDialog = false
}
},
)
}
ToastEffect(
toastMessage = state.toastMessage,
onClearToast = { viewModel.clearToast() },
@@ -130,6 +144,15 @@ fun BotsScreen(
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
actions = {
if (!isSearchActive) {
IconButton(
onClick = { showCreateGroupDialog = true },
modifier = Modifier.testTag("bots_action_create_group"),
) {
Icon(
imageVector = Icons.Filled.GroupAdd,
contentDescription = stringResource(R.string.bots_action_create_group),
)
}
IconButton(
onClick = { showCreateDialog = true },
modifier = Modifier.testTag("bots_action_create"),
@@ -34,6 +34,13 @@ data class BotsUiState(
val hasHiddenBots: Boolean
get() = profiles.any { it.isHidden }
val allGroups: List<String>
get() =
profiles
.flatMap { it.botMeta()?.groups.orEmpty() }
.distinct()
.sorted()
val activeNowBots: List<ProfileInfo>
get() {
val nowSeconds = System.currentTimeMillis() / 1000
@@ -181,6 +188,29 @@ class BotsViewModel(
}
}
fun createGroupChat(
groupName: String,
botNames: List<String>,
onSuccess: () -> Unit,
) {
viewModelScope.launch(ioDispatcher) {
val currentProfiles = _uiState.value.profiles
for (name in botNames) {
val bot = currentProfiles.find { it.name == name } ?: continue
val existingGroups = bot.botMeta()?.groups.orEmpty()
if (!existingGroups.contains(groupName)) {
val updatedMeta =
(bot.botMeta() ?: BotRosterMeta()).copy(
groups = existingGroups + groupName,
)
wsClientConfigureBot(name, updatedMeta)
}
}
loadBots()
onSuccess()
}
}
private fun wsClientConfigureBot(
name: String,
meta: BotRosterMeta,
@@ -199,6 +229,9 @@ class BotsViewModel(
},
)
}
if (!meta.groups.isNullOrEmpty()) {
put("groups", meta.groups)
}
}
HermesWsClient.send(
@@ -0,0 +1,161 @@
package com.m57.hermescontrol.ui.bots
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.ProfileInfo
import com.m57.hermescontrol.ui.common.BotAvatar
@Composable
fun CreateGroupChatDialog(
availableBots: List<ProfileInfo>,
onDismiss: () -> Unit,
onCreateGroup: (groupName: String, selectedBotNames: List<String>) -> Unit,
) {
var groupName by remember { mutableStateOf("") }
var selectedBots by remember { mutableStateOf(setOf<String>()) }
val defaultPlaceholder =
if (selectedBots.isNotEmpty()) {
selectedBots.joinToString(", ")
} else {
stringResource(R.string.bots_create_group_name_placeholder)
}
val effectiveName = groupName.ifBlank { selectedBots.joinToString(", ") }
val isValid = selectedBots.size >= 2 && effectiveName.isNotBlank()
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = stringResource(R.string.bots_create_group_title),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
)
},
text = {
Column(modifier = Modifier.fillMaxWidth()) {
OutlinedTextField(
value = groupName,
onValueChange = { groupName = it },
label = { Text(stringResource(R.string.bots_create_group_name_label)) },
placeholder = { Text(defaultPlaceholder) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(R.string.bots_create_group_select_bots),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(4.dp))
LazyColumn(
modifier =
Modifier
.fillMaxWidth()
.heightIn(max = 240.dp),
) {
items(availableBots, key = { it.name }) { bot ->
val isChecked = selectedBots.contains(bot.name)
Row(
modifier =
Modifier
.fillMaxWidth()
.clickable {
selectedBots =
if (isChecked) {
selectedBots - bot.name
} else {
selectedBots + bot.name
}
}
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = isChecked,
onCheckedChange = { checked ->
selectedBots =
if (checked) {
selectedBots + bot.name
} else {
selectedBots - bot.name
}
},
)
Spacer(modifier = Modifier.width(4.dp))
BotAvatar(
name = bot.name,
avatar = bot.botMeta()?.avatar,
size = 28.dp,
showPresence = false,
)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = bot.effectiveTitle,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold,
)
Text(
text = "@${bot.name}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}
}
},
confirmButton = {
Button(
onClick = {
if (isValid) {
onCreateGroup(effectiveName.trim(), selectedBots.toList())
}
},
enabled = isValid,
) {
Text(stringResource(R.string.bots_create_group_button))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.action_cancel))
}
},
)
}