enhancement: better contact detail screen ux
This commit is contained in:
+165
-34
@@ -1,5 +1,6 @@
|
||||
package com.bitcointxoko.gudariwallet.ui.contacts
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -44,6 +45,9 @@ fun ContactDetailScreen(
|
||||
val showAddAddressSheet by viewModel.showAddAddressSheet.collectAsStateWithLifecycle()
|
||||
val showEditAliasSheet by viewModel.showEditAliasSheet.collectAsStateWithLifecycle()
|
||||
|
||||
var isEditing by remember { mutableStateOf(false) }
|
||||
var pendingAddressDelete by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -68,22 +72,25 @@ fun ContactDetailScreen(
|
||||
},
|
||||
actions = {
|
||||
if (uiState is ContactDetailUiState.Success) {
|
||||
IconButton(onClick = { viewModel.openEditAliasSheet() }) {
|
||||
Icon(Icons.Default.Edit, contentDescription = "Edit name")
|
||||
if (isEditing) {
|
||||
// Done button exits edit mode
|
||||
TextButton(onClick = { isEditing = false }) {
|
||||
Text("Done")
|
||||
}
|
||||
} else {
|
||||
// Edit button opens edit mode
|
||||
IconButton(onClick = { isEditing = true }) {
|
||||
Icon(Icons.Default.Edit, contentDescription = "Edit contact")
|
||||
}
|
||||
IconButton(onClick = { viewModel.requestDelete() }) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Delete contact",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (uiState is ContactDetailUiState.Success) {
|
||||
// FAB only visible in edit mode so users don't accidentally add
|
||||
// addresses while browsing
|
||||
if (uiState is ContactDetailUiState.Success && isEditing) {
|
||||
FloatingActionButton(onClick = { viewModel.openAddAddressSheet() }) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add payment address")
|
||||
}
|
||||
@@ -93,14 +100,18 @@ fun ContactDetailScreen(
|
||||
when (val state = uiState) {
|
||||
is ContactDetailUiState.Loading -> {
|
||||
Box(
|
||||
Modifier.fillMaxSize().padding(innerPadding),
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
contentAlignment = Alignment.Center
|
||||
) { CircularProgressIndicator() }
|
||||
}
|
||||
|
||||
is ContactDetailUiState.NotFound -> {
|
||||
Box(
|
||||
Modifier.fillMaxSize().padding(innerPadding),
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
@@ -113,8 +124,11 @@ fun ContactDetailScreen(
|
||||
is ContactDetailUiState.Success -> {
|
||||
ContactDetailContent(
|
||||
state = state,
|
||||
onRemoveAddress = { viewModel.removeAddress(it) },
|
||||
isEditing = isEditing,
|
||||
onRemoveAddress = { addressId -> pendingAddressDelete = addressId },
|
||||
onSetDefault = { viewModel.setDefaultAddress(it) },
|
||||
onEditAlias = { viewModel.openEditAliasSheet() },
|
||||
onDeleteContact = { viewModel.requestDelete() },
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
@@ -140,6 +154,27 @@ fun ContactDetailScreen(
|
||||
)
|
||||
}
|
||||
|
||||
pendingAddressDelete?.let { addressId ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { pendingAddressDelete = null },
|
||||
title = { Text("Remove address") },
|
||||
text = { Text("This payment address will be permanently removed from the contact.") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.removeAddress(addressId)
|
||||
pendingAddressDelete = null
|
||||
}) {
|
||||
Text("Remove", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { pendingAddressDelete = null }) {
|
||||
Text(strings.cancel)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ── Add address sheet ─────────────────────────────────────────────────
|
||||
if (showAddAddressSheet) {
|
||||
AddAddressSheet(
|
||||
@@ -171,8 +206,11 @@ fun ContactDetailScreen(
|
||||
@Composable
|
||||
private fun ContactDetailContent(
|
||||
state: ContactDetailUiState.Success,
|
||||
isEditing: Boolean,
|
||||
onRemoveAddress: (addressId: String) -> Unit,
|
||||
onSetDefault: (PaymentAddressEntity) -> Unit,
|
||||
onEditAlias: () -> Unit,
|
||||
onDeleteContact: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val contact = state.data.contact
|
||||
@@ -225,31 +263,33 @@ private fun ContactDetailContent(
|
||||
|
||||
// ── Payment addresses section ─────────────────────────────────────
|
||||
item {
|
||||
DetailSection(title = "Payment addresses") {}
|
||||
}
|
||||
|
||||
DetailSection(title = "Payment addresses") {
|
||||
if (addresses.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "No addresses added yet",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(addresses, key = { it.id }) { addr ->
|
||||
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||
addresses.forEachIndexed { index, addr ->
|
||||
AddressRow(
|
||||
address = addr,
|
||||
isEditing = isEditing,
|
||||
onRemove = { onRemoveAddress(addr.id) },
|
||||
onSetDefault = { onSetDefault(addr) }
|
||||
)
|
||||
if (index < addresses.lastIndex) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Notes / about (nostr-sourced, read-only for now) ─────────────
|
||||
contact.about?.let { about ->
|
||||
@@ -259,6 +299,47 @@ private fun ContactDetailContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Destructive actions (edit mode only) ─────────────────────────
|
||||
if (isEditing) {
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onEditAlias,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Edit name")
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onDeleteContact,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Delete contact")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,8 +348,9 @@ private fun ContactDetailContent(
|
||||
@Composable
|
||||
private fun AddressRow(
|
||||
address: PaymentAddressEntity,
|
||||
isEditing: Boolean,
|
||||
onRemove: () -> Unit,
|
||||
onSetDefault: () -> Unit
|
||||
onSetDefault: () -> Unit,
|
||||
) {
|
||||
val typeLabel = when (address.type) {
|
||||
PaymentAddressType.LIGHTNING_ADDRESS.name -> "Lightning Address"
|
||||
@@ -279,45 +361,94 @@ private fun AddressRow(
|
||||
PaymentAddressType.BIP353.name -> "BIP-353"
|
||||
else -> address.type
|
||||
}
|
||||
val label = address.label?.let { "$typeLabel · $it" } ?: typeLabel
|
||||
|
||||
DetailRow(
|
||||
label = label,
|
||||
value = address.address,
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// ── Label + value ─────────────────────────────────────────────
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = typeLabel,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
address.label?.let { lbl ->
|
||||
Text(
|
||||
text = "· $lbl",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (address.isDefault) {
|
||||
SuggestionChip(
|
||||
onClick = {},
|
||||
label = {
|
||||
Text(
|
||||
"default",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
},
|
||||
modifier = Modifier.height(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = address.address,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
trailingContent = {
|
||||
Row {
|
||||
// Set default star — filled if already default
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
// ── Trailing actions ──────────────────────────────────────────
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||
// Copy always available
|
||||
CopyIconButton(
|
||||
label = "Copy address",
|
||||
value = address.address,
|
||||
size = 36.dp
|
||||
)
|
||||
// Star + delete only in edit mode
|
||||
if (isEditing) {
|
||||
IconButton(
|
||||
onClick = onSetDefault,
|
||||
modifier = Modifier.size(32.dp)
|
||||
modifier = Modifier.size(36.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (address.isDefault) Icons.Filled.Star
|
||||
else Icons.Outlined.Star,
|
||||
contentDescription = if (address.isDefault) "Default address"
|
||||
else "Set as default",
|
||||
modifier = Modifier.size(16.dp),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = if (address.isDefault) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
// Remove
|
||||
IconButton(
|
||||
onClick = onRemove,
|
||||
modifier = Modifier.size(32.dp)
|
||||
modifier = Modifier.size(36.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Remove address",
|
||||
modifier = Modifier.size(16.dp),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ── Add address sheet ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user