From 5a956a55d360c68180b5b3e1661ae53ce47056ec Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Fri, 27 Mar 2020 16:46:38 -0400 Subject: [PATCH] Improved transaction detail UI and behavior. Differentiate pending transactions, show address when we can parse it from the memo, allow address copy. --- .../ui/detail/TransactionViewHolder.kt | 65 ++++++++++++++++--- .../cash/z/ecc/android/ui/util/MemoUtil.kt | 28 ++++++++ .../drawable/background_indicator_unknown.xml | 9 +++ 3 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/cash/z/ecc/android/ui/util/MemoUtil.kt create mode 100644 app/src/main/res/drawable/background_indicator_unknown.xml diff --git a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt index 8c43d56..3bca6c7 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt @@ -2,11 +2,15 @@ package cash.z.ecc.android.ui.detail import android.view.View import android.widget.TextView +import android.widget.Toast import androidx.recyclerview.widget.RecyclerView import cash.z.ecc.android.R import cash.z.ecc.android.ext.goneIf import cash.z.ecc.android.ext.toAppColor 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.ext.* import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -21,6 +25,7 @@ class TransactionViewHolder(itemView: View) : Recycler private val bottomText = itemView.findViewById(R.id.text_transaction_bottom) private val shieldIcon = itemView.findViewById(R.id.image_shield) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) + private val addressRegex = """zs\d\w{65,}""".toRegex() fun bindTo(transaction: T?) { @@ -29,13 +34,19 @@ class TransactionViewHolder(itemView: View) : Recycler var lineTwo: String = "" var amountZec: String = "" var amountDisplay: String = "" - var amountColor: Int = 0 - var indicatorBackground: Int = 0 + var amountColor: Int = R.color.text_light_dimmed + 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 { itemView.setOnClickListener { onTransactionClicked(this) } + itemView.setOnLongClickListener { + onTransactionLongPressed(this) + true + } amountZec = value.convertZatoshiToZecString() // TODO: these might be good extension functions val timestamp = formatter.format(blockTimeInSeconds * 1000L) @@ -45,11 +56,16 @@ class TransactionViewHolder(itemView: View) : Recycler lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}" lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation" amountDisplay = "- $amountZec" - amountColor = R.color.zcashRed - indicatorBackground = R.drawable.background_indicator_outbound + if (isMined) { + 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 -> { - lineOne = "Unknown paid you" + toAddress.isNullOrEmpty() && value > 0L && minedHeight > 0 -> { + lineOne = getSender(transaction) lineTwo = "Received $timestamp" amountDisplay = "+ $amountZec" amountColor = R.color.zcashGreen @@ -58,9 +74,10 @@ class TransactionViewHolder(itemView: View) : Recycler else -> { lineOne = "Unknown" lineTwo = "Unknown" + amountDisplay = "$amountZec" + amountColor = R.color.text_light } } - // sanitize amount 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 @@ -73,17 +90,41 @@ class TransactionViewHolder(itemView: View) : Recycler bottomText.text = lineTwo amountText.text = amountDisplay amountText.setTextColor(amountColor.toAppColor()) + topText.setTextColor(lineOneColor.toAppColor()) + bottomText.setTextColor(lineTwoColor.toAppColor()) val context = itemView.context indicator.background = context.resources.getDrawable(indicatorBackground) 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) { val txId = transaction.rawTransactionId.toTxId() val detailsMessage: String = "Zatoshi amount: ${transaction.value}\n\n" + "Transaction: $txId" + - "${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.toAddress != null) "\n\nTo: ${transaction.toAddress}" else ""}" + + "${if (transaction.memo != null) "\n\nMemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}" MaterialAlertDialogBuilder(itemView.context) .setMessage(detailsMessage) @@ -98,6 +139,12 @@ class TransactionViewHolder(itemView: View) : Recycler } .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 { diff --git a/app/src/main/java/cash/z/ecc/android/ui/util/MemoUtil.kt b/app/src/main/java/cash/z/ecc/android/ui/util/MemoUtil.kt new file mode 100644 index 0000000..16231e7 --- /dev/null +++ b/app/src/main/java/cash/z/ecc/android/ui/util/MemoUtil.kt @@ -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 + } + */ \ No newline at end of file diff --git a/app/src/main/res/drawable/background_indicator_unknown.xml b/app/src/main/res/drawable/background_indicator_unknown.xml new file mode 100644 index 0000000..3747383 --- /dev/null +++ b/app/src/main/res/drawable/background_indicator_unknown.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file