android settings: split menu into telegram-like folder pages
This commit is contained in:
@@ -786,3 +786,14 @@
|
||||
- restart realtime socket on new active account token,
|
||||
- force refresh chats for both `archived=false` and `archived=true` right after switch.
|
||||
- Fixed navigation behavior on account switch to avoid noisy `popBackStack ... not found` and stale restored stack state.
|
||||
|
||||
### Step 115 - Settings UI restructured into Telegram-like folders
|
||||
- Reworked Settings into a menu-first screen with Telegram-style grouped rows.
|
||||
- Added per-item folder pages (subscreens) for:
|
||||
- Account
|
||||
- Chat settings
|
||||
- Privacy
|
||||
- Notifications
|
||||
- Devices
|
||||
- Data/Chat folders/Power/Language placeholders
|
||||
- Kept theme logic intact and moved appearance controls into `Chat settings` folder.
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package ru.daemonlord.messenger.ui.settings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -19,18 +21,25 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.BatterySaver
|
||||
import androidx.compose.material.icons.filled.Chat
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.Devices
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
@@ -49,22 +58,34 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.compose.AsyncImage
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import ru.daemonlord.messenger.domain.settings.model.AppThemeMode
|
||||
import ru.daemonlord.messenger.ui.account.AccountUiState
|
||||
import ru.daemonlord.messenger.ui.account.AccountViewModel
|
||||
|
||||
private enum class SettingsFolder(val title: String) {
|
||||
Account("Аккаунт"),
|
||||
Chat("Настройки чатов"),
|
||||
Privacy("Конфиденциальность"),
|
||||
Notifications("Уведомления"),
|
||||
Data("Данные и память"),
|
||||
Folders("Папки с чатами"),
|
||||
Devices("Устройства"),
|
||||
Power("Энергосбережение"),
|
||||
Language("Язык"),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsRoute(
|
||||
onBackToChats: () -> Unit,
|
||||
@@ -95,84 +116,13 @@ fun SettingsScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val profile = state.profile
|
||||
val scrollState = rememberScrollState()
|
||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||
|
||||
var showAddAccountDialog by remember { mutableStateOf(false) }
|
||||
var addEmail by remember { mutableStateOf("") }
|
||||
var addPassword by remember { mutableStateOf("") }
|
||||
|
||||
var privacyPm by remember(profile?.privacyPrivateMessages) { mutableStateOf(profile?.privacyPrivateMessages ?: "everyone") }
|
||||
var privacyLastSeen by remember(profile?.privacyLastSeen) { mutableStateOf(profile?.privacyLastSeen ?: "everyone") }
|
||||
var privacyAvatar by remember(profile?.privacyAvatar) { mutableStateOf(profile?.privacyAvatar ?: "everyone") }
|
||||
var privacyGroupInvites by remember(profile?.privacyGroupInvites) { mutableStateOf(profile?.privacyGroupInvites ?: "everyone") }
|
||||
var twoFactorCode by remember { mutableStateOf("") }
|
||||
var recoveryRegenerateCode by remember { mutableStateOf("") }
|
||||
val isTablet = LocalConfiguration.current.screenWidthDp >= 840
|
||||
var folder by rememberSaveable { mutableStateOf<SettingsFolder?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.refresh()
|
||||
onMainBarVisibilityChanged(true)
|
||||
}
|
||||
LaunchedEffect(scrollState) {
|
||||
var prevOffset = 0
|
||||
snapshotFlow { scrollState.value }
|
||||
.collectLatest { offset ->
|
||||
when {
|
||||
offset == 0 -> onMainBarVisibilityChanged(true)
|
||||
offset > prevOffset -> onMainBarVisibilityChanged(false)
|
||||
offset < prevOffset -> onMainBarVisibilityChanged(true)
|
||||
}
|
||||
prevOffset = offset
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddAccountDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!state.isAddingAccount) showAddAccountDialog = false },
|
||||
title = { Text("Add account") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = addEmail,
|
||||
onValueChange = { addEmail = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
label = { Text("Email") },
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = addPassword,
|
||||
onValueChange = { addPassword = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
label = { Text("Password") },
|
||||
)
|
||||
if (state.isAddingAccount) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.addAccount(addEmail, addPassword) { success ->
|
||||
if (success) {
|
||||
showAddAccountDialog = false
|
||||
addEmail = ""
|
||||
addPassword = ""
|
||||
onSwitchAccount()
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !state.isAddingAccount,
|
||||
) { Text("Sign in") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { if (!state.isAddingAccount) showAddAccountDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -184,455 +134,389 @@ fun SettingsScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isTabletLayout) Modifier.widthIn(max = 720.dp) else Modifier)
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.padding(bottom = 96.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text("Settings", style = MaterialTheme.typography.headlineSmall)
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(22.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
if (!profile?.avatarUrl.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = profile?.avatarUrl,
|
||||
contentDescription = "Profile avatar",
|
||||
modifier = Modifier
|
||||
.size(58.dp)
|
||||
.clip(CircleShape),
|
||||
if (folder == null) {
|
||||
SettingsHome(
|
||||
state = state,
|
||||
name = profile?.name.orEmpty(),
|
||||
email = profile?.email.orEmpty(),
|
||||
username = profile?.username.orEmpty(),
|
||||
avatarUrl = profile?.avatarUrl,
|
||||
onOpenProfile = onOpenProfile,
|
||||
onOpenFolder = { folder = it },
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
SettingsFolderView(
|
||||
state = state,
|
||||
folder = folder ?: SettingsFolder.Account,
|
||||
onBack = { folder = null },
|
||||
onBackToChats = onBackToChats,
|
||||
onSwitchAccount = onSwitchAccount,
|
||||
onLogout = onLogout,
|
||||
onOpenProfile = onOpenProfile,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsHome(
|
||||
state: AccountUiState,
|
||||
name: String,
|
||||
email: String,
|
||||
username: String,
|
||||
avatarUrl: String?,
|
||||
onOpenProfile: () -> Unit,
|
||||
onOpenFolder: (SettingsFolder) -> Unit,
|
||||
) {
|
||||
val scroll = rememberScrollState()
|
||||
val active = state.storedAccounts.firstOrNull { it.isActive }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.size(58.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center,
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scroll)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.padding(bottom = 96.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = profile?.name?.firstOrNull()?.uppercase() ?: "?",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
ProfileHeader(name.ifBlank { "User" }, email, username, avatarUrl, onOpenProfile)
|
||||
|
||||
SettingsCard {
|
||||
Text("АККАУНТЫ", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelMedium)
|
||||
SettingsShortcut(
|
||||
title = active?.title ?: "No active account",
|
||||
subtitle = active?.subtitle ?: "Add account",
|
||||
onClick = { onOpenFolder(SettingsFolder.Account) },
|
||||
)
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
SettingsRow(Icons.Filled.AccountCircle, "Аккаунт", "Номер, имя пользователя, о себе") { onOpenFolder(SettingsFolder.Account) }
|
||||
SettingsRow(Icons.Filled.Chat, "Настройки чатов", "Обои, ночной режим, анимации") { onOpenFolder(SettingsFolder.Chat) }
|
||||
SettingsRow(Icons.Filled.Lock, "Конфиденциальность", "Время захода, устройства, ключи доступа") { onOpenFolder(SettingsFolder.Privacy) }
|
||||
SettingsRow(Icons.Filled.Notifications, "Уведомления", "Звуки, счётчик сообщений") { onOpenFolder(SettingsFolder.Notifications) }
|
||||
SettingsRow(Icons.Filled.Storage, "Данные и память", "Настройки загрузки медиафайлов") { onOpenFolder(SettingsFolder.Data) }
|
||||
SettingsRow(Icons.Filled.Folder, "Папки с чатами", "Сортировка чатов по папкам") { onOpenFolder(SettingsFolder.Folders) }
|
||||
SettingsRow(Icons.Filled.Devices, "Устройства", "Управление активными сеансами") { onOpenFolder(SettingsFolder.Devices) }
|
||||
SettingsRow(Icons.Filled.BatterySaver, "Энергосбережение", "Экономия энергии при низком заряде") { onOpenFolder(SettingsFolder.Power) }
|
||||
SettingsRow(Icons.Filled.Language, "Язык", "Русский", divider = false) { onOpenFolder(SettingsFolder.Language) }
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = profile?.name ?: "Loading...",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = listOfNotNull(
|
||||
profile?.email,
|
||||
profile?.username?.takeIf { it.isNotBlank() }?.let { "@$it" },
|
||||
).joinToString(" • "),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onOpenProfile) {
|
||||
Icon(Icons.Filled.Person, contentDescription = "Profile")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsFolderView(
|
||||
state: AccountUiState,
|
||||
folder: SettingsFolder,
|
||||
onBack: () -> Unit,
|
||||
onBackToChats: () -> Unit,
|
||||
onSwitchAccount: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
onOpenProfile: () -> Unit,
|
||||
viewModel: AccountViewModel,
|
||||
) {
|
||||
val scroll = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scroll)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.padding(bottom = 96.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
Text(folder.title, style = MaterialTheme.typography.headlineSmall)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (state.isLoading || state.isSaving) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
when (folder) {
|
||||
SettingsFolder.Account -> AccountFolder(state, onSwitchAccount, onOpenProfile, onLogout, viewModel)
|
||||
SettingsFolder.Chat -> ChatFolder(state, viewModel)
|
||||
SettingsFolder.Privacy -> PrivacyFolder(state, viewModel)
|
||||
SettingsFolder.Notifications -> NotificationsFolder(state, viewModel)
|
||||
SettingsFolder.Devices -> DevicesFolder(state, viewModel)
|
||||
SettingsFolder.Data -> PlaceholderFolder("Раздел будет расширен в следующем шаге.")
|
||||
SettingsFolder.Folders -> PlaceholderFolder("Управление папками добавим следующей итерацией.")
|
||||
SettingsFolder.Power -> PlaceholderFolder("Параметры энергосбережения добавим отдельным шагом.")
|
||||
SettingsFolder.Language -> PlaceholderFolder("Смена языка будет вынесена в отдельный экран.")
|
||||
}
|
||||
|
||||
if (!state.message.isNullOrBlank()) Text(state.message.orEmpty(), color = MaterialTheme.colorScheme.primary)
|
||||
if (!state.errorMessage.isNullOrBlank()) Text(state.errorMessage.orEmpty(), color = MaterialTheme.colorScheme.error)
|
||||
|
||||
OutlinedButton(onClick = onBackToChats, modifier = Modifier.fillMaxWidth()) { Text("Back to chats") }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountFolder(
|
||||
state: AccountUiState,
|
||||
onSwitchAccount: () -> Unit,
|
||||
onOpenProfile: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
viewModel: AccountViewModel,
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var addEmail by remember { mutableStateOf("") }
|
||||
var addPassword by remember { mutableStateOf("") }
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!state.isAddingAccount) showDialog = false },
|
||||
title = { Text("Add account") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(value = addEmail, onValueChange = { addEmail = it }, label = { Text("Email") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = addPassword, onValueChange = { addPassword = it }, label = { Text("Password") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
viewModel.addAccount(addEmail, addPassword) { ok ->
|
||||
if (ok) {
|
||||
showDialog = false
|
||||
addEmail = ""
|
||||
addPassword = ""
|
||||
onSwitchAccount()
|
||||
}
|
||||
}
|
||||
}, enabled = !state.isAddingAccount) { Text("Sign in") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { if (!state.isAddingAccount) showDialog = false }) { Text("Cancel") } },
|
||||
)
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Accounts", style = MaterialTheme.typography.titleSmall)
|
||||
OutlinedButton(onClick = { showAddAccountDialog = true }) {
|
||||
OutlinedButton(onClick = { showDialog = true }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = null)
|
||||
Text("Add account", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
|
||||
state.storedAccounts.forEach { account ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
.padding(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
if (!account.avatarUrl.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = account.avatarUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(34.dp).clip(CircleShape),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
Text(account.title.firstOrNull()?.uppercase() ?: "?", modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = account.title.firstOrNull()?.uppercase() ?: "?",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.padding(top = 6.dp), maxLines = 1)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = account.title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = account.subtitle,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(account.title)
|
||||
Text(account.subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
if (account.isActive) {
|
||||
Text("Active", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelSmall)
|
||||
Text("Active", color = MaterialTheme.colorScheme.primary)
|
||||
} else {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
viewModel.switchStoredAccount(account.userId) { switched ->
|
||||
if (switched) onSwitchAccount()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text("Switch")
|
||||
}
|
||||
}
|
||||
OutlinedButton(onClick = { viewModel.removeStoredAccount(account.userId) }) {
|
||||
Icon(Icons.Filled.DeleteOutline, contentDescription = null)
|
||||
OutlinedButton(onClick = { viewModel.switchStoredAccount(account.userId) { if (it) onSwitchAccount() } }) { Text("Switch") }
|
||||
}
|
||||
OutlinedButton(onClick = { viewModel.removeStoredAccount(account.userId) }) { Icon(Icons.Filled.DeleteOutline, contentDescription = null) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
SettingsCard {
|
||||
OutlinedButton(onClick = onOpenProfile, modifier = Modifier.fillMaxWidth()) { Text("Open profile") }
|
||||
Button(onClick = onLogout, modifier = Modifier.fillMaxWidth()) { Text("Logout") }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatFolder(state: AccountUiState, viewModel: AccountViewModel) {
|
||||
SettingsCard {
|
||||
Text("Appearance", style = MaterialTheme.typography.titleSmall)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ThemeButton(
|
||||
text = "Light",
|
||||
selected = state.appThemeMode == AppThemeMode.LIGHT,
|
||||
) { viewModel.setThemeMode(AppThemeMode.LIGHT) }
|
||||
ThemeButton(
|
||||
text = "Dark",
|
||||
selected = state.appThemeMode == AppThemeMode.DARK,
|
||||
) { viewModel.setThemeMode(AppThemeMode.DARK) }
|
||||
ThemeButton(
|
||||
text = "System",
|
||||
selected = state.appThemeMode == AppThemeMode.SYSTEM,
|
||||
) { viewModel.setThemeMode(AppThemeMode.SYSTEM) }
|
||||
ThemeButton("Light", state.appThemeMode == AppThemeMode.LIGHT) { viewModel.setThemeMode(AppThemeMode.LIGHT) }
|
||||
ThemeButton("Dark", state.appThemeMode == AppThemeMode.DARK) { viewModel.setThemeMode(AppThemeMode.DARK) }
|
||||
ThemeButton("System", state.appThemeMode == AppThemeMode.SYSTEM) { viewModel.setThemeMode(AppThemeMode.SYSTEM) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
Text("Notifications", style = MaterialTheme.typography.titleSmall)
|
||||
SettingsToggleRow(
|
||||
icon = Icons.Filled.Notifications,
|
||||
title = "Enable notifications",
|
||||
checked = state.notificationsEnabled,
|
||||
onCheckedChange = viewModel::setGlobalNotificationsEnabled,
|
||||
)
|
||||
SettingsToggleRow(
|
||||
icon = Icons.Filled.Visibility,
|
||||
title = "Show message preview",
|
||||
checked = state.notificationsPreviewEnabled,
|
||||
onCheckedChange = viewModel::setNotificationPreviewEnabled,
|
||||
)
|
||||
@Composable
|
||||
private fun NotificationsFolder(state: AccountUiState, viewModel: AccountViewModel) {
|
||||
SettingsCard {
|
||||
SettingsToggle(Icons.Filled.Notifications, "Enable notifications", state.notificationsEnabled, viewModel::setGlobalNotificationsEnabled)
|
||||
SettingsToggle(Icons.Filled.Visibility, "Show message preview", state.notificationsPreviewEnabled, viewModel::setNotificationPreviewEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
Text("Privacy", style = MaterialTheme.typography.titleSmall)
|
||||
PrivacyDropdownField(
|
||||
label = "Private messages",
|
||||
value = privacyPm,
|
||||
onValueChange = { privacyPm = it },
|
||||
)
|
||||
PrivacyDropdownField(
|
||||
label = "Last seen",
|
||||
value = privacyLastSeen,
|
||||
onValueChange = { privacyLastSeen = it },
|
||||
)
|
||||
PrivacyDropdownField(
|
||||
label = "Avatar",
|
||||
value = privacyAvatar,
|
||||
onValueChange = { privacyAvatar = it },
|
||||
)
|
||||
PrivacyDropdownField(
|
||||
label = "Group invites",
|
||||
value = privacyGroupInvites,
|
||||
onValueChange = { privacyGroupInvites = it },
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.updatePrivacy(
|
||||
privateMessages = privacyPm,
|
||||
lastSeen = privacyLastSeen,
|
||||
avatar = privacyAvatar,
|
||||
groupInvites = privacyGroupInvites,
|
||||
)
|
||||
},
|
||||
enabled = !state.isSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
@Composable
|
||||
private fun PrivacyFolder(state: AccountUiState, viewModel: AccountViewModel) {
|
||||
var pm by remember(state.profile?.privacyPrivateMessages) { mutableStateOf(state.profile?.privacyPrivateMessages ?: "everyone") }
|
||||
var lastSeen by remember(state.profile?.privacyLastSeen) { mutableStateOf(state.profile?.privacyLastSeen ?: "everyone") }
|
||||
var avatar by remember(state.profile?.privacyAvatar) { mutableStateOf(state.profile?.privacyAvatar ?: "everyone") }
|
||||
var invites by remember(state.profile?.privacyGroupInvites) { mutableStateOf(state.profile?.privacyGroupInvites ?: "everyone") }
|
||||
|
||||
SettingsCard {
|
||||
PrivacyDropdown("Private messages", pm) { pm = it }
|
||||
PrivacyDropdown("Last seen", lastSeen) { lastSeen = it }
|
||||
PrivacyDropdown("Avatar", avatar) { avatar = it }
|
||||
PrivacyDropdown("Group invites", invites) { invites = it }
|
||||
Button(onClick = { viewModel.updatePrivacy(pm, lastSeen, avatar, invites) }, enabled = !state.isSaving, modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(Icons.Filled.Lock, contentDescription = null)
|
||||
Text("Save privacy", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
Text("Blocked users", style = MaterialTheme.typography.titleSmall)
|
||||
state.blockedUsers.forEach { blocked ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(blocked.name, style = MaterialTheme.typography.bodyMedium)
|
||||
if (!blocked.username.isNullOrBlank()) {
|
||||
Text("@${blocked.username}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
OutlinedButton(onClick = { viewModel.unblockUser(blocked.id) }) {
|
||||
Text("Unblock")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
private fun DevicesFolder(state: AccountUiState, viewModel: AccountViewModel) {
|
||||
var twoFactorCode by remember { mutableStateOf("") }
|
||||
var recoveryCode by remember { mutableStateOf("") }
|
||||
|
||||
SettingsSectionCard {
|
||||
SettingsCard {
|
||||
Text("Sessions & Security", style = MaterialTheme.typography.titleSmall)
|
||||
state.sessions.forEach { session ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
state.sessions.forEach { s ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Icon(Icons.Filled.Devices, contentDescription = null)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(session.userAgent ?: "Unknown device", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(session.ipAddress ?: "Unknown IP", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(s.userAgent ?: "Unknown device")
|
||||
Text(s.ipAddress ?: "Unknown IP", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.revokeSession(session.jti) },
|
||||
enabled = !state.isSaving && session.current != true,
|
||||
) {
|
||||
Text("Revoke")
|
||||
OutlinedButton(onClick = { viewModel.revokeSession(s.jti) }, enabled = !state.isSaving && s.current != true) { Text("Revoke") }
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = viewModel::revokeAllSessions,
|
||||
enabled = !state.isSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
OutlinedButton(onClick = viewModel::revokeAllSessions, enabled = !state.isSaving, modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(Icons.Filled.Security, contentDescription = null)
|
||||
Text("Revoke all sessions", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
OutlinedTextField(value = twoFactorCode, onValueChange = { twoFactorCode = it }, label = { Text("2FA code") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = viewModel::setupTwoFactor, enabled = !state.isSaving) {
|
||||
Text("Setup 2FA")
|
||||
Button(onClick = { viewModel.enableTwoFactor(twoFactorCode) }, enabled = twoFactorCode.isNotBlank() && !state.isSaving) { Text("Enable") }
|
||||
OutlinedButton(onClick = { viewModel.disableTwoFactor(twoFactorCode) }, enabled = twoFactorCode.isNotBlank() && !state.isSaving) { Text("Disable") }
|
||||
}
|
||||
OutlinedButton(onClick = viewModel::refreshRecoveryStatus, enabled = !state.isSaving) {
|
||||
Text("Recovery status")
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = twoFactorCode,
|
||||
onValueChange = { twoFactorCode = it },
|
||||
label = { Text("2FA code") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { viewModel.enableTwoFactor(twoFactorCode) }, enabled = twoFactorCode.isNotBlank() && !state.isSaving) {
|
||||
Text("Enable")
|
||||
}
|
||||
OutlinedButton(onClick = { viewModel.disableTwoFactor(twoFactorCode) }, enabled = twoFactorCode.isNotBlank() && !state.isSaving) {
|
||||
Text("Disable")
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = recoveryRegenerateCode,
|
||||
onValueChange = { recoveryRegenerateCode = it },
|
||||
label = { Text("Code for recovery regeneration") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.regenerateRecoveryCodes(recoveryRegenerateCode) },
|
||||
enabled = recoveryRegenerateCode.isNotBlank() && !state.isSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
OutlinedTextField(value = recoveryCode, onValueChange = { recoveryCode = it }, label = { Text("Code for recovery regeneration") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||
OutlinedButton(onClick = { viewModel.regenerateRecoveryCodes(recoveryCode) }, enabled = recoveryCode.isNotBlank() && !state.isSaving, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Regenerate recovery codes")
|
||||
}
|
||||
if (state.recoveryCodesRemaining != null) {
|
||||
Text("Recovery codes left: ${state.recoveryCodesRemaining}", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
state.recoveryCodes.forEach { code ->
|
||||
Text(code, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.message.isNullOrBlank()) {
|
||||
Text(state.message.orEmpty(), color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
if (!state.errorMessage.isNullOrBlank()) {
|
||||
Text(state.errorMessage.orEmpty(), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
|
||||
Button(onClick = onLogout, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Logout")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsSectionCard(content: @Composable ColumnScope.() -> Unit) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = content,
|
||||
)
|
||||
private fun PlaceholderFolder(text: String) {
|
||||
SettingsCard { Text(text) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileHeader(name: String, email: String, username: String, avatarUrl: String?, onOpenProfile: () -> Unit) {
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (!avatarUrl.isNullOrBlank()) {
|
||||
AsyncImage(model = avatarUrl, contentDescription = null, modifier = Modifier.size(84.dp).clip(CircleShape))
|
||||
} else {
|
||||
Box(modifier = Modifier.size(84.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primaryContainer), contentAlignment = Alignment.Center) {
|
||||
Text(name.firstOrNull()?.uppercase() ?: "?")
|
||||
}
|
||||
}
|
||||
Text(name, style = MaterialTheme.typography.headlineSmall)
|
||||
Text(listOfNotNull(email.takeIf { it.isNotBlank() }, username.takeIf { it.isNotBlank() }?.let { "@$it" }).joinToString(" • "), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
TextButton(onClick = onOpenProfile) { Text("Open profile") }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsActionRow(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
private fun SettingsCard(content: @Composable ColumnScope.() -> Unit) {
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(22.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(8.dp), content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsShortcut(title: String, subtitle: String, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp)).clickable(onClick = onClick).padding(horizontal = 8.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text(title.firstOrNull()?.uppercase() ?: "A", modifier = Modifier.size(30.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primaryContainer).padding(top = 6.dp), maxLines = 1)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(title)
|
||||
Text(subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsRow(icon: ImageVector, title: String, subtitle: String, divider: Boolean = true, onClick: () -> Unit) {
|
||||
Column(modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(vertical = 4.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp, vertical = 6.dp),
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
|
||||
Box(modifier = Modifier.size(34.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primaryContainer), contentAlignment = Alignment.Center) {
|
||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(title, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(title)
|
||||
Text(subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
OutlinedButton(onClick = onClick) {
|
||||
Text("Open")
|
||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null)
|
||||
}
|
||||
if (divider) {
|
||||
Spacer(modifier = Modifier.fillMaxWidth().padding(start = 50.dp).size(width = 1.dp, height = 0.5.dp).background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.7f)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsToggleRow(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
title: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
private fun SettingsToggle(icon: ImageVector, title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp)).background(MaterialTheme.colorScheme.surfaceContainerHighest).padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
|
||||
Text(title, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
||||
Text(title, modifier = Modifier.weight(1f))
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeButton(
|
||||
text: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
if (selected) {
|
||||
Button(onClick = onClick) { Text(text) }
|
||||
} else {
|
||||
OutlinedButton(onClick = onClick) { Text(text) }
|
||||
}
|
||||
private fun ThemeButton(text: String, selected: Boolean, onClick: () -> Unit) {
|
||||
if (selected) Button(onClick = onClick) { Text(text) } else OutlinedButton(onClick = onClick) { Text(text) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun PrivacyDropdownField(
|
||||
label: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
) {
|
||||
private fun PrivacyDropdown(label: String, value: String, onChange: (String) -> Unit) {
|
||||
val options = listOf("everyone", "contacts", "nobody")
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded },
|
||||
) {
|
||||
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = !expanded }) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(label) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth(),
|
||||
modifier = Modifier.menuAnchor().fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
options.forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(option) },
|
||||
onClick = {
|
||||
onValueChange(option)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(text = { Text(option) }, onClick = { onChange(option); expanded = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user