diff --git a/app/build.gradle b/app/build.gradle index 6dba2b1d60..89fc092c4a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -61,7 +61,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.1' - implementation 'com.google.android.material:material:1.2.0' + implementation 'com.google.android.material:material:1.2.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' implementation 'com.tangem:blockchain:1.27.0' diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Int.kt b/app/src/main/java/com/tangem/tap/common/extensions/Int.kt new file mode 100644 index 0000000000..555dc252bd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Int.kt @@ -0,0 +1,3 @@ +package com.tangem.tap.common.extensions + +fun Int.isEven() = this and 1 == 0 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt index b350c9ebe4..da69eed9ae 100644 --- a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt +++ b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.qrCodeScan import android.Manifest +import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle @@ -9,14 +10,17 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.google.zxing.Result import com.otaliastudios.cameraview.CameraView.PERMISSION_REQUEST_CODE -import com.tangem.tap.features.send.redux.AddressPayIdActionUI -import com.tangem.tap.store import me.dm7.barcodescanner.zxing.ZXingScannerView /** [REDACTED_AUTHOR] */ class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler { + companion object { + val SCAN_QR_REQUEST_CODE = 1001 + val SCAN_RESULT = "scanResult" + } + private lateinit var mScannerView: ZXingScannerView override fun onCreate(state: Bundle?) { @@ -40,7 +44,7 @@ class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler { } override fun handleResult(result: Result) { - store.dispatch(AddressPayIdActionUI.SetAddressOrPayId(result.text)) + setResult(SCAN_QR_REQUEST_CODE, Intent().apply { putExtra(SCAN_RESULT, result.text) }) finish() } diff --git a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt index 9e7276f9bd..15ea778fed 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt @@ -9,7 +9,7 @@ import timber.log.Timber val logMiddleware: Middleware = { dispatch, appState -> { nextDispatch -> { action -> - Timber.d("$action") + Timber.d("Dispatch action: $action") nextDispatch(action) } } diff --git a/app/src/main/java/com/tangem/tap/common/text/Truncate.kt b/app/src/main/java/com/tangem/tap/common/text/Truncate.kt new file mode 100644 index 0000000000..3ed50ca1d2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/text/Truncate.kt @@ -0,0 +1,136 @@ +package com.tangem.tap.common.text + +import android.widget.TextView +import com.tangem.tap.common.extensions.isEven + +/** +[REDACTED_AUTHOR] + */ +enum class TruncateType { + START, MIDDLE, END +} + +interface Truncate { + fun apply(tv: TextView, text: String, with: String): String + + companion object { + fun create(type: TruncateType): Truncate { + return when (type) { + TruncateType.START -> TruncateStart() + TruncateType.MIDDLE -> TruncateMiddle() + TruncateType.END -> TruncateEnd() + } + } + } +} + +abstract class BaseTruncate : Truncate { + protected var hasBeenTruncated = false + + override fun apply(tv: TextView, text: String, with: String): String { + val roughLength = getRoughFitLength(tv, text, with) + val fittedText = preciseFitting(tv, roughTruncate(text, roughLength), with) + return if (hasBeenTruncated) attachWith(fittedText, with) else fittedText + } + + protected fun getRoughFitLength(tv: TextView, text: String, with: String): Int { + val existingSpace = tv.measuredWidth - (tv.paddingStart + tv.paddingEnd) + val textWillTakeSpace = tv.paint.measureText(text) + val overSizeRatio: Float = textWillTakeSpace / existingSpace + + val maxLengthOfText = text.length / overSizeRatio + if (text.length <= maxLengthOfText) return text.length + + return maxLengthOfText.toInt() + } + + protected fun preciseFitting(tv: TextView, text: String, with: String): String { + if (!hasBeenTruncated) return text + + val spaceForText = tv.measuredWidth - (tv.paddingStart + tv.paddingEnd) + + var fittedText = text + while (tv.paint.measureText(fittedText + with) > spaceForText) { + fittedText = preciseTruncate(fittedText) + } + return fittedText + } + + protected abstract fun roughTruncate(text: String, residualLength: Int): String + protected abstract fun preciseTruncate(text: String): String + protected abstract fun attachWith(text: String, with: String): String +} + +class TruncateStart : BaseTruncate() { + override fun roughTruncate(text: String, residualLength: Int): String { + if (text.length <= residualLength) return text + + hasBeenTruncated = true + return text.substring(residualLength, text.length) + } + + override fun preciseTruncate(text: String): String = text.substring(1, text.length) + + override fun attachWith(text: String, with: String): String = with + text +} + +class TruncateMiddle : BaseTruncate() { + + override fun roughTruncate(text: String, residualLength: Int): String { + if (text.length <= residualLength) return text + + hasBeenTruncated = true + val halfOfResidualLength = residualLength / 2 + val leftSide = text.substring(0, halfOfResidualLength) + val rightSide = text.substring(text.length - halfOfResidualLength, text.length) + + return leftSide + rightSide + } + + override fun preciseTruncate(text: String): String { + val middlePosition = text.length / 2 + return if (text.length.isEven()) { + val leftSide = text.substring(0, middlePosition - 1) + val rightSide = text.substring(middlePosition, text.length) + leftSide + rightSide + } else { + val leftSide = text.substring(0, middlePosition) + val rightSide = text.substring(middlePosition + 1, text.length) + leftSide + rightSide + } + } + + override fun attachWith(text: String, with: String): String { + val cuttingPosition = text.length / 2 + val leftSide = text.substring(0, cuttingPosition) + val rightSide = text.substring(cuttingPosition, text.length) + return leftSide + with + rightSide + } +} + +class TruncateEnd : BaseTruncate() { + override fun roughTruncate(text: String, residualLength: Int): String { + if (text.length <= residualLength) return text + + hasBeenTruncated = true + return text.substring(0, residualLength) + } + + override fun preciseTruncate(text: String): String = text.substring(0, text.length - 1) + + override fun attachWith(text: String, with: String): String = text + with +} + +fun TextView.truncateWith(text: String, type: TruncateType, with: String = "..."): String { + val truncate = Truncate.create(type) + return truncate.apply(this, text, with) +} + +fun TextView.truncateStartWith(text: String, with: String = "..."): String = + this.truncateWith(text, TruncateType.START, with) + +fun TextView.truncateMiddleWith(text: String, with: String = "..."): String = + this.truncateWith(text, TruncateType.MIDDLE, with) + +fun TextView.truncateEndWith(text: String, with: String = "..."): String = + this.truncateWith(text, TruncateType.END, with) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendMiddleware.kt index 604d7c7a22..f2f52398e3 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendMiddleware.kt @@ -44,14 +44,13 @@ private fun handleSendAction(action: Action) { internal class AddressPayIdHandler { fun handle(data: String) { val walletManager = store.state.globalState.walletManager ?: return + if (data == store.state.sendState.addressPayIDState.etFieldValue) return if (isPayId(data)) { verifyPayId(data, walletManager) - store.dispatch(ProcessedButNotVerifiedAddressPayId(data)) } else { val supposedAddress = extractAddressFromShareUri(data) store.dispatch(PayIdVerification.Failed(supposedAddress, FailReason.IS_NOT_PAY_ID)) - store.dispatch(ProcessedButNotVerifiedAddressPayId(supposedAddress)) verifyAddress(walletManager, supposedAddress) } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt index 27f36ed995..dc9e216301 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt @@ -1,6 +1,9 @@ package com.tangem.tap.features.send.redux import android.view.View +import com.tangem.tap.features.send.redux.AddressPayIdActionUI.* +import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification +import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification import com.tangem.tap.features.send.redux.FeeActionUI.* import org.rekotlin.Action import org.rekotlin.StateType @@ -11,7 +14,12 @@ import timber.log.Timber */ class SendReducer { companion object { - fun reduce(action: Action, sendState: SendState): SendState = internalReduce(action, sendState) + fun reduce(action: Action, sendState: SendState): SendState { + val newState = internalReduce(action, sendState) + if (newState == sendState) Timber.i("state didn't modified.") + else Timber.i("state was updated to: $newState.") + return newState + } } } @@ -20,66 +28,53 @@ private fun internalReduce(incomingAction: Action, sendState: SendState): SendSt val action = incomingAction as? SendScreenAction ?: return sendState return when (action) { - is FeeActionUI -> handleFeeLayoutAction(action, sendState, sendState.feeLayoutState) -// is SetAddressOrPayId -> handleAddressPayIdUIAction(action, sendState, sendState.addressPayIDState) + is AddressPayIdActionUI -> handleAddressPayIdActionUI(action, sendState, sendState.addressPayIDState) is AddressPayIdVerifyAction -> handleAddressPayIdAction(action, sendState, sendState.addressPayIDState) + is FeeActionUI -> handleFeeActionUI(action, sendState, sendState.feeLayoutState) else -> sendState } } +fun handleAddressPayIdActionUI( + action: AddressPayIdActionUI, + sendState: SendState, + state: AddressPayIDState +): SendState { + val result = when (action) { + is SetAddressOrPayId -> state + is SetTruncateHandler -> state.copy(truncateHandler = action.handler) + is TruncateOrRestore -> { + if(action.truncate) state.copy(etFieldValue = state.truncatedFieldValue) + else state.copy(etFieldValue = state.normalFieldValue) + } + } + return updateLastState(sendState.copy(addressPayIDState = result), result) +} + private fun handleAddressPayIdAction( action: AddressPayIdVerifyAction, sendState: SendState, state: AddressPayIDState ): SendState { - val result: AddressPayIDState? = when (action) { - is AddressPayIdVerifyAction.PayIdVerification.Success -> { - state.copyPayIdWalletAddress(action.payId, action.payIdWalletAddress) - } - is AddressPayIdVerifyAction.PayIdVerification.Failed -> { - state.copyPaiIdError(action.payId, action.reason) - } - is AddressPayIdVerifyAction.AddressVerification.Success -> { - state.copyWalletAddress(action.address) - } - is AddressPayIdVerifyAction.AddressVerification.Failed -> { - state.copyError(action.address, action.reason) - } - is AddressPayIdVerifyAction.ProcessedButNotVerifiedAddressPayId -> { - state.copy(etFieldValue = action.data) - } + val result = when (action) { + is PayIdVerification.Success -> state.copyPayIdWalletAddress(action.payId, action.payIdWalletAddress) + is PayIdVerification.Failed -> state.copyPaiIdError(action.payId, action.reason) + is AddressVerification.Success -> state.copyWalletAddress(action.address) + is AddressVerification.Failed -> state.copyError(action.address, action.reason) } - return if (result == null) { - Timber.e("AddressPayIDState didn't modified.") - sendState - } else { - updateLastState(sendState.copy(addressPayIDState = result), result) - } - + return updateLastState(sendState.copy(addressPayIDState = result), result) } -private fun handleFeeLayoutAction(action: FeeActionUI, sendState: SendState, state: FeeLayoutState): SendState { - return when (action) { +private fun handleFeeActionUI(action: FeeActionUI, sendState: SendState, state: FeeLayoutState): SendState { + val result = when (action) { is ToggleFeeLayoutVisibility -> { - val visibility = if (state.visibility == View.VISIBLE) View.GONE - else View.VISIBLE - - val result = state.copy(visibility = visibility) - updateLastState(sendState.copy(feeLayoutState = result), result) - } - is ChangeSelectedFee -> { - val result = state.copy(selectedFeeId = action.id) - updateLastState(sendState.copy(feeLayoutState = result), result) - } - is ChangeIncludeFee -> { - val result = state.copy(includeFeeIsChecked = action.isChecked) - updateLastState(sendState.copy(feeLayoutState = result), result) + state.copy(visibility = if (state.visibility == View.VISIBLE) View.GONE else View.VISIBLE) } + is ChangeSelectedFee -> state.copy(selectedFeeId = action.id) + is ChangeIncludeFee -> state.copy(includeFeeIsChecked = action.isChecked) } + return updateLastState(sendState.copy(feeLayoutState = result), result) } -private fun updateLastState(sendState: SendState, lastChangedState: StateType): SendState { - val sendState = sendState.copy(lastChangedStateType = lastChangedState) - Timber.d("$sendState") - return sendState -} +private fun updateLastState(sendState: SendState, lastChangedState: StateType): SendState = + sendState.copy(lastChangedStateType = lastChangedState) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index e37b243aa2..3ea66b87d5 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -19,6 +19,8 @@ sealed class FeeActionUI : SendScreenActionUI { // shortness AddressOrPayId = APid sealed class AddressPayIdActionUI : SendScreenActionUI { data class SetAddressOrPayId(val data: String) : AddressPayIdActionUI() + data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUI() + data class TruncateOrRestore(val truncate: Boolean) : AddressPayIdActionUI() } sealed class AddressPayIdVerifyAction : SendScreenAction { @@ -32,8 +34,6 @@ sealed class AddressPayIdVerifyAction : SendScreenAction { ADDRESS_SAME_AS_WALLET } - data class ProcessedButNotVerifiedAddressPayId(val data: String) : AddressPayIdVerifyAction() - sealed class PayIdVerification : AddressPayIdVerifyAction() { data class Failed(val payId: String, val reason: FailReason) : PayIdVerification() data class Success(val payId: String, val payIdWalletAddress: String) : PayIdVerification() diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt index 221290b937..aefebf34ba 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt @@ -19,25 +19,57 @@ class NoneState : StateType data class AddressPayIDState( val etFieldValue: String? = null, + val normalFieldValue: String? = null, + val truncatedFieldValue: String? = null, val walletAddress: String? = null, val error: AddressPayIdVerifyAction.FailReason? = null, + val truncateHandler: ((String) -> String)? = null ) : StateType { - fun isPayIdState():Boolean = walletAddress != null && walletAddress != etFieldValue + fun isPayIdState(): Boolean = walletAddress != null && walletAddress != normalFieldValue fun copyWalletAddress(address: String): AddressPayIDState { - return this.copy(etFieldValue = address, walletAddress = address, error = null) + val truncated = truncateHandler?.invoke(address) ?: address + return this.copy( + etFieldValue = address, + normalFieldValue = address, + truncatedFieldValue = truncated, + walletAddress = address, + error = null + ) } + fun copyError(address: String, error: AddressPayIdVerifyAction.FailReason): AddressPayIDState { - return this.copy(etFieldValue = address, error = error, walletAddress = null) + val truncated = truncateHandler?.invoke(address) ?: address + return this.copy( + etFieldValue = address, + normalFieldValue = address, + truncatedFieldValue = truncated, + error = error, + walletAddress = null + ) } fun copyPayIdWalletAddress(payId: String, address: String): AddressPayIDState { - return this.copy(etFieldValue = payId, walletAddress = address, error = null) + val truncated = truncateHandler?.invoke(address) ?: address + return this.copy( + etFieldValue = payId, + normalFieldValue = payId, + truncatedFieldValue = truncated, + walletAddress = address, + error = null + ) } fun copyPaiIdError(payId: String, error: AddressPayIdVerifyAction.FailReason): AddressPayIDState { - return this.copy(etFieldValue = payId, error = error, walletAddress = null) + val truncated = truncateHandler?.invoke(payId) ?: payId + return this.copy( + etFieldValue = payId, + normalFieldValue = payId, + truncatedFieldValue = truncated, + error = error, + walletAddress = null + ) } } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 94e0c1bc34..81d48ba2fc 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -7,12 +7,12 @@ import android.widget.EditText import androidx.core.widget.addTextChangedListener import com.tangem.tap.common.extensions.getFromClipboard import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity +import com.tangem.tap.common.text.truncateMiddleWith import com.tangem.tap.features.send.BaseStoreFragment -import com.tangem.tap.features.send.redux.AddressPayIdActionUI.SetAddressOrPayId +import com.tangem.tap.features.send.redux.AddressPayIdActionUI.* import com.tangem.tap.features.send.redux.FeeActionUI.* import com.tangem.tap.features.send.redux.ReleaseSendState import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber -import com.tangem.tap.features.send.ui.stateSubscribers.WalletStateSubscriber import com.tangem.tap.mainScope import com.tangem.tap.store import com.tangem.wallet.R @@ -24,17 +24,53 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* - /** [REDACTED_AUTHOR] */ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { private val sendSubscriber = SendStateSubscriber(this) - private val walletSubscriber = WalletStateSubscriber(this) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + setupAddressOrPayIdLayout() + setupFeeLayout() + } + + private fun setupAddressOrPayIdLayout() { + store.dispatch(SetTruncateHandler { etAddressOrPayId.truncateMiddleWith(it, " *** ") }) + + etAddressOrPayId.setOnFocusChangeListener { v, hasFocus -> + store.dispatch(TruncateOrRestore(!hasFocus)) + } + etAddressOrPayId.inputedTextAsFlow() + .debounce(400) + .filter { store.state.sendState.addressPayIDState.etFieldValue != it } + .onEach { store.dispatch(SetAddressOrPayId(it)) } + .launchIn(mainScope) + + imvPaste.setOnClickListener { + store.dispatch(SetAddressOrPayId(requireContext().getFromClipboard()?.toString() ?: "")) + store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused)) + } + imvQrCode.setOnClickListener { + startActivityForResult( + Intent(requireContext(), ScanQrCodeActivity::class.java), + ScanQrCodeActivity.SCAN_QR_REQUEST_CODE + ) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode != ScanQrCodeActivity.SCAN_QR_REQUEST_CODE) return + + val scannedCode = data?.getStringExtra(ScanQrCodeActivity.SCAN_RESULT) ?: "" + store.dispatch(SetAddressOrPayId(scannedCode)) + store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused)) + } + + private fun setupFeeLayout() { flExpandCollapse.setOnClickListener { store.dispatch(ToggleFeeLayoutVisibility) } @@ -44,30 +80,13 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { swIncludeFee.setOnCheckedChangeListener { btn, isChecked -> store.dispatch(ChangeIncludeFee(isChecked)) } - - etAddressOrPayId.inputedTextAsFlow() - .debounce(400) - .filter { store.state.sendState.addressPayIDState.etFieldValue != it } - .onEach { store.dispatch(SetAddressOrPayId(it)) } - .launchIn(mainScope) - - imvPaste.setOnClickListener { - store.dispatch(SetAddressOrPayId(requireContext().getFromClipboard()?.toString() ?: "")) - } - imvQrCode.setOnClickListener { - requireActivity().startActivity(Intent(requireContext(), ScanQrCodeActivity::class.java)) - } } override fun subscribeToStore() { - store.subscribe(walletSubscriber) { appState -> - appState.skipRepeats { oldState, newState -> false }.select { it.walletState } - } store.subscribe(sendSubscriber) { appState -> - appState.skipRepeats { oldState, newState -> false }.select { it.sendState } + appState.skipRepeats { oldState, newState -> oldState == newState }.select { it.sendState } } - storeSubscribersList.add(walletSubscriber) storeSubscribersList.add(sendSubscriber) } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 12e4deee26..780a2a23aa 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -40,14 +40,25 @@ class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber(fragment) { - override fun updateWithNewState(fg: Fragment, state: WalletState) { - - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/layout_send_address_payid.xml b/app/src/main/res/layout/layout_send_address_payid.xml index a399a74119..5cb09462f0 100644 --- a/app/src/main/res/layout/layout_send_address_payid.xml +++ b/app/src/main/res/layout/layout_send_address_payid.xml @@ -2,17 +2,18 @@ + android:layout_height="wrap_content" + android:paddingStart="16dp" + android:paddingEnd="16dp"> @@ -23,7 +24,7 @@ android:layout_height="wrap_content" android:ellipsize="middle" android:paddingStart="0dp" - android:paddingEnd="96dp" + android:paddingEnd="98dp" android:singleLine="true" android:textSize="16sp" tools:text="139mrsJgyWnJ **** y9BV" />