contact details ui

This commit is contained in:
2026-06-13 23:22:27 +02:00
parent effe8f4b04
commit 53cb600408
5 changed files with 222 additions and 20 deletions
@@ -104,6 +104,35 @@ class ContactRepository(app: Application) {
)
}
suspend fun updateAddress(
addressId: String,
type: PaymentAddressType,
address: String,
label: String?,
makeDefault: Boolean
) {
val existing = dao.getAddressById(addressId) ?: return
val contactId = existing.contactId
if (makeDefault) dao.clearDefault(contactId, type.name)
// If LIGHTNING_ADDRESS and default, also sync the fast-lookup column
if (type == PaymentAddressType.LIGHTNING_ADDRESS && makeDefault) {
dao.getById(contactId)?.let {
dao.update(it.copy(lnAddress = address, updatedAt = System.currentTimeMillis()))
}
}
dao.updateAddress(
addressId = addressId,
type = type.name,
address = address,
label = label,
isDefault = makeDefault
)
}
suspend fun removeAddress(addressId: String) {
dao.deleteAddressById(addressId)
}
@@ -54,8 +54,24 @@ interface ContactDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAddress(address: PaymentAddressEntity)
@Update
suspend fun updateAddress(address: PaymentAddressEntity)
@Query("""
UPDATE payment_addresses
SET type = :type,
address = :address,
label = :label,
isDefault = :isDefault
WHERE id = :addressId
""")
suspend fun updateAddress(
addressId: String,
type: String,
address: String,
label: String?,
isDefault: Boolean
)
@Query("SELECT * FROM payment_addresses WHERE id = :addressId")
suspend fun getAddressById(addressId: String): PaymentAddressEntity?
@Query("DELETE FROM payment_addresses WHERE id = :id")
suspend fun deleteAddressById(id: String)
@@ -4,7 +4,6 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
@@ -49,6 +48,7 @@ fun ContactDetailScreen(
var isEditing by remember { mutableStateOf(false) }
var pendingAddressDelete by remember { mutableStateOf<String?>(null) }
var editingAddress by remember { mutableStateOf<PaymentAddressEntity?>(null) }
Scaffold(
topBar = {
@@ -131,8 +131,9 @@ fun ContactDetailScreen(
onSetDefault = { viewModel.setDefaultAddress(it) },
onPayAddress = onPayAddress,
onEditAlias = { viewModel.openEditAliasSheet() },
onEditAddress = { addr -> editingAddress = addr },
onDeleteContact = { viewModel.requestDelete() },
modifier = Modifier.padding(innerPadding)
modifier = Modifier.padding(innerPadding).padding(horizontal = 16.dp)
)
}
}
@@ -189,6 +190,18 @@ fun ContactDetailScreen(
)
}
// ── Edit address sheet ──────────────────────────────────────────
editingAddress?.let { addr ->
EditAddressSheet(
address = addr,
onSave = { id, type, addressValue, label, makeDefault ->
viewModel.updateAddress(id, type, addressValue, label, makeDefault)
editingAddress = null
},
onDismiss = { editingAddress = null }
)
}
// ── Edit alias sheet ──────────────────────────────────────────────────
if (showEditAliasSheet) {
val currentAlias = (uiState as? ContactDetailUiState.Success)
@@ -214,6 +227,7 @@ private fun ContactDetailContent(
onSetDefault: (PaymentAddressEntity) -> Unit,
onPayAddress: (address: String) -> Unit,
onEditAlias: () -> Unit,
onEditAddress: (PaymentAddressEntity) -> Unit,
onDeleteContact: () -> Unit,
modifier: Modifier = Modifier
) {
@@ -282,7 +296,8 @@ private fun ContactDetailContent(
isEditing = isEditing,
onPay = { onPayAddress(addr.address) },
onRemove = { onRemoveAddress(addr.id) },
onSetDefault = { onSetDefault(addr) }
onSetDefault = { onSetDefault(addr) },
onEdit = { onEditAddress(addr) }
)
if (index < addresses.lastIndex) {
HorizontalDivider(
@@ -357,6 +372,7 @@ private fun AddressRow(
onPay: () -> Unit,
onRemove: () -> Unit,
onSetDefault: () -> Unit,
onEdit: () -> Unit
) {
val typeLabel = when (address.type) {
PaymentAddressType.LIGHTNING_ADDRESS.name -> "Lightning Address"
@@ -380,6 +396,13 @@ private fun AddressRow(
) {
// ── Label + value ─────────────────────────────────────────────
Column(modifier = Modifier.weight(1f)) {
Text(
text = address.address,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
@@ -409,12 +432,6 @@ private fun AddressRow(
)
}
}
Text(
text = address.address,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
// ── Trailing actions ──────────────────────────────────────────
@@ -428,17 +445,14 @@ private fun AddressRow(
// Star + delete only in edit mode
if (isEditing) {
IconButton(
onClick = onSetDefault,
onClick = onEdit,
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",
imageVector = Icons.Default.Edit,
contentDescription = "Edit address",
modifier = Modifier.size(18.dp),
tint = if (address.isDefault) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant
tint = MaterialTheme.colorScheme.primary
)
}
IconButton(
@@ -458,8 +472,6 @@ private fun AddressRow(
}
// ── Add address sheet ──────────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class)
@@ -585,6 +597,136 @@ private fun AddAddressSheet(
}
}
// ── Edit address sheet ────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun EditAddressSheet(
address: PaymentAddressEntity,
onSave: (id: String, type: PaymentAddressType, addressValue: String, label: String?, makeDefault: Boolean) -> Unit,
onDismiss: () -> Unit
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val focusManager = LocalFocusManager.current
val addressFocus = remember { FocusRequester() }
var selectedType by remember {
mutableStateOf(PaymentAddressType.entries.find { it.name == address.type }
?: PaymentAddressType.LIGHTNING_ADDRESS)
}
var addressValue by remember { mutableStateOf(address.address) }
var label by remember { mutableStateOf(address.label ?: "") }
var makeDefault by remember { mutableStateOf(address.isDefault) }
val canSave = addressValue.isNotBlank()
LaunchedEffect(Unit) { addressFocus.requestFocus() }
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 32.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Edit payment address", style = MaterialTheme.typography.titleMedium)
// Type chips
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(
selected = selectedType == PaymentAddressType.LIGHTNING_ADDRESS,
onClick = { selectedType = PaymentAddressType.LIGHTNING_ADDRESS },
label = { Text("Lightning Address") }
)
FilterChip(
selected = selectedType == PaymentAddressType.LNURL,
onClick = { selectedType = PaymentAddressType.LNURL },
label = { Text("LNURL") }
)
}
// Address field
val addressLabel = if (selectedType == PaymentAddressType.LIGHTNING_ADDRESS)
"Lightning Address" else "LNURL"
val addressPlaceholder = if (selectedType == PaymentAddressType.LIGHTNING_ADDRESS)
"user@domain.com" else "LNURL1..."
OutlinedTextField(
value = addressValue,
onValueChange = { addressValue = it },
label = { Text(addressLabel) },
placeholder = { Text(addressPlaceholder) },
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next,
autoCorrectEnabled = false
),
modifier = Modifier
.fillMaxWidth()
.focusRequester(addressFocus)
)
// Optional label
OutlinedTextField(
value = label,
onValueChange = { label = it },
label = { Text("Label (optional)") },
placeholder = { Text("e.g. personal, savings") },
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
if (canSave) onSave(address.id, selectedType, addressValue, label.takeIf { it.isNotBlank() }, makeDefault)
}
),
modifier = Modifier.fillMaxWidth()
)
// Set as default toggle
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Set as default",
style = MaterialTheme.typography.bodyMedium
)
Switch(
checked = makeDefault,
onCheckedChange = { makeDefault = it }
)
}
// Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End)
) {
val strings = LocalAppStrings.current
TextButton(onClick = onDismiss) { Text(strings.cancel) }
Button(
onClick = {
focusManager.clearFocus()
onSave(address.id, selectedType, addressValue, label.takeIf { it.isNotBlank() }, makeDefault)
},
enabled = canSave
) { Text("Save") }
}
}
}
}
// ── Edit alias sheet ───────────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class)
@@ -68,6 +68,19 @@ class ContactDetailViewModel(
}
}
fun updateAddress(
addressId: String,
type: PaymentAddressType,
address: String,
label: String?,
makeDefault: Boolean
) {
viewModelScope.launch {
repo.updateAddress(addressId, type, address, label, makeDefault)
}
}
fun removeAddress(addressId: String) {
viewModelScope.launch { repo.removeAddress(addressId) }
}
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.data.ContactRepository
import com.bitcointxoko.gudariwallet.data.db.ContactWithAddresses
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressEntity
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressType
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -50,6 +51,7 @@ class ContactsViewModel(
}
}
class Factory(private val repo: ContactRepository) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =