feat: connect existing status ens username

- Show welcome page only when there are no ens names registered
- If you already have an ens username for the current account, connect and mark it as preferred name
- State machine navigation
This commit is contained in:
Richard Ramos 2020-08-04 18:22:51 -04:00 committed by Pascal Precht
parent 613c70c4a6
commit 43f4f8775b
No known key found for this signature in database
GPG Key ID: 0EE28D8D6FD85D7D
10 changed files with 462 additions and 47 deletions

View File

@ -11,6 +11,7 @@ import ../../status/chat as status_chat
import ../../status/devices import ../../status/devices
import ../../status/chat/chat import ../../status/chat/chat
import view import view
import views/ens_manager
import chronicles import chronicles
type ProfileController* = ref object of SignalSubscriber type ProfileController* = ref object of SignalSubscriber
@ -31,10 +32,6 @@ proc delete*(self: ProfileController) =
proc init*(self: ProfileController, account: Account) = proc init*(self: ProfileController, account: Account) =
let profile = account.toProfileModel() let profile = account.toProfileModel()
# (rramos) TODO: I added this because I needed the public key
# Ideally, this module should call getSettings once, and fill the
# profile with all the information comming from the settings.
let response = status_settings.getSettings()
let pubKey = status_settings.getSetting[string](Setting.PublicKey, "0x0") let pubKey = status_settings.getSetting[string](Setting.PublicKey, "0x0")
let mnemonic = status_settings.getSetting[string](Setting.Mnemonic, "") let mnemonic = status_settings.getSetting[string](Setting.Mnemonic, "")
let network = status_settings.getSetting[string](Setting.Networks_CurrentNetwork, constants.DEFAULT_NETWORK_NAME) let network = status_settings.getSetting[string](Setting.Networks_CurrentNetwork, constants.DEFAULT_NETWORK_NAME)
@ -48,6 +45,7 @@ proc init*(self: ProfileController, account: Account) =
self.view.setNewProfile(profile) self.view.setNewProfile(profile)
self.view.setMnemonic(mnemonic) self.view.setMnemonic(mnemonic)
self.view.setNetwork(network) self.view.setNetwork(network)
self.view.ens.init()
var mailservers = status_mailservers.getMailservers() var mailservers = status_mailservers.getMailservers()
for mailserver_config in mailservers: for mailserver_config in mailservers:

View File

@ -49,7 +49,7 @@ QtObject:
result.addedContacts = newContactList() result.addedContacts = newContactList()
result.blockedContacts = newContactList() result.blockedContacts = newContactList()
result.deviceList = newDeviceList() result.deviceList = newDeviceList()
result.ens = newEnsManager() result.ens = newEnsManager(status)
result.mnemonic = "" result.mnemonic = ""
result.network = "" result.network = ""
result.status = status result.status = status

View File

@ -1,50 +1,95 @@
import NimQml import NimQml
import Tables import Tables
import json
import json_serialization
import sequtils
from ../../../status/libstatus/types import Setting
import ../../../status/threads import ../../../status/threads
import ../../../status/ens as status_ens import ../../../status/ens as status_ens
import ../../../status/libstatus/settings as status_settings import ../../../status/libstatus/settings as status_settings
import ../../../status/libstatus/types import ../../../status/status
type
EnsRoles {.pure.} = enum
UserName = UserRole + 1
QtObject: QtObject:
type EnsManager* = ref object of QAbstractListModel type EnsManager* = ref object of QAbstractListModel
usernames*: seq[string]
status: Status
proc setup(self: EnsManager) = self.QAbstractListModel.setup proc setup(self: EnsManager) = self.QAbstractListModel.setup
proc delete(self: EnsManager) = proc delete(self: EnsManager) =
self.usernames = @[]
self.QAbstractListModel.delete self.QAbstractListModel.delete
proc newEnsManager*(): EnsManager = proc newEnsManager*(status: Status): EnsManager =
new(result, delete) new(result, delete)
result.usernames = @[]
result.status = status
result.setup result.setup
proc validate*(self: EnsManager, ens: string, isStatus: bool) {.slot.} = proc init*(self: EnsManager) =
spawnAndSend(self, "ensResolved") do: self.usernames = status_settings.getSetting[seq[string]](Setting.Usernames, @[])
var username = ens
if(isStatus): username = username & status_ens.domain
let ownerAddr = status_ens.owner(username)
var output = ""
if ownerAddr == "" and isStatus:
output = "available"
else:
if not isStatus:
let userPubKey = status_settings.getSetting[string](Setting.PublicKey, "0x0")
if ownerAddr != "":
let pubkey = status_ens.pubkey(ens)
if pubkey == "":
output = "owned"
else:
if pubkey == userPubKey:
output = "connected"
else:
output = "connected-different-key"
else:
output = "taken"
else:
output = "taken"
output
proc ensWasResolved*(self: EnsManager, ensResult: string) {.signal.} proc ensWasResolved*(self: EnsManager, ensResult: string) {.signal.}
proc ensResolved(self: EnsManager, ensResult: string) {.slot.} = proc ensResolved(self: EnsManager, ensResult: string) {.slot.} =
self.ensWasResolved(ensResult) self.ensWasResolved(ensResult)
proc validate*(self: EnsManager, ens: string, isStatus: bool) {.slot.} =
let username = ens & (if(isStatus): status_ens.domain else: "")
if self.usernames.filter(proc(x: string):bool = x == username).len > 0:
self.ensResolved("already-connected")
else:
spawnAndSend(self, "ensResolved") do:
let ownerAddr = status_ens.owner(username)
var output = ""
if ownerAddr == "" and isStatus:
output = "available"
else:
let userPubKey = status_settings.getSetting[string](Setting.PublicKey, "0x0")
let userWallet = status_settings.getSetting[string](Setting.WalletRootAddress, "0x0")
let pubkey = status_ens.pubkey(ens)
if ownerAddr != "":
if pubkey == "":
output = "owned" # "Continuing will connect this username with your chat key."
elif pubkey == userPubkey:
output = "connected"
elif ownerAddr == userWallet:
output = "connected-different-key" # "Continuing will require a transaction to connect the username with your current chat key.",
else:
output = "taken"
else:
output = "taken"
output
proc connect(self: EnsManager, username: string) {.slot.} =
let ensUsername = username & status_ens.domain
var usernames = status_settings.getSetting[seq[string]](Setting.Usernames, @[])
let preferredUsername = status_settings.getSetting[string](Setting.PreferredUsername, "")
usernames.add ensUsername
discard status_settings.saveSetting(Setting.Usernames, %*usernames)
discard status_settings.saveSetting(Setting.PreferredUsername, ensUsername)
method rowCount(self: EnsManager, index: QModelIndex = nil): int =
return self.usernames.len
method data(self: EnsManager, index: QModelIndex, role: int): QVariant =
if not index.isValid:
return
if index.row < 0 or index.row >= self.usernames.len:
return
let username = self.usernames[index.row]
result = newQVariant(username)
method roleNames(self: EnsManager): Table[int, string] =
{
EnsRoles.UserName.int:"username"
}.toTable
proc add*(self: EnsManager, username: string) =
self.beginInsertRows(newQModelIndex(), self.usernames.len, self.usernames.len)
self.usernames.add(username)
self.endInsertRows()

View File

@ -150,6 +150,8 @@ type
Stickers_Recent = "stickers/recent-stickers" Stickers_Recent = "stickers/recent-stickers"
WalletRootAddress = "wallet-root-address" WalletRootAddress = "wallet-root-address"
LatestDerivedPath = "latest-derived-path" LatestDerivedPath = "latest-derived-path"
PreferredUsername = "preferred-name"
Usernames = "usernames"
UpstreamConfig* = ref object UpstreamConfig* = ref object
enabled* {.serializedFieldName("Enabled").}: bool enabled* {.serializedFieldName("Enabled").}: bool

View File

@ -46,9 +46,17 @@ SplitView {
address: profileModel.profile.address address: profileModel.profile.address
} }
onCurrentIndexChanged: {
if(visibleChildren[0] === ensContainer){
ensContainer.goToStart();
}
}
ContactsContainer {} ContactsContainer {}
EnsContainer {} EnsContainer {
id: ensContainer
}
PrivacyContainer {} PrivacyContainer {}

View File

@ -0,0 +1,78 @@
import QtQuick 2.14
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.14
import "../../../../../imports"
import "../../../../../shared"
Item {
property string ensUsername: ""
property var onClick: function(){}
StyledText {
id: sectionTitle
//% "ENS usernames"
text: qsTrId("ens-usernames")
anchors.left: parent.left
anchors.leftMargin: 24
anchors.top: parent.top
anchors.topMargin: 24
font.weight: Font.Bold
font.pixelSize: 20
}
Rectangle {
id: circle
anchors.top: sectionTitle.bottom
anchors.topMargin: 24
anchors.horizontalCenter: parent.horizontalCenter
width: 60
height: 60
radius: 120
color: Style.current.blue
StyledText {
text: "✓"
opacity: 0.7
font.weight: Font.Bold
font.pixelSize: 18
color: Style.current.white
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
id: title
text: qsTr("Username added")
anchors.top: circle.bottom
anchors.topMargin: 24
font.weight: Font.Bold
font.pixelSize: 24
anchors.left: parent.left
anchors.right: parent.right
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
StyledText {
id: subtitle
text: qsTr("%1 is now connected with your chat key and can be used in Status.").arg(ensUsername)
anchors.top: title.bottom
anchors.topMargin: 24
font.pixelSize: 14
anchors.left: parent.left
anchors.right: parent.right
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
StyledButton {
id: startBtn
anchors.top: subtitle.bottom
anchors.topMargin: Style.current.padding
anchors.horizontalCenter: parent.horizontalCenter
label: qsTr("Ok, got it")
onClicked: onClick()
}
}

View File

@ -0,0 +1,77 @@
import QtQuick 2.14
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.14
import "../../../../../imports"
import "../../../../../shared"
Item {
property var onClick: function(){}
StyledText {
id: sectionTitle
//% "ENS usernames"
text: qsTrId("ens-usernames")
anchors.left: parent.left
anchors.leftMargin: 24
anchors.top: parent.top
anchors.topMargin: 24
font.weight: Font.Bold
font.pixelSize: 20
}
Item {
id: addUsername
anchors.top: sectionTitle.bottom
anchors.topMargin: Style.current.bigPadding
width: addButton.width + usernameText.width + Style.current.padding
height: addButton.height
AddButton {
id: addButton
clickable: false
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 40
}
StyledText {
id: usernameText
text: qsTr("Add username")
color: Style.current.blue
anchors.left: addButton.right
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: addButton.verticalCenter
font.pixelSize: 15
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: onClick()
}
}
ScrollView {
id: sview
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentHeight: contentItem.childrenRect.height
anchors.top: addUsername.bottom
anchors.topMargin: Style.current.padding
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
Item {
id: contentItem
anchors.right: parent.right;
anchors.left: parent.left;
StyledText {
id: title
text: "TODO: Show ENS username list"
anchors.top: parent.top
}
}
}
}

View File

@ -6,9 +6,13 @@ import "../../../../../shared"
Item { Item {
id: searchENS id: searchENS
property var onClick: function(){}
property string validationMessage: "" property string validationMessage: ""
property bool valid: false property bool valid: false
property bool isStatus: true property bool isStatus: true
property string ensStatus: ""
property var validateENS: Backpressure.debounce(searchENS, 500, function (ensName, isStatus){ property var validateENS: Backpressure.debounce(searchENS, 500, function (ensName, isStatus){
profileModel.ens.validate(ensName, isStatus) profileModel.ens.validate(ensName, isStatus)
@ -17,6 +21,7 @@ Item {
function validate() { function validate() {
validationMessage = ""; validationMessage = "";
valid = false; valid = false;
ensStatus = "";
if (ensUsername.text.length < 4) { if (ensUsername.text.length < 4) {
validationMessage = qsTr("At least 4 characters. Latin letters, numbers, and lowercase only."); validationMessage = qsTr("At least 4 characters. Latin letters, numbers, and lowercase only.");
} else if(isStatus && !ensUsername.text.match(/^[a-z0-9]+$/)){ } else if(isStatus && !ensUsername.text.match(/^[a-z0-9]+$/)){
@ -80,25 +85,32 @@ Item {
Connections { Connections {
target: profileModel.ens target: profileModel.ens
onEnsWasResolved: { onEnsWasResolved: {
valid = false valid = false;
ensStatus = ensResult;
switch(ensResult){ switch(ensResult){
case "available": case "available":
valid = true; valid = true;
validationMessage = qsTr("✓ Username available!"); validationMessage = qsTr("✓ Username available!");
break; break;
case "owned": case "owned":
console.log("TODO: -"); console.log("TODO: - Continuing will connect this username with your chat key.");
case "taken": case "taken":
validationMessage = !isStatus ? validationMessage = !isStatus ?
qsTr("Username doesnt belong to you :(") qsTr("Username doesnt belong to you :(")
: :
qsTr("Username already taken :("); qsTr("Username already taken :(");
break; break;
case "already-connected":
validationMessage = qsTr("Username is already connected with your chat key and can be used inside Status.");
break;
case "connected": case "connected":
validationMessage = qsTr("This user name is owned by you and connected with your chat key."); valid = true;
validationMessage = qsTr("This user name is owned by you and connected with your chat key. Continue to set `Show my ENS username in chats`.");
break; break;
case "connected-different-key": case "connected-different-key":
validationMessage = qsTr("Username doesnt belong to you :("); valid = true;
validationMessage = qsTr("Continuing will require a transaction to connect the username with your current chat key.");
break;
} }
} }
} }
@ -125,7 +137,17 @@ Item {
anchors.fill: parent anchors.fill: parent
onClicked : { onClicked : {
if(!valid) return; if(!valid) return;
console.log("TODO: show ens T&C")
if(ensStatus === "connected"){
profileModel.ens.connect(ensUsername.text);
onClick(ensStatus, ensUsername.text);
return;
}
if(ensStatus === "available"){
onClick(ensStatus, ensUsername.text);
return;
}
} }
} }
} }

View File

@ -0,0 +1,62 @@
import QtQuick 2.14
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.14
import "../../../../../imports"
import "../../../../../shared"
Item {
property var onClick: function(){}
StyledText {
id: sectionTitle
//% "ENS usernames"
text: qsTrId("ens-usernames")
anchors.left: parent.left
anchors.leftMargin: 24
anchors.top: parent.top
anchors.topMargin: 24
font.weight: Font.Bold
font.pixelSize: 20
}
ScrollView {
id: sview
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentHeight: contentItem.childrenRect.height
anchors.top: sectionTitle.bottom
anchors.topMargin: Style.current.padding
anchors.bottom: startBtn.top
anchors.bottomMargin: Style.current.padding
anchors.left: parent.left
anchors.right: parent.right
Item {
id: contentItem
anchors.right: parent.right;
anchors.left: parent.left;
StyledText {
id: title
text: qsTr("TODO: show T&C and confirmation screen for acquiring a ens username")
anchors.top: parent.top
anchors.topMargin: 24
font.weight: Font.Bold
font.pixelSize: 24
anchors.left: parent.left
anchors.right: parent.right
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
}
}
StyledButton {
id: startBtn
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.current.padding
anchors.horizontalCenter: parent.horizontalCenter
label: qsTr("Ok")
onClicked: onClick()
}
}

View File

@ -12,9 +12,21 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
property bool showSearchScreen: false property bool showSearchScreen: false
property string addedUsername: ""
signal next() signal next(output: string)
signal back() signal connect(ensUsername: string)
signal goToWelcome();
signal goToList();
function goToStart(){
if(profileModel.ens.rowCount() > 0){
goToList();
} else {
goToWelcome();
}
}
DSM.StateMachine { DSM.StateMachine {
id: stateMachine id: stateMachine
@ -28,15 +40,84 @@ Item {
targetState: searchState targetState: searchState
signal: next signal: next
} }
DSM.SignalTransition {
targetState: listState
signal: goToList
}
} }
DSM.State { DSM.State {
id: searchState id: searchState
onEntered: loader.sourceComponent = search onEntered: loader.sourceComponent = search
DSM.SignalTransition {
targetState: tAndCState
signal: next
guard: output === "available"
}
DSM.SignalTransition {
targetState: addedState
signal: connect
}
DSM.SignalTransition {
targetState: listState
signal: goToList
}
DSM.SignalTransition {
targetState: welcomeState
signal: goToWelcome
}
} }
DSM.FinalState { DSM.State {
id: ensFinalState id: addedState
onEntered: {
loader.sourceComponent = added;
loader.item.ensUsername = addedUsername;
}
DSM.SignalTransition {
targetState: listState
signal: next
}
DSM.SignalTransition {
targetState: listState
signal: goToList
}
DSM.SignalTransition {
targetState: welcomeState
signal: goToWelcome
}
}
DSM.State {
id: listState
onEntered: {
loader.sourceComponent = list;
}
DSM.SignalTransition {
targetState: searchState
signal: next
}
DSM.SignalTransition {
targetState: listState
signal: goToList
}
DSM.SignalTransition {
targetState: welcomeState
signal: goToWelcome
}
}
DSM.State {
id: tAndCState
onEntered:loader.sourceComponent = termsAndConditions
DSM.SignalTransition {
targetState: listState
signal: goToList
}
DSM.SignalTransition {
targetState: welcomeState
signal: goToWelcome
}
} }
} }
@ -49,13 +130,55 @@ Item {
id: welcome id: welcome
Welcome { Welcome {
onClick: function(){ onClick: function(){
next(); next(null);
} }
} }
} }
Component { Component {
id: search id: search
Search {} Search {
onClick: function(output, username){
if(output === "connected"){
connect(username)
} else {
next(output);
}
}
}
}
Component {
id: termsAndConditions
TermsAndConditions {
onClick: function(output){
next(output);
}
}
}
Component {
id: added
Added {
onClick: function(){
next(null);
}
}
}
Component {
id: list
List {
onClick: function(){
next(null);
}
}
}
Connections {
target: ensContainer
onConnect: {
addedUsername = ensUsername;
}
} }
} }