feat(savedaddresses): update adding saved address popup to match new design

Implemented:
- adding selection color section
- all input field validations
- interactions within the popup
- an ephemeral notification when adding an address

Closes #13089
This commit is contained in:
Sale Djenic 2023-12-29 14:10:55 +01:00 committed by saledjenic
parent 3d5b24b87f
commit ff9062a1b0
27 changed files with 335 additions and 232 deletions

View File

@ -27,7 +27,7 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_SAVED_ADDRESS_UPDATED) do(e:Args):
let args = SavedAddressArgs(e)
self.delegate.savedAddressUpdated(args.address, args.ens, args.errorMsg)
self.delegate.savedAddressUpdated(args.name, args.address, args.ens, args.errorMsg)
self.events.on(SIGNAL_SAVED_ADDRESS_DELETED) do(e:Args):
let args = SavedAddressArgs(e)
@ -36,8 +36,9 @@ proc init*(self: Controller) =
proc getSavedAddresses*(self: Controller): seq[saved_address_service.SavedAddressDto] =
return self.savedAddressService.getSavedAddresses()
proc createOrUpdateSavedAddress*(self: Controller, name: string, address: string, favourite: bool, chainShortNames: string, ens: string) =
self.savedAddressService.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
proc createOrUpdateSavedAddress*(self: Controller, name: string, address: string, ens: string, colorId: string,
favourite: bool, chainShortNames: string) =
self.savedAddressService.createOrUpdateSavedAddress(name, address, ens, colorId, favourite, chainShortNames)
proc deleteSavedAddress*(self: Controller, address: string, ens: string) =
self.savedAddressService.deleteSavedAddress(address, ens)

View File

@ -17,18 +17,25 @@ method viewDidLoad*(self: AccessInterface) {.base.} =
method loadSavedAddresses*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method createOrUpdateSavedAddress*(self: AccessInterface, name: string, address: string, favourite: bool, chainShortNames: string, ens: string) {.base.} =
method createOrUpdateSavedAddress*(self: AccessInterface, name: string, address: string, ens: string, colorId: string,
favourite: bool, chainShortNames: string) {.base.} =
raise newException(ValueError, "No implementation available")
method deleteSavedAddress*(self: AccessInterface, address: string, ens: string) {.base.} =
raise newException(ValueError, "No implementation available")
method savedAddressUpdated*(self: AccessInterface, address: string, ens: string, errorMsg: string) {.base.} =
method savedAddressUpdated*(self: AccessInterface, name: string, address: string, ens: string, errorMsg: string) {.base.} =
raise newException(ValueError, "No implementation available")
method savedAddressDeleted*(self: AccessInterface, address: string, ens: string, errorMsg: string) {.base.} =
raise newException(ValueError, "No implementation available")
method savedAddressNameExists*(self: AccessInterface, name: string): bool {.base.} =
raise newException(ValueError, "No implementation available")
method getSavedAddressAsJson*(self: AccessInterface, address: string): string {.base.} =
raise newException(ValueError, "No implementation available")
type
## Abstract class (concept) which must be implemented by object/s used in this
## module.

View File

@ -5,6 +5,7 @@ type
name: string
address: string
ens: string
colorId: string
favourite: bool
chainShortNames: string
isTest: bool
@ -12,8 +13,9 @@ type
proc initItem*(
name: string,
address: string,
favourite: bool,
ens: string,
colorId: string,
favourite: bool,
chainShortNames: string,
isTest: bool
): Item =
@ -21,6 +23,7 @@ proc initItem*(
result.address = address
result.favourite = favourite
result.ens = ens
result.colorId = colorId
result.chainShortNames = chainShortNames
result.isTest = isTest
@ -28,8 +31,9 @@ proc `$`*(self: Item): string =
result = fmt"""SavedAddressItem(
name: {self.name},
address: {self.address},
favourite: {self.favourite},
ens: {self.ens},
colorId: {self.colorId},
favourite: {self.favourite},
chainShortNames: {self.chainShortNames},
isTest: {self.isTest},
]"""
@ -43,6 +47,9 @@ proc getEns*(self: Item): string =
proc getAddress*(self: Item): string =
return self.address
proc getColorId*(self: Item): string =
return self.colorId
proc getFavourite*(self: Item): bool =
return self.favourite

View File

@ -1,13 +1,16 @@
import NimQml, Tables, strutils, strformat
import ./item
import item
export item
type
ModelRole {.pure.} = enum
Name = UserRole + 1,
Address
Favourite
Ens
ColorId
Favourite
ChainShortNames
IsTest
@ -48,8 +51,9 @@ QtObject:
{
ModelRole.Name.int:"name",
ModelRole.Address.int:"address",
ModelRole.Favourite.int:"favourite",
ModelRole.Ens.int:"ens",
ModelRole.ColorId.int:"colorId",
ModelRole.Favourite.int:"favourite",
ModelRole.ChainShortNames.int:"chainShortNames",
ModelRole.IsTest.int:"isTest",
}.toTable
@ -69,10 +73,12 @@ QtObject:
result = newQVariant(item.getName())
of ModelRole.Address:
result = newQVariant(item.getAddress())
of ModelRole.Favourite:
result = newQVariant(item.getFavourite())
of ModelRole.Ens:
result = newQVariant(item.getEns())
of ModelRole.ColorId:
result = newQVariant(item.getColorId())
of ModelRole.Favourite:
result = newQVariant(item.getFavourite())
of ModelRole.ChainShortNames:
result = newQVariant(item.getChainShortNames())
of ModelRole.IsTest:
@ -85,8 +91,9 @@ QtObject:
case column:
of "name": result = $item.getName()
of "address": result = $item.getAddress()
of "favourite": result = $item.getFavourite()
of "ens": result = $item.getEns()
of "colorId": result = $item.getColorId()
of "favourite": result = $item.getFavourite()
of "chainShortNames": result = $item.getChainShortNames()
of "isTest": result = $item.getIsTest()
@ -99,20 +106,13 @@ QtObject:
for item in items:
self.itemChanged(item.getAddress())
proc getNameByAddress*(self: Model, address: string): string =
proc getItemByAddress*(self: Model, address: string): Item =
for item in self.items:
if(cmpIgnoreCase(item.getAddress(), address) == 0):
return item.getName()
return ""
if cmpIgnoreCase(item.getAddress(), address) == 0:
return item
proc getChainShortNamesForAddress*(self: Model, address: string): string =
proc nameExists*(self: Model, name: string): bool =
for item in self.items:
if(cmpIgnoreCase(item.getAddress(), address) == 0):
return item.getChainShortNames()
return ""
proc getEnsForAddress*(self: Model, address: string): string =
for item in self.items:
if(cmpIgnoreCase(item.getAddress(), address) == 0):
return item.getEns()
return ""
if item.getName() == name:
return true
return false

View File

@ -1,11 +1,11 @@
import NimQml, sugar, sequtils
import NimQml, json, sugar, sequtils
import ../io_interface as delegate_interface
import app/global/global_singleton
import app/core/eventemitter
import app_service/service/saved_address/service as saved_address_service
import ./io_interface, ./view, ./controller, ./item
import io_interface, view, controller, model
export io_interface
@ -36,8 +36,9 @@ method loadSavedAddresses*(self: Module) =
savedAddresses.map(s => initItem(
s.name,
s.address,
s.favourite,
s.ens,
s.colorId,
s.favourite,
s.chainShortNames,
s.isTest,
))
@ -57,16 +58,33 @@ method viewDidLoad*(self: Module) =
self.moduleLoaded = true
self.delegate.savedAddressesModuleDidLoad()
method createOrUpdateSavedAddress*(self: Module, name: string, address: string, favourite: bool, chainShortNames: string, ens: string) =
self.controller.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
method createOrUpdateSavedAddress*(self: Module, name: string, address: string, ens: string, colorId: string,
favourite: bool, chainShortNames: string) =
self.controller.createOrUpdateSavedAddress(name, address, ens, colorId, favourite, chainShortNames)
method deleteSavedAddress*(self: Module, address: string, ens: string) =
self.controller.deleteSavedAddress(address, ens)
method savedAddressUpdated*(self: Module, address: string, ens: string, errorMsg: string) =
method savedAddressUpdated*(self: Module, name: string, address: string, ens: string, errorMsg: string) =
self.loadSavedAddresses()
self.view.savedAddressUpdated(address, ens, errorMsg)
self.view.savedAddressUpdated(name, address, ens, errorMsg)
method savedAddressDeleted*(self: Module, address: string, ens: string, errorMsg: string) =
self.loadSavedAddresses()
self.view.savedAddressDeleted(address, ens, errorMsg)
method savedAddressNameExists*(self: Module, name: string): bool =
return self.view.getModel().nameExists(name)
method getSavedAddressAsJson*(self: Module, address: string): string =
let item = self.view.getModel().getItemByAddress(address)
let jsonObj = %* {
"name": item.getName(),
"address": item.getAddress(),
"ens": item.getEns(),
"colorId": item.getColorId(),
"favourite": item.getFavourite(),
"chainShortNames": item.getChainShortNames(),
"isTest": item.getIsTest(),
}
return $jsonObj

View File

@ -1,7 +1,7 @@
import NimQml
import ./model, ./item
import ./io_interface
import model
import io_interface
QtObject:
type
@ -27,31 +27,32 @@ QtObject:
proc modelChanged*(self: View) {.signal.}
proc getModel(self: View): QVariant {.slot.} =
proc getModel*(self: View): Model =
return self.model
proc getModelVariant(self: View): QVariant {.slot.} =
return self.modelVariant
QtProperty[QVariant] model:
read = getModel
read = getModelVariant
notify = modelChanged
proc setItems*(self: View, items: seq[Item]) =
self.model.setItems(items)
proc savedAddressUpdated*(self: View, address: string, ens: string, errorMsg: string) {.signal.}
proc savedAddressUpdated*(self: View, name: string, address: string, ens: string, errorMsg: string) {.signal.}
proc createOrUpdateSavedAddress*(self: View, name: string, address: string, favourite: bool, chainShortNames: string, ens: string) {.slot.} =
self.delegate.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
proc createOrUpdateSavedAddress*(self: View, name: string, address: string, ens: string, colorId: string,
favourite: bool, chainShortNames: string) {.slot.} =
self.delegate.createOrUpdateSavedAddress(name, address, ens, colorId, favourite, chainShortNames)
proc savedAddressDeleted*(self: View, address: string, ens: string, errorMsg: string) {.signal.}
proc deleteSavedAddress*(self: View, address: string, ens: string) {.slot.} =
self.delegate.deleteSavedAddress(address, ens)
proc getNameByAddress*(self: View, address: string): string {.slot.} =
return self.model.getNameByAddress(address)
proc savedAddressNameExists*(self: View, name: string): bool {.slot.} =
return self.delegate.savedAddressNameExists(name)
proc getChainShortNamesForAddress*(self: View, address: string): string {.slot.} =
return self.model.getChainShortNamesForAddress(address)
proc getEnsForAddress*(self: View, address: string): string {.slot.} =
return self.model.getEnsForAddress(address)
proc getSavedAddressAsJson*(self: View, address: string): string {.slot.} =
return self.delegate.getSavedAddressAsJson(address)

View File

@ -7,6 +7,7 @@ type
SavedAddressTaskArg = ref object of QObjectTaskArg
name: string
address: string
colorId: string
favourite: bool
chainShortNames: string
ens: string
@ -16,14 +17,16 @@ const upsertSavedAddressTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.
let arg = decode[SavedAddressTaskArg](argEncoded)
var response = %* {
"response": "",
"name": %* arg.name,
"address": %* arg.address,
"ens": %* arg.ens,
"error": "",
}
try:
let rpcResponse = backend.upsertSavedAddress(backend.SavedAddress(
let rpcResponse = backend.upsertSavedAddress(SavedAddressDto(
name: arg.name,
address: arg.address,
colorId: arg.colorId,
favourite: arg.favourite,
chainShortNames: arg.chainShortNames,
ens: arg.ens,

View File

@ -1,4 +1,4 @@
import json
import json, strutils
include ../../common/json_utils
@ -7,33 +7,19 @@ type
name*: string
address*: string
ens*: string
colorId*: string
favourite*: bool
chainShortNames*: string
isTest*: bool
createdAt*: int64
proc newSavedAddressDto*(
name: string,
address: string,
ens: string,
favourite: bool,
chainShortNames: string,
isTest: bool
): SavedAddressDto =
return SavedAddressDto(
name: name,
address: address,
ens: ens,
favourite: favourite,
chainShortNames: chainShortNames,
isTest: isTest
)
proc toSavedAddressDto*(jsonObj: JsonNode): SavedAddressDto =
result = SavedAddressDto()
discard jsonObj.getProp("name", result.name)
discard jsonObj.getProp("address", result.address)
discard jsonObj.getProp("ens", result.ens)
discard jsonObj.getProp("colorId", result.colorId)
result.colorId = result.colorId.toUpper() # to match `preDefinedWalletAccountColors` on the qml side
discard jsonObj.getProp("favourite", result.favourite)
discard jsonObj.getProp("chainShortNames", result.chainShortNames)
discard jsonObj.getProp("isTest", result.isTest)

View File

@ -24,6 +24,7 @@ const SIGNAL_SAVED_ADDRESS_DELETED* = "savedAddressDeleted"
type
SavedAddressArgs* = ref object of Args
name*: string
address*: string
ens*: string
errorMsg*: string
@ -83,14 +84,15 @@ QtObject:
proc getSavedAddresses*(self: Service): seq[SavedAddressDto] =
return self.savedAddresses
proc createOrUpdateSavedAddress*(self: Service, name: string, address: string, favourite: bool, chainShortNames: string,
ens: string) =
proc createOrUpdateSavedAddress*(self: Service, name: string, address: string, ens: string, colorId: string,
favourite: bool, chainShortNames: string) =
let arg = SavedAddressTaskArg(
name: name,
address: address,
ens: ens,
colorId: colorId,
favourite: favourite,
chainShortNames: chainShortNames,
ens: ens,
isTestAddress: self.settingsService.areTestNetworksEnabled(),
tptr: cast[ByteAddress](upsertSavedAddressTask),
vptr: cast[ByteAddress](self.vptr),
@ -107,6 +109,7 @@ QtObject:
if rpcResponseObj{"response"}.kind != JNull and rpcResponseObj{"response"}.getStr != "ok":
raise newException(CatchableError, "invalid response")
arg.name = rpcResponseObj{"name"}.getStr
arg.address = rpcResponseObj{"address"}.getStr
arg.ens = rpcResponseObj{"ens"}.getStr
except Exception as e:

View File

@ -1,6 +1,7 @@
import json, json_serialization, strformat
import hashes
import ./core, ./response_type
import app_service/service/saved_address/dto as saved_address_dto
from ./gen import rpc
export response_type
@ -26,14 +27,6 @@ type
address* {.serializedFieldName("address").}: string
permissions* {.serializedFieldName("permissions").}: seq[string]
SavedAddress* = ref object of RootObj
name* {.serializedFieldName("name").}: string
address* {.serializedFieldName("address").}: string
favourite* {.serializedFieldName("favourite").}: bool
chainShortNames* {.serializedFieldName("chainShortNames").}: string
ens* {.serializedFieldName("ens").}: string
isTest* {.serializedFieldName("isTest").}: bool
Network* = ref object of RootObj
chainId* {.serializedFieldName("chainId").}: int
nativeCurrencyDecimals* {.serializedFieldName("nativeCurrencyDecimals").}: int
@ -94,7 +87,7 @@ rpc(fetchChainIDForURL, "wallet"):
url: string
rpc(upsertSavedAddress, "wakuext"):
savedAddress: SavedAddress
savedAddress: SavedAddressDto
rpc(deleteSavedAddress, "wakuext"):
address: string

View File

@ -27,7 +27,7 @@ Column {
id: title
width: parent.width
verticalAlignment: Text.AlignVCenter
font.pixelSize: 13
font.pixelSize: 15
color: Theme.palette.baseColor1
}

View File

@ -22,6 +22,7 @@ StatusListItem {
property string name
property string address
property string ens
property string colorId
property string chainShortNames
property bool favourite: false
property bool areTestNetworksEnabled: false
@ -88,6 +89,7 @@ StatusListItem {
favourite: root.favourite,
chainShortNames: root.chainShortNames,
ens: root.ens,
colorId: root.colorId,
}
);
}
@ -115,6 +117,7 @@ StatusListItem {
property bool storeFavourite
property string contactChainShortNames
property string contactEns
property string colorId
readonly property int maxHeight: 341
height: implicitHeight > maxHeight ? maxHeight : implicitHeight
@ -126,6 +129,7 @@ StatusListItem {
storeFavourite = model.favourite;
contactChainShortNames = model.chainShortNames;
contactEns = model.ens;
colorId = model.colorId;
popup(parent, x, y);
}
onClosed: {
@ -146,7 +150,8 @@ StatusListItem {
name: editDeleteMenu.contactName,
favourite: editDeleteMenu.storeFavourite,
chainShortNames: editDeleteMenu.contactChainShortNames,
ens: editDeleteMenu.contactEns
ens: editDeleteMenu.contactEns,
colorId: editDeleteMenu.colorId
})
}
}

View File

@ -189,7 +189,7 @@ Rectangle {
StatusNetworkListItemTag {
id: networkTag
title: model.chainName
title: model.shortName
asset.height: root.asset.height
asset.width: root.asset.width

View File

@ -201,8 +201,14 @@ Column {
Repeater {
model: activityFilterStore.savedAddressFilters
delegate: ActivityFilterTagItem {
tagPrimaryLabel.text: activityFilterStore.getEnsForSavedWalletAddress(modelData)
|| activityFilterStore.getChainShortNamesForSavedWalletAddress(modelData) + StatusQUtils.Utils.elideText(modelData,6,4)
tagPrimaryLabel.text: {
let savedAddress = root.store.getSavedAddress(modelData)
if (!!savedAddress.ens) {
return savedAddress.ens
}
return savedAddress.chainShortNames + StatusQUtils.Utils.elideText(modelData,6,4)
}
onClosed: activityFilterStore.toggleSavedAddress(modelData)
}
}

View File

@ -25,41 +25,80 @@ import ".."
StatusDialog {
id: root
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
property bool edit: false
property bool addAddress: false
property string address: Constants.zeroAddress // Setting as zero address since we don't have the address yet
property string chainShortNames
property string ens
property alias name: nameInput.text
property bool favourite: false
property var allNetworks
function applyParams(params = {}) {
root.addAddress = params.addAddress?? false
root.address = params.address?? Constants.zeroAddress
root.ens = params.ens?? ""
root.edit = params.edit?? false
root.name = params.name?? ""
root.favourite = params.favourite?? false
root.chainShortNames = params.chainShortNames?? ""
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
width: 477
topPadding: 24 // (16 + 8 for Name, until we add it to the StatusInput component)
bottomPadding: 28
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()
}
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.favourite = params.favourite?? false
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 validationMode: root.edit ?
StatusInput.ValidationMode.Always
: StatusInput.ValidationMode.OnlyWhenDirty
readonly property bool valid: addressInput.valid && nameInput.valid
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 bool favourite: false
property string storedName: ""
property string storedColorId: ""
property string storedChainShortNames: ""
property bool chainShortNamesDirty: false
readonly property bool dirty: nameInput.input.dirty || chainShortNamesDirty
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 string visibleAddress: root.address == Constants.zeroAddress ? "" : root.address
readonly property bool addressInputIsENS: !!root.ens
readonly property bool addressInputIsENS: !!d.ens
/// Ensures that the \c root.address and \c root.chainShortNames are not reset when the initial text is set
property bool initialized: false
@ -69,60 +108,53 @@ StatusDialog {
}
function resetAddressValues() {
root.ens = ""
root.address = Constants.zeroAddress
root.chainShortNames = ""
d.ens = ""
d.address = Constants.zeroAddress
d.chainShortNames = ""
allNetworksModelCopy.setEnabledNetworks([])
}
}
width: 574
height: 490
header: StatusDialogHeader {
headline.title: edit ? qsTr("Edit saved address") : qsTr("Add saved address")
headline.subtitle: edit ? name : ""
actions.closeButton.onClicked: root.close()
}
onOpened: {
d.initialized = true
if(edit || addAddress) {
if (root.ens)
addressInput.setPlainText(root.ens)
else
addressInput.setPlainText(root.chainShortNames + d.visibleAddress)
}
nameInput.input.edit.forceActiveFocus(Qt.MouseFocusReason)
}
Column {
width: parent.width
height: childrenRect.height
topPadding: Style.current.bigPadding
spacing: Style.current.bigPadding
spacing: Style.current.xlPadding
StatusInput {
id: nameInput
implicitWidth: parent.width
charLimit: 24
input.edit.objectName: "savedAddressNameInput"
placeholderText: qsTr("Address name")
label: qsTr("Name")
validators: [
StatusMinLengthValidator {
minLength: 1
errorMessage: qsTr("Name must not be blank")
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
},
StatusRegularExpressionValidator {
regularExpression: /^[^<>]+$/
errorMessage: qsTr("This is not a valid account name")
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
validationMode: d.validationMode
}
StatusInput {
@ -134,7 +166,7 @@ StatusDialog {
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: !(root.edit || root.addAddress)
enabled: !(d.editMode || d.addAddress)
validators: [
StatusMinLengthValidator {
minLength: 1
@ -148,10 +180,9 @@ StatusDialog {
}
}
]
validationMode: d.validationMode
input.edit.textFormat: TextEdit.RichText
input.asset.name: addressInput.valid && !root.edit ? "checkbox" : ""
input.asset.name: addressInput.valid && !d.editMode ? "checkbox" : ""
input.asset.color: enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
input.rightPadding: 16
input.leftIcon: false
@ -185,14 +216,14 @@ StatusDialog {
// Update root values
if (Utils.isLikelyEnsName(plainText)) {
root.ens = plainText
root.address = Constants.zeroAddress
root.chainShortNames = ""
d.ens = plainText
d.address = Constants.zeroAddress
d.chainShortNames = ""
}
else {
root.ens = ""
root.address = prefixAndAddress.address
root.chainShortNames = prefixAndAddress.prefix
d.ens = ""
d.address = prefixAndAddress.address
d.chainShortNames = prefixAndAddress.prefix
let prefixArrWithColumn = d.getPrefixArrayWithColumns(prefixAndAddress.prefix)
if (!prefixArrWithColumn)
@ -253,10 +284,26 @@ StatusDialog {
}
}
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)
}
}
StatusNetworkSelector {
id: networkSelector
objectName: "addSavedAddressNetworkSelector"
title: "Network preference"
implicitWidth: parent.width
enabled: addressInput.valid && !d.addressInputIsENS
defaultItemText: "Add networks"
defaultItemImageSource: "add"
@ -278,11 +325,11 @@ StatusDialog {
onCountChanged: {
if (!networkSelector.modelUpdateBlocked && d.initialized) {
// Initially source model is empty, filter proxy is also empty, but does
// extra work and mistakenly overwrites root.chainShortNames property
// 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)
root.chainShortNames = syncedPrefix
d.chainShortNames = syncedPrefix
addressInput.setPlainText(syncedPrefix + prefixAndAddress.address)
}
}
@ -345,10 +392,10 @@ StatusDialog {
footer: StatusDialogFooter {
rightButtons: ObjectModel {
StatusButton {
text: root.edit ? qsTr("Save") : qsTr("Add address")
text: d.editMode? qsTr("Save") : qsTr("Add address")
enabled: d.valid && d.dirty
onClicked: {
RootStore.createOrUpdateSavedAddress(name, address, root.favourite, chainShortNames, ens)
RootStore.createOrUpdateSavedAddress(d.name, d.address, d.ens, d.colorId, d.favourite, d.chainShortNames)
root.close()
}
objectName: "addSavedAddress"

View File

@ -52,6 +52,7 @@ StatusMenu {
property string addressName: ""
property string addressEns: ""
property string colorId: ""
property string addressChains: ""
property string contractName: ""
@ -136,17 +137,19 @@ StatusMenu {
if (isContact) {
d.addressName = contactData.name
} else {
// Revisit here after this issue (resolving source for preferred chains...):
// https://github.com/status-im/status-desktop/issues/13109
d.addressName = WalletStores.RootStore.getNameForWalletAddress(address)
isWalletAccount = d.addressName.length > 0
if (!isWalletAccount) {
d.addressName = WalletStores.RootStore.getNameForSavedWalletAddress(address)
let savedAddress = WalletStores.RootStore.getSavedAddress(address)
d.addressName = savedAddress.name
d.addressEns = savedAddress.ens
d.colorId = savedAddress.colorId
d.addressChains = savedAddress.chainShortNames
}
}
d.addressName = contactData.isContact ? contactData.name : WalletStores.RootStore.getNameForAddress(address)
d.addressEns = RootStore.getEnsForSavedWalletAddress(address)
d.addressChains = RootStore.getChainShortNamesForSavedWalletAddress(address)
showOnEtherscanAction.enabled = true
showOnArbiscanAction.enabled = address.includes(Constants.networkShortChainNames.arbiscan + ":")
showOnOptimismAction.enabled = address.includes(Constants.networkShortChainNames.optimism + ":")
@ -329,6 +332,7 @@ StatusMenu {
name: d.addressName,
address: d.selectedAddress,
ens: d.addressEns,
colorId: d.colorId,
chainShortNames: d.addressChains
})
}

View File

@ -228,14 +228,6 @@ QtObject {
activityController.updateFilter()
}
function getChainShortNamesForSavedWalletAddress(address) {
return walletSectionSavedAddresses.getChainShortNamesForAddress(address)
}
function getEnsForSavedWalletAddress(address) {
return walletSectionSavedAddresses.getEnsForAddress(address)
}
property var savedAddressesModel: walletSectionSavedAddresses.model
property bool areTestNetworksEnabled: networksModule.areTestNetworksEnabled
property var savedAddressList: SortFilterProxyModel {

View File

@ -18,6 +18,7 @@ QtObject {
readonly property bool showAllAccounts: !root.showSavedAddresses && !root.selectedAddress
property var lastCreatedSavedAddress
property var lastDeletedSavedAddress
property bool addingSavedAddress: false
property bool deletingSavedAddress: false
@ -249,18 +250,37 @@ QtObject {
return globalUtils.hex2Dec(value)
}
function getNameForSavedWalletAddress(address) {
return walletSectionSavedAddresses.getNameByAddress(address)
}
function getNameForWalletAddress(address) {
return walletSectionAccounts.getNameByAddress(address)
}
function getSavedAddress(address) {
const defaultValue = {
name: "",
address: "",
ens: "",
colorId: Constants.walletAccountColors.primary,
favourite: false,
chainShortNames: "",
isTest: false,
}
const jsonObj = root.walletSectionSavedAddressesInst.getSavedAddressAsJson(address)
try {
return JSON.parse(jsonObj)
}
catch (e) {
console.warn("error parsing saved address for address: ", address, " error: ", e.message)
return defaultValue
}
}
function getNameForAddress(address) {
var name = getNameForWalletAddress(address)
if (name.length === 0) {
name = getNameForSavedWalletAddress(address)
let savedAddress = getSavedAddress(address)
name = savedAddress.name
}
return name
}
@ -330,9 +350,9 @@ QtObject {
return walletSectionAccounts.getColorByAddress(address)
}
function createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens) {
function createOrUpdateSavedAddress(name, address, ens, colorId, favourite, chainShortNames) {
root.addingSavedAddress = true
walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, ens, colorId, favourite, chainShortNames)
}
function deleteSavedAddress(address, ens) {
@ -340,6 +360,10 @@ QtObject {
walletSectionSavedAddresses.deleteSavedAddress(address, ens)
}
function savedAddressNameExists(name) {
return walletSectionSavedAddresses.savedAddressNameExists(name)
}
function toggleNetwork(chainId) {
networksModule.toggleNetwork(chainId)
}

View File

@ -22,23 +22,11 @@ ColumnLayout {
QtObject {
id: d
function saveAddress(name, address, favourite, chainShortNames, ens) {
RootStore.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
}
function reset() {
RootStore.lastCreatedSavedAddress = undefined
}
}
SavedAddressesError {
id: error
Layout.alignment: Qt.AlignHCenter
text: RootStore.lastCreatedSavedAddress? RootStore.lastCreatedSavedAddress.error?? "" : ""
visible: !!text
height: visible ? 36 : 0
}
ShapeRectangle {
id: noSavedAddresses
Layout.fillWidth: true
@ -54,7 +42,7 @@ ColumnLayout {
}
Item {
visible: error.visible || noSavedAddresses.visible || loadingIndicator.visible
visible: noSavedAddresses.visible || loadingIndicator.visible
Layout.fillWidth: true
Layout.fillHeight: true
}
@ -77,6 +65,7 @@ ColumnLayout {
address: model.address
chainShortNames: model.chainShortNames
ens: model.ens
colorId: model.colorId
favourite: model.favourite
store: RootStore
contactsStore: root.contactsStore
@ -84,15 +73,12 @@ ColumnLayout {
isSepoliaEnabled: RootStore.isSepoliaEnabled
onOpenSendModal: root.sendModal.open(recipient);
saveAddress: function(name, address, favourite, chainShortNames, ens) {
d.saveAddress(name, address, favourite, chainShortNames, ens)
}
states: [
State {
name: "highlighted"
when: RootStore.lastCreatedSavedAddress ? (RootStore.lastCreatedSavedAddress.address.toLowerCase() === address.toLowerCase() &&
RootStore.lastCreatedSavedAddress.ens === ens) : false
when: RootStore.lastCreatedSavedAddress ? (!RootStore.lastCreatedSavedAddress.error &&
RootStore.lastCreatedSavedAddress.address.toLowerCase() === address.toLowerCase() &&
RootStore.lastCreatedSavedAddress.ens === ens) : false
PropertyChanges { target: savedAddressDelegate; color: Theme.palette.baseColor2 }
StateChangeScript {
script: Qt.callLater(d.reset)

View File

@ -1700,7 +1700,7 @@ Item {
}
onLoaded: {
addEditSavedAddress.item.applyParams(addEditSavedAddress.params)
addEditSavedAddress.item.initWithParams(addEditSavedAddress.params)
addEditSavedAddress.item.open()
}
@ -1715,15 +1715,29 @@ Item {
Connections {
target: WalletStore.RootStore.walletSectionSavedAddressesInst
function onSavedAddressUpdated(address: string, ens: string, errorMsg: string) {
function onSavedAddressUpdated(name: string, address: string, ens: string, errorMsg: string) {
WalletStore.RootStore.addingSavedAddress = false
WalletStore.RootStore.lastCreatedSavedAddress = { address: address, ens: ens, error: errorMsg }
if (!!errorMsg) {
WalletStore.RootStore.lastCreatedSavedAddress = { error: errorMsg }
Global.displayToastMessage(qsTr("An error occurred while adding %1 addresses").arg(name),
"",
"warning",
false,
Constants.ephemeralNotificationType.danger,
""
)
return
}
WalletStore.RootStore.lastCreatedSavedAddress = { address: address, ens: ens }
Global.displayToastMessage(qsTr("%1 successfully added to your saved addresses").arg(name),
"",
"checkmark-circle",
false,
Constants.ephemeralNotificationType.success,
""
)
}
}
}
@ -1804,7 +1818,7 @@ Item {
function onSavedAddressDeleted(address: string, ens: string, errorMsg: string) {
WalletStore.RootStore.deletingSavedAddress = false
WalletStore.RootStore.lastCreatedSavedAddress = { error: errorMsg }
WalletStore.RootStore.lastDeletedSavedAddress = { address: address, ens: ens, error: errorMsg }
}
}
}

View File

@ -90,7 +90,11 @@ Item {
}
function refreshSavedAddressName() {
d.savedAddressName = !!root.rootStore ? root.rootStore.getNameForSavedWalletAddress(root.address) : ""
if (!root.rootStore) {
return
}
let savedAddress = root.rootStore.getSavedAddress(root.address)
d.savedAddressName = savedAddress.name
}
function refreshWalletAddress() {

View File

@ -26,7 +26,7 @@ Item {
colorSelection.selectedColorIndex = Math.floor(Math.random() * colorSelection.model.length)
}
else {
let ind = d.evaluateColorIndex(Utils.getColorForId(root.store.addAccountModule.selectedColorId))
let ind = Utils.getColorIndexForId(root.store.addAccountModule.selectedColorId)
colorSelection.selectedColorIndex = ind
}
@ -48,15 +48,6 @@ Item {
id: d
readonly property bool isEdit: root.store.editMode
function evaluateColorIndex(color) {
for (let i = 0; i < Theme.palette.customisationColorsArray.length; i++) {
if(Theme.palette.customisationColorsArray[i] === color) {
return i
}
}
return 0
}
function openEmojiPopup(showLeft) {
if (!root.store.emojiPopup) {
return

View File

@ -179,18 +179,6 @@ QtObject {
return ""
}
function getNameForSavedWalletAddress(address) {
return walletSectionSavedAddresses.getNameByAddress(address)
}
function getChainShortNamesForSavedWalletAddress(address) {
return walletSectionSavedAddresses.getChainShortNamesForAddress(address)
}
function getEnsForSavedWalletAddress(address) {
return walletSectionSavedAddresses.getEnsForAddress(address)
}
function getCurrencyAmount(amount, symbol) {
return currencyStore.getCurrencyAmount(amount, symbol)
}

View File

@ -159,7 +159,19 @@ Control {
}
delegate: StatusListItem {
id: accountDelegate
property bool saved: root.walletStore.getNameForSavedWalletAddress(model.address) !== ""
property bool saved: {
let savedAddress = root.walletStore.getSavedAddress(model.address)
if (savedAddress.name !== "")
return true
if (!!root.walletStore.lastCreatedSavedAddress) {
if (root.walletStore.lastCreatedSavedAddress.address.toLowerCase() === model.address.toLowerCase()) {
return !!root.walletStore.lastCreatedSavedAddress.error
}
}
return false
}
border.width: 1
border.color: Theme.palette.baseColor2
width: ListView.view.width
@ -184,14 +196,11 @@ Control {
enabled: !accountDelegate.saved
text: accountDelegate.saved ? qsTr("Address saved") : qsTr("Save Address")
onClicked: {
accountDelegate.saved = root.walletStore.createOrUpdateSavedAddress(model.name, model.address, false) === ""
Global.displayToastMessage(qsTr("%1 saved to your wallet").arg(accountDelegate.subTitle),
qsTr("Go to your wallet"),
"wallet",
false,
Constants.ephemeralNotificationType.normal,
`#${Constants.appSection.wallet}` // internal link to wallet section
)
// From here, we should just run add saved address popup
Global.openAddEditSavedAddressesPopup({
addAddress: true,
address: model.address
})
}
},
StatusFlatRoundButton {

View File

@ -679,18 +679,22 @@ QtObject {
readonly property QtObject regularExpressions: QtObject {
readonly property var alphanumerical: /^$|^[a-zA-Z0-9]+$/
readonly property var alphanumericalExpanded: /^$|^[a-zA-Z0-9\-_.\u0020]+$/
readonly property var alphanumericalExpanded1: /^[a-zA-Z0-9\-_]+(?: [a-zA-Z0-9\-_]+)*$/
readonly property var alphanumericalWithSpace: /^$|^[a-zA-Z0-9\s]+$/
readonly property var asciiPrintable: /^$|^[!-~]+$/
readonly property var ascii: /^$|^[\x00-\x7F]+$/
readonly property var capitalOnly: /^$|^[A-Z]+$/
readonly property var numerical: /^$|^[0-9]+$/
readonly property var emoji: /\ud83c\udff4(\udb40[\udc61-\udc7a])+\udb40\udc7f|(\ud83c[\udde6-\uddff]){2}|([\#\*0-9]\ufe0f?\u20e3)|(\u00a9|\u00ae|[\u203c\u2049\u20e3\u2122\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23e9-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u261d\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u265f\u2660\u2663\u2665\u2666\u2668\u267b\u267e\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26ce\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299]|\ud83c[\udc04\udccf\udd70\udd71\udd7e\udd7f\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude02\ude1a\ude2f\ude32-\ude3a\ude50\ude51\udf00-\udf21\udf24-\udf93\udf96\udf97\udf99-\udf9b\udf9e-\udff0\udff3-\udff5\udff7-\udfff]|\ud83d[\udc00-\udcfd\udcff-\udd3d\udd49-\udd4e\udd50-\udd67\udd6f\udd70\udd73-\udd7a\udd87\udd8a-\udd8d\udd90\udd95\udd96\udda4\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa-\ude4f\ude80-\udec5\udecb-\uded2\uded5-\uded7\udedc-\udee5\udee9\udeeb\udeec\udef0\udef3-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0c-\udd3a\udd3c-\udd45\udd47-\ude7c\ude80-\ude88\ude90-\udebd\udebf-\udec5\udece-\udedb\udee0-\udee8\udef0-\udef8])((\ud83c[\udffb-\udfff])?(\ud83e[\uddb0-\uddb3])?(\ufe0f?\u200d([\u2000-\u3300]|[\ud83c-\ud83e][\ud000-\udfff])\ufe0f?)?)*/g;
}
readonly property QtObject errorMessages: QtObject {
readonly property string alphanumericalRegExp: qsTr("Only letters and numbers allowed")
readonly property string alphanumericalExpandedRegExp: qsTr("Only letters, numbers, underscores, whitespaces and hyphens allowed")
readonly property string alphanumericalExpandedRegExp: qsTr("Only letters, numbers, underscores, periods, whitespaces and hyphens allowed")
readonly property string alphanumericalExpanded1RegExp: qsTr("Only letters, numbers, underscores, whitespaces and hyphens allowed")
readonly property string alphanumericalWithSpaceRegExp: qsTr("Special characters are not allowed")
readonly property string asciiRegExp: qsTr("Only letters, numbers and ASCII characters allowed")
readonly property string emojRegExp: qsTr("Name is too cool (use letters, numbers, underscores, whitespaces and hyphens only)")
}
readonly property QtObject socialLinkType: QtObject {

View File

@ -836,6 +836,16 @@ QtObject {
return Theme.palette.customisationColors.blue
}
function getColorIndexForId(colorId) {
let color = getColorForId(colorId)
for (let i = 0; i < Theme.palette.customisationColorsArray.length; i++) {
if(Theme.palette.customisationColorsArray[i] === color) {
return i
}
}
return 0
}
function getPathForDisplay(path) {
return path.split("/").join(" / ")
}

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 843bae56597ecf3811d724504bd0216b867979f7
Subproject commit a8357dceacd1b737952660272bf80251df19b8f8