android: add avatar editors and localized group properties dialog

This commit is contained in:
evgeny committed 2026-10-06 16:24:25 +02:00
1 parent 73019cfd83
commit b9d41d179d
3 files changed
+149

No files matched your search

@@ -0,0 +1,123 @@
package com.utun.chat.ui.components
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.utun.chat.ChatApplication
import com.utun.chat.R
import com.utun.chat.data.*
import kotlinx.coroutines.launch
/** Один диалог для меню списка и кнопки в шапке группового чата. */
@Composable
fun GroupPropertiesDialog(channel: Channel, onDismiss: () -> Unit) {
val repository = ChatApplication.instance.avatars
val groups by repository.groups.collectAsState()
val edits by repository.edits.collectAsState()
val state = groups[channel.id]
val editState = edits["group:${channel.id}"] ?: AvatarEditState.IDLE
val working = editState == AvatarEditState.WORKING
var soundOn by rememberSaveable(channel.id) { mutableStateOf(!NotificationSound.isMuted(channel.id)) }
var pickedAccount by rememberSaveable { mutableStateOf(0L) }
var pickedGroup by rememberSaveable { mutableStateOf("") }
val scope = rememberCoroutineScope()
val picker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null) repository.editGroup(pickedGroup, uri, pickedAccount)
}
LaunchedEffect(channel.id) { repository.ensureGroup(channel.id, true) }
AlertDialog(onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.group_properties)) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
GroupAvatar(channel.id, channel.name, size = 64.dp)
Column { Text(channel.name, style = MaterialTheme.typography.titleMedium); Text(channel.id) }
}
if (state?.loading == true || working) LinearProgressIndicator(Modifier.fillMaxWidth())
if (state?.error == true) {
Text(stringResource(R.string.avatar_load_failed), color = MaterialTheme.colorScheme.error)
TextButton(onClick = { repository.ensureGroup(channel.id, true) }) { Text(stringResource(R.string.avatar_retry)) }
}
if (state?.canEdit == true) {
Text(stringResource(R.string.group_avatar_limits), style = MaterialTheme.typography.bodySmall)
Row {
TextButton(enabled = !working, onClick = {
pickedAccount = repository.accountId; pickedGroup = channel.id; picker.launch("image/*")
}) { Text(stringResource(R.string.avatar_upload)) }
TextButton(enabled = !working && state.hasImage, onClick = {
repository.editGroup(channel.id, null, repository.accountId)
}) { Text(stringResource(R.string.avatar_remove)) }
}
} else Text(stringResource(R.string.group_avatar_key_required), style = MaterialTheme.typography.bodySmall)
if (editState == AvatarEditState.ERROR)
Text(stringResource(R.string.avatar_edit_failed), color = MaterialTheme.colorScheme.error)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.sound), modifier = Modifier.weight(1f))
Switch(checked = soundOn, onCheckedChange = {
soundOn = it; NotificationSound.setMuted(channel.id, !it)
scope.launch { ChatApplication.instance.configProvider.setChannelMuted(channel.id, !it) }
})
}
}
},
confirmButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.close)) } })
}
@Composable
fun ProfileAvatarControls() {
val repository = ChatApplication.instance.avatars
val members by repository.members.collectAsState()
val edits by repository.edits.collectAsState()
val servers = remember(members) { repository.storageServers() }
var selectedServer by rememberSaveable { mutableStateOf(0L) }
var menu by remember { mutableStateOf(false) }
var pickedAccount by rememberSaveable { mutableStateOf(0L) }
var pickedServer by rememberSaveable { mutableStateOf(0L) }
val state = edits["profile"] ?: AvatarEditState.IDLE
LaunchedEffect(servers) {
if (servers.none { it.nodeId == selectedServer }) selectedServer = servers.firstOrNull()?.nodeId ?: 0L
}
val picker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null) repository.editProfile(pickedServer, uri, pickedAccount)
}
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text(stringResource(R.string.profile_avatar), style = MaterialTheme.typography.titleMedium)
UserAvatar(null, repository.accountId, "", size = 64.dp, profile = true)
Text(stringResource(R.string.profile_avatar_hint), style = MaterialTheme.typography.bodySmall)
if (servers.isEmpty()) Text(stringResource(R.string.avatar_no_storage), style = MaterialTheme.typography.bodySmall)
else Box {
TextButton(onClick = { menu = true }, enabled = state != AvatarEditState.WORKING) {
Text(stringResource(R.string.avatar_storage_server, servers.firstOrNull { it.nodeId == selectedServer }?.let {
"${it.name.ifBlank { AvatarProtocol.hex(it.nodeId) }} (${AvatarProtocol.hex(it.nodeId)})"
} ?: "…"))
}
DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) {
servers.forEach { server ->
DropdownMenuItem(text = { Text("${server.name} (${AvatarProtocol.hex(server.nodeId)})") },
onClick = { selectedServer = server.nodeId; menu = false })
}
}
}
Row {
TextButton(enabled = selectedServer != 0L && state != AvatarEditState.WORKING, onClick = {
pickedAccount = repository.accountId; pickedServer = selectedServer; picker.launch("image/*")
}) { Text(stringResource(R.string.avatar_upload)) }
TextButton(enabled = repository.accountId != 0L && state != AvatarEditState.WORKING, onClick = {
repository.editProfile(0, null, repository.accountId)
}) { Text(stringResource(R.string.avatar_remove)) }
}
when (state) {
AvatarEditState.WORKING -> LinearProgressIndicator(Modifier.fillMaxWidth())
AvatarEditState.ERROR -> Text(stringResource(R.string.avatar_edit_failed), color = MaterialTheme.colorScheme.error)
AvatarEditState.SAVED -> Text(stringResource(R.string.profile_avatar_saved), style = MaterialTheme.typography.bodySmall)
else -> Unit
}
}
}