Improved transaction detail UI and behavior.

Differentiate pending transactions, show address when we can parse it from the memo, allow address copy.
This commit is contained in:
Kevin Gorham 2020-03-27 16:46:38 -04:00
parent 8371f9c53a
commit 5a956a55d3
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
3 changed files with 93 additions and 9 deletions

View File

@ -2,11 +2,15 @@ package cash.z.ecc.android.ui.detail
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R import cash.z.ecc.android.R
import cash.z.ecc.android.ext.goneIf import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.toAppColor import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ui.MainActivity import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX
import cash.z.ecc.android.ui.util.toUtf8Memo
import cash.z.wallet.sdk.entity.ConfirmedTransaction import cash.z.wallet.sdk.entity.ConfirmedTransaction
import cash.z.wallet.sdk.ext.* import cash.z.wallet.sdk.ext.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -21,6 +25,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
private val bottomText = itemView.findViewById<TextView>(R.id.text_transaction_bottom) private val bottomText = itemView.findViewById<TextView>(R.id.text_transaction_bottom)
private val shieldIcon = itemView.findViewById<View>(R.id.image_shield) private val shieldIcon = itemView.findViewById<View>(R.id.image_shield)
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
private val addressRegex = """zs\d\w{65,}""".toRegex()
fun bindTo(transaction: T?) { fun bindTo(transaction: T?) {
@ -29,13 +34,19 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
var lineTwo: String = "" var lineTwo: String = ""
var amountZec: String = "" var amountZec: String = ""
var amountDisplay: String = "" var amountDisplay: String = ""
var amountColor: Int = 0 var amountColor: Int = R.color.text_light_dimmed
var indicatorBackground: Int = 0 var lineOneColor: Int = R.color.text_light
var lineTwoColor: Int = R.color.text_light_dimmed
var indicatorBackground: Int = R.drawable.background_indicator_unknown
transaction?.apply { transaction?.apply {
itemView.setOnClickListener { itemView.setOnClickListener {
onTransactionClicked(this) onTransactionClicked(this)
} }
itemView.setOnLongClickListener {
onTransactionLongPressed(this)
true
}
amountZec = value.convertZatoshiToZecString() amountZec = value.convertZatoshiToZecString()
// TODO: these might be good extension functions // TODO: these might be good extension functions
val timestamp = formatter.format(blockTimeInSeconds * 1000L) val timestamp = formatter.format(blockTimeInSeconds * 1000L)
@ -45,11 +56,16 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}" lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}"
lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation" lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation"
amountDisplay = "- $amountZec" amountDisplay = "- $amountZec"
amountColor = R.color.zcashRed if (isMined) {
indicatorBackground = R.drawable.background_indicator_outbound amountColor = R.color.zcashRed
indicatorBackground = R.drawable.background_indicator_outbound
} else {
lineOneColor = R.color.text_light_dimmed
lineTwoColor = R.color.text_light
}
} }
raw == null || raw?.isEmpty() == true -> { toAddress.isNullOrEmpty() && value > 0L && minedHeight > 0 -> {
lineOne = "Unknown paid you" lineOne = getSender(transaction)
lineTwo = "Received $timestamp" lineTwo = "Received $timestamp"
amountDisplay = "+ $amountZec" amountDisplay = "+ $amountZec"
amountColor = R.color.zcashGreen amountColor = R.color.zcashGreen
@ -58,9 +74,10 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
else -> { else -> {
lineOne = "Unknown" lineOne = "Unknown"
lineTwo = "Unknown" lineTwo = "Unknown"
amountDisplay = "$amountZec"
amountColor = R.color.text_light
} }
} }
// sanitize amount // sanitize amount
if (value < ZcashSdk.MINERS_FEE_ZATOSHI) amountDisplay = "< 0.001" if (value < ZcashSdk.MINERS_FEE_ZATOSHI) amountDisplay = "< 0.001"
else if (amountZec.length > 10) { // 10 allows 3 digits to the left and 6 to the right of the decimal else if (amountZec.length > 10) { // 10 allows 3 digits to the left and 6 to the right of the decimal
@ -73,17 +90,41 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
bottomText.text = lineTwo bottomText.text = lineTwo
amountText.text = amountDisplay amountText.text = amountDisplay
amountText.setTextColor(amountColor.toAppColor()) amountText.setTextColor(amountColor.toAppColor())
topText.setTextColor(lineOneColor.toAppColor())
bottomText.setTextColor(lineTwoColor.toAppColor())
val context = itemView.context val context = itemView.context
indicator.background = context.resources.getDrawable(indicatorBackground) indicator.background = context.resources.getDrawable(indicatorBackground)
shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded()) shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded())
} }
private fun getSender(transaction: ConfirmedTransaction): String {
val memo = transaction.memo.toUtf8Memo()
return when {
memo.contains(INCLUDE_MEMO_PREFIX) -> {
val address = memo.split(INCLUDE_MEMO_PREFIX)[1].trim()
"${address.toAbbreviatedAddress()} paid you"
}
memo.contains("eply to:") -> {
val address = memo.split("eply to:")[1].trim()
"${address.toAbbreviatedAddress()} paid you"
}
memo.contains("zs") -> {
val who = extractAddress(memo)?.toAbbreviatedAddress() ?: "Unknown"
"$who paid you"
}
else -> "Unknown paid you"
}
}
private fun extractAddress(memo: String?) =
addressRegex.findAll(memo ?: "").lastOrNull()?.value
private fun onTransactionClicked(transaction: ConfirmedTransaction) { private fun onTransactionClicked(transaction: ConfirmedTransaction) {
val txId = transaction.rawTransactionId.toTxId() val txId = transaction.rawTransactionId.toTxId()
val detailsMessage: String = "Zatoshi amount: ${transaction.value}\n\n" + val detailsMessage: String = "Zatoshi amount: ${transaction.value}\n\n" +
"Transaction: $txId" + "Transaction: $txId" +
"${if (transaction.toAddress != null) "\n\nto: ${transaction.toAddress}" else ""}" + "${if (transaction.toAddress != null) "\n\nTo: ${transaction.toAddress}" else ""}" +
"${if (transaction.memo != null) "\n\nmemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}" "${if (transaction.memo != null) "\n\nMemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}"
MaterialAlertDialogBuilder(itemView.context) MaterialAlertDialogBuilder(itemView.context)
.setMessage(detailsMessage) .setMessage(detailsMessage)
@ -98,6 +139,12 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
} }
.show() .show()
} }
private fun onTransactionLongPressed(transaction: ConfirmedTransaction) {
(transaction.toAddress ?: extractAddress(transaction.memo.toUtf8Memo()))?.let {
(itemView.context as MainActivity).copyText(it, "Transaction Address")
}
}
} }
private fun ByteArray.toTxId(): String { private fun ByteArray.toTxId(): String {

View File

@ -0,0 +1,28 @@
package cash.z.ecc.android.ui.util
import java.nio.charset.StandardCharsets
const val INCLUDE_MEMO_PREFIX = "sent from"
inline fun ByteArray?.toUtf8Memo(): String {
// TODO: make this more official but for now, this will do
return if (this == null || this[0] >= 0xF5) "" else try {
String(this, StandardCharsets.UTF_8).trim('\u0000')
} catch (t: Throwable) {
"unable to parse memo"
}
}
/*
if self.0[0] < 0xF5 {
// Check if it is valid UTF8
Some(str::from_utf8(&self.0).map(|memo| {
// Drop trailing zeroes
memo.trim_end_matches(char::from(0)).to_owned()
}))
} else {
None
}
*/

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient
android:angle="270"
android:endColor="@color/zcashWhite_12"
android:startColor="@color/zcashWhite_60" />
</shape>