commit 1a1753086266dd04ac8db12f3ae1198f2f4adbfb
parent a3a188cee788a599f127aa8a8844a8e1b4df7a6c
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Sat, 31 Oct 2026 15:33:45 +0100
mailbox: fix flow for subject hint previews
Diffstat:
3 files changed, 143 insertions(+), 47 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/mailbox/MailboxManager.kt b/wallet/src/main/java/net/taler/wallet/mailbox/MailboxManager.kt
@@ -18,12 +18,14 @@ package net.taler.wallet.mailbox
import android.util.Log
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import net.taler.common.Amount
@@ -56,6 +58,7 @@ data class MailboxMessageRecord(
val originMailboxBaseUrl: String,
val downloadedAt: Timestamp,
val talerUri: String,
+ val subjectHint: String? = null,
)
@Serializable
@@ -121,37 +124,76 @@ class MailboxManager(
}
}
- fun fetchDetailsForMessage(message: MailboxMessageRecord) {
+ fun prepareAndStoreSubject(message: MailboxMessageRecord, onReady: () -> Unit) {
val uri = message.talerUri
- if (_messageDetails.value.containsKey(uri)) return
+ if (!message.subjectHint.isNullOrBlank()) {
+ scope.launch(Dispatchers.Main) { onReady() }
+ return
+ }
- scope.launch {
+ scope.launch(Dispatchers.IO) {
try {
+ var summary: String? = null
if (uri.startsWith("taler://pay-push")) {
api.request("preparePeerPushCredit", PreparePeerPushCreditResponse.serializer()) {
put("talerUri", uri)
}.onSuccess { res ->
- _messageDetails.update {
- it + (uri to MailboxMessageDetails(
- summary = res.contractTerms.summary.ifBlank { null },
- amount = res.amountRaw
- ))
- }
+ summary = res.contractTerms.summary.ifBlank { "Incoming payment" }
+ }.onError {
+ summary = "Incoming payment"
}
} else if (uri.startsWith("taler://pay-pull")) {
api.request("preparePeerPullDebit", PreparePeerPullDebitResponse.serializer()) {
put("talerUri", uri)
}.onSuccess { res ->
- _messageDetails.update {
- it + (uri to MailboxMessageDetails(
- summary = res.contractTerms.summary.ifBlank { null },
- amount = res.amountRaw
- ))
+ summary = res.contractTerms.summary.ifBlank { "Payment request" }
+ }.onError {
+ summary = "Payment request"
+ }
+ } else {
+ summary = "Transaction"
+ }
+
+ if (!summary.isNullOrBlank()) {
+ _messageDetails.update {
+ it + (uri to MailboxMessageDetails(
+ summary = summary,
+ amount = null
+ ))
+ }
+ val updated = message.copy(subjectHint = summary)
+ api.request<Unit>("upsertMailboxMessage") {
+ put("message", JSONObject(BackendManager.json.encodeToString(updated)))
+ }.onSuccess {
+ val state = _mailbox.value
+ if (state is MailboxResult.Success) {
+ getMessages(state.config)
}
+ }.onError {
+ Log.e(TAG, "Error upserting mailbox message in prepareAndStoreSubject: $it")
}
}
} catch (e: Exception) {
- Log.e(TAG, "Error fetching details for message URI: $uri", e)
+ Log.e(TAG, "Error preparing transaction for summary: $uri", e)
+ } finally {
+ withContext(Dispatchers.Main) {
+ onReady()
+ }
+ }
+ }
+ }
+
+ fun upsertMessage(message: MailboxMessageRecord) {
+ scope.launch {
+ api.request<Unit>("upsertMailboxMessage") {
+ put("message", JSONObject(BackendManager.json.encodeToString(message)))
+ }.onSuccess {
+ val state = _mailbox.value
+ if (state is MailboxResult.Success) {
+ getMessages(state.config)
+ }
+ }.onError {
+ Log.e(TAG, "Error upserting mailbox message: $it")
}
}
}
@@ -199,9 +241,6 @@ class MailboxManager(
api.request("getMailboxMessages", MailboxMessagesResponse.serializer())
.onSuccess { response ->
_mailbox.value = MailboxResult.Success(config, response.messages)
- response.messages.forEach { msg ->
- fetchDetailsForMessage(msg)
- }
}.onError {
Log.e(TAG, "Error getting mailbox messages: $it")
_mailbox.value = MailboxResult.Error(it)
@@ -235,7 +274,7 @@ class MailboxManager(
private fun addMessage(message: MailboxMessageRecord) {
scope.launch {
- api.request<Unit>("addMailboxMessage") {
+ api.request<Unit>("upsertMailboxMessage") {
put("message", JSONObject(BackendManager.json.encodeToString(message)))
}.onError {
Log.e(TAG, "Error adding mailbox message: $it")
diff --git a/wallet/src/main/java/net/taler/wallet/mailbox/MailboxScreen.kt b/wallet/src/main/java/net/taler/wallet/mailbox/MailboxScreen.kt
@@ -16,17 +16,21 @@
package net.taler.wallet.mailbox
+import androidx.compose.foundation.background
+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.padding
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
-import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Refresh
@@ -58,6 +62,7 @@ import androidx.compose.runtime.rememberCoroutineScope
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.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
@@ -246,6 +251,23 @@ fun MailboxScreen(
}
@Composable
+fun NewBadge(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .clip(RoundedCornerShape(8.dp))
+ .background(MaterialTheme.colorScheme.tertiaryContainer)
+ .padding(horizontal = 6.dp, vertical = 2.dp)
+ ) {
+ Text(
+ text = "NEW",
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onTertiaryContainer,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+}
+
+@Composable
fun MessageList(
messages: List<MailboxMessageRecord>,
mailboxManager: MailboxManager? = null,
@@ -255,9 +277,20 @@ fun MessageList(
if (messages.isEmpty()) {
Text("No messages yet.", modifier = Modifier.padding(16.dp))
} else {
+ val initiallyNewUris = remember {
+ messages.filter { it.subjectHint.isNullOrBlank() }.map { it.talerUri }.toSet()
+ }
+
LazyColumn {
items(messages) { message ->
- MessageItem(message, mailboxManager, onNavigate, onDelete)
+ val isNew = initiallyNewUris.contains(message.talerUri)
+ MessageItem(
+ message = message,
+ isNew = isNew,
+ mailboxManager = mailboxManager,
+ onNavigate = onNavigate,
+ onDelete = onDelete
+ )
HorizontalDivider()
}
}
@@ -267,6 +300,7 @@ fun MessageList(
@Composable
fun MessageItem(
message: MailboxMessageRecord,
+ isNew: Boolean,
mailboxManager: MailboxManager? = null,
onNavigate: NavigateCallback,
onDelete: (MailboxMessageRecord) -> Unit,
@@ -277,12 +311,7 @@ fun MessageItem(
message.talerUri.startsWith("taler://pay/") ||
message.talerUri.startsWith("taler://pay-template")
- val messageDetailsState = mailboxManager?.messageDetails?.collectAsState()
- val details = messageDetailsState?.value?.get(message.talerUri)
-
- LaunchedEffect(message.talerUri) {
- mailboxManager?.fetchDetailsForMessage(message)
- }
+ val hasSubject = !message.subjectHint.isNullOrBlank()
Row(
modifier = Modifier
@@ -291,27 +320,49 @@ fun MessageItem(
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
- if (details?.summary != null) {
+ if (isNew) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 4.dp)
+ ) {
+ NewBadge()
+ if (hasSubject) {
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = message.subjectHint,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+ }
+ } else if (hasSubject) {
Text(
- text = details.summary,
+ text = message.subjectHint,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 2.dp)
)
}
- if (details?.amount != null) {
- Text(
- text = details.amount.toString(),
- style = MaterialTheme.typography.bodyLarge,
- fontWeight = FontWeight.SemiBold,
- color = if (isPayPush) Color(0xFF2E7D32) else Color(0xFFE65100),
- modifier = Modifier.padding(bottom = 6.dp)
- )
- }
- if (isPayPush) {
+ if (hasSubject) {
+ Button(
+ onClick = {
+ onNavigate(WalletDestination.HandleUri(message.talerUri), false)
+ },
+ modifier = Modifier.padding(bottom = 6.dp)
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowForward,
+ contentDescription = null,
+ modifier = Modifier.padding(end = 6.dp)
+ )
+ Text("Go to transaction")
+ }
+ } else if (isPayPush) {
Button(
- onClick = { onNavigate(WalletDestination.HandleUri(message.talerUri), false) },
+ onClick = {
+ mailboxManager?.prepareAndStoreSubject(message) {}
+ },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF2E7D32),
contentColor = Color.White,
@@ -327,7 +378,9 @@ fun MessageItem(
}
} else if (isPayPull) {
Button(
- onClick = { onNavigate(WalletDestination.HandleUri(message.talerUri), false) },
+ onClick = {
+ mailboxManager?.prepareAndStoreSubject(message) {}
+ },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFE65100),
contentColor = Color.White,
@@ -342,11 +395,14 @@ fun MessageItem(
Text(stringResource(R.string.mailbox_payment_request))
}
} else {
- Text(
- text = message.talerUri,
- style = MaterialTheme.typography.bodyMedium,
+ Button(
+ onClick = {
+ mailboxManager?.prepareAndStoreSubject(message) {}
+ },
modifier = Modifier.padding(bottom = 6.dp)
- )
+ ) {
+ Text(message.talerUri)
+ }
}
Text(
"From: ${message.originMailboxBaseUrl}",
@@ -375,6 +431,7 @@ fun MessageItemPreview() {
downloadedAt = Timestamp.now(),
talerUri = "taler://pay-push/...",
),
+ isNew = true,
onNavigate = { _, _ -> },
onDelete = {}
)
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
@@ -531,8 +531,8 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
<string name="contact_mailbox_base_uri">Mailbox Base URI</string>
<string name="contact_petname">Nickname</string>
<string name="contacts_title">Contacts</string>
- <string name="mailbox_incoming_payment">Incoming payment</string>
- <string name="mailbox_payment_request">Payment request</string>
+ <string name="mailbox_incoming_payment">Open incoming transaction message</string>
+ <string name="mailbox_payment_request">Open payment request message</string>
<!-- Settings -->