fix(savedaddresses): making add/edit saved address popup's content scrollable when there's no enough space for the content

This commit is contained in:
Sale Djenic 2024-01-10 14:31:44 +01:00 committed by saledjenic
parent b515f536d1
commit 919d4baf53
1 changed files with 301 additions and 287 deletions

View File

@ -11,7 +11,7 @@ import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1 import StatusQ.Controls.Validators 0.1
import StatusQ.Popups.Dialog 0.1 import StatusQ.Popups 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
@ -22,21 +22,20 @@ import "../stores"
import "../controls" import "../controls"
import ".." import ".."
StatusDialog { StatusModal {
id: root id: root
property var allNetworks property var allNetworks
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
width: 477 width: 477
topPadding: 24 // (16 + 8 for Name, until we add it to the StatusInput component)
bottomPadding: 28
header: StatusDialogHeader { headerSettings.title: d.editMode? qsTr("Edit saved address") : qsTr("Add new saved address")
headline.title: d.editMode? qsTr("Edit saved address") : qsTr("Add new saved address") headerSettings.subTitle: d.editMode? d.name : ""
headline.subtitle: d.editMode? d.name : ""
actions.closeButton.onClicked: root.close() onClosed: {
root.close()
} }
function initWithParams(params = {}) { function initWithParams(params = {}) {
@ -76,6 +75,8 @@ StatusDialog {
QtObject { QtObject {
id: d id: d
readonly property int componentWidth: 445
property bool editMode: false property bool editMode: false
property bool addAddress: false property bool addAddress: false
property alias name: nameInput.text property alias name: nameInput.text
@ -121,8 +122,8 @@ StatusDialog {
function submit(event) { function submit(event) {
if (!d.valid if (!d.valid
|| !d.dirty || !d.dirty
|| event !== undefined && event.key !== Qt.Key_Return && event.key !== Qt.Key_Enter) || event !== undefined && event.key !== Qt.Key_Return && event.key !== Qt.Key_Enter)
return return
RootStore.createOrUpdateSavedAddress(d.name, d.address, d.ens, d.colorId, d.chainShortNames) RootStore.createOrUpdateSavedAddress(d.name, d.address, d.ens, d.colorId, d.chainShortNames)
@ -137,300 +138,315 @@ StatusDialog {
}); });
} }
Column { StatusScrollView {
width: parent.width id: scrollView
height: childrenRect.height
spacing: Style.current.xlPadding anchors.fill: parent
padding: 0
contentWidth: availableWidth
StatusInput { Column {
id: nameInput width: scrollView.availableWidth
implicitWidth: parent.width height: childrenRect.height
charLimit: 24 topPadding: 24 // (16 + 8 for Name, until we add it to the StatusInput component)
input.edit.objectName: "savedAddressNameInput" bottomPadding: 28
placeholderText: qsTr("Address name")
label: qsTr("Name") spacing: Style.current.xlPadding
validators: [
StatusMinLengthValidator { StatusInput {
minLength: 1 id: nameInput
errorMessage: qsTr("Please name your saved address") implicitWidth: d.componentWidth
}, anchors.horizontalCenter: parent.horizontalCenter
StatusValidator { charLimit: 24
name: "check-for-no-emojis" input.edit.objectName: "savedAddressNameInput"
validate: (value) => { placeholderText: qsTr("Address name")
return !Constants.regularExpressions.emoji.test(value) label: qsTr("Name")
} validators: [
errorMessage: Constants.errorMessages.emojRegExp StatusMinLengthValidator {
}, minLength: 1
StatusRegularExpressionValidator { errorMessage: qsTr("Please name your saved address")
regularExpression: Constants.regularExpressions.alphanumericalExpanded1 },
errorMessage: Constants.errorMessages.alphanumericalExpanded1RegExp StatusValidator {
}, name: "check-for-no-emojis"
StatusValidator { validate: (value) => {
name: "check-saved-address-existence" return !Constants.regularExpressions.emoji.test(value)
validate: (value) => { }
return !RootStore.savedAddressNameExists(value) errorMessage: Constants.errorMessages.emojRegExp
|| d.editMode && d.storedName == value },
} StatusRegularExpressionValidator {
errorMessage: qsTr("Name already in use") 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")
}
]
input.clearable: true
input.rightPadding: 16
onKeyPressed: {
d.submit(event)
} }
]
input.clearable: true
input.rightPadding: 16
onKeyPressed: {
d.submit(event)
} }
}
StatusInput { StatusInput {
id: addressInput id: addressInput
implicitWidth: parent.width implicitWidth: d.componentWidth
label: qsTr("Address") anchors.horizontalCenter: parent.horizontalCenter
objectName: "savedAddressAddressInput" label: qsTr("Address")
input.edit.objectName: "savedAddressAddressInputEdit" objectName: "savedAddressAddressInput"
placeholderText: qsTr("Ethereum address") input.edit.objectName: "savedAddressAddressInputEdit"
maximumHeight: 66 placeholderText: qsTr("Ethereum address")
input.implicitHeight: Math.min(Math.max(input.edit.contentHeight + topPadding + bottomPadding, minimumHeight), maximumHeight) // setting height instead does not work maximumHeight: 66
enabled: !(d.editMode || d.addAddress) input.implicitHeight: Math.min(Math.max(input.edit.contentHeight + topPadding + bottomPadding, minimumHeight), maximumHeight) // setting height instead does not work
validators: [ enabled: !(d.editMode || d.addAddress)
StatusMinLengthValidator { validators: [
minLength: 1 StatusMinLengthValidator {
errorMessage: qsTr("Please enter an ethereum address") minLength: 1
}, errorMessage: qsTr("Please enter an ethereum address")
StatusValidator { },
errorMessage: d.addressAlreadyAdded? qsTr("This address is already saved") : qsTr("Ethereum address invalid") StatusValidator {
validate: function (value) { errorMessage: d.addressAlreadyAdded? qsTr("This address is already saved") : qsTr("Ethereum address invalid")
if (value !== Constants.zeroAddress) { validate: function (value) {
if (Utils.isValidEns(value)) { if (value !== Constants.zeroAddress) {
return true if (Utils.isValidEns(value)) {
}
if (Utils.isValidAddressWithChainPrefix(value)) {
if (d.editMode) {
return true return true
} }
const prefixAndAddress = Utils.splitToChainPrefixAndAddress(value) if (Utils.isValidAddressWithChainPrefix(value)) {
return d.checkIfAddressIsAlreadyAddded(prefixAndAddress.address) if (d.editMode) {
} return true
}
return false
}
}
]
asyncValidators: [
StatusAsyncValidator {
id: resolvingEnsName
name: "resolving-ens-name"
errorMessage: d.addressAlreadyAdded? 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) => { const prefixAndAddress = Utils.splitToChainPrefixAndAddress(value)
if (d.editMode || value === "not-ens") { return d.checkIfAddressIsAlreadyAddded(prefixAndAddress.address)
return true }
}
if (!!value) {
return d.checkIfAddressIsAlreadyAddded(value)
}
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.addressAlreadyAdded = 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 false
}
} }
return true ]
}) asyncValidators: [
StatusAsyncValidator {
id: resolvingEnsName
name: "resolving-ens-name"
errorMessage: d.addressAlreadyAdded? 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) {
return d.checkIfAddressIsAlreadyAddded(value)
}
return false
}
return unknownPrefixes 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)
}
}
}
]
// Add all chain short names from model, while keeping existing input.edit.textFormat: TextEdit.RichText
function syncChainPrefixWithModel(prefix, model) { input.asset.name: addressInput.valid && !d.editMode ? "checkbox" : ""
let prefixes = prefix.split(":").filter(Boolean) input.asset.color: enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
let prefixStr = "" input.rightPadding: 16
input.leftIcon: false
// Keep unknown prefixes from user input, the rest must be taken multiline: true
// 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) property string plainText: input.edit.getText(0, text.length)
if (unknownPrefixes.length > 0) {
prefixStr += unknownPrefixes.join(":") + ":"
}
return prefixStr onTextChanged: {
} if (skipTextUpdate || !d.initialized)
} return
StatusColorSelectorGrid { d.addressAlreadyAdded = false
id: colorSelection plainText = input.edit.getText(0, text.length)
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: { if (input.edit.previousText != plainText) {
d.colorId = Utils.getIdForColor(selectedColor) let newText = plainText
} const prefixAndAddress = Utils.splitToChainPrefixAndAddress(plainText)
}
StatusNetworkSelector { if (!Utils.isLikelyEnsName(plainText)) {
id: networkSelector newText = WalletUtils.colorizedChainPrefix(prefixAndAddress.prefix) +
objectName: "addSavedAddressNetworkSelector" prefixAndAddress.address
title: "Network preference" }
implicitWidth: parent.width
enabled: addressInput.valid && !d.addressInputIsENS
defaultItemText: "Add networks"
defaultItemImageSource: "add"
rightButtonVisible: true
property bool modelUpdateBlocked: false setRichText(newText)
function blockModelUpdate(value) { // Reset
modelUpdateBlocked = value if (plainText.length == 0) {
} d.resetAddressValues()
return
}
itemsModel: SortFilterProxyModel { // Update root values
sourceModel: allNetworksModelCopy if (Utils.isLikelyEnsName(plainText)) {
filters: ValueFilter { d.resolvingEnsName = true
roleName: "isEnabled" d.ens = plainText
value: true d.address = Constants.zeroAddress
} d.chainShortNames = ""
}
else {
d.resolvingEnsName = false
d.ens = ""
d.address = prefixAndAddress.address
d.chainShortNames = prefixAndAddress.prefix
onCountChanged: { let prefixArrWithColumn = d.getPrefixArrayWithColumns(prefixAndAddress.prefix)
if (!networkSelector.modelUpdateBlocked && d.initialized) { if (!prefixArrWithColumn)
// Initially source model is empty, filter proxy is also empty, but does prefixArrWithColumn = []
// extra work and mistakenly overwrites d.chainShortNames property
if (sourceModel.count != 0) { allNetworksModelCopy.setEnabledNetworks(prefixArrWithColumn)
const prefixAndAddress = Utils.splitToChainPrefixAndAddress(addressInput.plainText)
const syncedPrefix = addressInput.syncChainPrefixWithModel(prefixAndAddress.prefix, this)
d.chainShortNames = syncedPrefix
addressInput.setPlainText(syncedPrefix + prefixAndAddress.address)
} }
} }
} }
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
}
} }
addButton.highlighted: networkSelectPopup.visible StatusColorSelectorGrid {
addButton.onClicked: { id: colorSelection
networkSelectPopup.openAtPosition(addButton.x, networkSelector.y + addButton.height + Style.current.xlPadding) 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)
}
} }
onItemClicked: function (item, index, mouse) { StatusNetworkSelector {
// Append first item id: networkSelector
if (index === 0 && defaultItem.visible) objectName: "addSavedAddressNetworkSelector"
networkSelectPopup.openAtPosition(defaultItem.x, networkSelector.y + defaultItem.height + Style.current.xlPadding) title: "Network preference"
} implicitWidth: d.componentWidth
anchors.horizontalCenter: parent.horizontalCenter
onItemRightButtonClicked: function (item, index, mouse) { enabled: addressInput.valid && !d.addressInputIsENS
item.modelRef.isEnabled = !item.modelRef.isEnabled defaultItemText: "Add networks"
d.chainShortNamesDirty = true 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
}
} }
} }
} }
@ -454,9 +470,9 @@ StatusDialog {
} }
onToggleNetwork: (network) => { onToggleNetwork: (network) => {
network.isEnabled = !network.isEnabled network.isEnabled = !network.isEnabled
d.chainShortNamesDirty = true d.chainShortNamesDirty = true
} }
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
@ -470,19 +486,17 @@ StatusDialog {
dim: false dim: false
} }
footer: StatusDialogFooter { rightButtons: [
rightButtons: ObjectModel { StatusButton {
StatusButton { text: d.editMode? qsTr("Save") : qsTr("Add address")
text: d.editMode? qsTr("Save") : qsTr("Add address") enabled: d.valid && d.dirty && !d.resolvingEnsName
enabled: d.valid && d.dirty && !d.resolvingEnsName loading: d.resolvingEnsName
loading: d.resolvingEnsName onClicked: {
onClicked: { d.submit()
d.submit()
}
objectName: "addSavedAddress"
} }
objectName: "addSavedAddress"
} }
} ]
CloneModel { CloneModel {
id: allNetworksModelCopy id: allNetworksModelCopy