feat(@desktop/wallet): Wallet -> Send: polish Send/Bridge Modals

fixes #10344, #10321, #10320
This commit is contained in:
Khushboo Mehta 2023-04-18 18:05:24 +02:00 committed by Khushboo-dev-cpp
parent c3d1133dfd
commit c94997ddec
19 changed files with 648 additions and 267 deletions

View File

@ -17,6 +17,7 @@ QtObject:
fetchingHistoryState: Table[string, bool]
enabledChainIds: seq[int]
isNonArchivalNode: bool
tempAddress: string
proc delete*(self: View) =
self.model.delete
@ -197,3 +198,12 @@ QtObject:
if not self.models.hasKey(fromAddress):
self.models[fromAddress] = newModel()
self.models[fromAddress].addNewTransactions(@[tx], wasFetchMore=false)
proc prepareTransactionsForAddress*(self: View, address: string) {.slot.} =
self.tempAddress = address
proc getTransactions*(self: View): QVariant {.slot.} =
if self.models.hasKey(self.tempAddress):
return newQVariant(self.models[self.tempAddress])
else:
return newQVariant()

View File

@ -20,28 +20,38 @@ type
const lookupContactTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[LookupContactTaskArg](argEncoded)
var pubkey = arg.value
var address = ""
if (pubkey.startsWith("0x") or isCompressedPubKey(pubkey)):
if pubkey.startsWith("0x"):
var num64: int64
let parsedChars = parseHex(pubkey, num64)
if(parsedChars != PK_LENGTH_0X_INCLUDED):
pubkey = ""
address = ""
else:
# TODO refactor those calls to use the new backend and also do it in a signle call
pubkey = ens_utils.publicKeyOf(arg.chainId, arg.value)
address = ens_utils.addressOf(arg.chainid, arg.value)
let output = %*{
"id": pubkey,
"address": address,
var output = %*{
"id": "",
"address": "",
"uuid": arg.uuid,
"reason": arg.reason
}
arg.finish(output)
try:
var pubkey = arg.value
var address = ""
if (pubkey.startsWith("0x") or isCompressedPubKey(pubkey)):
if pubkey.startsWith("0x"):
var num64: int64
let parsedChars = parseHex(pubkey, num64)
if(parsedChars != PK_LENGTH_0X_INCLUDED):
pubkey = ""
address = ""
else:
# TODO refactor those calls to use the new backend and also do it in a signle call
pubkey = ens_utils.publicKeyOf(arg.chainId, arg.value)
address = ens_utils.addressOf(arg.chainid, arg.value)
output = %*{
"id": pubkey,
"address": address,
"uuid": arg.uuid,
"reason": arg.reason
}
arg.finish(output)
except Exception as e:
error "error lookupContactTask: ", message = e.msg
arg.finish(output)
#################################################
# Async request contact info
@ -62,4 +72,4 @@ const asyncRequestContactInfoTask: Task = proc(argEncoded: string) {.gcsafe, nim
except Exception as e:
arg.finish(%* {
"error": e.msg,
})
})

View File

@ -111,6 +111,7 @@ ThemePalette {
statusListItem: QtObject {
property color backgroundColor: baseColor3
property color secondaryHoverBackgroundColor: primaryColor3
property color highlightColor: getColor('blue3', 0.05)
}
statusChatListItem: QtObject {

View File

@ -109,6 +109,7 @@ ThemePalette {
statusListItem: QtObject {
property color backgroundColor: white
property color secondaryHoverBackgroundColor: getColor('blue6')
property color highlightColor: getColor('blue', 0.05)
}
statusChatListItem: QtObject {

View File

@ -196,6 +196,7 @@ QtObject {
property QtObject statusListItem: QtObject {
property color backgroundColor
property color secondaryHoverBackgroundColor
property color highlightColor
}
property QtObject statusChatListItem: QtObject {

View File

@ -22,7 +22,6 @@ StatusListItem {
asset.bgColor: Theme.palette.primaryColor3
asset.width: 40
asset.height: 40
width: parent.width
components: !showShevronIcon ? [] : [ shevronIcon ]

View File

@ -94,6 +94,7 @@ Column {
}
}
delegate: WalletAccountDelegate {
width: ListView.view.width
account: model
onGoToAccountView: {
root.goToAccountView(model)
@ -122,6 +123,7 @@ Column {
Repeater {
model: importedAccounts
delegate: WalletAccountDelegate {
width: ListView.view.width
account: model
onGoToAccountView: {
root.goToAccountView(model)
@ -149,6 +151,7 @@ Column {
Repeater {
model: watchOnlyAccounts
delegate: WalletAccountDelegate {
width: ListView.view.width
account: model
onGoToAccountView: {
root.goToAccountView(model)

View File

@ -273,17 +273,16 @@ Rectangle {
}
components: [
StatusIcon {
icon: {
if (model.walletType === Constants.watchWalletType)
return "show"
if (model.walletType === Constants.keyWalletType)
return "keycard"
return ""
}
width: !!icon ? 15: 0
height: !!icon ? 15: 0
color: Theme.palette.directColor1
width: 15
height: 15
icon: model.walletType === Constants.watchWalletType ? "show" : ""
},
StatusIcon {
width: !!icon ? 15: 0
height: !!icon ? 15: 0
color: Theme.palette.directColor1
icon: model.migratedToKeycard ? "keycard" : ""
}
]

View File

@ -0,0 +1,13 @@
import QtQuick 2.14
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
StatusFlatRoundButton {
type: StatusFlatRoundButton.Type.Secondary
icon.name: "clear"
icon.width: 16
icon.height: 16
icon.color: Theme.palette.baseColor1
backgroundHoverColor: "transparent"
}

View File

@ -0,0 +1,44 @@
import QtQuick 2.15
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import AppLayouts.Wallet 1.0
import utils 1.0
StatusListItem {
id: root
property var modelData
property bool clearVisible: false
signal cleared()
implicitHeight: visible ? 64 : 0
title: !!modelData ? modelData.name: ""
subTitle: {
if(!!modelData) {
if (modelData.ens.length > 0) {
return sensor.containsMouse ? Utils.richColorText(modelData.ens, Theme.palette.directColor1) : modelData.ens
}
else {
let elidedAddress = StatusQUtils.Utils.elideText(modelData.address,6,4)
return sensor.containsMouse ? WalletUtils.colorizedChainPrefix(modelData.chainShortNames) + Utils.richColorText(elidedAddress, Theme.palette.directColor1): modelData.chainShortNames + elidedAddress
}
}
return ""
}
statusListItemSubTitle.elide: Text.ElideMiddle
statusListItemSubTitle.wrapMode: Text.NoWrap
radius: 0
color: sensor.containsMouse || highlighted ? Theme.palette.statusListItem.highlightColor : "transparent"
components: [
ClearButton {
width: 24
height: 24
visible: root.clearVisible
onClicked: root.cleared()
}
]
}

View File

@ -0,0 +1,22 @@
import QtQuick 2.14
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
StatusInput {
placeholderText: qsTr("Search")
input.implicitHeight: 56
input.background.color: Theme.palette.indirectColor1
input.background.border.width: 0
input.rightComponent: StatusFlatRoundButton {
icon.name: "search"
type: StatusFlatRoundButton.Type.Secondary
enabled: false
}
Rectangle {
anchors.bottom: parent.bottom
height: 1
width: parent.width
color: Theme.palette.baseColor2
}
}

View File

@ -13,20 +13,33 @@ StatusListItem {
return ""
}
signal tokenSelected(var selectedToken)
signal tokenHovered(var selectedToken, bool hovered)
title: name
titleAsideText: symbol
statusListItemTitleAside.font.pixelSize: 15
label: LocaleUtils.currencyAmountToLocaleString(totalCurrencyBalance)
asset.name: symbol ? Style.png("tokens/" + symbol) : ""
asset.isImage: true
asset.width: 32
asset.height: 32
statusListItemLabel.anchors.verticalCenterOffset: -12
statusListItemLabel.color: Theme.palette.directColor1
statusListItemInlineTagsSlot.spacing: sensor.containsMouse ? 0 : -8
tagsModel: balances.count > 0 ? balances : []
tagsDelegate: sensor.containsMouse ? expandedItem : compactItem
radius: sensor.containsMouse || root.highlighted ? 0 : 8
color: sensor.containsMouse || highlighted ? Theme.palette.statusListItem.highlightColor : "transparent"
onClicked: d.selectToken()
Connections {
target: root.sensor
function onContainsMouseChanged() {
root.tokenHovered({name, symbol, totalBalance, totalCurrencyBalance, balances, decimals}, root.sensor.containsMouse)
}
}
QtObject {
id: d
@ -42,7 +55,6 @@ StatusListItem {
width: 16
height: 16
image.source: Style.svg("tiny/%1".arg(root.getNetworkIcon(chainId)))
visible: balance.amount > 0
}
}
Component {
@ -57,7 +69,6 @@ StatusListItem {
asset.height: 16
asset.isImage: true
asset.name: Style.svg("tiny/%1".arg(root.getNetworkIcon(chainId)))
visible: balance.amount > 0
}
}
}

View File

@ -0,0 +1,76 @@
import QtQuick 2.15
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import AppLayouts.Wallet 1.0
import utils 1.0
StatusListItem {
id: root
property var modelData
property string chainShortNames
property bool clearVisible: false
signal cleared()
objectName: !!modelData ? modelData.name: ""
height: visible ? 64 : 0
title: !!modelData && !!modelData.name ? modelData.name : ""
subTitle:{
if(!!modelData) {
let elidedAddress = StatusQUtils.Utils.elideText(modelData.address,6,4)
return sensor.containsMouse ? WalletUtils.colorizedChainPrefix(chainShortNames) + Utils.richColorText(elidedAddress, Theme.palette.directColor1): chainShortNames + elidedAddress
}
return ""
}
statusListItemSubTitle.wrapMode: Text.NoWrap
asset.emoji: !!modelData && !!modelData.emoji ? modelData.emoji: ""
asset.color: !!modelData ? modelData.color: ""
asset.name: !!modelData && !modelData.emoji ? "filled-account": ""
asset.letterSize: 14
asset.isLetterIdenticon: !!modelData && !!modelData.emoji ? true : false
asset.bgColor: Theme.palette.indirectColor1
asset.width: 40
asset.height: 40
radius: 0
color: sensor.containsMouse || highlighted ? Theme.palette.statusListItem.highlightColor : "transparent"
components: [
Column {
anchors.verticalCenter: parent.verticalCenter
StatusTextWithLoadingState {
anchors.right: parent.right
font.pixelSize: 15
text: LocaleUtils.currencyAmountToLocaleString(!!modelData ? modelData.currencyBalance: "")
}
Row {
anchors.right: parent.right
spacing: 6
StatusIcon {
width: !!icon ? 15: 0
height: !!icon ? 15 : 0
color: Theme.palette.directColor1
icon: modelData.walletType === Constants.watchWalletType ? "show" : ""
}
StatusIcon {
width: !!icon ? 15: 0
height: !!icon ? 15 : 0
color: Theme.palette.directColor1
icon: modelData.migratedToKeycard ? "keycard" : ""
}
}
},
ClearButton {
anchors.verticalCenter: parent.verticalCenter
width: 24
height: 24
visible: root.clearVisible
onClicked: root.cleared()
}
]
}

View File

@ -25,6 +25,7 @@ Item {
property string userSelectedToken
property string currentCurrencySymbol
property string placeholderText
property var hoveredToken
property var tokenAssetSourceFn: function (symbol) {
return ""
@ -51,12 +52,19 @@ Item {
}
}
onHoveredTokenChanged: {
if (hoveredToken && hoveredToken.symbol) {
d.iconSource = tokenAssetSourceFn(hoveredToken.symbol)
d.text = hoveredToken.symbol
}
}
QtObject {
id: d
property string iconSource: ""
property string text: ""
property string searchString
property bool isTokenSelected: !!root.selectedAsset
readonly property bool isTokenSelected: !!root.selectedAsset || !!root.hoveredToken
readonly property var updateSearchText: Backpressure.debounce(root, 1000, function(inputText) {
d.searchString = inputText
@ -70,8 +78,8 @@ Item {
control.padding: 4
control.popup.width: 492
control.popup.height: 416
control.popup.x: -root.x
control.popup.verticalPadding: 0
popupContentItemObjectName: "assetSelectorList"
@ -171,11 +179,11 @@ Item {
placeholderText: qsTr("Search for token or enter token address")
onTextChanged: Qt.callLater(d.updateSearchText, text)
input.clearable: true
input.rightComponent: StatusIcon {
width: 16
height: 16
color: Theme.palette.baseColor1
icon: "search"
input.implicitHeight: 56
input.rightComponent: StatusFlatRoundButton {
icon.name: "search"
type: StatusFlatRoundButton.Type.Secondary
enabled: false
}
Rectangle {
anchors.bottom: parent.bottom

View File

@ -38,7 +38,7 @@ StatusDialog {
property var currencyStore: store.currencyStore
property var selectedAccount: store.currentAccount
property var bestRoutes
property string addressText
property alias addressText: recipientLoader.addressText
property bool isLoading: false
property int sendType: isBridgeTx ? Constants.SendType.Bridge : Constants.SendType.Transfer
property MessageDialog sendingError: MessageDialog {
@ -49,7 +49,7 @@ StatusDialog {
}
property var sendTransaction: function() {
let recipientAddress = Utils.isValidAddress(popup.addressText) ? popup.addressText : d.resolvedENSAddress
let recipientAddress = Utils.isValidAddress(popup.addressText) ? popup.addressText : recipientLoader.resolvedENSAddress
d.isPendingTx = true
popup.store.authenticateAndTransfer(
popup.selectedAccount.address,
@ -62,7 +62,7 @@ StatusDialog {
}
property var recalculateRoutesAndFees: Backpressure.debounce(popup, 600, function() {
if(!!popup.selectedAccount && !!assetSelector.selectedAsset && d.recipientReady && amountToSendInput.inputNumberValid) {
if(!!popup.selectedAccount && !!assetSelector.selectedAsset && recipientLoader.ready && amountToSendInput.inputNumberValid) {
popup.isLoading = true
let amount = Math.round(amountToSendInput.cryptoValueToSend * Math.pow(10, assetSelector.selectedAsset.decimals))
popup.store.suggestedRoutes(popup.selectedAccount.address, amount.toString(16), assetSelector.selectedAsset.symbol,
@ -74,21 +74,14 @@ StatusDialog {
QtObject {
id: d
readonly property int errorType: !amountToSendInput.input.valid ? Constants.SendAmountExceedsBalance :
(networkSelector.bestRoutes && networkSelector.bestRoutes.length <= 0 && !!amountToSendInput.input.text && recipientReady && !popup.isLoading) ?
(networkSelector.bestRoutes && networkSelector.bestRoutes.length <= 0 && !!amountToSendInput.input.text && recipientLoader.ready && !popup.isLoading) ?
Constants.NoRoute : Constants.NoError
readonly property double maxFiatBalance: !!assetSelector.selectedAsset ? assetSelector.selectedAsset.totalCurrencyBalance.amount : 0
readonly property double maxCryptoBalance: !!assetSelector.selectedAsset ? assetSelector.selectedAsset.totalBalance.amount : 0
readonly property double maxInputBalance: amountToSendInput.inputIsFiat ? maxFiatBalance : maxCryptoBalance
readonly property string selectedSymbol: !!assetSelector.selectedAsset ? assetSelector.selectedAsset.symbol : ""
readonly property string inputSymbol: amountToSendInput.inputIsFiat ? popup.store.currentCurrency : selectedSymbol
readonly property bool errorMode: popup.isLoading || !recipientReady ? false : errorType !== Constants.NoError || networkSelector.errorMode || !amountToSendInput.inputNumberValid
readonly property bool recipientReady: (isAddressValid || isENSValid) && !recipientSelector.isPending
property bool isAddressValid: Utils.isValidAddress(popup.addressText)
property bool isENSValid: false
readonly property var resolveENS: Backpressure.debounce(popup, 500, function (ensName) {
store.resolveENS(ensName)
})
property string resolvedENSAddress
readonly property bool errorMode: popup.isLoading || !recipientLoader.ready ? false : errorType !== Constants.NoError || networkSelector.errorMode || !amountToSendInput.inputNumberValid
readonly property string uuid: Utils.uuid()
property bool isPendingTx: false
property string totalTimeEstimate
@ -96,24 +89,6 @@ StatusDialog {
property double totalFeesInFiat
property double totalAmountToReceive
property Timer waitTimer: Timer {
interval: 1500
onTriggered: {
if (recipientSelector.isPending) {
return
}
if (d.isENSValid) {
recipientSelector.input.text = d.resolvedENSAddress
popup.addressText = d.resolvedENSAddress
} else {
let result = store.splitAndFormatAddressPrefix(recipientSelector.input.text, isBridgeTx, networkSelector.showUnpreferredNetworks)
popup.addressText = result.address
recipientSelector.input.text = result.formattedText
}
popup.recalculateRoutesAndFees()
}
}
readonly property NetworkConnectionStore networkConnectionStore: NetworkConnectionStore {}
onErrorTypeChanged: {
@ -124,6 +99,7 @@ StatusDialog {
width: 556
topMargin: 64 + header.height
bottomPadding: footer.visible ? 0 : 32
padding: 0
background: StatusDialogBackground {
@ -153,13 +129,13 @@ StatusDialog {
}
if(!!popup.preSelectedRecipient) {
recipientSelector.input.text = popup.preSelectedRecipient
d.waitTimer.restart()
recipientLoader.selectedRecipientType = TabAddressSelectorView.Type.Address
recipientLoader.selectedRecipient = {address: popup.preSelectedRecipient}
}
if(popup.isBridgeTx) {
recipientSelector.input.text = popup.selectedAccount.address
d.waitTimer.restart()
recipientLoader.selectedRecipientType = TabAddressSelectorView.Type.Address
recipientLoader.selectedRecipient = {address: popup.selectedAccount.address}
}
// add networks that are down to disabled list
@ -186,6 +162,7 @@ StatusDialog {
selectedAccount: popup.selectedAccount
changeSelectedAccount: function(newAccount) {
popup.selectedAccount = newAccount
assetSelector.selectedAsset = store.getAsset(selectedAccount.assets, assetSelector.selectedAsset.symbol)
}
}
@ -261,23 +238,56 @@ StatusDialog {
}
popup.recalculateRoutesAndFees()
}
visible: !!assetSelector.selectedAsset || !!assetSelector.hoveredToken
}
StatusListItemTag {
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.preferredHeight: 22
visible: !!assetSelector.selectedAsset || !!assetSelector.hoveredToken
title: {
if(!!assetSelector.hoveredToken) {
const balance = popup.currencyStore.formatCurrencyAmount(assetSelector.hoveredToken.totalCurrencyBalance.amount, assetSelector.hoveredToken.symbol)
return qsTr("Max: %1").arg(balance)
}
if (d.maxInputBalance <= 0)
return qsTr("No balances active")
const balance = popup.currencyStore.formatCurrencyAmount(d.maxInputBalance, d.inputSymbol)
return qsTr("Max: %1").arg(balance)
}
tagClickable: true
closeButtonVisible: false
titleText.font.pixelSize: 12
bgColor: amountToSendInput.input.valid ? Theme.palette.primaryColor3 : Theme.palette.dangerColor2
titleText.color: amountToSendInput.input.valid ? Theme.palette.primaryColor1 : Theme.palette.dangerColor1
bgColor: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor3 : Theme.palette.dangerColor2
titleText.color: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor1 : Theme.palette.dangerColor1
onTagClicked: amountToSendInput.input.text = d.maxInputBalance
}
}
TokenListView {
id: tokenListRect
Layout.fillWidth: true
visible: !assetSelector.selectedAsset
assets: popup.selectedAccount && popup.selectedAccount.assets ? popup.selectedAccount.assets : null
searchTokenSymbolByAddressFn: function (address) {
return store.findTokenSymbolByAddress(address)
}
getNetworkIcon: function(chainId){
return RootStore.getNetworkIcon(chainId)
}
onTokenSelected: {
assetSelector.userSelectedToken = selectedToken.symbol
assetSelector.selectedAsset = selectedToken
}
onTokenHovered: {
if(hovered)
assetSelector.hoveredToken = selectedToken
else
assetSelector.hoveredToken = null
}
}
RowLayout {
visible: !!assetSelector.selectedAsset
AmountToSend {
id: amountToSendInput
Layout.fillWidth:true
@ -319,24 +329,6 @@ StatusDialog {
formatCurrencyAmount: popup.currencyStore.formatCurrencyAmount
}
}
TokenListView {
id: tokenListRect
Layout.fillWidth: true
visible: !assetSelector.selectedAsset
assets: popup.selectedAccount && popup.selectedAccount.assets ? popup.selectedAccount.assets : null
searchTokenSymbolByAddressFn: function (address) {
return store.findTokenSymbolByAddress(address)
}
getNetworkIcon: function(chainId){
return RootStore.getNetworkIcon(chainId)
}
onTokenSelected: {
assetSelector.userSelectedToken = selectedToken.symbol
assetSelector.selectedAsset = selectedToken
}
}
}
}
@ -360,72 +352,31 @@ StatusDialog {
spacing: Style.current.bigPadding
anchors.left: parent.left
StatusInput {
id: recipientSelector
property bool isPending: false
height: visible ? implicitHeight: 0
visible: !isBridgeTx && !!assetSelector.selectedAsset
ColumnLayout {
spacing: 8
width: parent.width
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Style.current.bigPadding
anchors.rightMargin: Style.current.bigPadding
label: qsTr("To")
placeholderText: qsTr("Enter an ENS name or address")
text: popup.addressText
input.background.color: Theme.palette.indirectColor1
input.background.border.width: 0
input.implicitHeight: 56
input.clearable: popup.interactive
input.edit.readOnly: !popup.interactive
multiline: false
input.edit.textFormat: TextEdit.RichText
input.rightComponent: RowLayout {
StatusIcon {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
icon: "tiny/checkmark"
color: Theme.palette.primaryColor1
visible: d.recipientReady
}
StatusFlatRoundButton {
visible: recipientSelector.text !== ""
type: StatusFlatRoundButton.Type.Secondary
Layout.preferredWidth: 24
Layout.preferredHeight: 24
icon.name: "clear"
icon.width: 16
icon.height: 16
icon.color: Theme.palette.baseColor1
backgroundHoverColor: "transparent"
onClicked: {
popup.isLoading = true
recipientSelector.input.edit.clear()
d.waitTimer.restart()
}
}
visible: !isBridgeTx && !!assetSelector.selectedAsset
StatusBaseText {
id: label
elide: Text.ElideRight
text: qsTr("To")
font.pixelSize: 15
color: Theme.palette.directColor1
}
Keys.onReleased: {
popup.isLoading = true
d.waitTimer.restart()
if(!d.isAddressValid) {
isPending = true
Qt.callLater(d.resolveENS, store.plainText(input.edit.text))
}
}
}
Connections {
target: store.mainModuleInst
function onResolvedENS(resolvedPubKey: string, resolvedAddress: string, uuid: string) {
recipientSelector.isPending = false
if(Utils.isValidAddress(resolvedAddress)) {
d.resolvedENSAddress = resolvedAddress
d.isENSValid = true
}
RecipientView {
id: recipientLoader
Layout.fillWidth: true
store: popup.store
isBridgeTx: popup.isBridgeTx
interactive: popup.interactive
selectedAsset: assetSelector.selectedAsset
showUnpreferredNetworks: networkSelector.showUnpreferredNetworks
onIsLoading: popup.isLoading = true
onRecalculateRoutesAndFees: popup.recalculateRoutesAndFees()
}
}
@ -438,12 +389,11 @@ StatusDialog {
anchors.rightMargin: Style.current.bigPadding
store: popup.store
selectedAccount: popup.selectedAccount
onContactSelected: {
recipientSelector.input.text = address
popup.isLoading = true
d.waitTimer.restart()
onRecipientSelected: {
recipientLoader.selectedRecipientType = type
recipientLoader.selectedRecipient = recipient
}
visible: !d.recipientReady && !isBridgeTx && !!assetSelector.selectedAsset
visible: !recipientLoader.ready && !isBridgeTx && !!assetSelector.selectedAsset
}
NetworkSelector {
@ -456,14 +406,14 @@ StatusDialog {
store: popup.store
interactive: popup.interactive
selectedAccount: popup.selectedAccount
ensAddressOrEmpty: d.isENSValid ? d.resolvedENSAddress : ""
ensAddressOrEmpty: recipientLoader.isENSValid ? recipientLoader.resolvedENSAddress : ""
amountToSend: amountToSendInput.cryptoValueToSend
minSendCryptoDecimals: amountToSendInput.minSendCryptoDecimals
minReceiveCryptoDecimals: amountToSendInput.minReceiveCryptoDecimals
requiredGasInEth: d.totalFeesInEth
selectedAsset: assetSelector.selectedAsset
onReCalculateSuggestedRoute: popup.recalculateRoutesAndFees()
visible: d.recipientReady && !!assetSelector.selectedAsset && amountToSendInput.inputNumberValid
visible: recipientLoader.ready && !!assetSelector.selectedAsset && amountToSendInput.inputNumberValid
errorType: d.errorType
isLoading: popup.isLoading
bestRoutes: popup.bestRoutes
@ -477,7 +427,7 @@ StatusDialog {
anchors.right: parent.right
anchors.leftMargin: Style.current.bigPadding
anchors.rightMargin: Style.current.bigPadding
visible: d.recipientReady && !!assetSelector.selectedAsset && networkSelector.advancedOrCustomMode && amountToSendInput.inputNumberValid
visible: recipientLoader.ready && !!assetSelector.selectedAsset && networkSelector.advancedOrCustomMode && amountToSendInput.inputNumberValid
selectedTokenSymbol: d.selectedSymbol
isLoading: popup.isLoading
bestRoutes: popup.bestRoutes
@ -495,7 +445,7 @@ StatusDialog {
maxFiatFees: popup.isLoading ? "..." : popup.currencyStore.formatCurrencyAmount(d.totalFeesInFiat, popup.store.currentCurrency)
totalTimeEstimate: popup.isLoading? "..." : d.totalTimeEstimate
pending: d.isPendingTx || popup.isLoading
visible: d.recipientReady && amountToSendInput.inputNumberValid && !d.errorMode
visible: recipientLoader.ready && amountToSendInput.inputNumberValid && !d.errorMode
onNextButtonClicked: popup.sendTransaction()
}

View File

@ -4,6 +4,7 @@ import utils 1.0
import shared.stores 1.0
import "../../../app/AppLayouts/Profile/stores"
import SortFilterProxyModel 0.2
QtObject {
id: root
@ -20,7 +21,15 @@ QtObject {
property var accounts: walletSectionAccounts.model
property var currentAccount: walletSectionCurrent
property string signingPhrase: walletSection.signingPhrase
property var savedAddressesModel: walletSectionSavedAddresses.model
property var savedAddressesModel: SortFilterProxyModel {
sourceModel: walletSectionSavedAddresses.model
filters: [
ValueFilter {
roleName: "isTest"
value: networksModule.areTestNetworksEnabled
}
]
}
property var disabledChainIdsFromList: []
property var disabledChainIdsToList: []
@ -280,4 +289,21 @@ QtObject {
}
return {}
}
function prepareTransactionsForAddress(address) {
walletSectionTransactions.prepareTransactionsForAddress(address)
}
function getTransactions() {
return walletSectionTransactions.getTransactions()
}
function getAllNetworksSupportedString() {
let result = ""
for(var i = 0; i < allNetworks.count; i++) {
let shortName = allNetworks.rowData(i, "shortName")
result += shortName + ':'
}
return result
}
}

View File

@ -0,0 +1,230 @@
import QtQuick 2.13
import QtQuick.Layouts 1.13
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import AppLayouts.Wallet 1.0
import utils 1.0
import "../controls"
Loader {
id: root
property var store
property bool isBridgeTx: false
property bool interactive: true
property bool showUnpreferredNetworks: false
property var selectedAsset
property var selectedRecipient: null
property int selectedRecipientType
readonly property bool ready: (d.isAddressValid || root.isENSValid) && !d.isPending
property string addressText
property bool isENSValid: false
property string resolvedENSAddress
signal recalculateRoutesAndFees()
signal isLoading()
onAddressTextChanged: d.isPending = false
onSelectedRecipientChanged: {
root.isLoading()
d.waitTimer.restart()
if(!!root.selectedRecipient && root.selectedRecipientType !== TabAddressSelectorView.Type.None) {
switch(root.selectedRecipientType) {
case TabAddressSelectorView.Type.SavedAddress: {
if (root.selectedRecipient.ens.length > 0) {
d.isPending = true
return store.resolveENS(root.selectedRecipient.ens)
}
}
case TabAddressSelectorView.Type.RecentsAddress: {
let isIncoming = root.selectedRecipient.to === root.selectedRecipient.address
root.addressText = isIncoming ? root.selectedRecipient.from : root.selectedRecipient.to
root.item.input.text = root.addressText
return
}
case TabAddressSelectorView.Type.Address: {
root.item.input.text = root.selectedRecipient.address
}
}
root.addressText = root.selectedRecipient.address
}
}
QtObject {
id: d
property bool isAddressValid: Utils.isValidAddress(root.addressText)
readonly property var resolveENS: Backpressure.debounce(root, 500, function (ensName) {
store.resolveENS(ensName)
})
property bool isPending: false
function clearValues() {
root.addressText = ""
root.isENSValid = false
root.resolvedENSAddress = ""
root.selectedRecipientType = TabAddressSelectorView.Type.None
root.selectedRecipient = null
}
property Timer waitTimer: Timer {
interval: 1500
onTriggered: {
if(!!root.item) {
if (d.isPending) {
return
}
if (root.isENSValid) {
if(!!root.item.input)
root.item.input.text = root.resolvedENSAddress
root.addressText = root.resolvedENSAddress
store.splitAndFormatAddressPrefix(root.address, root.isBridgeTx, root.showUnpreferredNetworks)
} else {
let address = d.getAddress()
let result = store.splitAndFormatAddressPrefix(address, root.isBridgeTx, root.showUnpreferredNetworks)
if(!!result.address) {
root.addressText = result.address
if(!!root.item.input)
root.item.input.text = result.formattedText
}
}
root.recalculateRoutesAndFees()
}
}
}
function getAddress() {
if(root.selectedRecipientType === TabAddressSelectorView.Type.SavedAddress || root.selectedRecipientType === TabAddressSelectorView.Type.Account){
return root.item.chainShortNames + root.selectedRecipient.address
}
else {
return !!root.item.input && !!root.store.plainText(root.item.input.text) ? root.store.plainText(root.item.input.text): ""
}
}
}
sourceComponent: root.selectedRecipientType === TabAddressSelectorView.Type.SavedAddress ? savedAddressRecipient:
root.selectedRecipientType === TabAddressSelectorView.Type.Account ? myAccountRecipient : addressRecipient
Component {
id: savedAddressRecipient
SavedAddressListItem {
property string chainShortNames: !!modelData ? modelData.chainShortNames: ""
implicitWidth: parent.width
modelData: root.selectedRecipient
radius: 8
clearVisible: true
color: Theme.palette.indirectColor1
sensor.enabled: false
subTitle: {
if(!!modelData) {
if (!!modelData && !!modelData.ens && modelData.ens.length > 0)
return Utils.richColorText(modelData.ens, Theme.palette.directColor1)
else
return WalletUtils.colorizedChainPrefix(modelData.chainShortNames) + StatusQUtils.Utils.elideText(modelData.address,6,4)
}
return ""
}
onCleared: d.clearValues()
}
}
Component {
id: myAccountRecipient
WalletAccountListItem {
implicitWidth: parent.width
chainShortNames: store.getAllNetworksSupportedString()
modelData: root.selectedRecipient
radius: 8
clearVisible: true
color: Theme.palette.indirectColor1
sensor.enabled: false
subTitle: {
if(!!modelData) {
let elidedAddress = StatusQUtils.Utils.elideText(modelData.address,6,4)
return WalletUtils.colorizedChainPrefix(chainShortNames) + StatusQUtils.Utils.elideText(elidedAddress,6,4)
}
return ""
}
onCleared: d.clearValues()
}
}
Component {
id: addressRecipient
StatusInput {
id: recipientInput
width: parent.width
height: visible ? implicitHeight: 0
visible: !root.isBridgeTx && !!root.selectedAsset
placeholderText: qsTr("Enter an ENS name or address")
input.background.color: Theme.palette.indirectColor1
input.background.border.width: 0
input.implicitHeight: 56
input.clearable: root.interactive
input.edit.readOnly: !root.interactive
multiline: false
input.edit.textFormat: TextEdit.RichText
input.rightComponent: RowLayout {
StatusButton {
font.weight: Font.Normal
borderColor: Theme.palette.primaryColor1
size: StatusBaseButton.Size.Tiny
text: qsTr("Paste")
visible: !store.plainText(recipientInput.text)
onClicked: recipientInput.input.edit.paste()
}
StatusIcon {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
icon: "tiny/checkmark"
color: Theme.palette.primaryColor1
visible: !!store.plainText(recipientInput.text)
}
ClearButton {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
visible: !!store.plainText(recipientInput.text)
onClicked: {
recipientInput.input.edit.clear()
d.clearValues()
}
}
}
Keys.onReleased: {
let plainText = store.plainText(input.edit.text)
if(!plainText) {
d.clearValues()
}
else {
root.isLoading()
d.waitTimer.restart()
if(!Utils.isValidAddress(plainText)) {
d.isPending = true
d.resolveENS(plainText)
}
}
}
}
}
Connections {
target: store.mainModuleInst
function onResolvedENS(resolvedPubKey: string, resolvedAddress: string, uuid: string) {
d.isPending = false
if(Utils.isValidAddress(resolvedAddress)) {
root.resolvedENSAddress = resolvedAddress
root.isENSValid = true
}
d.waitTimer.restart()
}
}
}

View File

@ -6,11 +6,14 @@ import QtQuick.Dialogs 1.3
import utils 1.0
import shared.stores 1.0
import AppLayouts.Wallet 1.0
import StatusQ.Controls 0.1
import StatusQ.Popups 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import "../panels"
import "../controls"
@ -24,7 +27,16 @@ Item {
property var selectedAccount
property var store
signal contactSelected(string address, int type)
signal recipientSelected(var recipient, int type)
enum Type {
Address,
Contact,
Account,
SavedAddress,
RecentsAddress,
None
}
QtObject {
id: d
@ -58,6 +70,7 @@ Item {
StackLayout {
id: stackLayout
anchors.top: accountSelectionTabBar.bottom
anchors.topMargin: -5
height: currentIndex === 0 ? savedAddresses.height: currentIndex === 1 ? myAccounts.height : recents.height
width: parent.width
currentIndex: accountSelectionTabBar.currentIndex
@ -77,46 +90,20 @@ Item {
height: Math.min(d.maxHeightForList, savedAddresses.contentHeight)
model: root.store.savedAddressesModel
header: savedAddresses.count > 0 ? search : nothingInList
header: savedAddresses.count > 0 ? search : nothingInList
headerPositioning: ListView.OverlayHeader
delegate: StatusListItem {
implicitWidth: parent.width
height: visible ? 64 : 0
title: name
subTitle: address
radius: 0
color: sensor.containsMouse || highlighted ? Theme.palette.baseColor3 : "transparent"
delegate: SavedAddressListItem {
implicitWidth: ListView.view.width
modelData: model
visible: !savedAddresses.headerItem.text || name.toLowerCase().includes(savedAddresses.headerItem.text)
// TODO uncomment when #6456 is fixed
// components: [
// StatusIcon {
// icon: "star-icon"
// width: 12
// height: 12
// }
// ]
onClicked: contactSelected(address, RecipientSelector.Type.Address )
onClicked: recipientSelected(modelData, TabAddressSelectorView.Type.SavedAddress)
}
Component {
id: search
ColumnLayout {
SearchBoxWithRightIcon {
width: parent.width
StatusInput {
Layout.preferredHeight: 55
Layout.preferredWidth: parent.width
input.showBackground: false
placeholderText: qsTr("Search for saved address")
input.rightComponent: StatusIcon {
icon: "search"
height: 17
color: Theme.palette.baseColor1
}
}
Rectangle {
Layout.preferredHeight: 1
Layout.preferredWidth: parent.width
color: Theme.palette.baseColor3
}
placeholderText: qsTr("Search for saved address")
z: 2
}
}
Component {
@ -148,21 +135,11 @@ Item {
width: parent.width
height: Math.min(d.maxHeightForList, myAccounts.contentHeight)
delegate: StatusListItem {
implicitWidth: parent.width
objectName: model.name
height: visible ? 64 : 0
title: !!model.name ? model.name : ""
subTitle: LocaleUtils.currencyAmountToLocaleString(model.currencyBalance)
asset.emoji: !!model.emoji ? model.emoji: ""
asset.color: model.color
asset.name: !model.emoji ? "filled-account": ""
asset.letterSize: 14
asset.isLetterIdenticon: !!model.emoji ? true : false
asset.bgColor: Theme.palette.indirectColor1
radius: 0
color: sensor.containsMouse || highlighted ? Theme.palette.baseColor3 : "transparent"
onClicked: contactSelected(model.address, RecipientSelector.Type.Account )
delegate: WalletAccountListItem {
implicitWidth: ListView.view.width
modelData: model
chainShortNames: root.store.getAllNetworksSupportedString()
onClicked: recipientSelected(modelData, TabAddressSelectorView.Type.Account )
}
model: root.store.accounts
@ -194,15 +171,18 @@ Item {
}
delegate: StatusListItem {
id: listItem
property bool isIncoming: root.selectedAccount ? to === root.selectedAccount.address : false
implicitWidth: parent.width
implicitWidth: ListView.view.width
height: visible ? 64 : 0
title: isIncoming ? from : to
title: loading ? Constants.dummyText : isIncoming ? StatusQUtils.Utils.elideText(from,6,4) : StatusQUtils.Utils.elideText(to,6,4)
subTitle: LocaleUtils.getTimeDifference(new Date(parseInt(timestamp) * 1000), new Date())
statusListItemTitle.elide: Text.ElideMiddle
statusListItemTitle.wrapMode: Text.NoWrap
radius: 0
color: sensor.containsMouse || highlighted ? Theme.palette.baseColor3 : "transparent"
color: sensor.containsMouse || highlighted ? Theme.palette.statusListItem.highlightColor : "transparent"
statusListItemComponentsSlot.spacing: 5
loading: loadingTransaction
components: [
StatusIcon {
id: transferIcon
@ -211,18 +191,25 @@ Item {
color: isIncoming ? Style.current.success : Style.current.danger
icon: isIncoming ? "arrow-down" : "arrow-up"
rotation: 45
visible: !listItem.loading
},
StatusBaseText {
StatusTextWithLoadingState {
id: contactsLabel
loading: listItem.loading
font.pixelSize: 15
color: Theme.palette.directColor1
text: LocaleUtils.currencyAmountToLocaleString(value)
customColor: Theme.palette.directColor1
text: loading ? Constants.dummyText : LocaleUtils.currencyAmountToLocaleString(value)
}
]
onClicked: contactSelected(title, RecipientSelector.Type.Address)
onClicked: recipientSelected(model, TabAddressSelectorView.Type.RecentsAddress)
}
model: root.store.walletSectionTransactionsInst.model
model: {
if(root.selectedAccount) {
root.store.prepareTransactionsForAddress(root.selectedAccount.address)
return root.store.getTransactions()
}
}
}
}
}

View File

@ -17,6 +17,7 @@ Item {
property var assets: null
signal tokenSelected(var selectedToken)
signal tokenHovered(var selectedToken, bool hovered)
property var searchTokenSymbolByAddressFn: function (address) {
return ""
}
@ -37,66 +38,55 @@ Item {
id: contentLayout
anchors.fill: parent
spacing: 8
StatusBaseText {
id: label
elide: Text.ElideRight
text: qsTr("Token to send")
font.pixelSize: 13
color: Theme.palette.directColor1
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: headerColumn.height
Layout.preferredHeight: tokenList.height
color: Theme.palette.indirectColor1
radius: 8
Column {
id: headerColumn
StatusListView {
id: tokenList
width: parent.width
Item {
height: 5
height: tokenList.contentHeight
header: SearchBoxWithRightIcon {
width: parent.width
}
StatusInput {
height: 50
width: parent.width
input.showBackground: false
placeholderText: qsTr("Search for token or enter token address")
input.rightComponent: StatusIcon {
icon: "search"
height: 17
color: Theme.palette.baseColor1
}
onTextChanged: Qt.callLater(d.updateSearchText, text)
}
Rectangle {
height: 1
width: parent.width
color: Theme.palette.baseColor3
}
}
}
StatusListView {
id: tokenList
Layout.fillWidth: true
Layout.preferredHeight: 396
model: SortFilterProxyModel {
sourceModel: root.assets
filters: [
ExpressionFilter {
expression: {
var tokenSymbolByAddress = searchTokenSymbolByAddressFn(d.searchString)
tokenList.positionViewAtBeginning()
return visibleForNetwork && (
symbol.startsWith(d.searchString.toUpperCase()) || name.toUpperCase().startsWith(d.searchString.toUpperCase()) || (tokenSymbolByAddress!=="" && symbol.startsWith(tokenSymbolByAddress))
)
model: SortFilterProxyModel {
sourceModel: root.assets
filters: [
ExpressionFilter {
expression: {
var tokenSymbolByAddress = searchTokenSymbolByAddressFn(d.searchString)
tokenList.positionViewAtBeginning()
return visibleForNetwork && (
symbol.startsWith(d.searchString.toUpperCase()) || name.toUpperCase().startsWith(d.searchString.toUpperCase()) || (tokenSymbolByAddress!=="" && symbol.startsWith(tokenSymbolByAddress))
)
}
}
}
]
}
delegate: TokenBalancePerChainDelegate {
width: ListView.view.width
getNetworkIcon: root.getNetworkIcon
onTokenSelected: root.tokenSelected(selectedToken)
]
}
delegate: TokenBalancePerChainDelegate {
width: ListView.view.width
getNetworkIcon: root.getNetworkIcon
onTokenSelected: root.tokenSelected(selectedToken)
onTokenHovered: root.tokenHovered(selectedToken, hovered)
}
}
}
}