parent
3897b42828
commit
e27f2ec667
|
@ -204,7 +204,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
|
|||
# result.mnemonicService = mnemonic_service.newService()
|
||||
result.privacyService = privacy_service.newService(statusFoundation.events, result.settingsService,
|
||||
result.accountsService)
|
||||
result.savedAddressService = saved_address_service.newService(statusFoundation.events, result.networkService)
|
||||
result.savedAddressService = saved_address_service.newService(statusFoundation.events, result.networkService, result.settingsService)
|
||||
result.devicesService = devices_service.newService(statusFoundation.events, statusFoundation.threadpool, result.settingsService)
|
||||
result.mailserversService = mailservers_service.newService(statusFoundation.events, statusFoundation.threadpool,
|
||||
result.settingsService, result.nodeConfigurationService, statusFoundation.fleetConfiguration)
|
||||
|
@ -541,4 +541,4 @@ proc addToKeycardUidPairsToCheckForAChangeAfterLogin*(self: AppController, oldKe
|
|||
self.changedKeycardUids.add((oldKcUid: oldKeycardUid, newKcUid: newKeycardUid))
|
||||
|
||||
proc removeAllKeycardUidPairsForCheckingForAChangeAfterLogin*(self: AppController) =
|
||||
self.changedKeycardUids = @[]
|
||||
self.changedKeycardUids = @[]
|
||||
|
|
|
@ -28,8 +28,8 @@ 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): string =
|
||||
return self.savedAddressService.createOrUpdateSavedAddress(name, address, favourite)
|
||||
proc createOrUpdateSavedAddress*(self: Controller, name: string, address: string, favourite: bool, chainShortNames: string, ens: string): string =
|
||||
return self.savedAddressService.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
|
||||
|
||||
proc deleteSavedAddress*(self: Controller, address: string): string =
|
||||
return self.savedAddressService.deleteSavedAddress(address)
|
||||
proc deleteSavedAddress*(self: Controller, address: string, ens: string): string =
|
||||
return self.savedAddressService.deleteSavedAddress(address, ens)
|
||||
|
|
|
@ -17,10 +17,10 @@ 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): string {.base.} =
|
||||
method createOrUpdateSavedAddress*(self: AccessInterface, name: string, address: string, favourite: bool, chainShortNames: string, ens: string): string {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
method deleteSavedAddress*(self: AccessInterface, address: string): string {.base.} =
|
||||
method deleteSavedAddress*(self: AccessInterface, address: string, ens: string): string {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
type
|
||||
|
|
|
@ -6,17 +6,23 @@ type
|
|||
address: string
|
||||
ens: string
|
||||
favourite: bool
|
||||
chainShortNames: string
|
||||
isTest: bool
|
||||
|
||||
proc initItem*(
|
||||
name: string,
|
||||
address: string,
|
||||
favourite: bool,
|
||||
ens: string
|
||||
ens: string,
|
||||
chainShortNames: string,
|
||||
isTest: bool
|
||||
): Item =
|
||||
result.name = name
|
||||
result.address = address
|
||||
result.favourite = favourite
|
||||
result.ens = ens
|
||||
result.chainShortNames = chainShortNames
|
||||
result.isTest = isTest
|
||||
|
||||
proc `$`*(self: Item): string =
|
||||
result = fmt"""AllTokensItem(
|
||||
|
@ -24,6 +30,8 @@ proc `$`*(self: Item): string =
|
|||
address: {self.address},
|
||||
favourite: {self.favourite},
|
||||
ens: {self.ens},
|
||||
chainShortNames: {self.chainShortNames},
|
||||
isTest: {self.isTest},
|
||||
]"""
|
||||
|
||||
proc getName*(self: Item): string =
|
||||
|
@ -37,3 +45,9 @@ proc getAddress*(self: Item): string =
|
|||
|
||||
proc getFavourite*(self: Item): bool =
|
||||
return self.favourite
|
||||
|
||||
proc getChainShortNames*(self: Item): string =
|
||||
return self.chainShortNames
|
||||
|
||||
proc getIsTest*(self: Item): bool =
|
||||
return self.isTest
|
||||
|
|
|
@ -8,6 +8,8 @@ type
|
|||
Address
|
||||
Favourite
|
||||
Ens
|
||||
ChainShortNames
|
||||
IsTest
|
||||
|
||||
QtObject:
|
||||
type
|
||||
|
@ -47,6 +49,8 @@ QtObject:
|
|||
ModelRole.Address.int:"address",
|
||||
ModelRole.Favourite.int:"favourite",
|
||||
ModelRole.Ens.int:"ens",
|
||||
ModelRole.ChainShortNames.int:"chainShortNames",
|
||||
ModelRole.IsTest.int:"isTest",
|
||||
}.toTable
|
||||
|
||||
method data(self: Model, index: QModelIndex, role: int): QVariant =
|
||||
|
@ -68,6 +72,10 @@ QtObject:
|
|||
result = newQVariant(item.getFavourite())
|
||||
of ModelRole.Ens:
|
||||
result = newQVariant(item.getEns())
|
||||
of ModelRole.ChainShortNames:
|
||||
result = newQVariant(item.getChainShortNames())
|
||||
of ModelRole.IsTest:
|
||||
result = newQVariant(item.getIsTest())
|
||||
|
||||
proc rowData(self: Model, index: int, column: string): string {.slot.} =
|
||||
if (index >= self.items.len):
|
||||
|
@ -78,6 +86,8 @@ QtObject:
|
|||
of "address": result = $item.getAddress()
|
||||
of "favourite": result = $item.getFavourite()
|
||||
of "ens": result = $item.getEns()
|
||||
of "chainShortNames": result = $item.getChainShortNames()
|
||||
of "isTest": result = $item.getIsTest()
|
||||
|
||||
proc setItems*(self: Model, items: seq[Item]) =
|
||||
self.beginResetModel()
|
||||
|
@ -90,3 +100,15 @@ QtObject:
|
|||
if(item.getAddress() == address):
|
||||
return item.getName()
|
||||
return ""
|
||||
|
||||
proc getChainShortNamesForAddress*(self: Model, address: string): string =
|
||||
for item in self.items:
|
||||
if(item.getAddress() == address):
|
||||
return item.getChainShortNames()
|
||||
return ""
|
||||
|
||||
proc getEnsForAddress*(self: Model, address: string): string =
|
||||
for item in self.items:
|
||||
if(item.getAddress() == address):
|
||||
return item.getEns()
|
||||
return ""
|
||||
|
|
|
@ -38,6 +38,8 @@ method loadSavedAddresses*(self: Module) =
|
|||
s.address,
|
||||
s.favourite,
|
||||
s.ens,
|
||||
s.chainShortNames,
|
||||
s.isTest,
|
||||
))
|
||||
)
|
||||
|
||||
|
@ -55,8 +57,8 @@ method viewDidLoad*(self: Module) =
|
|||
self.moduleLoaded = true
|
||||
self.delegate.savedAddressesModuleDidLoad()
|
||||
|
||||
method createOrUpdateSavedAddress*(self: Module, name: string, address: string, favourite: bool): string =
|
||||
return self.controller.createOrUpdateSavedAddress(name, address, favourite)
|
||||
method createOrUpdateSavedAddress*(self: Module, name: string, address: string, favourite: bool, chainShortNames: string, ens: string): string =
|
||||
return self.controller.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
|
||||
|
||||
method deleteSavedAddress*(self: Module, address: string): string =
|
||||
return self.controller.deleteSavedAddress(address)
|
||||
method deleteSavedAddress*(self: Module, address: string, ens: string): string =
|
||||
return self.controller.deleteSavedAddress(address, ens)
|
||||
|
|
|
@ -37,11 +37,17 @@ QtObject:
|
|||
proc setItems*(self: View, items: seq[Item]) =
|
||||
self.model.setItems(items)
|
||||
|
||||
proc createOrUpdateSavedAddress*(self: View, name: string, address: string, favourite: bool): string {.slot.} =
|
||||
return self.delegate.createOrUpdateSavedAddress(name, address, favourite)
|
||||
proc createOrUpdateSavedAddress*(self: View, name: string, address: string, favourite: bool, chainShortNames: string, ens: string): string {.slot.} =
|
||||
return self.delegate.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
|
||||
|
||||
proc deleteSavedAddress*(self: View, address: string): string {.slot.} =
|
||||
return self.delegate.deleteSavedAddress(address)
|
||||
proc deleteSavedAddress*(self: View, address: string, ens: string): string {.slot.} =
|
||||
return self.delegate.deleteSavedAddress(address, ens)
|
||||
|
||||
proc getNameByAddress*(self: View, address: string): string {.slot.} =
|
||||
return self.model.getNameByAddress(address)
|
||||
|
||||
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)
|
||||
|
|
|
@ -8,20 +8,31 @@ type
|
|||
address*: string
|
||||
ens*: string
|
||||
favourite*: bool
|
||||
chainShortNames*: string
|
||||
isTest*: bool
|
||||
|
||||
proc newSavedAddressDto*(
|
||||
name: string,
|
||||
address: string,
|
||||
favourite: bool
|
||||
ens: string,
|
||||
favourite: bool,
|
||||
chainShortNames: string,
|
||||
isTest: bool
|
||||
): SavedAddressDto =
|
||||
return SavedAddressDto(
|
||||
name: name,
|
||||
address: address,
|
||||
favourite: favourite
|
||||
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("favourite", result.favourite)
|
||||
discard jsonObj.getProp("chainShortNames", result.chainShortNames)
|
||||
discard jsonObj.getProp("isTest", result.isTest)
|
||||
|
|
|
@ -6,6 +6,7 @@ import ../../../app/core/eventemitter
|
|||
import ../../../backend/backend
|
||||
import ../../../app/core/[main]
|
||||
import ../network/service as network_service
|
||||
import ../settings/service as settings_service
|
||||
|
||||
export dto
|
||||
|
||||
|
@ -20,14 +21,17 @@ type
|
|||
events: EventEmitter
|
||||
savedAddresses: seq[SavedAddressDto]
|
||||
networkService: network_service.Service
|
||||
settingsService: settings_service.Service
|
||||
|
||||
proc delete*(self: Service) =
|
||||
discard
|
||||
|
||||
proc newService*(events: EventEmitter, networkService: network_service.Service): Service =
|
||||
proc newService*(events: EventEmitter, networkService: network_service.Service,
|
||||
settingsService: settings_service.Service): Service =
|
||||
result = Service()
|
||||
result.events = events
|
||||
result.networkService = networkService
|
||||
result.settingsService = settingsService
|
||||
|
||||
proc fetchAddresses(self: Service) =
|
||||
try:
|
||||
|
@ -39,11 +43,12 @@ proc fetchAddresses(self: Service) =
|
|||
)
|
||||
let chainId = self.networkService.getNetworkForEns().chainId
|
||||
for savedAddress in self.savedAddresses:
|
||||
try:
|
||||
let nameResponse = backend.getName(chainId, savedAddress.address)
|
||||
savedAddress.ens = nameResponse.result.getStr
|
||||
except:
|
||||
continue
|
||||
if savedAddress.ens != "":
|
||||
try:
|
||||
let nameResponse = backend.getName(chainId, savedAddress.address)
|
||||
savedAddress.ens = nameResponse.result.getStr
|
||||
except:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
error "error: ", procName="fetchAddress", errName = e.name, errDesription = e.msg
|
||||
|
@ -64,9 +69,10 @@ proc init*(self: Service) =
|
|||
proc getSavedAddresses*(self: Service): seq[SavedAddressDto] =
|
||||
return self.savedAddresses
|
||||
|
||||
proc createOrUpdateSavedAddress*(self: Service, name: string, address: string, favourite: bool): string =
|
||||
proc createOrUpdateSavedAddress*(self: Service, name: string, address: string, favourite: bool, chainShortNames: string, ens: string): string =
|
||||
try:
|
||||
discard backend.upsertSavedAddress(backend.SavedAddress(name: name, address: address, favourite: favourite))
|
||||
let isTestAddress = self.settingsService.areTestNetworksEnabled()
|
||||
discard backend.upsertSavedAddress(backend.SavedAddress(name: name, address: address, favourite: favourite, chainShortNames: chainShortNames, ens: ens, isTest: isTestAddress))
|
||||
self.updateAddresses()
|
||||
return ""
|
||||
except Exception as e:
|
||||
|
@ -74,9 +80,10 @@ proc createOrUpdateSavedAddress*(self: Service, name: string, address: string, f
|
|||
error "error: ", errDesription
|
||||
return errDesription
|
||||
|
||||
proc deleteSavedAddress*(self: Service, address: string): string =
|
||||
proc deleteSavedAddress*(self: Service, address: string, ens: string): string =
|
||||
try:
|
||||
var response = backend.deleteSavedAddress(0, address)
|
||||
let isTestAddress = self.settingsService.areTestNetworksEnabled()
|
||||
var response = backend.deleteSavedAddress(address, ens, isTestAddress)
|
||||
if not response.error.isNil:
|
||||
raise newException(Exception, response.error.message)
|
||||
|
||||
|
|
|
@ -29,6 +29,9 @@ type
|
|||
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
|
||||
|
@ -82,8 +85,9 @@ rpc(upsertSavedAddress, "wakuext"):
|
|||
savedAddress: SavedAddress
|
||||
|
||||
rpc(deleteSavedAddress, "wakuext"):
|
||||
chainId: int
|
||||
address: string
|
||||
ens: string
|
||||
isTest: bool
|
||||
|
||||
rpc(getSavedAddresses, "wallet"):
|
||||
discard
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick 2.14
|
||||
|
||||
import utils 1.0
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
import "stores" as WalletStores
|
||||
|
||||
QtObject {
|
||||
function colorizedChainPrefix(prefix) {
|
||||
if (!prefix)
|
||||
return ""
|
||||
|
||||
const prefixes = prefix.split(":").filter(Boolean)
|
||||
let prefixStr = ""
|
||||
const lastPrefixEndsWithColumn = prefix.endsWith(":")
|
||||
const defaultColor = Theme.palette.baseColor1
|
||||
|
||||
for (let i in prefixes) {
|
||||
const pref = prefixes[i]
|
||||
let col = WalletStores.RootStore.colorForChainShortName(pref)
|
||||
if (!col)
|
||||
col = defaultColor
|
||||
|
||||
prefixStr += Utils.richColorText(pref, col)
|
||||
// Avoid adding ":" if it was not there for the last prefix,
|
||||
// because when user manually edits the address, it breaks editing
|
||||
if (!(i == (prefixes.length - 1) && !lastPrefixEndsWithColumn)) {
|
||||
prefixStr += Utils.richColorText(":", Theme.palette.baseColor1)
|
||||
}
|
||||
}
|
||||
|
||||
return prefixStr
|
||||
}
|
||||
}
|
|
@ -99,7 +99,7 @@ Item {
|
|||
multiSelection: root.multiSelection
|
||||
|
||||
onToggleNetwork: {
|
||||
store.toggleNetwork(chainId)
|
||||
store.toggleNetwork(network.chainId)
|
||||
}
|
||||
|
||||
onSingleNetworkSelected: {
|
||||
|
|
|
@ -6,13 +6,13 @@ import utils 1.0
|
|||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Popups 0.1
|
||||
import shared.controls 1.0
|
||||
|
||||
import "../popups"
|
||||
import "../controls"
|
||||
import ".."
|
||||
|
||||
StatusListItem {
|
||||
id: root
|
||||
|
@ -22,55 +22,64 @@ StatusListItem {
|
|||
property string name
|
||||
property string address
|
||||
property string ens
|
||||
property string chainShortNames
|
||||
property bool favourite: false
|
||||
property var saveAddress: function (name, address, favourite) {}
|
||||
property var deleteSavedAddress: function (address) {}
|
||||
property var saveAddress: function (name, address, favourite, chainShortNames, ens) {}
|
||||
property var deleteSavedAddress: function (address, ens) {}
|
||||
|
||||
signal openSendModal()
|
||||
signal openSendModal(string recipient)
|
||||
|
||||
implicitWidth: parent.width
|
||||
implicitWidth: ListView.view.width
|
||||
|
||||
title: name
|
||||
objectName: name
|
||||
subTitle: (ens.length > 0 ? ens + " \u2022 " : "")
|
||||
+ Utils.elideText(address, 6, 4)
|
||||
color: "transparent"
|
||||
subTitle: {
|
||||
if (ens.length > 0)
|
||||
return ens
|
||||
else
|
||||
return WalletUtils.colorizedChainPrefix(chainShortNames) + address
|
||||
}
|
||||
border.color: Theme.palette.baseColor5
|
||||
titleTextIcon: root.favourite ? "star-icon" : ""
|
||||
asset.name: root.favourite ? "star-icon" : "favourite"
|
||||
asset.color: root.favourite ? Theme.palette.pinColor1 : (showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1) // star icon color default
|
||||
asset.hoverColor: root.favourite ? "transparent": Theme.palette.directColor1 // star icon color on hover
|
||||
asset.bgColor: statusListItemIcon.hovered ? Theme.palette.primaryColor3 : "transparent" // icon outer background color
|
||||
asset.bgRadius: 8
|
||||
|
||||
statusListItemIcon.hoverEnabled: true
|
||||
|
||||
onIconClicked: {
|
||||
root.saveAddress(root.name, root.address, !root.favourite, root.chainShortNames, root.ens)
|
||||
}
|
||||
|
||||
statusListItemSubTitle.font.pixelSize: 13
|
||||
statusListItemSubTitle.customColor: !enabled ? Theme.palette.baseColor1 : Theme.palette.directColor1
|
||||
statusListItemComponentsSlot.spacing: 0
|
||||
property bool showButtons: sensor.containsMouse
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
readonly property string visibleAddress: root.address == Constants.zeroAddress ? root.ens : root.address
|
||||
}
|
||||
|
||||
components: [
|
||||
StatusRoundButton {
|
||||
icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1
|
||||
type: StatusRoundButton.Type.Tertiary
|
||||
type: StatusRoundButton.Type.Quinary
|
||||
radius: 8
|
||||
icon.name: "send"
|
||||
onClicked: openSendModal()
|
||||
},
|
||||
CopyToClipBoardButton {
|
||||
id: copyButton
|
||||
type: StatusRoundButton.Type.Tertiary
|
||||
icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1
|
||||
textToCopy: root.address
|
||||
onCopyClicked: root.store.copyToClipboard(textToCopy)
|
||||
},
|
||||
StatusRoundButton {
|
||||
objectName: "savedAddressView_Delegate_favouriteButton"
|
||||
icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1
|
||||
type: StatusRoundButton.Type.Tertiary
|
||||
icon.name: root.favourite ? "unfavourite" : "favourite"
|
||||
onClicked: {
|
||||
root.saveAddress(root.name, root.address, !root.favourite)
|
||||
}
|
||||
onClicked: openSendModal(d.visibleAddress)
|
||||
},
|
||||
StatusRoundButton {
|
||||
objectName: "savedAddressView_Delegate_menuButton"
|
||||
visible: !!root.name
|
||||
icon.color: root.showButtons ? Theme.palette.directColor1 : Theme.palette.baseColor1
|
||||
type: StatusRoundButton.Type.Tertiary
|
||||
type: StatusRoundButton.Type.Quinary
|
||||
radius: 8
|
||||
icon.name: "more"
|
||||
onClicked: {
|
||||
editDeleteMenu.openMenu(root.name, root.address, root.favourite);
|
||||
editDeleteMenu.openMenu(root.name, root.address, root.favourite, root.chainShortNames, root.ens);
|
||||
}
|
||||
},
|
||||
StatusRoundButton {
|
||||
|
@ -82,7 +91,8 @@ StatusListItem {
|
|||
Global.openPopup(addEditSavedAddress,
|
||||
{
|
||||
addAddress: true,
|
||||
address: root.address
|
||||
address: d.visibleAddress,
|
||||
ens: root.ens
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -93,16 +103,22 @@ StatusListItem {
|
|||
property string contactName
|
||||
property string contactAddress
|
||||
property bool storeFavourite
|
||||
function openMenu(name, address, favourite) {
|
||||
property string contactChainShortNames
|
||||
property string contactEns
|
||||
function openMenu(name, address, favourite, chainShortNames, ens) {
|
||||
contactName = name;
|
||||
contactAddress = address;
|
||||
storeFavourite = favourite;
|
||||
contactChainShortNames = chainShortNames;
|
||||
contactEns = ens;
|
||||
popup();
|
||||
}
|
||||
onClosed: {
|
||||
contactName = "";
|
||||
contactAddress = "";
|
||||
storeFavourite = false;
|
||||
contactChainShortNames = ""
|
||||
contactEns = ""
|
||||
}
|
||||
StatusAction {
|
||||
text: qsTr("Edit")
|
||||
|
@ -114,10 +130,32 @@ StatusListItem {
|
|||
edit: true,
|
||||
address: editDeleteMenu.contactAddress,
|
||||
name: editDeleteMenu.contactName,
|
||||
favourite: editDeleteMenu.storeFavourite
|
||||
favourite: editDeleteMenu.storeFavourite,
|
||||
chainShortNames: editDeleteMenu.contactChainShortNames,
|
||||
ens: editDeleteMenu.contactEns
|
||||
})
|
||||
}
|
||||
}
|
||||
StatusAction {
|
||||
text: qsTr("Copy")
|
||||
objectName: "copySavedAddressAction"
|
||||
assetSettings.name: "copy"
|
||||
onTriggered: {
|
||||
if (d.visibleAddress)
|
||||
store.copyToClipboard(d.visibleAddress)
|
||||
else
|
||||
store.copyToClipboard(root.ens)
|
||||
}
|
||||
}
|
||||
StatusMenuSeparator { }
|
||||
StatusAction {
|
||||
text: qsTr("View on Etherscan")
|
||||
objectName: "viewOnEtherscanAction"
|
||||
assetSettings.name: "external"
|
||||
onTriggered: {
|
||||
Global.openLink("https://etherscan.io/address/%1".arg(d.visibleAddress ? d.visibleAddress : root.ens))
|
||||
}
|
||||
}
|
||||
StatusMenuSeparator { }
|
||||
StatusAction {
|
||||
text: qsTr("Delete")
|
||||
|
@ -128,6 +166,7 @@ StatusListItem {
|
|||
deleteAddressConfirm.name = editDeleteMenu.contactName;
|
||||
deleteAddressConfirm.address = editDeleteMenu.contactAddress;
|
||||
deleteAddressConfirm.favourite = editDeleteMenu.storeFavourite;
|
||||
deleteAddressConfirm.ens = editDeleteMenu.contactEns
|
||||
deleteAddressConfirm.open()
|
||||
}
|
||||
}
|
||||
|
@ -140,8 +179,9 @@ StatusListItem {
|
|||
anchors.centerIn: parent
|
||||
onClosed: destroy()
|
||||
contactsStore: root.contactsStore
|
||||
store: root.store
|
||||
onSave: {
|
||||
root.saveAddress(name, address, favourite)
|
||||
root.saveAddress(name, address, favourite, chainShortNames, ens)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
@ -150,6 +190,7 @@ StatusListItem {
|
|||
StatusModal {
|
||||
id: deleteAddressConfirm
|
||||
property string address
|
||||
property string ens
|
||||
property string name
|
||||
property bool favourite
|
||||
anchors.centerIn: parent
|
||||
|
@ -177,7 +218,7 @@ StatusListItem {
|
|||
objectName: "confirmDeleteSavedAddress"
|
||||
text: qsTr("Delete")
|
||||
onClicked: {
|
||||
root.deleteSavedAddress(deleteAddressConfirm.address)
|
||||
root.deleteSavedAddress(deleteAddressConfirm.address, deleteAddressConfirm.ens)
|
||||
deleteAddressConfirm.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
|
||||
Control {
|
||||
id: root
|
||||
|
||||
property alias titleText: titleText
|
||||
property alias button: button
|
||||
|
||||
property string title: ""
|
||||
|
||||
signal clicked(var mouse)
|
||||
|
||||
property StatusAssetSettings asset: StatusAssetSettings {
|
||||
height: 20
|
||||
width: 20
|
||||
rotation: 0
|
||||
isLetterIdenticon: false
|
||||
letterSize: 10
|
||||
color: "transparent"
|
||||
bgWidth: 15
|
||||
bgHeight: 15
|
||||
bgColor: "transparent"
|
||||
bgBorderColor: Theme.palette.baseColor2
|
||||
bgRadius: 16
|
||||
imgIsIdenticon: false
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
readonly property int commonMargin: 5
|
||||
readonly property int leftMargin: 8
|
||||
readonly property int minHeight: 32
|
||||
}
|
||||
|
||||
leftPadding: d.leftMargin
|
||||
spacing: d.commonMargin
|
||||
implicitHeight: d.minHeight
|
||||
|
||||
background: Rectangle {
|
||||
color: root.hovered ? Theme.palette.primaryColor3 : asset.bgColor
|
||||
radius: asset.bgRadius
|
||||
border.color: asset.bgBorderColor
|
||||
|
||||
MouseArea {
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
anchors.fill: parent
|
||||
onClicked: root.clicked(mouse)
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: root.spacing
|
||||
|
||||
StatusSmartIdenticon {
|
||||
id: iconOrImage
|
||||
asset: root.asset
|
||||
name: root.title
|
||||
active: root.asset.isLetterIdenticon ||
|
||||
!!root.asset.name
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
id: titleText
|
||||
|
||||
Layout.rightMargin: button.visible ? 0 : d.commonMargin
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
|
||||
text: root.title
|
||||
font.pixelSize: 15
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StatusRoundButton {
|
||||
id: button
|
||||
|
||||
Layout.preferredHeight: root.height - d.commonMargin
|
||||
Layout.preferredWidth: root.height - d.commonMargin
|
||||
Layout.rightMargin: d.commonMargin
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
|
||||
radius: height / 2
|
||||
|
||||
type: StatusRoundButton.Tertiary
|
||||
icon.name: "close"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtQuick.Controls 2.14 as QC
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
|
||||
import utils 1.0
|
||||
|
||||
/*!
|
||||
\qmltype StatusNetworkSelector
|
||||
\inherits Rectangle
|
||||
\inqmlmodule StatusQ.Components
|
||||
\since StatusQ.Components 0.1
|
||||
\brief It allows to add items and display them as a tag item with an image and text. It also allows to store and display logical `and` / `or` operators into the list. Inherits \l{https://doc.qt.io/qt-6/qml-qtquick-rectangle.html}{Item}.
|
||||
|
||||
The \c StatusNetworkSelector is populated with a data model. The data model is commonly a JavaScript array or a ListModel object with specific expected roles.
|
||||
|
||||
Example of how the component looks like:
|
||||
\image status_item_selector.png
|
||||
|
||||
Example of how to use it:
|
||||
\qml
|
||||
StatusNetworkSelector {
|
||||
id: networkSelector
|
||||
|
||||
title: "Network preference"
|
||||
enabled: addressInput.valid
|
||||
defaultItemText: "Add networks"
|
||||
defaultItemImageSource: "add"
|
||||
|
||||
itemsModel: ListModel {}
|
||||
|
||||
addButton.onClicked: {
|
||||
}
|
||||
|
||||
onItemClicked: {
|
||||
}
|
||||
|
||||
onItemRightButtonClicked: {
|
||||
}
|
||||
}
|
||||
\endqml
|
||||
For a list of components available see StatusQ.
|
||||
*/
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
/*!
|
||||
\qmlproperty string StatusNetworkSelector::title
|
||||
This property holds the title shown on top of the component.
|
||||
*/
|
||||
property string title
|
||||
/*!
|
||||
\qmlproperty string StatusNetworkSelector::defaultItemText
|
||||
This property holds the default item text shown when the list of items is empty.
|
||||
*/
|
||||
property string defaultItemText
|
||||
/*!
|
||||
\qmlproperty url StatusNetworkSelector::defaultItemImageSource
|
||||
This property holds the default item icon shown when the list of items is empty.
|
||||
*/
|
||||
property string defaultItemImageSource: ""
|
||||
/*!
|
||||
\qmlproperty StatusRoundButton StatusNetworkSelector::addButton
|
||||
This property holds an alias to the `add` button.
|
||||
*/
|
||||
readonly property alias addButton: addItemButton
|
||||
/*!
|
||||
\qmlproperty ListModel StatusNetworkSelector::itemsModel
|
||||
This property holds the data that will be populated in the items selector.
|
||||
|
||||
Here an example of the model roles expected:
|
||||
\qml
|
||||
itemsModel: ListModel {
|
||||
ListElement {
|
||||
text: "Ethereum"
|
||||
iconUrl: "Network=Ethereum"
|
||||
}
|
||||
ListElement {
|
||||
text: "Optimism"
|
||||
iconUrl: "Network=Optimism"
|
||||
}
|
||||
}
|
||||
\endqml
|
||||
*/
|
||||
property var itemsModel: ListModel { }
|
||||
/*!
|
||||
\qmlproperty bool StatusNetworkSelector::useIcons
|
||||
This property determines if the imageSource role from the model will be handled as
|
||||
an image or an icon.
|
||||
*/
|
||||
property bool useIcons: false
|
||||
|
||||
property StatusAssetSettings asset: StatusAssetSettings {
|
||||
height: 20
|
||||
width: 20
|
||||
bgColor: "transparent"
|
||||
isImage: !root.useIcons
|
||||
isLetterIdenticon: root.useLetterIdenticons
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty bool StatusNetworkSelector::useLetterIdenticons
|
||||
This property determines if letter identicons should be used. If set to
|
||||
true, the model is expected to contain roles "color" and "emoji".
|
||||
*/
|
||||
property bool useLetterIdenticons: false
|
||||
|
||||
/*!
|
||||
\qmlsignal StatusNetworkSelector::itemClicked
|
||||
This signal is emitted when the item is clicked.
|
||||
*/
|
||||
signal itemClicked(var item, int index, var mouse)
|
||||
|
||||
/*!
|
||||
\qmlsignal StatusNetworkSelector::itemRightButtonClicked
|
||||
This signal is emitted when the item's right button is clicked.
|
||||
*/
|
||||
signal itemRightButtonClicked(var item, int index, var mouse)
|
||||
|
||||
color: "transparent"
|
||||
|
||||
implicitHeight: columnLayout.implicitHeight
|
||||
implicitWidth: 560
|
||||
|
||||
property bool rightButtonVisible: false
|
||||
|
||||
/*!
|
||||
\qmlproperty StatusNetworkListItemTag StatusNetworkSelector::defaultItem
|
||||
This property holds an alias to the `defaultItem` tag
|
||||
*/
|
||||
|
||||
property alias defaultItem: defaultListItemTag
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
|
||||
spacing: 8
|
||||
|
||||
StatusBaseText {
|
||||
text: root.title
|
||||
color: Theme.palette.directColor1
|
||||
font.pixelSize: 15
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: flow
|
||||
|
||||
Layout.preferredWidth: root.width
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 6
|
||||
|
||||
StatusRoundButton {
|
||||
id: addItemButton
|
||||
|
||||
implicitHeight: 32
|
||||
implicitWidth: implicitHeight
|
||||
height: width
|
||||
type: StatusRoundButton.Type.Tertiary
|
||||
border.color: Theme.palette.baseColor2
|
||||
icon.name: root.defaultItemImageSource
|
||||
visible: itemsModel.count > 0
|
||||
icon.color: Theme.palette.primaryColor1
|
||||
}
|
||||
|
||||
StatusNetworkListItemTag {
|
||||
id: defaultListItemTag
|
||||
|
||||
visible: !itemsModel || itemsModel.count === 0
|
||||
title: root.defaultItemText
|
||||
button.visible: true
|
||||
button.icon.name: root.defaultItemImageSource
|
||||
button.enabled: false
|
||||
button.icon.disabledColor: titleText.color
|
||||
button.icon.color: titleText.color
|
||||
onClicked: {
|
||||
root.itemClicked(this, 0, mouse)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: itemsModel
|
||||
|
||||
StatusNetworkListItemTag {
|
||||
id: networkTag
|
||||
|
||||
title: model.chainName
|
||||
|
||||
asset.height: root.asset.height
|
||||
asset.width: root.asset.width
|
||||
asset.name: root.useLetterIdenticons ? model.text : Style.svg(model.iconUrl)
|
||||
asset.isImage: root.asset.isImage
|
||||
asset.bgColor: root.asset.bgColor
|
||||
asset.isLetterIdenticon: root.useLetterIdenticons
|
||||
button.visible: root.rightButtonVisible
|
||||
titleText.color: Theme.palette.primaryColor1
|
||||
button.icon.disabledColor: titleText.color
|
||||
button.icon.color: titleText.color
|
||||
hoverEnabled: false
|
||||
|
||||
property var modelRef: model // model is not reachable outside via item.model.someData, so expose it
|
||||
|
||||
onClicked: {
|
||||
root.itemClicked(this, index, mouse)
|
||||
}
|
||||
|
||||
button.onClicked: {
|
||||
root.itemRightButtonClicked(networkTag, index, mouse)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
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
|
||||
|
@ -11,28 +12,53 @@ import StatusQ.Core.Theme 0.1
|
|||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Controls.Validators 0.1
|
||||
import StatusQ.Popups.Dialog 0.1
|
||||
import StatusQ.Components 0.1
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
import "../controls"
|
||||
import "../stores"
|
||||
import ".."
|
||||
|
||||
StatusDialog {
|
||||
id: root
|
||||
|
||||
property bool edit: false
|
||||
property bool addAddress: false
|
||||
property string address
|
||||
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 contactsStore
|
||||
property var store
|
||||
|
||||
signal save(string name, string address)
|
||||
signal save(string name, string address, string chainShortNames, string ens)
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
property int validationMode: root.edit ?
|
||||
readonly property int validationMode: root.edit ?
|
||||
StatusInput.ValidationMode.Always
|
||||
: StatusInput.ValidationMode.OnlyWhenDirty
|
||||
property bool valid: addressInput.isValid && nameInput.valid // TODO: Add network preference and emoji
|
||||
property bool dirty: nameInput.input.dirty
|
||||
readonly property bool valid: addressInput.valid && nameInput.valid
|
||||
property bool chainShortNamesDirty: false
|
||||
readonly property bool dirty: nameInput.input.dirty || chainShortNamesDirty
|
||||
|
||||
readonly property var chainPrefixRegexPattern: /[^:]+\:?|:/g
|
||||
readonly property string visibleAddress: root.address == Constants.zeroAddress ? "" : root.address
|
||||
readonly property bool addressInputIsENS: !visibleAddress
|
||||
|
||||
function getPrefixArrayWithColumns(prefixStr) {
|
||||
return prefixStr.match(d.chainPrefixRegexPattern)
|
||||
}
|
||||
|
||||
function resetAddressValues() {
|
||||
root.ens = ""
|
||||
root.address = Constants.zeroAddress
|
||||
root.chainShortNames = ""
|
||||
allNetworksModelCopy.setEnabledNetworks([])
|
||||
}
|
||||
}
|
||||
|
||||
width: 574
|
||||
|
@ -46,7 +72,10 @@ StatusDialog {
|
|||
|
||||
onOpened: {
|
||||
if(edit || addAddress) {
|
||||
addressInput.input.text = root.address
|
||||
if (root.ens)
|
||||
addressInput.setPlainText(root.ens)
|
||||
else
|
||||
addressInput.setPlainText(root.chainShortNames + d.visibleAddress)
|
||||
}
|
||||
nameInput.input.edit.forceActiveFocus(Qt.MouseFocusReason)
|
||||
}
|
||||
|
@ -54,7 +83,7 @@ StatusDialog {
|
|||
Column {
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
topPadding: Style.current.xlPadding
|
||||
topPadding: Style.current.bigPadding
|
||||
|
||||
spacing: Style.current.bigPadding
|
||||
|
||||
|
@ -62,9 +91,7 @@ StatusDialog {
|
|||
id: nameInput
|
||||
implicitWidth: parent.width
|
||||
input.edit.objectName: "savedAddressNameInput"
|
||||
minimumHeight: 56
|
||||
maximumHeight: 56
|
||||
placeholderText: qsTr("Enter a name")
|
||||
placeholderText: qsTr("Address owner")
|
||||
label: qsTr("Name")
|
||||
validators: [
|
||||
StatusMinLengthValidator {
|
||||
|
@ -76,33 +103,226 @@ StatusDialog {
|
|||
errorMessage: qsTr("This is not a valid account name")
|
||||
}
|
||||
]
|
||||
input.clearable: true
|
||||
input.rightPadding: 16
|
||||
charLimit: 40
|
||||
validationMode: d.validationMode
|
||||
}
|
||||
|
||||
// To-Do use StatusInput within the below component
|
||||
RecipientSelector {
|
||||
StatusInput {
|
||||
id: addressInput
|
||||
implicitWidth: parent.width
|
||||
inputWidth: implicitWidth
|
||||
accounts: RootStore.accounts
|
||||
contactsStore: root.contactsStore
|
||||
label: qsTr("Address")
|
||||
input.textField.objectName: "savedAddressAddressInput"
|
||||
input.placeholderText: qsTr("Enter ENS Name or Ethereum Address")
|
||||
labelFont.pixelSize: 15
|
||||
labelFont.weight: Font.Normal
|
||||
input.implicitHeight: 56
|
||||
input.textField.anchors.rightMargin: 0
|
||||
isSelectorVisible: false
|
||||
addContactEnabled: false
|
||||
onSelectedRecipientChanged: {
|
||||
root.address = selectedRecipient.address
|
||||
input.edit.objectName: "savedAddressAddressInput"
|
||||
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)
|
||||
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) {
|
||||
return Utils.isValidAddressWithChainPrefix(t) || Utils.isValidEns(t)
|
||||
? true : { actual: t }
|
||||
}
|
||||
}
|
||||
]
|
||||
validationMode: d.validationMode
|
||||
|
||||
input.edit.textFormat: TextEdit.RichText
|
||||
input.asset.name: addressInput.valid && !root.edit ? "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)
|
||||
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)) {
|
||||
root.ens = plainText
|
||||
root.address = Constants.zeroAddress
|
||||
root.chainShortNames = ""
|
||||
}
|
||||
else {
|
||||
root.ens = ""
|
||||
root.address = prefixAndAddress.address
|
||||
root.chainShortNames = prefixAndAddress.prefix
|
||||
|
||||
let prefixArrWithColumn = d.getPrefixArrayWithColumns(prefixAndAddress.prefix)
|
||||
if (!prefixArrWithColumn)
|
||||
prefixArrWithColumn = []
|
||||
|
||||
allNetworksModelCopy.setEnabledNetworks(prefixArrWithColumn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
readOnly: root.edit || root.addAddress
|
||||
wrongInputValidationError: qsTr("Please enter a valid ENS name OR Ethereum Address")
|
||||
ownAddressError: qsTr("Can't add yourself as a saved address")
|
||||
}
|
||||
|
||||
StatusNetworkSelector {
|
||||
id: networkSelector
|
||||
|
||||
title: "Network preference"
|
||||
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) {
|
||||
// Initially source model is empty, filter proxy is also empty, but does
|
||||
// extra work and mistakenly overwrites root.chainShortNames property
|
||||
if (sourceModel.count != 0) {
|
||||
const prefixAndAddress = Utils.splitToChainPrefixAndAddress(addressInput.plainText)
|
||||
const syncedPrefix = addressInput.syncChainPrefixWithModel(prefixAndAddress.prefix, this)
|
||||
root.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.isEnabled = !network.isEnabled
|
||||
d.chainShortNamesDirty = true
|
||||
}
|
||||
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
function openAtPosition(xPos, yPos) {
|
||||
x = xPos
|
||||
y = yPos
|
||||
open()
|
||||
}
|
||||
|
||||
modal: true
|
||||
dim: false
|
||||
}
|
||||
|
||||
footer: StatusDialogFooter {
|
||||
|
@ -110,9 +330,43 @@ StatusDialog {
|
|||
StatusButton {
|
||||
text: root.edit ? qsTr("Save") : qsTr("Add address")
|
||||
enabled: d.valid && d.dirty
|
||||
onClicked: root.save(name, address)
|
||||
onClicked: root.save(name, address, chainShortNames, ens)
|
||||
objectName: "addSavedAddress"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: allNetworksModelCopy
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function init(model, address) {
|
||||
const prefixStr = Utils.getChainsPrefix(address)
|
||||
for (let i = 0; i < model.count; i++) {
|
||||
const clonedItem = {
|
||||
layer: model.rowData(i, "layer"),
|
||||
chainId: model.rowData(i, "chainId"),
|
||||
chainColor: model.rowData(i, "chainColor"),
|
||||
chainName: model.rowData(i, "chainName"),
|
||||
shortName: model.rowData(i, "shortName"),
|
||||
iconUrl: model.rowData(i, "iconUrl"),
|
||||
isEnabled: Boolean(prefixStr.length > 0 && prefixStr.includes(shortName))
|
||||
}
|
||||
|
||||
append(clonedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
allNetworksModelCopy.init(store.allNetworks, root.address)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ Popup {
|
|||
|
||||
property bool multiSelection: true
|
||||
|
||||
signal toggleNetwork(int chainId)
|
||||
signal toggleNetwork(var network)
|
||||
signal singleNetworkSelected(int chainId, string chainName, string iconUrl)
|
||||
|
||||
background: Rectangle {
|
||||
|
@ -114,7 +114,7 @@ Popup {
|
|||
asset.name: Style.svg(model.iconUrl)
|
||||
onClicked: {
|
||||
if(root.multiSelection)
|
||||
checkBox.toggle()
|
||||
checkBox.toggled()
|
||||
else
|
||||
radioButton.toggle()
|
||||
}
|
||||
|
@ -124,10 +124,10 @@ Popup {
|
|||
visible: root.multiSelection
|
||||
checked: root.useNetworksExtraStoreProxy ? model.isActive : model.isEnabled
|
||||
onToggled: {
|
||||
if(root.useNetworksExtraStoreProxy && model.isActive !== checked) {
|
||||
model.isActive = checked
|
||||
} else if (model.isEnabled !== checked) {
|
||||
root.toggleNetwork(model.chainId)
|
||||
if (root.useNetworksExtraStoreProxy) {
|
||||
model.isActive = !model.isActive
|
||||
} else {
|
||||
root.toggleNetwork(model)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2,3 +2,4 @@ LeftTabView 1.0 LeftTabView.qml
|
|||
WalletHeader 1.0 WalletHeader.qml
|
||||
CollectiblesView 1.0 CollectiblesView.qml
|
||||
WalletLayout 1.0 WalletLayout.qml
|
||||
singleton WalletUtils 1.0 WalletUtils.qml
|
||||
|
|
|
@ -3,7 +3,9 @@ pragma Singleton
|
|||
import QtQuick 2.13
|
||||
|
||||
import utils 1.0
|
||||
import "../panels"
|
||||
import SortFilterProxyModel 0.2
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
@ -30,7 +32,30 @@ QtObject {
|
|||
property var flatCollectibles: walletSectionCollectibles.flatModel
|
||||
property var currentCollectible: walletSectionCurrentCollectible
|
||||
|
||||
property var savedAddresses: walletSectionSavedAddresses.model
|
||||
property var savedAddresses: SortFilterProxyModel {
|
||||
sourceModel: walletSectionSavedAddresses.model
|
||||
filters: [
|
||||
ValueFilter {
|
||||
roleName: "isTest"
|
||||
value: networksModule.areTestNetworksEnabled
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
property QtObject _d: QtObject {
|
||||
id: d
|
||||
property var chainColors: ({})
|
||||
|
||||
function initChainColors(model) {
|
||||
for (let i = 0; i < model.count; i++) {
|
||||
chainColors[model.rowData(i, "shortName")] = model.rowData(i, "chainColor")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function colorForChainShortName(chainShortName) {
|
||||
return d.chainColors[chainShortName]
|
||||
}
|
||||
|
||||
// Used for new wallet account generation
|
||||
property var generatedAccountsViewModel: walletSectionAccounts.generatedAccounts
|
||||
|
@ -41,6 +66,9 @@ QtObject {
|
|||
property var testNetworks: networksModule.test
|
||||
property var enabledNetworks: networksModule.enabled
|
||||
property var allNetworks: networksModule.all
|
||||
onAllNetworksChanged: {
|
||||
d.initChainColors(allNetworks)
|
||||
}
|
||||
property var layer1NetworksProxy: networksModule.layer1Proxy
|
||||
property var layer2NetworksProxy: networksModule.layer2Proxy
|
||||
|
||||
|
@ -170,12 +198,12 @@ QtObject {
|
|||
walletSectionCurrentCollectible.update(slug, id)
|
||||
}
|
||||
|
||||
function createOrUpdateSavedAddress(name, address, favourite) {
|
||||
return walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, favourite)
|
||||
function createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens) {
|
||||
return walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
|
||||
}
|
||||
|
||||
function deleteSavedAddress(address) {
|
||||
return walletSectionSavedAddresses.deleteSavedAddress(address)
|
||||
function deleteSavedAddress(address, ens) {
|
||||
return walletSectionSavedAddresses.deleteSavedAddress(address, ens)
|
||||
}
|
||||
|
||||
function toggleNetwork(chainId) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import utils 1.0
|
|||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Popups 0.1
|
||||
import shared.controls 1.0
|
||||
|
@ -27,16 +26,21 @@ Item {
|
|||
id: _internal
|
||||
property bool loading: false
|
||||
property string error: ""
|
||||
function saveAddress(name, address, favourite) {
|
||||
property var lastCreatedAddress // used to display animation for the newly saved address
|
||||
function saveAddress(name, address, favourite, chainShortNames, ens) {
|
||||
loading = true
|
||||
error = RootStore.createOrUpdateSavedAddress(name, address, favourite)
|
||||
error = RootStore.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
|
||||
loading = false
|
||||
}
|
||||
function deleteSavedAddress(address) {
|
||||
function deleteSavedAddress(address, ens) {
|
||||
loading = true
|
||||
error = RootStore.deleteSavedAddress(address)
|
||||
error = RootStore.deleteSavedAddress(address, ens)
|
||||
loading = false
|
||||
}
|
||||
|
||||
function resetLastCreatedAddress() {
|
||||
lastCreatedAddress = undefined
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
|
@ -105,21 +109,46 @@ Item {
|
|||
spacing: 5
|
||||
model: RootStore.savedAddresses
|
||||
delegate: SavedAddressesDelegate {
|
||||
id: savedAddressDelegate
|
||||
|
||||
objectName: "savedAddressView_Delegate_" + name
|
||||
|
||||
name: model.name
|
||||
address: model.address
|
||||
chainShortNames: model.chainShortNames
|
||||
ens: model.ens
|
||||
favourite: model.favourite
|
||||
store: RootStore
|
||||
contactsStore: root.contactsStore
|
||||
onOpenSendModal: root.sendModal.open(address);
|
||||
saveAddress: function(name, address, favourite) {
|
||||
_internal.saveAddress(name, address, favourite)
|
||||
onOpenSendModal: root.sendModal.open(recipient);
|
||||
saveAddress: function(name, address, favourite, chainShortNames, ens) {
|
||||
_internal.saveAddress(name, address, favourite, chainShortNames, ens)
|
||||
}
|
||||
deleteSavedAddress: function(address) {
|
||||
_internal.deleteSavedAddress(address)
|
||||
deleteSavedAddress: function(address, ens) {
|
||||
_internal.deleteSavedAddress(address, ens)
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "highlighted"
|
||||
when: _internal.lastCreatedAddress ? (_internal.lastCreatedAddress.address.toLowerCase() === address.toLowerCase() &&
|
||||
_internal.lastCreatedAddress.ens === ens) : false
|
||||
PropertyChanges { target: savedAddressDelegate; color: Theme.palette.baseColor2 }
|
||||
StateChangeScript {
|
||||
script: Qt.callLater(_internal.resetLastCreatedAddress)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "highlighted"
|
||||
ColorAnimation {
|
||||
target: savedAddressDelegate
|
||||
duration: 3000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,8 +159,10 @@ Item {
|
|||
anchors.centerIn: parent
|
||||
onClosed: destroy()
|
||||
contactsStore: root.contactsStore
|
||||
store: RootStore
|
||||
onSave: {
|
||||
_internal.saveAddress(name, address, favourite)
|
||||
_internal.lastCreatedAddress = { address: address, ens: ens }
|
||||
_internal.saveAddress(name, address, favourite, chainShortNames, ens)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import utils 1.0
|
|||
import shared.stores 1.0
|
||||
|
||||
import "../controls"
|
||||
import "../stores" as WalletStores
|
||||
import ".."
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
@ -31,6 +33,8 @@ Item {
|
|||
readonly property string savedAddressNameFrom: root.isTransactionValid ? d.getNameForSavedWalletAddress(transaction.from): ""
|
||||
readonly property string from: root.isTransactionValid ? !!savedAddressNameFrom ? savedAddressNameFrom : Utils.compactAddress(transaction.from, 4): ""
|
||||
readonly property string to: root.isTransactionValid ? !!savedAddressNameTo ? savedAddressNameTo : Utils.compactAddress(transaction.to, 4): ""
|
||||
readonly property string savedAddressEns: RootStore.getEnsForSavedWalletAddress(isIncoming ? transaction.from : transaction.to)
|
||||
readonly property string savedAddressChains: RootStore.getChainShortNamesForSavedWalletAddress(isIncoming ? transaction.from : transaction.to)
|
||||
|
||||
function getNameForSavedWalletAddress(address) {
|
||||
return RootStore.getNameForSavedWalletAddress(address)
|
||||
|
@ -81,16 +85,18 @@ Item {
|
|||
|
||||
name: d.isIncoming ? d.savedAddressNameFrom : d.savedAddressNameTo
|
||||
address: root.isTransactionValid ? d.isIncoming ? transaction.from : transaction.to : ""
|
||||
ens: d.savedAddressEns
|
||||
chainShortNames: d.savedAddressChains
|
||||
title: d.isIncoming ? d.from : d.to
|
||||
subTitle: root.isTransactionValid ? d.isIncoming ? !!d.savedAddressNameFrom ? Utils.compactAddress(transaction.from, 4) : "" : !!d.savedAddressNameTo ? Utils.compactAddress(transaction.to, 4) : "": ""
|
||||
store: RootStore
|
||||
store: WalletStores.RootStore
|
||||
contactsStore: root.contactsStore
|
||||
onOpenSendModal: root.sendModal.open(address);
|
||||
saveAddress: function(name, address, favourite) {
|
||||
RootStore.createOrUpdateSavedAddress(name, address, favourite)
|
||||
saveAddress: function(name, address, favourite, chainShortNames, ens) {
|
||||
RootStore.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
|
||||
}
|
||||
deleteSavedAddress: function(address) {
|
||||
RootStore.deleteSavedAddress(address)
|
||||
deleteSavedAddress: function(address, ens) {
|
||||
RootStore.deleteSavedAddress(address, ens)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -375,6 +375,7 @@ StatusDialog {
|
|||
|
||||
label: qsTr("To")
|
||||
placeholderText: qsTr("Enter an ENS name or address")
|
||||
text: popup.addressText
|
||||
input.background.color: Theme.palette.indirectColor1
|
||||
input.background.border.width: 0
|
||||
input.implicitHeight: 56
|
||||
|
|
|
@ -193,12 +193,20 @@ QtObject {
|
|||
return walletSectionSavedAddresses.getNameByAddress(address)
|
||||
}
|
||||
|
||||
function createOrUpdateSavedAddress(name, address, favourite) {
|
||||
return walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, favourite)
|
||||
function getChainShortNamesForSavedWalletAddress(address) {
|
||||
return walletSectionSavedAddresses.getChainShortNamesForAddress(address)
|
||||
}
|
||||
|
||||
function deleteSavedAddress(address) {
|
||||
return walletSectionSavedAddresses.deleteSavedAddress(address)
|
||||
function getEnsForSavedWalletAddress(address) {
|
||||
return walletSectionSavedAddresses.getEnsForAddress(address)
|
||||
}
|
||||
|
||||
function createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens) {
|
||||
return walletSectionSavedAddresses.createOrUpdateSavedAddress(name, address, favourite, chainShortNames, ens)
|
||||
}
|
||||
|
||||
function deleteSavedAddress(addresse, ens) {
|
||||
return walletSectionSavedAddresses.deleteSavedAddress(address, ens)
|
||||
}
|
||||
|
||||
function getLatestBlockNumber() {
|
||||
|
|
|
@ -38,6 +38,35 @@ QtObject {
|
|||
return startsWith0x(value) && isHex(value) && value.length === 42
|
||||
}
|
||||
|
||||
function isValidAddressWithChainPrefix(value) {
|
||||
return value.match(/^(([a-zA-Z]{3,5}:)*)?(0x[a-fA-F0-9]{40})$/)
|
||||
}
|
||||
|
||||
function getChainsPrefix(address) {
|
||||
// matchAll is not supported by QML JS engine
|
||||
return address.match(/([a-zA-Z]{3,5}:)*/)[0].split(':').filter(e => !!e)
|
||||
}
|
||||
|
||||
function isLikelyEnsName(text) {
|
||||
return text.startsWith("@") || !isLikelyAddress(text)
|
||||
}
|
||||
|
||||
function isLikelyAddress(text) {
|
||||
return text.includes(":") || text.includes('0x')
|
||||
}
|
||||
|
||||
function richColorText(text, color) {
|
||||
return "<font color=\"" + color + "\">" + text + "</font>"
|
||||
}
|
||||
|
||||
function splitToChainPrefixAndAddress(input) {
|
||||
const addressIdx = input.indexOf('0x')
|
||||
if (addressIdx < 0)
|
||||
return { prefix: input, address: "" }
|
||||
|
||||
return { prefix: input.substring(0, addressIdx), address: input.substring(addressIdx) }
|
||||
}
|
||||
|
||||
function isPrivateKey(value) {
|
||||
return isHex(value) && ((startsWith0x(value) && value.length === 66) ||
|
||||
(!startsWith0x(value) && value.length === 64))
|
||||
|
@ -109,7 +138,7 @@ QtObject {
|
|||
return false
|
||||
}
|
||||
const isEmail = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(inputValue)
|
||||
const isDomain = /(?:(?:(?<thld>[\w\-]*)(?:\.))?(?<sld>[\w\-]*))\.(?<tld>[\w\-]*)/.test(inputValue)
|
||||
const isDomain = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/.test(inputValue)
|
||||
return isEmail || isDomain || (inputValue.startsWith("@") && inputValue.length > 1)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue