commit 3b44ea0e5ed210a8abf832301c3e658b63babcf0
parent 6d829902907911426a48f1f3669e48f4a035f205
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Sat, 24 Oct 2026 16:03:11 +0200
alias/contacts: Allow display of QR to scan and various UX improvements
Diffstat:
4 files changed, 482 insertions(+), 31 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/contacts/AliasRegistrationScreen.kt b/wallet/src/main/java/net/taler/wallet/contacts/AliasRegistrationScreen.kt
@@ -37,7 +37,9 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Error
+import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
@@ -93,6 +95,9 @@ fun AliasRegistrationScreen(
var showAddAliasDialog by remember { mutableStateOf(false) }
var activeRegistrationAlias by remember { mutableStateOf<ContactEntry?>(null) }
+ var contactForQr by remember { mutableStateOf<ContactEntry?>(null) }
+ var aliasToDelete by remember { mutableStateOf<ContactEntry?>(null) }
+ var contactToEdit by remember { mutableStateOf<ContactEntry?>(null) }
var isUnlinking by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
@@ -208,21 +213,10 @@ fun AliasRegistrationScreen(
isUnlinking = false
model.contactManager.resetRegistration()
},
+ onEditNickname = { contact -> contactToEdit = contact },
+ onShowQr = { contact -> contactForQr = contact },
onDeleteAlias = { contact ->
- val key = "${contact.aliasType}:${contact.alias}"
- val isRegistered = aliasStatuses[key] is AliasRegistrationStatus.Registered
- if (isRegistered) {
- activeRegistrationAlias = contact
- isUnlinking = true
- model.contactManager.taldirDeleteAlias(
- alias = contact.alias,
- aliasType = contact.aliasType,
- taldirBaseUri = "https://taldir.gnunet.org",
- durationMonths = 12,
- )
- } else {
- model.contactManager.deleteContact(contact)
- }
+ aliasToDelete = contact
},
onRegistrationCompleted = {
activeRegistrationAlias?.let { contact ->
@@ -243,6 +237,69 @@ fun AliasRegistrationScreen(
}
)
+ aliasToDelete?.let { contact ->
+ AlertDialog(
+ onDismissRequest = { aliasToDelete = null },
+ title = { Text(stringResource(R.string.alias_registration_delete)) },
+ text = { Text("Are you sure you want to delete alias ${contact.alias}?") },
+ confirmButton = {
+ TextButton(onClick = {
+ val key = "${contact.aliasType}:${contact.alias}"
+ val isRegistered = aliasStatuses[key] is AliasRegistrationStatus.Registered
+ if (isRegistered) {
+ activeRegistrationAlias = contact
+ isUnlinking = true
+ model.contactManager.taldirDeleteAlias(
+ alias = contact.alias,
+ aliasType = contact.aliasType,
+ taldirBaseUri = "https://taldir.gnunet.org",
+ durationMonths = 12,
+ )
+ } else {
+ model.contactManager.deleteContact(contact)
+ }
+ aliasToDelete = null
+ }) {
+ Text(
+ text = stringResource(R.string.transactions_delete),
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { aliasToDelete = null }) {
+ Text(stringResource(R.string.cancel))
+ }
+ }
+ )
+ }
+
+ contactToEdit?.let { contact ->
+ EditNicknameDialog(
+ contact = contact,
+ onDismiss = { contactToEdit = null },
+ onSave = { newNickname ->
+ model.contactManager.addContact(
+ alias = contact.alias,
+ aliasType = contact.aliasType,
+ mailboxBaseUri = contact.mailboxBaseUrl,
+ mailboxAddress = contact.mailboxAddress,
+ source = contact.source,
+ petname = newNickname,
+ isSelf = true,
+ )
+ contactToEdit = null
+ }
+ )
+ }
+
+ contactForQr?.let { contact ->
+ ShowContactQrDialog(
+ contact = contact,
+ onDismiss = { contactForQr = null }
+ )
+ }
+
if (showAddAliasDialog && mailboxConfig != null) {
AddAliasDialog(
supportedAliasTypes = supportedAliasTypes,
@@ -284,6 +341,8 @@ fun AliasRegistrationContent(
onCompleteRegister: (solution: String) -> Unit,
onRecoverRegister: () -> Unit,
onCancelRegister: () -> Unit,
+ onEditNickname: (ContactEntry) -> Unit,
+ onShowQr: (ContactEntry) -> Unit,
onDeleteAlias: (ContactEntry) -> Unit,
onRegistrationCompleted: () -> Unit,
) {
@@ -298,11 +357,6 @@ fun AliasRegistrationContent(
contentDescription = null,
)
}
- },
- actions = {
- IconButton(onClick = onAddAliasClick, enabled = mailboxConfig != null) {
- Icon(Icons.Default.Add, contentDescription = "Add Alias")
- }
}
)
},
@@ -353,6 +407,8 @@ fun AliasRegistrationContent(
status = status,
isRegistering = activeRegistrationAlias?.alias == contact.alias && activeRegistrationAlias?.aliasType == contact.aliasType,
onRegister = { onStartRegister(contact) },
+ onEditNickname = { onEditNickname(contact) },
+ onShowQr = { onShowQr(contact) },
onDelete = { onDeleteAlias(contact) }
)
}
@@ -381,6 +437,8 @@ fun UserAliasCard(
status: AliasRegistrationStatus,
isRegistering: Boolean,
onRegister: () -> Unit,
+ onEditNickname: () -> Unit,
+ onShowQr: () -> Unit,
onDelete: () -> Unit,
) {
Card(
@@ -412,8 +470,16 @@ fun UserAliasCard(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
- IconButton(onClick = onDelete) {
- Icon(Icons.Default.Delete, contentDescription = "Delete Alias")
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ IconButton(onClick = onEditNickname) {
+ Icon(Icons.Default.Edit, contentDescription = "Edit Nickname")
+ }
+ IconButton(onClick = onShowQr) {
+ Icon(Icons.Default.QrCode, contentDescription = "Show QR Code")
+ }
+ IconButton(onClick = onDelete) {
+ Icon(Icons.Default.Delete, contentDescription = "Delete Alias")
+ }
}
}
@@ -716,6 +782,8 @@ fun AliasRegistrationContentPreview() {
onCompleteRegister = {},
onRecoverRegister = {},
onCancelRegister = {},
+ onEditNickname = {},
+ onShowQr = {},
onDeleteAlias = {},
onRegistrationCompleted = {},
)
diff --git a/wallet/src/main/java/net/taler/wallet/contacts/ContactManager.kt b/wallet/src/main/java/net/taler/wallet/contacts/ContactManager.kt
@@ -399,3 +399,12 @@ class ContactManager(
}
}
}
+
+fun ContactEntry.toAddContactUri(): String {
+ val cleanBase = mailboxBaseUrl
+ .removePrefix("https://")
+ .removePrefix("http://")
+ .trimEnd('/')
+ val cleanBaseWithSlash = if (cleanBase.isNotBlank()) "$cleanBase/" else ""
+ return "taler://add-contact/$aliasType/$alias/$cleanBaseWithSlash$mailboxAddress"
+}
diff --git a/wallet/src/main/java/net/taler/wallet/contacts/ContactScreen.kt b/wallet/src/main/java/net/taler/wallet/contacts/ContactScreen.kt
@@ -16,23 +16,35 @@
package net.taler.wallet.contacts
+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.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
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.foundation.rememberScrollState
+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.Send
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -57,7 +69,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
@@ -65,6 +79,8 @@ import net.taler.wallet.NavigateCallback
import net.taler.wallet.R
import net.taler.wallet.WalletDestination
import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.compose.QrCodeParams
+import net.taler.wallet.compose.QrCodeUriComposable
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.main.MainViewModel
@@ -86,6 +102,9 @@ fun ContactScreen(
var pendingContact by remember { mutableStateOf<PendingContact?>(null) }
var searchQuery by remember { mutableStateOf("") }
var contactToConfirm by remember { mutableStateOf<ContactEntry?>(null) }
+ var contactForQr by remember { mutableStateOf<ContactEntry?>(null) }
+ var contactToDelete by remember { mutableStateOf<ContactEntry?>(null) }
+ var contactToEdit by remember { mutableStateOf<ContactEntry?>(null) }
var initialAlias by remember { mutableStateOf("") }
var initialAliasType by remember { mutableStateOf("email") }
@@ -185,7 +204,9 @@ fun ContactScreen(
}
ContactList(
contacts = filteredContacts,
- onDelete = { contact -> contactManager.deleteContact(contact) },
+ onDelete = { contact -> contactToDelete = contact },
+ onEditNickname = { contact -> contactToEdit = contact },
+ onShowQr = { contact -> contactForQr = contact },
onSelect = if (sendUri != null) { contact ->
contactToConfirm = contact
} else null
@@ -195,6 +216,49 @@ fun ContactScreen(
}
}
+ contactToDelete?.let { contact ->
+ AlertDialog(
+ onDismissRequest = { contactToDelete = null },
+ title = { Text("Delete Contact") },
+ text = { Text("Are you sure you want to delete ${contact.petname.ifBlank { contact.alias }}?") },
+ confirmButton = {
+ TextButton(onClick = {
+ contactManager.deleteContact(contact)
+ contactToDelete = null
+ }) {
+ Text(
+ text = stringResource(R.string.transactions_delete),
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { contactToDelete = null }) {
+ Text(stringResource(R.string.cancel))
+ }
+ }
+ )
+ }
+
+ contactToEdit?.let { contact ->
+ EditNicknameDialog(
+ contact = contact,
+ onDismiss = { contactToEdit = null },
+ onSave = { newNickname ->
+ contactManager.addContact(
+ alias = contact.alias,
+ aliasType = contact.aliasType,
+ mailboxBaseUri = contact.mailboxBaseUrl,
+ mailboxAddress = contact.mailboxAddress,
+ source = contact.source,
+ petname = newNickname,
+ isSelf = contact.isSelf,
+ )
+ contactToEdit = null
+ }
+ )
+ }
+
contactToConfirm?.let { contact ->
AlertDialog(
onDismissRequest = { contactToConfirm = null },
@@ -220,6 +284,13 @@ fun ContactScreen(
)
}
+ contactForQr?.let { contact ->
+ ShowContactQrDialog(
+ contact = contact,
+ onDismiss = { contactForQr = null }
+ )
+ }
+
if (showAddDialog) {
AddContactDialog(
initialAlias = initialAlias,
@@ -314,6 +385,82 @@ data class PendingContact(
val source: String,
)
+@Composable
+fun EditNicknameDialog(
+ contact: ContactEntry,
+ onDismiss: () -> Unit,
+ onSave: (newNickname: String) -> Unit,
+) {
+ var nickname by remember { mutableStateOf(contact.petname) }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("Edit Nickname") },
+ text = {
+ Column {
+ Text(
+ text = "Alias: ${contact.alias}",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+ OutlinedTextField(
+ value = nickname,
+ onValueChange = { nickname = it },
+ label = { Text(stringResource(R.string.contact_petname)) },
+ singleLine = true,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = { onSave(nickname.trim()) },
+ enabled = nickname.isNotBlank()
+ ) {
+ Text(stringResource(R.string.ok))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.cancel))
+ }
+ }
+ )
+}
+
+@Composable
+fun ShowContactQrDialog(
+ contact: ContactEntry,
+ onDismiss: () -> Unit,
+) {
+ val qrUri = contact.toAddContactUri()
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(contact.petname.ifBlank { contact.alias }) },
+ text = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ QrCodeUriComposable(
+ qrData = qrUri,
+ clipboardLabel = "Contact QR Code",
+ params = QrCodeParams.Taler,
+ shareAsQrCode = true,
+ )
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.close))
+ }
+ }
+ )
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddContactDialog(
@@ -475,18 +622,190 @@ fun AddContactDialog(
}
@Composable
+fun SelfBadge(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .clip(RoundedCornerShape(8.dp))
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .padding(horizontal = 6.dp, vertical = 2.dp)
+ ) {
+ Text(
+ text = "You",
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+}
+
+@Composable
+fun ContactGroupCard(
+ targetUriKey: String,
+ contactsInGroup: List<ContactEntry>,
+ onDelete: (ContactEntry) -> Unit,
+ onEditNickname: (ContactEntry) -> Unit,
+ onShowQr: (ContactEntry) -> Unit,
+ onSelect: ((ContactEntry) -> Unit)? = null,
+) {
+ val isSelfGroup = contactsInGroup.any { it.isSelf || it.source == "self" }
+
+ val distinctPetnames = contactsInGroup
+ .map { it.petname.trim() }
+ .filter { it.isNotBlank() }
+ .distinct()
+
+ val primaryPetname = distinctPetnames.firstOrNull()
+ ?: contactsInGroup.firstOrNull()?.alias
+ ?: targetUriKey
+
+ val otherPetnames = distinctPetnames.drop(1)
+
+ val firstContact = contactsInGroup.firstOrNull()
+ val displayTargetUri = if (firstContact != null && firstContact.mailboxBaseUrl.isNotBlank()) {
+ val cleanBase = firstContact.mailboxBaseUrl.trimEnd('/')
+ val addr = firstContact.mailboxAddress
+ val shortAddr = if (addr.length > 5) "${addr.take(5)}…" else addr
+ "$cleanBase/$shortAddr"
+ } else {
+ targetUriKey
+ }
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 6.dp),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(
+ text = primaryPetname,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ if (isSelfGroup) {
+ Spacer(modifier = Modifier.width(8.dp))
+ SelfBadge()
+ }
+ }
+ }
+
+ if (otherPetnames.isNotEmpty()) {
+ Text(
+ text = "Also: ${otherPetnames.joinToString(", ")}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.padding(top = 2.dp)
+ )
+ }
+
+ Text(
+ text = displayTargetUri,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(top = 2.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+ HorizontalDivider()
+
+ contactsInGroup.forEach { contact ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(enabled = onSelect != null) { onSelect?.invoke(contact) }
+ .padding(vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(contact.alias, style = MaterialTheme.typography.bodyMedium)
+ if (contact.isSelf || contact.source == "self") {
+ Spacer(modifier = Modifier.width(6.dp))
+ SelfBadge()
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = "Type: ${contact.aliasType}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ if (contact.petname.isNotBlank() && contact.petname.trim() != primaryPetname) {
+ Text(
+ text = " • ${contact.petname}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+ if (onSelect != null) {
+ IconButton(onClick = { onSelect(contact) }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.Send,
+ contentDescription = "Send Message to Contact",
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ } else {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ IconButton(onClick = { onEditNickname(contact) }) {
+ Icon(Icons.Default.Edit, contentDescription = "Edit Nickname")
+ }
+ IconButton(onClick = { onShowQr(contact) }) {
+ Icon(Icons.Default.QrCode, contentDescription = "Show QR Code")
+ }
+ IconButton(onClick = { onDelete(contact) }) {
+ Icon(Icons.Default.Delete, contentDescription = "Delete")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
fun ContactList(
contacts: List<ContactEntry>,
onDelete: (ContactEntry) -> Unit,
+ onEditNickname: (ContactEntry) -> Unit,
+ onShowQr: (ContactEntry) -> Unit,
onSelect: ((ContactEntry) -> Unit)? = null,
) {
if (contacts.isEmpty()) {
Text("No contacts yet.", modifier = Modifier.padding(16.dp))
} else {
- LazyColumn {
- items(contacts) { contact ->
- ContactItem(contact, onDelete, onSelect)
- HorizontalDivider()
+ val grouped = contacts.groupBy {
+ val base = it.mailboxBaseUrl.trimEnd('/')
+ if (base.isNotBlank() && it.mailboxAddress.isNotBlank()) {
+ "$base/${it.mailboxAddress}"
+ } else {
+ it.alias
+ }
+ }
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(grouped.entries.toList()) { (targetUriKey, contactsInGroup) ->
+ ContactGroupCard(
+ targetUriKey = targetUriKey,
+ contactsInGroup = contactsInGroup,
+ onDelete = onDelete,
+ onEditNickname = onEditNickname,
+ onShowQr = onShowQr,
+ onSelect = onSelect
+ )
}
}
}
@@ -511,6 +830,7 @@ private fun String.fuzzyMatches(query: String): Boolean {
fun ContactItem(
contact: ContactEntry,
onDelete: (ContactEntry) -> Unit,
+ onShowQr: (ContactEntry) -> Unit,
onSelect: ((ContactEntry) -> Unit)? = null,
) {
Row(
@@ -521,12 +841,33 @@ fun ContactItem(
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
- Text(contact.petname, style = MaterialTheme.typography.titleMedium)
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(contact.petname, style = MaterialTheme.typography.titleMedium)
+ if (contact.isSelf || contact.source == "self") {
+ Spacer(modifier = Modifier.width(6.dp))
+ SelfBadge()
+ }
+ }
Text(contact.alias, style = MaterialTheme.typography.bodyMedium)
Text(contact.mailboxBaseUri, style = MaterialTheme.typography.bodySmall)
}
- IconButton(onClick = { onDelete(contact) }) {
- Icon(Icons.Default.Delete, contentDescription = "Delete")
+ if (onSelect != null) {
+ IconButton(onClick = { onSelect(contact) }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.Send,
+ contentDescription = "Send Message to Contact",
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ } else {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ IconButton(onClick = { onShowQr(contact) }) {
+ Icon(Icons.Default.QrCode, contentDescription = "Show QR Code")
+ }
+ IconButton(onClick = { onDelete(contact) }) {
+ Icon(Icons.Default.Delete, contentDescription = "Delete")
+ }
+ }
}
}
}
@@ -543,8 +884,10 @@ fun ContactItemPreview() {
mailboxAddress = "alice",
source = "manual",
petname = "Alice",
+ isSelf = true,
),
onDelete = {},
+ onShowQr = {},
onSelect = null
)
}
@@ -563,6 +906,7 @@ fun ContactListPreview() {
mailboxAddress = "alice",
source = "manual",
petname = "Alice",
+ isSelf = true,
),
ContactEntry(
alias = "bob@taler.net",
@@ -573,7 +917,9 @@ fun ContactListPreview() {
petname = "Bob",
),
),
- onDelete = {}
+ onDelete = {},
+ onEditNickname = {},
+ onShowQr = {},
)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/mailbox/MailboxScreen.kt b/wallet/src/main/java/net/taler/wallet/mailbox/MailboxScreen.kt
@@ -87,6 +87,7 @@ fun MailboxScreen(
val clipboardManager = LocalClipboardManager.current
var showUrlDialog by remember { mutableStateOf(false) }
var showAddressDialog by remember { mutableStateOf(false) }
+ var messageToDelete by remember { mutableStateOf<MailboxMessageRecord?>(null) }
LaunchedEffect(showMessageEvent) {
showMessageEvent?.getIfNotConsumed()?.let { notify ->
@@ -150,13 +151,40 @@ fun MailboxScreen(
is MailboxResult.Error -> Text("Error: ${res.error}", modifier = Modifier.padding(16.dp))
is MailboxResult.Success -> {
MessageList(res.messages, onNavigate) { message ->
- mailboxManager.deleteMessage(res.config, message)
+ messageToDelete = message
}
}
}
}
}
+ messageToDelete?.let { message ->
+ AlertDialog(
+ onDismissRequest = { messageToDelete = null },
+ title = { Text("Delete Message") },
+ text = { Text("Are you sure you want to delete this mailbox message?") },
+ confirmButton = {
+ TextButton(onClick = {
+ val state = mailboxState
+ if (state is MailboxResult.Success) {
+ mailboxManager.deleteMessage(state.config, message)
+ }
+ messageToDelete = null
+ }) {
+ Text(
+ text = stringResource(R.string.transactions_delete),
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { messageToDelete = null }) {
+ Text(stringResource(R.string.cancel))
+ }
+ }
+ )
+ }
+
if (showAddressDialog) {
val state = mailboxState
if (state is MailboxResult.Success) {