diff --git a/src/app/profile/core.nim b/src/app/profile/core.nim index 74ea6b4c0e..f0d3998f78 100644 --- a/src/app/profile/core.nim +++ b/src/app/profile/core.nim @@ -11,6 +11,7 @@ import ../../status/chat as status_chat import ../../status/devices import ../../status/chat/chat import view +import views/ens_manager import chronicles type ProfileController* = ref object of SignalSubscriber @@ -31,10 +32,6 @@ proc delete*(self: ProfileController) = proc init*(self: ProfileController, account: Account) = 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 mnemonic = status_settings.getSetting[string](Setting.Mnemonic, "") 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.setMnemonic(mnemonic) self.view.setNetwork(network) + self.view.ens.init() var mailservers = status_mailservers.getMailservers() for mailserver_config in mailservers: diff --git a/src/app/profile/view.nim b/src/app/profile/view.nim index b555a06926..c2f14f85c3 100644 --- a/src/app/profile/view.nim +++ b/src/app/profile/view.nim @@ -49,7 +49,7 @@ QtObject: result.addedContacts = newContactList() result.blockedContacts = newContactList() result.deviceList = newDeviceList() - result.ens = newEnsManager() + result.ens = newEnsManager(status) result.mnemonic = "" result.network = "" result.status = status diff --git a/src/app/profile/views/ens_manager.nim b/src/app/profile/views/ens_manager.nim index 80c513e209..03faf52ac5 100644 --- a/src/app/profile/views/ens_manager.nim +++ b/src/app/profile/views/ens_manager.nim @@ -1,50 +1,95 @@ import NimQml import Tables +import json +import json_serialization +import sequtils +from ../../../status/libstatus/types import Setting import ../../../status/threads import ../../../status/ens as status_ens import ../../../status/libstatus/settings as status_settings -import ../../../status/libstatus/types +import ../../../status/status + +type + EnsRoles {.pure.} = enum + UserName = UserRole + 1 QtObject: type EnsManager* = ref object of QAbstractListModel + usernames*: seq[string] + status: Status proc setup(self: EnsManager) = self.QAbstractListModel.setup proc delete(self: EnsManager) = + self.usernames = @[] self.QAbstractListModel.delete - proc newEnsManager*(): EnsManager = + proc newEnsManager*(status: Status): EnsManager = new(result, delete) + result.usernames = @[] + result.status = status result.setup - proc validate*(self: EnsManager, ens: string, isStatus: bool) {.slot.} = - spawnAndSend(self, "ensResolved") do: - 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 init*(self: EnsManager) = + self.usernames = status_settings.getSetting[seq[string]](Setting.Usernames, @[]) proc ensWasResolved*(self: EnsManager, ensResult: string) {.signal.} proc ensResolved(self: EnsManager, ensResult: string) {.slot.} = - self.ensWasResolved(ensResult) \ No newline at end of file + 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() diff --git a/src/status/libstatus/types.nim b/src/status/libstatus/types.nim index 324abcc5be..fed5735f59 100644 --- a/src/status/libstatus/types.nim +++ b/src/status/libstatus/types.nim @@ -150,6 +150,8 @@ type Stickers_Recent = "stickers/recent-stickers" WalletRootAddress = "wallet-root-address" LatestDerivedPath = "latest-derived-path" + PreferredUsername = "preferred-name" + Usernames = "usernames" UpstreamConfig* = ref object enabled* {.serializedFieldName("Enabled").}: bool diff --git a/ui/app/AppLayouts/Profile/ProfileLayout.qml b/ui/app/AppLayouts/Profile/ProfileLayout.qml index 93ac69eeb5..8283a540bb 100644 --- a/ui/app/AppLayouts/Profile/ProfileLayout.qml +++ b/ui/app/AppLayouts/Profile/ProfileLayout.qml @@ -46,9 +46,17 @@ SplitView { address: profileModel.profile.address } + onCurrentIndexChanged: { + if(visibleChildren[0] === ensContainer){ + ensContainer.goToStart(); + } + } + ContactsContainer {} - EnsContainer {} + EnsContainer { + id: ensContainer + } PrivacyContainer {} diff --git a/ui/app/AppLayouts/Profile/Sections/Ens/Added.qml b/ui/app/AppLayouts/Profile/Sections/Ens/Added.qml new file mode 100644 index 0000000000..9c2326ec99 --- /dev/null +++ b/ui/app/AppLayouts/Profile/Sections/Ens/Added.qml @@ -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() + } +} \ No newline at end of file diff --git a/ui/app/AppLayouts/Profile/Sections/Ens/List.qml b/ui/app/AppLayouts/Profile/Sections/Ens/List.qml new file mode 100644 index 0000000000..3b21fdb660 --- /dev/null +++ b/ui/app/AppLayouts/Profile/Sections/Ens/List.qml @@ -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 + } + } + } +} \ No newline at end of file diff --git a/ui/app/AppLayouts/Profile/Sections/Ens/Search.qml b/ui/app/AppLayouts/Profile/Sections/Ens/Search.qml index 102d7de490..89d4aa5976 100644 --- a/ui/app/AppLayouts/Profile/Sections/Ens/Search.qml +++ b/ui/app/AppLayouts/Profile/Sections/Ens/Search.qml @@ -6,9 +6,13 @@ import "../../../../../shared" Item { id: searchENS + + property var onClick: function(){} + property string validationMessage: "" property bool valid: false property bool isStatus: true + property string ensStatus: "" property var validateENS: Backpressure.debounce(searchENS, 500, function (ensName, isStatus){ profileModel.ens.validate(ensName, isStatus) @@ -17,6 +21,7 @@ Item { function validate() { validationMessage = ""; valid = false; + ensStatus = ""; if (ensUsername.text.length < 4) { validationMessage = qsTr("At least 4 characters. Latin letters, numbers, and lowercase only."); } else if(isStatus && !ensUsername.text.match(/^[a-z0-9]+$/)){ @@ -80,25 +85,32 @@ Item { Connections { target: profileModel.ens onEnsWasResolved: { - valid = false + valid = false; + ensStatus = ensResult; switch(ensResult){ case "available": valid = true; validationMessage = qsTr("✓ Username available!"); break; case "owned": - console.log("TODO: -"); + console.log("TODO: - Continuing will connect this username with your chat key."); case "taken": validationMessage = !isStatus ? qsTr("Username doesn’t belong to you :(") : qsTr("Username already taken :("); break; + case "already-connected": + validationMessage = qsTr("Username is already connected with your chat key and can be used inside Status."); + break; 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; case "connected-different-key": - validationMessage = qsTr("Username doesn’t 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 onClicked : { 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; + } } } } diff --git a/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml b/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml new file mode 100644 index 0000000000..52a8945593 --- /dev/null +++ b/ui/app/AppLayouts/Profile/Sections/Ens/TermsAndConditions.qml @@ -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() + } +} \ No newline at end of file diff --git a/ui/app/AppLayouts/Profile/Sections/EnsContainer.qml b/ui/app/AppLayouts/Profile/Sections/EnsContainer.qml index eb846645fd..efeb448486 100644 --- a/ui/app/AppLayouts/Profile/Sections/EnsContainer.qml +++ b/ui/app/AppLayouts/Profile/Sections/EnsContainer.qml @@ -12,9 +12,21 @@ Item { Layout.fillWidth: true property bool showSearchScreen: false + property string addedUsername: "" - signal next() - signal back() + signal next(output: string) + signal connect(ensUsername: string) + + signal goToWelcome(); + signal goToList(); + + function goToStart(){ + if(profileModel.ens.rowCount() > 0){ + goToList(); + } else { + goToWelcome(); + } + } DSM.StateMachine { id: stateMachine @@ -28,15 +40,84 @@ Item { targetState: searchState signal: next } + DSM.SignalTransition { + targetState: listState + signal: goToList + } } DSM.State { id: searchState 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 { - id: ensFinalState + + DSM.State { + 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 Welcome { onClick: function(){ - next(); + next(null); } } } Component { 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; + } } }