feat(@desktop/wallet): Added transaction address component (#10665)

closes #10583
This commit is contained in:
Cuteivist 2023-05-19 11:51:47 +02:00 committed by GitHub
parent 92f77e5fd9
commit f7e75208a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 569 additions and 7 deletions

View File

@ -1,4 +1,4 @@
import NimQml, Tables, strutils, strformat
import NimQml, Tables, strutils, strformat, macros
import ./item
import ../../../shared_models/currency_amount
@ -100,6 +100,6 @@ QtObject:
proc getNameByAddress*(self: Model, address: string): string =
for item in self.items:
if(item.address() == address):
if(cmpIgnoreCase(item.address(), address) == 0):
return item.name()
return ""

View File

@ -97,18 +97,18 @@ QtObject:
proc getNameByAddress*(self: Model, address: string): string =
for item in self.items:
if(item.getAddress() == address):
if(cmpIgnoreCase(item.getAddress(), address) == 0):
return item.getName()
return ""
proc getChainShortNamesForAddress*(self: Model, address: string): string =
for item in self.items:
if(item.getAddress() == address):
if(cmpIgnoreCase(item.getAddress(), address) == 0):
return item.getChainShortNames()
return ""
proc getEnsForAddress*(self: Model, address: string): string =
for item in self.items:
if(item.getAddress() == address):
if(cmpIgnoreCase(item.getAddress(), address) == 0):
return item.getEns()
return ""

View File

@ -281,6 +281,14 @@ ListModel {
title: "CommunityPermissionsRow"
section: "Components"
}
ListElement {
title: "TransactionAddress"
section: "Components"
}
ListElement {
title: "TransactionAddressTile"
section: "Components"
}
ListElement {
title: "StatusImageCropPanel"
section: "Components"

View File

@ -58,7 +58,11 @@ int main(int argc, char *argv[])
qmlRegisterSingletonType<CacheCleaner>(
"Storybook", 1, 0, "CacheCleaner", cleanerFactory);
#ifdef Q_OS_WIN
const QUrl url(QUrl::fromLocalFile(QML_IMPORT_ROOT + QStringLiteral("/main.qml")));
#else
const QUrl url(QML_IMPORT_ROOT + QStringLiteral("/main.qml"));
#endif
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)

View File

@ -0,0 +1,146 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Storybook 1.0
import shared.controls 1.0
import utils 1.0
SplitView {
id: root
orientation: Qt.Vertical
property bool globalUtilsReady: false
property bool mainModuleReady: false
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
Rectangle {
anchors.fill: loader
anchors.margins: -1
color: "transparent"
border.width: 1
border.color: "#20000000"
}
// globalUtilsInst mock
QtObject {
function getCompressedPk(publicKey) { return "zx3sh" + publicKey }
function getColorHashAsJson(publicKey) {
return JSON.stringify([{"segmentLength":1,"colorId":12},{"segmentLength":5,"colorId":18},
{"segmentLength":3,"colorId":25},{"segmentLength":3,"colorId":23},
{"segmentLength":1,"colorId":10},{"segmentLength":3,"colorId":26},
{"segmentLength":2,"colorId":30},{"segmentLength":1,"colorId":18},
{"segmentLength":4,"colorId":28},{"segmentLength":1,"colorId":17},
{"segmentLength":2,"colorId":2}])
}
function isCompressedPubKey(publicKey) { return true }
function getColorId(publicKey) { return Math.floor(Math.random() * 10) }
Component.onCompleted: {
Utils.globalUtilsInst = this
root.globalUtilsReady = true
}
Component.onDestruction: {
root.globalUtilsReady = false
Utils.globalUtilsInst = {}
}
}
// mainModuleInst mock
QtObject {
function getContactDetailsAsJson(publicKey, getVerification) {
return JSON.stringify({
displayName: "ArianaP",
displayIcon: "",
publicKey: publicKey,
name: "",
alias: "",
localNickname: "",
isContact: true
})
}
function isEnsVerified(publicKey) { return false }
Component.onCompleted: {
Utils.mainModuleInst = this
root.mainModuleReady = true
}
Component.onDestruction: {
root.mainModuleReady = false
Utils.mainModuleInst = {}
}
}
QtObject {
id: d
property string addressPrefixString: "eth:opt:arb:"
property string addressName: "Ariana Pearlona"
property bool isContact: true
property bool showPrefix: true
property bool showAddressName: true
}
Loader {
id: loader
anchors.centerIn: parent
width: 400
active: root.globalUtilsReady && root.mainModuleReady
sourceComponent: TransactionAddress {
width: parent.width
address: (d.showPrefix ? d.addressPrefixString : "") + "0x29D7d1dd5B6f9C864d9db560D72a247c178aE86B"
addressName: d.showAddressName ? d.addressName : ""
contactPubKey: d.isContact ? "zQ3shWU7xpM5YoG19KP5JDRiSs1AdWtjpnrWEerMkxfQnYo7x" : ""
}
}
}
LogsAndControlsPanel {
SplitView.minimumHeight: 100
SplitView.preferredHeight: 150
SplitView.fillWidth: true
ColumnLayout {
spacing: 5
CheckBox {
text: "is contact"
checked: d.isContact
onCheckedChanged: d.isContact = checked
}
Label {
text: "Address prefix:"
}
RowLayout {
TextField {
text: d.addressPrefixString
onTextChanged: d.addressPrefixString = text
}
CheckBox {
text: "Show"
checked: d.showPrefix
onCheckedChanged: d.showPrefix = checked
}
}
Label {
text: "Address name:"
}
RowLayout {
TextField {
text: d.addressName
onTextChanged: d.addressName = text
}
CheckBox {
text: "use"
checked: d.showAddressName
onCheckedChanged: d.showAddressName = checked
}
}
}
}
}

View File

@ -0,0 +1,122 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Storybook 1.0
import shared.controls 1.0
import utils 1.0
SplitView {
id: root
orientation: Qt.Vertical
property bool globalUtilsReady: false
property bool mainModuleReady: false
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
// globalUtilsInst mock
QtObject {
function getCompressedPk(publicKey) { return "zx3sh" + publicKey }
function getColorHashAsJson(publicKey) {
return JSON.stringify([{"segmentLength":1,"colorId":12},{"segmentLength":5,"colorId":18},
{"segmentLength":3,"colorId":25},{"segmentLength":3,"colorId":23},
{"segmentLength":1,"colorId":10},{"segmentLength":3,"colorId":26},
{"segmentLength":2,"colorId":30},{"segmentLength":1,"colorId":18},
{"segmentLength":4,"colorId":28},{"segmentLength":1,"colorId":17},
{"segmentLength":2,"colorId":2}])
}
function isCompressedPubKey(publicKey) { return true }
function getColorId(publicKey) { return Math.floor(Math.random() * 10) }
Component.onCompleted: {
Utils.globalUtilsInst = this
root.globalUtilsReady = true
}
Component.onDestruction: {
root.globalUtilsReady = false
Utils.globalUtilsInst = {}
}
}
// mainModuleInst mock
QtObject {
function getContactDetailsAsJson(publicKey, getVerification) {
return JSON.stringify({
displayName: "ArianaP",
displayIcon: "",
publicKey: publicKey,
name: "",
alias: "",
localNickname: "",
isContact: true
})
}
function isEnsVerified(publicKey) { return false }
Component.onCompleted: {
Utils.mainModuleInst = this
root.mainModuleReady = true
}
Component.onDestruction: {
root.mainModuleReady = false
Utils.mainModuleInst = {}
}
}
QtObject {
id: mockupRootStore
function getNameForAddress(address) {
const nameList = [ 'Alice', 'Bob', 'Charlie', 'Dave', 'Eve','Frank', 'Grace', 'Hank', 'Iris', 'Jack' ];
const randomIndex = Math.floor(Math.random() * nameList.length);
return nameList[randomIndex];
}
}
Loader {
id: loader
anchors.centerIn: parent
width: 500
active: root.globalUtilsReady && root.mainModuleReady
sourceComponent: Column {
id: content
spacing: 0
TransactionAddressTile {
title: "From"
width: parent.width
rootStore: mockupRootStore
addresses: [
"0x29D7d1dd5B6f9C864d9db560D72a247c178aE86B"
]
}
TransactionAddressTile {
title: "To"
width: parent.width
rootStore: mockupRootStore
addresses: [
"0x29D7d1dd5B6f9C864d9db560D72a247c178aE86B",
"eth:arb:opt:0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35",
"0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35",
"eth:opt:arb:0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35",
"eth:opt:arb:0x29D7d1dd5B6f9C864d9db560D72a247c178aE86B",
"0x29D7d1dd5B6f9C864d9db560D72a247c178aE86B",
"eth:opt:arb:0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35"
]
}
}
}
}
LogsAndControlsPanel {
SplitView.minimumHeight: 100
SplitView.preferredHeight: 150
SplitView.fillWidth: true
}
}

View File

@ -134,6 +134,7 @@ Rectangle {
MouseArea {
id: sensor
z: 1 // Gives ability to hide siblings under the MouseArea
anchors.fill: parent
cursorShape: containsMouse ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton

View File

@ -0,0 +1,164 @@
import QtQuick 2.15
import QtQuick.Layouts 1.13
import StatusQ.Core 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import AppLayouts.Wallet 1.0
import utils 1.0
/*!
\qmltype TransactionAddress
\inherits Item
\inqmlmodule shared.controls
\since shared.controls 1.0
\brief It displays transaction address in depending on amount of data provided.
The \c TransactionAddress should be used to display transaction activity data.
\qml
TransactionAddress {
address: "0x29D7d1dd5B6f9C864d9db560D72a247c208aE86B"
addressName: "Test Name"
contactPubKey: "zQ3shWU7xpM5YoG19KP5JDRiSs1AdWtjpnrWEerMkxfQnYo8x"
}
\endqml
*/
Item {
id: root
/*!
\qmlproperty string TransactionAddress::address
This property holds wallet address.
*/
property string address
/*!
\qmlproperty string TransactionAddress::addressName
This property holds wallet address name.
If contact public key is not provided this name will be used for display.
*/
property string addressName
/*!
\qmlproperty string TransactionAddress::contactPubKey
This property hold contact public key used to identify contact wallet.
Contact icon will be displayed. Display name take place of \l{addressName}.
*/
property string contactPubKey
/* /internal Property hold reference to contacts store to refresh contact data on any change. */
property var contactsStore
/*!
\qmlproperty \l{StatusAssetSettings} TransactionAddress::asset
Property holds asset settings for contact icon.
*/
property StatusAssetSettings asset: StatusAssetSettings {
width: 36
height: 36
color: Utils.colorForPubkey(root.contactPubKey)
name: isImage ? d.contactData.displayIcon : nameText.text
isImage: d.isContact && d.contactData.displayIcon.includes("data")
isLetterIdenticon: d.isContact && !isImage
charactersLen: 2
}
implicitHeight: Math.max(identicon.height, contentColumn.height) + 12
QtObject {
id: d
readonly property var prefixAndAddress: Utils.splitToChainPrefixAndAddress(root.address)
readonly property bool isContactPubKeyValid: !!root.contactPubKey
readonly property bool isContact: isContactPubKeyValid && contactData.isContact
property var contactData: Utils.getContactDetailsAsJson(root.contactPubKey)
function refreshContactData() {
d.contactData = Utils.getContactDetailsAsJson(root.contactPubKey)
}
readonly property Connections myContactsModelConnection: Connections {
target: root.contactsStore.myContactsModel ?? null
function onItemChanged(pubKey) {
if (pubKey === root.contactPubKey)
d.refreshContactData()
}
}
readonly property Connections receivedContactsReqModelConnection: Connections {
target: root.contactsStore.receivedContactRequestsModel ?? null
function onItemChanged(pubKey) {
if (pubKey === root.contactPubKey)
d.refreshContactData()
}
}
readonly property Connections sentContactReqModelConnection: Connections {
target: root.contactsStore.sentContactRequestsModel ?? null
function onItemChanged(pubKey) {
if (pubKey === root.contactPubKey)
d.refreshContactData()
}
}
}
RowLayout {
id: contentRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
StatusSmartIdenticon {
id: identicon
Layout.alignment: Qt.AlignTop
asset: root.asset
name: nameText.text
ringSettings {
ringSpecModel: d.isContact ? Utils.getColorHashAsJson(d.contactData.publicKey) : []
ringPxSize: asset.width / 24
}
visible: d.isContact
}
ColumnLayout {
id: contentColumn
Layout.fillWidth: true
spacing: 0
StatusBaseText {
id: nameText
Layout.fillWidth: true
font.pixelSize: 15
color: Theme.palette.directColor1
text: {
let name = ""
if (d.isContact) {
name = ProfileUtils.displayName(d.contactData.localNickname, d.contactData.name, d.contactData.displayName, d.contactData.alias)
}
return name || root.addressName
}
visible: !!text
elide: Text.ElideRight
}
StatusBaseText {
Layout.fillWidth: true
font.pixelSize: 15
color: Theme.palette.directColor1
wrapMode: Text.WrapAnywhere
text: {
if(!!root.address == false)
return ""
if (d.prefixAndAddress.prefix.length > 0) {
return WalletUtils.colorizedChainPrefix(d.prefixAndAddress.prefix) + d.prefixAndAddress.address
} else {
return d.prefixAndAddress.address
}
}
visible: !!root.address
elide: Text.ElideRight
}
}
}
}

View File

@ -0,0 +1,115 @@
import QtQuick 2.13
import QtQuick.Layouts 1.15
import QtQuick.Shapes 1.15
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
/*!
\qmltype TransactionAddressTile
\inherits StatusListItem
\inqmlmodule shared.controls
\since shared.controls 1.0
\brief It displays list of addresses for wallet activity.
The \c TransactionAddressTile can display list of addresses formatted in specific way.
\qml
TransactionAddressTile {
title: qsTr("From")
width: parent.width
rootStore: WalletStores.RootStore
roundedCornersBottom: false
addresses: [
"eth:arb:opt:0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35",
"0x4de3f6278C0DdFd3F29df9DcD979038F5c7bbc35",
]
}
\endqml
*/
StatusListItem {
id: root
/*!
\qmlproperty var TransactionAddressTile::addresses
This property holds list or model of addresses to display in the tile.
*/
property var addresses: []
/*!
\qmlproperty var TransactionAddressTile::rootStore
This property holds rootStore object used to retrive data for each address.
*/
property var rootStore
/*!
\qmlproperty int TransactionAddressTile::topPadding
This property holds spacing between top and content item in tile.
*/
property int topPadding: 12
/*!
\qmlproperty int TransactionAddressTile::bottomPadding
This property holds spacing between bottom and content item in tile.
*/
property int bottomPadding: 12
/* /internal Property hold reference to contacts store to refresh contact data on any change. */
property var contactsStore
signal showContextMenu()
leftPadding: 12
rightPadding: 12
radius: 0
implicitHeight: transactionColumn.height + statusListItemTitleArea.height + root.topPadding + root.bottomPadding
statusListItemTitle.customColor: Theme.palette.directColor5
statusListItemTitleArea.anchors {
top: statusListItemTitleArea.parent.top
topMargin: root.topPadding
right: statusListItemTitleArea.parent.right
verticalCenter: undefined
}
components: [
StatusRoundButton {
id: button
width: 32
height: 32
icon.color: hovered ? Theme.palette.directColor1 : Theme.palette.baseColor1
icon.name: "more"
type: StatusRoundButton.Type.Quinary
radius: 8
visible: root.sensor.containsMouse
onClicked: root.showContextMenu()
}
]
Column {
id: transactionColumn
anchors {
left: parent.left
leftMargin: root.leftPadding
right: parent.right
rightMargin: button.width + root.rightPadding * 2
bottom: parent.bottom
bottomMargin: root.bottomPadding
}
height: childrenRect.height
spacing: 4
// Moving it under sensor, because Rich Text steals hovering
z: root.sensor.z - 1
Repeater {
model: root.addresses
delegate: TransactionAddress {
width: parent.width
address: modelData
addressName: !!root.rootStore ? root.rootStore.getNameForAddress(address) : ""
contactsStore: root.contactsStore
}
}
}
}

View File

@ -309,7 +309,7 @@ StatusListItem {
}
switch(root.transactionType) {
case TransactionDelegate.TransactionType.Receive:
return qsTr("%1 from %2 via %3").arg(transactionValue).arg(toAddress).arg(networkName)
return qsTr("%1 from %2 via %3").arg(transactionValue).arg(fromAddress).arg(networkName)
case TransactionDelegate.TransactionType.Buy:
case TransactionDelegate.TransactionType.Sell:
return qsTr("%1 on %2 via %3").arg(transactionValue).arg(toAddress).arg(networkName)

View File

@ -24,6 +24,8 @@ StyledTextEdit 1.0 StyledTextEdit.qml
StyledTextField 1.0 StyledTextField.qml
Timer 1.0 Timer.qml
TransactionDelegate 1.0 TransactionDelegate.qml
TransactionAddress 1.0 TransactionAddress.qml
TransactionAddressTile 1.0 TransactionAddressTile.qml
TransactionFormGroup 1.0 TransactionFormGroup.qml
EmojiHash 1.0 EmojiHash.qml
InformationTile 1.0 InformationTile.qml