2022-03-01 10:14:13 +00:00
import QtQuick 2.13
import QtQuick . Controls 2.13
2022-07-25 15:16:22 +00:00
import QtQml . Models 2.14
2023-02-20 10:57:45 +00:00
import QtQuick . Layouts 1.14
2022-03-01 10:14:13 +00:00
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
2022-07-25 15:16:22 +00:00
import StatusQ . Popups . Dialog 0.1
2023-02-20 10:57:45 +00:00
import StatusQ . Components 0.1
2022-03-01 10:14:13 +00:00
2023-02-20 10:57:45 +00:00
import SortFilterProxyModel 0.2
2023-04-05 11:10:44 +00:00
import AppLayouts . stores 1.0
2022-03-01 10:14:13 +00:00
import "../stores"
2023-04-05 11:10:44 +00:00
import "../controls"
2023-02-20 10:57:45 +00:00
import ".."
2022-03-01 10:14:13 +00:00
2022-07-25 15:16:22 +00:00
StatusDialog {
2022-03-01 10:14:13 +00:00
id: root
2023-12-29 13:10:55 +00:00
property var allNetworks
2023-03-08 10:57:15 +00:00
2023-12-29 13:10:55 +00:00
closePolicy: Popup . CloseOnEscape | Popup . CloseOnPressOutside
2023-02-20 10:57:45 +00:00
2023-12-29 13:10:55 +00:00
width: 477
topPadding: 24 // (16 + 8 for Name, until we add it to the StatusInput component)
bottomPadding: 28
2022-03-01 10:14:13 +00:00
2023-12-29 13:10:55 +00:00
header: StatusDialogHeader {
headline.title: d . editMode ? qsTr ( "Edit saved address" ) : qsTr ( "Add new saved address" )
headline.subtitle: d . editMode ? d.name : ""
actions.closeButton.onClicked: root . close ( )
}
2023-12-26 10:19:41 +00:00
2023-12-29 13:10:55 +00:00
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 )
2023-12-26 10:19:41 +00:00
}
2022-03-01 10:14:13 +00:00
QtObject {
2022-08-08 18:21:56 +00:00
id: d
2023-12-29 13:10:55 +00:00
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: ""
2023-02-20 10:57:45 +00:00
property bool chainShortNamesDirty: false
2023-12-29 13:10:55 +00:00
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 ( )
2023-02-20 10:57:45 +00:00
readonly property var chainPrefixRegexPattern: /[^:]+\:?|:/g
2023-12-29 13:10:55 +00:00
readonly property bool addressInputIsENS: ! ! d . ens
2023-02-20 10:57:45 +00:00
2023-04-05 11:10:44 +00:00
/// Ensures that the \c root.address and \c root.chainShortNames are not reset when the initial text is set
property bool initialized: false
2023-02-20 10:57:45 +00:00
function getPrefixArrayWithColumns ( prefixStr ) {
return prefixStr . match ( d . chainPrefixRegexPattern )
}
function resetAddressValues ( ) {
2023-12-29 13:10:55 +00:00
d . ens = ""
d . address = Constants . zeroAddress
d . chainShortNames = ""
2023-02-20 10:57:45 +00:00
allNetworksModelCopy . setEnabledNetworks ( [ ] )
}
2023-12-29 15:40:14 +00:00
function submit ( event ) {
if ( ! d . valid
|| ! d . dirty
|| event !== undefined && event . key !== Qt . Key_Return && event . key !== Qt . Key_Enter )
return
2024-01-09 13:49:07 +00:00
RootStore . createOrUpdateSavedAddress ( d . name , d . address , d . ens , d . colorId , d . chainShortNames )
2023-12-29 15:40:14 +00:00
root . close ( )
}
2022-03-01 10:14:13 +00:00
}
2022-07-25 15:16:22 +00:00
Column {
width: parent . width
2022-03-01 10:14:13 +00:00
height: childrenRect . height
2023-12-29 13:10:55 +00:00
spacing: Style . current . xlPadding
2022-03-01 10:14:13 +00:00
StatusInput {
id: nameInput
2022-07-25 15:16:22 +00:00
implicitWidth: parent . width
2023-12-29 13:10:55 +00:00
charLimit: 24
2022-08-03 10:08:46 +00:00
input.edit.objectName: "savedAddressNameInput"
2023-03-09 11:35:21 +00:00
placeholderText: qsTr ( "Address name" )
2022-03-01 10:14:13 +00:00
label: qsTr ( "Name" )
validators: [
StatusMinLengthValidator {
minLength: 1
2023-12-29 13:10:55 +00:00
errorMessage: qsTr ( "Please name your saved address" )
} ,
StatusValidator {
name: "check-for-no-emojis"
validate: ( value ) = > {
return ! Constants . regularExpressions . emoji . test ( value )
}
errorMessage: Constants . errorMessages . emojRegExp
2022-03-24 15:31:38 +00:00
} ,
StatusRegularExpressionValidator {
2023-12-29 13:10:55 +00:00
regularExpression: Constants . regularExpressions . alphanumericalExpanded1
errorMessage: 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" )
2022-03-01 10:14:13 +00:00
}
]
2023-02-20 10:57:45 +00:00
input.clearable: true
input.rightPadding: 16
2023-12-29 15:40:14 +00:00
onKeyPressed: {
d . submit ( event )
}
2022-03-01 10:14:13 +00:00
}
2023-02-20 10:57:45 +00:00
StatusInput {
2022-07-25 15:16:22 +00:00
id: addressInput
implicitWidth: parent . width
label: qsTr ( "Address" )
2023-03-06 12:30:58 +00:00
objectName: "savedAddressAddressInput"
input.edit.objectName: "savedAddressAddressInputEdit"
placeholderText: qsTr ( "Ethereum address" )
2023-02-20 10:57:45 +00:00
maximumHeight: 66
input.implicitHeight: Math . min ( Math . max ( input . edit . contentHeight + topPadding + bottomPadding , minimumHeight ) , maximumHeight ) // setting height instead does not work
2023-12-29 13:10:55 +00:00
enabled: ! ( d . editMode || d . addAddress )
2023-02-20 10:57:45 +00:00
validators: [
StatusMinLengthValidator {
minLength: 1
errorMessage: qsTr ( "Address must not be blank" )
} ,
StatusValidator {
errorMessage: addressInput . plainText ? qsTr ( "Please enter a valid address or ENS name." ) : ""
validate: function ( t ) {
2023-05-14 10:21:19 +00:00
return t !== Constants . zeroAddress && ( Utils . isValidAddressWithChainPrefix ( t ) || Utils . isValidEns ( t ) )
2023-02-20 10:57:45 +00:00
? true : { actual: t }
}
}
]
input.edit.textFormat: TextEdit . RichText
2023-12-29 13:10:55 +00:00
input.asset.name: addressInput . valid && ! d . editMode ? "checkbox" : ""
2023-02-20 10:57:45 +00:00
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: {
2023-04-05 11:10:44 +00:00
if ( skipTextUpdate || ! d . initialized )
2023-02-20 10:57:45 +00:00
return
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 ) ) {
2023-12-29 13:10:55 +00:00
d . ens = plainText
d . address = Constants . zeroAddress
d . chainShortNames = ""
2023-02-20 10:57:45 +00:00
}
else {
2023-12-29 13:10:55 +00:00
d . ens = ""
d . address = prefixAndAddress . address
d . chainShortNames = prefixAndAddress . prefix
2023-02-20 10:57:45 +00:00
let prefixArrWithColumn = d . getPrefixArrayWithColumns ( prefixAndAddress . prefix )
if ( ! prefixArrWithColumn )
prefixArrWithColumn = [ ]
allNetworksModelCopy . setEnabledNetworks ( prefixArrWithColumn )
}
}
}
2023-12-29 15:40:14 +00:00
onKeyPressed: {
d . submit ( event )
}
2023-02-20 10:57:45 +00:00
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
2022-03-01 10:14:13 +00:00
}
}
2023-02-20 10:57:45 +00:00
2023-12-29 13:10:55 +00:00
StatusColorSelectorGrid {
id: colorSelection
objectName: "addSavedAddressColor"
width: parent . width
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 )
}
}
2023-02-20 10:57:45 +00:00
StatusNetworkSelector {
id: networkSelector
2023-06-14 09:56:55 +00:00
objectName: "addSavedAddressNetworkSelector"
2023-02-20 10:57:45 +00:00
title: "Network preference"
2023-12-29 13:10:55 +00:00
implicitWidth: parent . width
2023-02-20 10:57:45 +00:00
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: {
2023-04-05 11:10:44 +00:00
if ( ! networkSelector . modelUpdateBlocked && d . initialized ) {
2023-02-20 10:57:45 +00:00
// Initially source model is empty, filter proxy is also empty, but does
2023-12-29 13:10:55 +00:00
// extra work and mistakenly overwrites d.chainShortNames property
2023-02-20 10:57:45 +00:00
if ( sourceModel . count != 0 ) {
const prefixAndAddress = Utils . splitToChainPrefixAndAddress ( addressInput . plainText )
const syncedPrefix = addressInput . syncChainPrefixWithModel ( prefixAndAddress . prefix , this )
2023-12-29 13:10:55 +00:00
d . chainShortNames = syncedPrefix
2023-02-20 10:57:45 +00:00
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
}
}
2023-04-05 11:10:44 +00:00
onToggleNetwork: ( network ) = > {
2023-02-20 10:57:45 +00:00
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
2022-03-01 10:14:13 +00:00
}
2022-07-25 15:16:22 +00:00
footer: StatusDialogFooter {
rightButtons: ObjectModel {
StatusButton {
2023-12-29 13:10:55 +00:00
text: d . editMode ? qsTr ( "Save" ) : qsTr ( "Add address" )
2022-08-08 18:21:56 +00:00
enabled: d . valid && d . dirty
2023-12-26 10:19:41 +00:00
onClicked: {
2023-12-29 15:40:14 +00:00
d . submit ( )
2023-12-26 10:19:41 +00:00
}
2022-08-03 10:08:46 +00:00
objectName: "addSavedAddress"
2022-07-25 15:16:22 +00:00
}
2022-03-01 10:14:13 +00:00
}
2022-07-25 15:16:22 +00:00
}
2023-02-20 10:57:45 +00:00
2023-04-05 11:10:44 +00:00
CloneModel {
2023-02-20 10:57:45 +00:00
id: allNetworksModelCopy
2023-12-26 10:19:41 +00:00
sourceModel: root . allNetworks
2023-04-05 11:10:44 +00:00
roles: [ "layer" , "chainId" , "chainColor" , "chainName" , "shortName" , "iconUrl" ]
rolesOverride: [ { role: "isEnabled" , transform: ( modelData ) = > Boolean ( false ) } ]
2023-02-20 10:57:45 +00:00
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 )
}
}
2022-03-01 10:14:13 +00:00
}