feat: display name

This commit is contained in:
Richard Ramos 2022-03-01 20:14:20 -04:00
parent 228a1ed88d
commit b07910e27f
29 changed files with 228 additions and 21 deletions

View File

@ -271,6 +271,7 @@ proc delete*(self: AppController) =
proc startupDidLoad*(self: AppController) =
singletonInstance.engine.setRootContextProperty("localAppSettings", self.localAppSettingsVariant)
singletonInstance.engine.setRootContextProperty("localAccountSettings", self.localAccountSettingsVariant)
singletonInstance.engine.setRootContextProperty("globalUtils", self.globalUtilsVariant)
singletonInstance.engine.load(newQUrl("qrc:///main.qml"))
# We need to init a language service once qml is loaded
@ -314,6 +315,10 @@ proc load(self: AppController) =
self.gifService.init()
singletonInstance.engine.setRootContextProperty("globalUtils", self.globalUtilsVariant)
let pubKey = self.settingsService.getPublicKey()
singletonInstance.localAccountSensitiveSettings.setFileName(pubKey)
singletonInstance.engine.setRootContextProperty("localAccountSensitiveSettings", self.localAccountSensitiveSettingsVariant)
self.buildAndRegisterLocalAccountSensitiveSettings()
self.buildAndRegisterUserProfile()
@ -352,6 +357,7 @@ proc buildAndRegisterLocalAccountSensitiveSettings(self: AppController) =
proc buildAndRegisterUserProfile(self: AppController) =
let pubKey = self.settingsService.getPublicKey()
let preferredName = self.settingsService.getPreferredName()
let displayName = self.settingsService.getDisplayName()
let ensUsernames = self.settingsService.getEnsUsernames()
let firstEnsName = if (ensUsernames.len > 0): ensUsernames[0] else: ""
let sendUserStatus = self.settingsService.getSendStatusUpdates()
@ -370,6 +376,7 @@ proc buildAndRegisterUserProfile(self: AppController) =
singletonInstance.userProfile.setFixedData(loggedInAccount.name, loggedInAccount.keyUid, loggedInAccount.identicon,
pubKey)
singletonInstance.userProfile.setDisplayName(displayName)
singletonInstance.userProfile.setPreferredName(preferredName)
singletonInstance.userProfile.setEnsName(meAsContact.name)
singletonInstance.userProfile.setFirstEnsName(firstEnsName)

View File

@ -12,6 +12,7 @@ QtObject:
# fields which may change during runtime
isIdenticon: bool
ensName: string
displayName: string
firstEnsName: string
preferredName: string
thumbnailImage: string
@ -49,7 +50,6 @@ QtObject:
QtProperty[string] pubKey:
read = getPubKey
proc nameChanged*(self: UserProfile) {.signal.}
proc getUsername*(self: UserProfile): string {.slot.} =
@ -118,6 +118,16 @@ QtObject:
read = getPrettyPreferredName
notify = nameChanged
proc setDisplayName*(self: UserProfile, displayName: string) = # Not a slot
self.displayName = displayName
self.nameChanged()
proc getDisplayName*(self: UserProfile): string {.slot.} =
self.displayName
QtProperty[string] displayName:
read = getDisplayName
notify = nameChanged
proc getName*(self: UserProfile): string {.slot.} =
if(self.preferredName.len > 0):
@ -126,13 +136,14 @@ QtObject:
return self.getPrettyFirstEnsName()
elif(self.ensName.len > 0):
return self.getPrettyEnsName()
elif(self.displayName.len > 0):
return self.getDisplayName()
return self.username
QtProperty[string] name:
read = getName
notify = nameChanged
proc imageChanged*(self: UserProfile) {.signal.}
proc getIsIdenticon*(self: UserProfile): bool {.slot.} =

View File

@ -26,6 +26,9 @@ QtObject:
# Windows doesn't work with paths starting with a slash
result.removePrefix('/')
proc isAlias*(self: Utils, value: string): bool {.slot.} =
result = procs_from_accounts.isAlias(value)
proc urlFromUserInput*(self: Utils, input: string): string {.slot.} =
result = url_fromUserInput(input)

View File

@ -27,3 +27,6 @@ method storeIdentityImage*(self: Controller, address: string, image: string, aX:
method deleteIdentityImage*(self: Controller, address: string) =
self.profileService.deleteIdentityImage(address)
method setDisplayName*(self: Controller, displayName: string): bool =
self.profileService.setDisplayName(displayName)

View File

@ -16,3 +16,6 @@ method storeIdentityImage*(self: AccessInterface, address: string, image: string
method deleteIdentityImage*(self: AccessInterface, address: string) {.base.} =
raise newException(ValueError, "No implementation available")
method setDisplayName*(self: AccessInterface, displayName: string): bool {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -22,6 +22,9 @@ method storeIdentityImage*(self: AccessInterface, imageUrl: string, aX: int, aY:
method deleteIdentityImage*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method setDisplayName*(self: AccessInterface, displayName: string) {.base.} =
raise newException(ValueError, "No implementation available")
# View Delegate Interface
# Delegate for the view must be declared here due to use of QtObject and multi
# inheritance, which is not well supported in Nim.

View File

@ -66,3 +66,9 @@ method deleteIdentityImage*(self: Module) =
self.controller.deleteIdentityImage(address)
singletonInstance.userProfile.setLargeImage("")
singletonInstance.userProfile.setThumbnailImage("")
method setDisplayName*(self: Module, displayName: string) =
if self.controller.setDisplayName(displayName):
singletonInstance.userProfile.setDisplayName(displayName)
else:
error "could not set display name"

View File

@ -27,6 +27,8 @@ QtObject:
self.delegate.storeIdentityImage(imageUrl, aX, aY, bX, bY)
proc remove*(self: View): string {.slot.} =
self.delegate.deleteIdentityImage()
proc setDisplayName(self: View, displayName: string) {.slot.} =
self.delegate.setDisplayName(displayName)

View File

@ -19,6 +19,7 @@ type
events: EventEmitter
accountsService: accounts_service.ServiceInterface
selectedAccountId: string
displayName: string
proc newController*(delegate: io_interface.AccessInterface,
events: EventEmitter,
@ -48,8 +49,11 @@ method setSelectedAccountByIndex*(self: Controller, index: int) =
let accounts = self.getGeneratedAccounts()
self.selectedAccountId = accounts[index].id
method setDisplayName*(self: Controller, displayName: string) =
self.displayName = displayName
method storeSelectedAccountAndLogin*(self: Controller, password: string) =
if(not self.accountsService.setupAccount(self.selectedAccountId, password)):
if(not self.accountsService.setupAccount(self.selectedAccountId, password, self.displayName)):
self.delegate.setupAccountError()
method validateMnemonic*(self: Controller, mnemonic: string): string =

View File

@ -16,6 +16,9 @@ method getGeneratedAccounts*(self: AccessInterface): seq[GeneratedAccountDto] {.
method setSelectedAccountByIndex*(self: AccessInterface, index: int) {.base.} =
raise newException(ValueError, "No implementation available")
method setDisplayName*(self: AccessInterface, displayName: string) {.base.} =
raise newException(ValueError, "No implementation available")
method storeSelectedAccountAndLogin*(self: AccessInterface, password: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -53,6 +53,9 @@ method viewDidLoad*(self: Module) =
method setSelectedAccountByIndex*(self: Module, index: int) =
self.controller.setSelectedAccountByIndex(index)
method setDisplayName*(self: Module, displayName: string) =
self.controller.setDisplayName(displayName)
method storeSelectedAccountAndLogin*(self: Module, password: string) =
self.controller.storeSelectedAccountAndLogin(password)

View File

@ -19,3 +19,6 @@ method validateMnemonic*(self: AccessInterface, mnemonic: string):
method importMnemonic*(self: AccessInterface, mnemonic: string) {.base.} =
raise newException(ValueError, "No implementation available")
method setDisplayName*(self: AccessInterface, displayName: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -60,6 +60,9 @@ QtObject:
read = getImportedAccountAddress
notify = importedAccountChanged
proc setDisplayName*(self: View, displayName: string) {.slot.} =
self.delegate.setDisplayName(displayName)
proc setSelectedAccountByIndex*(self: View, index: int) {.slot.} =
self.delegate.setSelectedAccountByIndex(index)

View File

@ -49,6 +49,9 @@ proc generateAliasFromPk*(publicKey: string): string =
proc generateIdenticonFromPk*(publicKey: string): string =
return status_account.generateIdenticon(publicKey).result.getStr
proc isAlias*(value: string): bool =
return status_account.isAlias(value)
method init*(self: Service) =
try:
let response = status_account.generateAddresses(PATHS)
@ -179,12 +182,13 @@ proc getSubaccountDataForAccountId(self: Service, accountId: string): JsonNode =
return self.prepareSubaccountJsonObject(self.importedAccount)
proc prepareAccountSettingsJsonObject(self: Service, account: GeneratedAccountDto,
installationId: string): JsonNode =
installationId: string, displayName: string): JsonNode =
result = %* {
"key-uid": account.keyUid,
"mnemonic": account.mnemonic,
"public-key": account.derivedAccounts.whisper.publicKey,
"name": account.alias,
"display-name": displayName,
"address": account.address,
"eip1581-address": account.derivedAccounts.eip1581.address,
"dapps-address": account.derivedAccounts.defaultWallet.address,
@ -206,14 +210,15 @@ proc prepareAccountSettingsJsonObject(self: Service, account: GeneratedAccountDt
}
proc getAccountSettings(self: Service, accountId: string,
installationId: string): JsonNode =
installationId: string,
displayName: string): JsonNode =
for acc in self.generatedAccounts:
if(acc.id == accountId):
return self.prepareAccountSettingsJsonObject(acc, installationId)
return self.prepareAccountSettingsJsonObject(acc, installationId, displayName)
if(self.importedAccount.isValid()):
if(self.importedAccount.id == accountId):
return self.prepareAccountSettingsJsonObject(self.importedAccount, installationId)
return self.prepareAccountSettingsJsonObject(self.importedAccount, installationId, displayName)
proc getDefaultNodeConfig*(self: Service, installationId: string): JsonNode =
let networkConfig = getNetworkConfig(DEFAULT_NETWORK_NAME)
@ -244,12 +249,12 @@ proc getDefaultNodeConfig*(self: Service, installationId: string): JsonNode =
# TODO: commented since it's not necessary (we do the connections thru C bindings). Enable it thru an option once status-nodes are able to be configured in desktop
# result["ListenAddr"] = if existsEnv("STATUS_PORT"): newJString("0.0.0.0:" & $getEnv("STATUS_PORT")) else: newJString("0.0.0.0:30305")
method setupAccount*(self: Service, accountId, password: string): bool =
method setupAccount*(self: Service, accountId, password, displayName: string): bool =
try:
let installationId = $genUUID()
let accountDataJson = self.getAccountDataForAccountId(accountId)
let subaccountDataJson = self.getSubaccountDataForAccountId(accountId)
let settingsJson = self.getAccountSettings(accountId, installationId)
let settingsJson = self.getAccountSettings(accountId, installationId, displayName)
let nodeConfigJson = self.getDefaultNodeConfig(installationId)
if(accountDataJson.isNil or subaccountDataJson.isNil or settingsJson.isNil or

View File

@ -20,7 +20,7 @@ method openedAccounts*(self: ServiceInterface): seq[AccountDto] {.base.} =
method generatedAccounts*(self: ServiceInterface): seq[GeneratedAccountDto] {.base.} =
raise newException(ValueError, "No implementation available")
method setupAccount*(self: ServiceInterface, accountId, password: string): bool {.base.} =
method setupAccount*(self: ServiceInterface, accountId, password, displayName: string): bool {.base.} =
raise newException(ValueError, "No implementation available")
method getLoggedInAccount*(self: ServiceInterface): AccountDto {.base.} =
@ -47,5 +47,8 @@ method clear*(self: ServiceInterface) {.base.} =
method generateAlias*(self: ServiceInterface, publicKey: string): string {.base.} =
raise newException(ValueError, "No implementation available")
method isAlias*(self: ServiceInterface, value: string): bool {.base.} =
raise newException(ValueError, "No implementation available")
method verifyAccountPassword*(self: ServiceInterface, account: string, password: string): bool {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -40,3 +40,11 @@ method deleteIdentityImage*(self: Service, address: string) =
except Exception as e:
error "error: ", methodName="deleteIdentityImage", errName = e.name, errDesription = e.msg
method setDisplayName*(self: Service, displayName: string): bool =
try:
discard status_accounts.setDisplayName(displayName)
return true
except Exception as e:
error "error: ", methodName="setDisplayName", errName = e.name, errDesription = e.msg
return false

View File

@ -18,3 +18,6 @@ method storeIdentityImage*(self: ServiceInterface, address: string, image: strin
method deleteIdentityImage*(self: ServiceInterface, address: string) {.base.} =
raise newException(ValueError, "No implementation available")
method setDisplayName*(self: ServiceInterface, displayName: string): bool {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -44,6 +44,7 @@ const KEY_AUTO_MESSAGE_ENABLED* = "auto-message-enabled?"
const KEY_GIF_FAVORITES* = "gifs/favorite-gifs"
const KEY_GIF_RECENTS* = "gifs/recent-gifs"
const KEY_GIF_API_KEY* = "gifs/api-key"
const KEY_DISPLAY_NAME = "display-name"
const PROFILE_PICTURES_VISIBILITY_CONTACTS_ONLY* = 1
const PROFILE_PICTURES_VISIBILITY_EVERYONE* = 2
@ -88,6 +89,7 @@ type
dappsAddress*: string
eip1581Address*: string
installationId*: string
displayName*: string
preferredName*: string
ensUsernames*: seq[string]
keyUid*: string
@ -183,6 +185,7 @@ proc toSettingsDto*(jsonObj: JsonNode): SettingsDto =
discard jsonObj.getProp(KEY_EIP1581_ADDRESS, result.eip1581Address)
discard jsonObj.getProp(KEY_INSTALLATION_ID, result.installationId)
discard jsonObj.getProp(KEY_PREFERRED_NAME, result.preferredName)
discard jsonObj.getProp(KEY_DISPLAY_NAME, result.displayName)
discard jsonObj.getProp(KEY_KEY_UID, result.keyUid)
discard jsonObj.getProp(KEY_LATEST_DERIVED_PATH, result.latestDerivedPath)
discard jsonObj.getProp(KEY_LINK_PREVIEW_REQUEST_ENABLED, result.linkPreviewRequestEnabled)

View File

@ -109,6 +109,9 @@ method savePreferredName*(self: Service, value: string): bool =
method getPreferredName*(self: Service): string =
return self.settings.preferredName
method getDisplayName*(self: Service): string =
return self.settings.displayName
method saveNewEnsUsername*(self: Service, username: string): bool =
var newEnsUsernames = self.settings.ensUsernames
newEnsUsernames.add(username)

View File

@ -65,6 +65,9 @@ method savePreferredName*(self: ServiceInterface, value: string): bool {.base.}
method getPreferredName*(self: ServiceInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method getDisplayName*(self: ServiceInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method saveNewEnsUsername*(self: ServiceInterface, username: string): bool {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -9,7 +9,7 @@ export response_type
logScope:
topics = "rpc-accounts"
const NUMBER_OF_ADDRESSES_TO_GENERATE = 5
const NUMBER_OF_ADDRESSES_TO_GENERATE = 1
const MNEMONIC_PHRASE_LENGTH = 12
const GENERATED* = "generated"
@ -61,6 +61,11 @@ proc generateAlias*(publicKey: string): RpcResponse[JsonNode] {.raises: [Excepti
error "error doing rpc request", methodName = "generateAlias", exception=e.msg
raise newException(RpcException, e.msg)
proc isAlias*(value: string): bool =
let response = status_go.isAlias(value)
let r = Json.decode(response, JsonNode)
return r["result"].getBool()
proc generateIdenticon*(publicKey: string): RpcResponse[JsonNode] {.raises: [Exception].} =
try:
let response = status_go.identicon(publicKey)
@ -255,3 +260,7 @@ proc storeIdentityImage*(keyUID: string, imagePath: string, aX, aY, bX, bY: int)
proc deleteIdentityImage*(keyUID: string): RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %* [keyUID]
result = core.callPrivateRPC("multiaccounts_deleteIdentityImage", payload)
proc setDisplayName*(displayName: string): RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %* [displayName]
result = core.callPrivateRPC("setDisplayName".prefix, payload)

View File

@ -5,10 +5,14 @@ import QtGraphicalEffects 1.13
import StatusQ.Controls 0.1
import utils 1.0
import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import "../panels"
import shared.controls 1.0
import StatusQ.Controls 0.1
import StatusQ.Popups 0.1
import "../panels"1
import "../stores"
// TODO: replace with StatusModal
@ -21,9 +25,40 @@ ModalPopup {
title: qsTrId("intro-wizard-title2")
height: 504
property string displayNameValidationError: ""
Input {
id: displayNameInput
placeholderText: "DisplayName"
validationError: displayNameValidationError
onTextChanged: {
if(displayNameInput.text === ""){
displayNameValidationError = qsTr("Display name is required")
} else if (!displayNameInput.text.match(/^[a-zA-Z0-9\- ]+$/)){
displayNameValidationError = qsTr("Only letters, numbers, underscores and hyphens allowed")
} else if (displayNameInput.text.length > 24) {
displayNameValidationError = qsTr("24 character username limit")
} else if (displayNameInput.text.length < 5) {
displayNameValidationError = qsTr("Username must be at least 5 characters")
} else if (displayNameInput.text.endsWith(".eth")) {
displayNameValidationError = qsTr(`Usernames ending with ".eth" are not allowed`)
} else if (displayNameInput.text.endsWith("-eth")) {
displayNameValidationError = qsTr(`Usernames ending with "-eth" are not allowed`)
} else if (displayNameInput.text.endsWith("_eth")) {
displayNameValidationError = qsTr(`Usernames ending with "_eth" are not allowed`)
} else if (globalUtils.isAlias(displayNameInput.text)){
displayNameValidationError = qsTr("Sorry, the name you have chosen is not allowed, try picking another username")
}
}
}
AccountListPanel {
id: accountList
anchors.fill: parent
anchors.top: displayNameInput.bottom
anchors.topMargin: 100
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
interactive: false
model: OnboardingStore.onBoardingModul.accountsModel
@ -34,9 +69,11 @@ ModalPopup {
selectedIndex = index
}
}
footer: StatusRoundButton {
objectName: "submitButton"
id: submitBtn
enabled: displayNameInput.text !== ""
anchors.bottom: parent.bottom
anchors.topMargin: Style.current.padding
anchors.right: parent.right
@ -44,7 +81,7 @@ ModalPopup {
icon.width: 20
icon.height: 16
onClicked : {
onNextClick(selectedIndex);
onNextClick(selectedIndex, displayNameInput.text);
popup.close()
}
}

View File

@ -9,7 +9,8 @@ QtObject {
onBoardingModul.importMnemonic(mnemonic)
}
function setCurrentAccount(selectedAccountIdx) {
function setCurrentAccountAndDisplayName(selectedAccountIdx, displayName) {
onBoardingModul.setDisplayName(displayName)
onBoardingModul.setSelectedAccountByIndex(selectedAccountIdx)
}

View File

@ -16,9 +16,9 @@ Item {
GenKeyModal {
property bool wentNext: false
id: genKeyModal
onNextClick: function (selectedIndex) {
onNextClick: function (selectedIndex, displayName) {
wentNext = true
OnboardingStore.setCurrentAccount(selectedIndex)
OnboardingStore.setCurrentAccountAndDisplayName(selectedIndex, displayName)
createPasswordModal.open()
}
onClosed: function () {

View File

@ -9,6 +9,7 @@ QtObject {
property string pubkey: userProfile.pubKey
property string name: userProfile.name // in case of ens returns pretty ens form
property string username: userProfile.username
property string displayName: userProfile.displayName
property string ensName: userProfile.preferredName || userProfile.firstEnsName || userProfile.ensName
property string profileLargeImage: userProfile.largeImage
property string icon: userProfile.icon
@ -30,4 +31,8 @@ QtObject {
function copyToClipboard(value) {
globalUtils.copyToClipboard(value)
}
function setDisplayName(displayName) {
root.profileModule.setDisplayName(displayName)
}
}

View File

@ -0,0 +1,53 @@
import QtQuick 2.12
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQml.Models 2.3
import utils 1.0
import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.controls 1.0
import StatusQ.Controls 0.1
import StatusQ.Popups 0.1
StatusModal {
id: popup
property var profileStore
onOpened: {
displayNameInput.forceActiveFocus(Qt.MouseFocusReason);
}
contentItem: Item {
width: popup.width
height: childrenRect.height
Column {
anchors.top: parent.top
anchors.topMargin: 16
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 32
spacing: 16
Input {
id: displayNameInput
placeholderText: "DisplayName"
text: root.profileStore.displayName
//validationError: popup.nicknameTooLong ? qsTrId("your-nickname-is-too-long") : ""
}
}
}
rightButtons: [
StatusButton {
id: doneBtn
text: "Ok"
// enabled: !popup.nicknameTooLong
onClicked: {
popup.profileStore.setDisplayName(displayNameInput.text)
}
}
]
}

View File

@ -85,6 +85,26 @@ Item {
color: Theme.palette.directColor1
}
Component {
id: displayNamePopupComponent
DisplayNamePopup {
profileStore: root.profileStore
onClosed: {
destroy()
}
}
}
StatusButton {
id: "editDisplayName"
text: "Edit"
anchors.left: profileName.right
anchors.leftMargin: Style.current.halfPadding
onClicked: {
Global.openPopup(displayNamePopupComponent);
}
}
Address {
id: pubkeyText
text: root.profileStore.ensName !== "" ? root.profileStore.username : root.profileStore.pubkey

@ -1 +1 @@
Subproject commit a47b8fa2eb66f270403121774220001ab0fbd59e
Subproject commit 50e8f7eef01d801fc3be0168a0eae37137b08439

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 4f5121b4efd4bd8bb3d0de5a357baa70f3068c6c
Subproject commit 50ec6f97e01bbd8b61672a1c7db938774b233dbe