544 lines
22 KiB
QML
544 lines
22 KiB
QML
import QtQuick 2.13
|
|
import QtQuick.Controls 2.13
|
|
import QtQml.Models 2.14
|
|
import QtQuick.Layouts 1.14
|
|
|
|
import utils 1.0
|
|
import shared.controls 1.0
|
|
import shared.panels 1.0
|
|
|
|
import StatusQ.Core 0.1
|
|
import StatusQ.Core.Theme 0.1
|
|
import StatusQ.Controls 0.1
|
|
import StatusQ.Controls.Validators 0.1
|
|
import StatusQ.Popups 0.1
|
|
import StatusQ.Components 0.1
|
|
|
|
import SortFilterProxyModel 0.2
|
|
|
|
import AppLayouts.stores 1.0
|
|
|
|
import "../stores"
|
|
import "../controls"
|
|
import ".."
|
|
|
|
StatusModal {
|
|
id: root
|
|
|
|
property var allNetworks
|
|
|
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
|
|
width: 477
|
|
|
|
headerSettings.title: d.editMode? qsTr("Edit saved address") : qsTr("Add new saved address")
|
|
headerSettings.subTitle: d.editMode? d.name : ""
|
|
|
|
onClosed: {
|
|
root.close()
|
|
}
|
|
|
|
function initWithParams(params = {}) {
|
|
d.storedName = params.name?? ""
|
|
d.storedColorId = params.colorId?? ""
|
|
d.storedChainShortNames = params.chainShortNames?? ""
|
|
|
|
d.editMode = params.edit?? false
|
|
d.addAddress = params.addAddress?? false
|
|
d.name = d.storedName
|
|
nameInput.input.dirty = false
|
|
d.address = params.address?? Constants.zeroAddress
|
|
d.ens = params.ens?? ""
|
|
d.colorId = d.storedColorId
|
|
d.chainShortNames = d.storedChainShortNames
|
|
|
|
d.initialized = true
|
|
|
|
if (d.colorId === "") {
|
|
colorSelection.selectedColorIndex = Math.floor(Math.random() * colorSelection.model.length)
|
|
}
|
|
else {
|
|
let ind = Utils.getColorIndexForId(d.colorId)
|
|
colorSelection.selectedColorIndex = ind
|
|
}
|
|
|
|
if (!!d.ens)
|
|
addressInput.setPlainText(d.ens)
|
|
else
|
|
addressInput.setPlainText("%1%2"
|
|
.arg(d.chainShortNames)
|
|
.arg(d.address == Constants.zeroAddress? "" : d.address))
|
|
|
|
nameInput.input.edit.forceActiveFocus(Qt.MouseFocusReason)
|
|
}
|
|
|
|
QtObject {
|
|
id: d
|
|
|
|
readonly property int componentWidth: 445
|
|
|
|
property bool editMode: false
|
|
property bool addAddress: false
|
|
property alias name: nameInput.text
|
|
property string address: Constants.zeroAddress // Setting as zero address since we don't have the address yet
|
|
property string ens: ""
|
|
property string colorId: ""
|
|
property string chainShortNames: ""
|
|
|
|
property string storedName: ""
|
|
property string storedColorId: ""
|
|
property string storedChainShortNames: ""
|
|
|
|
property bool chainShortNamesDirty: false
|
|
readonly property bool valid: addressInput.valid && nameInput.valid
|
|
readonly property bool dirty: nameInput.input.dirty && (!d.editMode || d.storedName !== d.name)
|
|
|| chainShortNamesDirty && (!d.editMode || d.storedChainShortNames !== d.chainShortNames)
|
|
|| d.colorId.toUpperCase() !== d.storedColorId.toUpperCase()
|
|
|
|
|
|
readonly property var chainPrefixRegexPattern: /[^:]+\:?|:/g
|
|
readonly property bool addressInputIsENS: !!d.ens
|
|
|
|
property bool addressAlreadyAddedToWallet: false
|
|
function checkIfAddressIsAlreadyAdddedToWallet(address) {
|
|
let name = RootStore.getNameForWalletAddress(address)
|
|
d.addressAlreadyAddedToWallet = !!name
|
|
}
|
|
|
|
property bool addressAlreadyAddedToSavedAddresses: false
|
|
function checkIfAddressIsAlreadyAdddedToSavedAddresses(address) {
|
|
let details = RootStore.getSavedAddress(address)
|
|
d.addressAlreadyAddedToSavedAddresses = !!details.address
|
|
}
|
|
|
|
/// Ensures that the \c root.address and \c root.chainShortNames are not reset when the initial text is set
|
|
property bool initialized: false
|
|
|
|
function getPrefixArrayWithColumns(prefixStr) {
|
|
return prefixStr.match(d.chainPrefixRegexPattern)
|
|
}
|
|
|
|
function resetAddressValues() {
|
|
d.ens = ""
|
|
d.address = Constants.zeroAddress
|
|
d.chainShortNames = ""
|
|
allNetworksModelCopy.setEnabledNetworks([])
|
|
}
|
|
|
|
function submit(event) {
|
|
if (!d.valid
|
|
|| !d.dirty
|
|
|| event !== undefined && event.key !== Qt.Key_Return && event.key !== Qt.Key_Enter)
|
|
return
|
|
|
|
RootStore.createOrUpdateSavedAddress(d.name, d.address, d.ens, d.colorId, d.chainShortNames)
|
|
root.close()
|
|
}
|
|
|
|
property bool resolvingEnsName: false
|
|
readonly property string uuid: Utils.uuid()
|
|
readonly property var validateEnsAsync: Backpressure.debounce(root, 500, function (value) {
|
|
var name = value.startsWith("@") ? value.substring(1) : value
|
|
mainModule.resolveENS(name, d.uuid)
|
|
});
|
|
}
|
|
|
|
StatusScrollView {
|
|
id: scrollView
|
|
|
|
anchors.fill: parent
|
|
padding: 0
|
|
contentWidth: availableWidth
|
|
|
|
Column {
|
|
width: scrollView.availableWidth
|
|
height: childrenRect.height
|
|
topPadding: 24 // (16 + 8 for Name, until we add it to the StatusInput component)
|
|
bottomPadding: 28
|
|
|
|
spacing: Style.current.xlPadding
|
|
|
|
StatusInput {
|
|
id: nameInput
|
|
implicitWidth: d.componentWidth
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
charLimit: 24
|
|
input.edit.objectName: "savedAddressNameInput"
|
|
placeholderText: qsTr("Address name")
|
|
label: qsTr("Name")
|
|
validators: [
|
|
StatusMinLengthValidator {
|
|
minLength: 1
|
|
errorMessage: qsTr("Please name your saved address")
|
|
},
|
|
StatusValidator {
|
|
property bool isEmoji: false
|
|
|
|
name: "check-for-no-emojis"
|
|
validate: (value) => {
|
|
if (!value) {
|
|
return true
|
|
}
|
|
|
|
isEmoji = Constants.regularExpressions.emoji.test(value)
|
|
if (isEmoji){
|
|
return false
|
|
}
|
|
|
|
return Constants.regularExpressions.alphanumericalExpanded1.test(value)
|
|
}
|
|
errorMessage: isEmoji?
|
|
Constants.errorMessages.emojRegExp
|
|
: Constants.errorMessages.alphanumericalExpanded1RegExp
|
|
},
|
|
StatusValidator {
|
|
name: "check-saved-address-existence"
|
|
validate: (value) => {
|
|
return !RootStore.savedAddressNameExists(value)
|
|
|| d.editMode && d.storedName == value
|
|
}
|
|
errorMessage: qsTr("Name already in use")
|
|
}
|
|
]
|
|
input.clearable: true
|
|
input.rightPadding: 16
|
|
|
|
onKeyPressed: {
|
|
d.submit(event)
|
|
}
|
|
}
|
|
|
|
StatusInput {
|
|
id: addressInput
|
|
implicitWidth: d.componentWidth
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
label: qsTr("Address")
|
|
objectName: "savedAddressAddressInput"
|
|
input.edit.objectName: "savedAddressAddressInputEdit"
|
|
placeholderText: qsTr("Ethereum address")
|
|
maximumHeight: 66
|
|
input.implicitHeight: Math.min(Math.max(input.edit.contentHeight + topPadding + bottomPadding, minimumHeight), maximumHeight) // setting height instead does not work
|
|
enabled: !(d.editMode || d.addAddress)
|
|
validators: [
|
|
StatusMinLengthValidator {
|
|
minLength: 1
|
|
errorMessage: qsTr("Please enter an ethereum address")
|
|
},
|
|
StatusValidator {
|
|
errorMessage: d.addressAlreadyAddedToWallet? qsTr("This address is already added to Wallet") :
|
|
d.addressAlreadyAddedToSavedAddresses? qsTr("This address is already saved") : qsTr("Ethereum address invalid")
|
|
validate: function (value) {
|
|
if (value !== Constants.zeroAddress) {
|
|
if (Utils.isValidEns(value)) {
|
|
return true
|
|
}
|
|
if (Utils.isValidAddressWithChainPrefix(value)) {
|
|
if (d.editMode) {
|
|
return true
|
|
}
|
|
const prefixAndAddress = Utils.splitToChainPrefixAndAddress(value)
|
|
d.checkIfAddressIsAlreadyAdddedToWallet(prefixAndAddress.address)
|
|
if (d.addressAlreadyAddedToWallet) {
|
|
return false
|
|
}
|
|
d.checkIfAddressIsAlreadyAdddedToSavedAddresses(prefixAndAddress.address)
|
|
return !d.addressAlreadyAddedToSavedAddresses
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
]
|
|
asyncValidators: [
|
|
StatusAsyncValidator {
|
|
id: resolvingEnsName
|
|
name: "resolving-ens-name"
|
|
errorMessage: d.addressAlreadyAddedToWallet? qsTr("This address is already added to Wallet") :
|
|
d.addressAlreadyAddedToSavedAddresses? qsTr("This address is already saved") : qsTr("Ethereum address invalid")
|
|
asyncOperation: (value) => {
|
|
if (!Utils.isValidEns(value)) {
|
|
resolvingEnsName.asyncComplete("not-ens")
|
|
return
|
|
}
|
|
d.resolvingEnsName = true
|
|
d.validateEnsAsync(value)
|
|
}
|
|
validate: (value) => {
|
|
if (d.editMode || value === "not-ens") {
|
|
return true
|
|
}
|
|
if (!!value) {
|
|
d.checkIfAddressIsAlreadyAdddedToWallet(prefixAndAddress.address)
|
|
if (d.addressAlreadyAddedToWallet) {
|
|
return false
|
|
}
|
|
d.checkIfAddressIsAlreadyAdddedToSavedAddresses(value)
|
|
return !d.addressAlreadyAddedToSavedAddresses
|
|
}
|
|
return false
|
|
}
|
|
|
|
Connections {
|
|
target: mainModule
|
|
function onResolvedENS(resolvedPubKey: string, resolvedAddress: string, uuid: string) {
|
|
if (uuid !== d.uuid) {
|
|
return
|
|
}
|
|
d.resolvingEnsName = false
|
|
d.address = resolvedAddress
|
|
resolvingEnsName.asyncComplete(resolvedAddress)
|
|
}
|
|
}
|
|
}
|
|
]
|
|
|
|
input.edit.textFormat: TextEdit.RichText
|
|
input.asset.name: addressInput.valid && !d.editMode ? "checkbox" : ""
|
|
input.asset.color: enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
|
|
input.rightPadding: 16
|
|
input.leftIcon: false
|
|
|
|
multiline: true
|
|
|
|
property string plainText: input.edit.getText(0, text.length)
|
|
|
|
onTextChanged: {
|
|
if (skipTextUpdate || !d.initialized)
|
|
return
|
|
|
|
d.addressAlreadyAddedToSavedAddresses = false
|
|
plainText = input.edit.getText(0, text.length)
|
|
|
|
if (input.edit.previousText != plainText) {
|
|
let newText = plainText
|
|
const prefixAndAddress = Utils.splitToChainPrefixAndAddress(plainText)
|
|
|
|
if (!Utils.isLikelyEnsName(plainText)) {
|
|
newText = WalletUtils.colorizedChainPrefix(prefixAndAddress.prefix) +
|
|
prefixAndAddress.address
|
|
}
|
|
|
|
setRichText(newText)
|
|
|
|
// Reset
|
|
if (plainText.length == 0) {
|
|
d.resetAddressValues()
|
|
return
|
|
}
|
|
|
|
// Update root values
|
|
if (Utils.isLikelyEnsName(plainText)) {
|
|
d.resolvingEnsName = true
|
|
d.ens = plainText
|
|
d.address = Constants.zeroAddress
|
|
d.chainShortNames = ""
|
|
}
|
|
else {
|
|
d.resolvingEnsName = false
|
|
d.ens = ""
|
|
d.address = prefixAndAddress.address
|
|
d.chainShortNames = prefixAndAddress.prefix
|
|
|
|
let prefixArrWithColumn = d.getPrefixArrayWithColumns(prefixAndAddress.prefix)
|
|
if (!prefixArrWithColumn)
|
|
prefixArrWithColumn = []
|
|
|
|
allNetworksModelCopy.setEnabledNetworks(prefixArrWithColumn)
|
|
}
|
|
}
|
|
}
|
|
|
|
onKeyPressed: {
|
|
d.submit(event)
|
|
}
|
|
|
|
property bool skipTextUpdate: false
|
|
|
|
function setPlainText(newText) {
|
|
text = newText
|
|
}
|
|
|
|
function setRichText(val) {
|
|
skipTextUpdate = true
|
|
input.edit.previousText = plainText
|
|
const curPos = input.cursorPosition
|
|
setPlainText(val)
|
|
input.cursorPosition = curPos
|
|
skipTextUpdate = false
|
|
}
|
|
|
|
function getUnknownPrefixes(prefixes) {
|
|
let unknownPrefixes = prefixes.filter(e => {
|
|
for (let i = 0; i < allNetworksModelCopy.count; i++) {
|
|
if (e == allNetworksModelCopy.get(i).shortName)
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
return unknownPrefixes
|
|
}
|
|
|
|
// Add all chain short names from model, while keeping existing
|
|
function syncChainPrefixWithModel(prefix, model) {
|
|
let prefixes = prefix.split(":").filter(Boolean)
|
|
let prefixStr = ""
|
|
|
|
// Keep unknown prefixes from user input, the rest must be taken
|
|
// from the model
|
|
for (let i = 0; i < model.count; i++) {
|
|
const item = model.get(i)
|
|
prefixStr += item.shortName + ":"
|
|
// Remove all added prefixes from initial array
|
|
prefixes = prefixes.filter(e => e !== item.shortName)
|
|
}
|
|
|
|
const unknownPrefixes = getUnknownPrefixes(prefixes)
|
|
if (unknownPrefixes.length > 0) {
|
|
prefixStr += unknownPrefixes.join(":") + ":"
|
|
}
|
|
|
|
return prefixStr
|
|
}
|
|
}
|
|
|
|
StatusColorSelectorGrid {
|
|
id: colorSelection
|
|
objectName: "addSavedAddressColor"
|
|
width: d.componentWidth
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
model: Theme.palette.customisationColorsArray
|
|
title.color: Theme.palette.directColor1
|
|
title.font.pixelSize: Constants.addAccountPopup.labelFontSize1
|
|
title.text: qsTr("Colour")
|
|
selectedColorIndex: -1
|
|
|
|
onSelectedColorChanged: {
|
|
d.colorId = Utils.getIdForColor(selectedColor)
|
|
}
|
|
}
|
|
|
|
StatusNetworkSelector {
|
|
id: networkSelector
|
|
objectName: "addSavedAddressNetworkSelector"
|
|
title: "Network preference"
|
|
implicitWidth: d.componentWidth
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
|
|
enabled: addressInput.valid && !d.addressInputIsENS
|
|
defaultItemText: "Add networks"
|
|
defaultItemImageSource: "add"
|
|
rightButtonVisible: true
|
|
|
|
property bool modelUpdateBlocked: false
|
|
|
|
function blockModelUpdate(value) {
|
|
modelUpdateBlocked = value
|
|
}
|
|
|
|
itemsModel: SortFilterProxyModel {
|
|
sourceModel: allNetworksModelCopy
|
|
filters: ValueFilter {
|
|
roleName: "isEnabled"
|
|
value: true
|
|
}
|
|
|
|
onCountChanged: {
|
|
if (!networkSelector.modelUpdateBlocked && d.initialized) {
|
|
// Initially source model is empty, filter proxy is also empty, but does
|
|
// extra work and mistakenly overwrites d.chainShortNames property
|
|
if (sourceModel.count != 0) {
|
|
const prefixAndAddress = Utils.splitToChainPrefixAndAddress(addressInput.plainText)
|
|
const syncedPrefix = addressInput.syncChainPrefixWithModel(prefixAndAddress.prefix, this)
|
|
d.chainShortNames = syncedPrefix
|
|
addressInput.setPlainText(syncedPrefix + prefixAndAddress.address)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
addButton.highlighted: networkSelectPopup.visible
|
|
addButton.onClicked: {
|
|
networkSelectPopup.openAtPosition(addButton.x, networkSelector.y + addButton.height + Style.current.xlPadding)
|
|
}
|
|
|
|
onItemClicked: function (item, index, mouse) {
|
|
// Append first item
|
|
if (index === 0 && defaultItem.visible)
|
|
networkSelectPopup.openAtPosition(defaultItem.x, networkSelector.y + defaultItem.height + Style.current.xlPadding)
|
|
}
|
|
|
|
onItemRightButtonClicked: function (item, index, mouse) {
|
|
item.modelRef.isEnabled = !item.modelRef.isEnabled
|
|
d.chainShortNamesDirty = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NetworkSelectPopup {
|
|
id: networkSelectPopup
|
|
|
|
layer1Networks: SortFilterProxyModel {
|
|
sourceModel: allNetworksModelCopy
|
|
filters: ValueFilter {
|
|
roleName: "layer"
|
|
value: 1
|
|
}
|
|
}
|
|
layer2Networks: SortFilterProxyModel {
|
|
sourceModel: allNetworksModelCopy
|
|
filters: ValueFilter {
|
|
roleName: "layer"
|
|
value: 2
|
|
}
|
|
}
|
|
|
|
onToggleNetwork: (network) => {
|
|
network.isEnabled = !network.isEnabled
|
|
d.chainShortNamesDirty = true
|
|
}
|
|
|
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
|
|
function openAtPosition(xPos, yPos) {
|
|
x = xPos
|
|
y = yPos
|
|
open()
|
|
}
|
|
|
|
modal: true
|
|
dim: false
|
|
}
|
|
|
|
rightButtons: [
|
|
StatusButton {
|
|
text: d.editMode? qsTr("Save") : qsTr("Add address")
|
|
enabled: d.valid && d.dirty && !d.resolvingEnsName
|
|
loading: d.resolvingEnsName
|
|
onClicked: {
|
|
d.submit()
|
|
}
|
|
objectName: "addSavedAddress"
|
|
}
|
|
]
|
|
|
|
CloneModel {
|
|
id: allNetworksModelCopy
|
|
|
|
sourceModel: root.allNetworks
|
|
roles: ["layer", "chainId", "chainColor", "chainName","shortName", "iconUrl"]
|
|
rolesOverride: [{ role: "isEnabled", transform: (modelData) => Boolean(false) }]
|
|
|
|
function setEnabledNetworks(prefixArr) {
|
|
networkSelector.blockModelUpdate(true)
|
|
for (let i = 0; i < count; i++) {
|
|
// Add only those chainShortNames to the model, that have column ":" at the end, making it a valid chain prefix
|
|
setProperty(i, "isEnabled", prefixArr.includes(get(i).shortName + ":"))
|
|
}
|
|
networkSelector.blockModelUpdate(false)
|
|
}
|
|
}
|
|
}
|