feat: start chats with ENS usernames

This commit is contained in:
Richard Ramos 2020-06-25 09:26:58 -04:00 committed by Iuri Matias
parent 4fe19e8130
commit 961139e778
8 changed files with 195 additions and 27 deletions

View File

@ -3,6 +3,7 @@ import NimQml, Tables, json, sequtils, chronicles
import ../../status/status
import ../../status/chat as status_chat
import ../../status/contacts as status_contacts
import ../../status/ens as status_ens
import ../../status/chat/[chat, message]
import ../../status/libstatus/types
import ../../status/profile/profile
@ -228,3 +229,9 @@ QtObject:
proc isEnsVerified*(self: ChatsView, id: string): bool {.slot.} =
if id == "": return false
self.status.contacts.getContactByID(id).ensVerified
proc resolveENS*(self: ChatsView, ens: string): string {.slot.} =
result = status_ens.pubkey(ens)
proc formatENSUsername*(self: ChatsView, username: string): string {.slot.} =
result = status_ens.addDomain(username)

View File

@ -47,9 +47,10 @@ proc mainProc() =
status.events.once("login") do(a: Args):
var args = AccountArgs(a)
status.startMessenger()
chat.init()
wallet.init()
profile.init(args.account)
wallet.init()
chat.init()
var login = login.newController(status)
var onboarding = onboarding.newController(status)

View File

@ -16,7 +16,12 @@ proc newContactModel*(events: EventEmitter): ContactModel =
proc getContactByID*(self: ContactModel, id: string): Profile =
let response = status_contacts.getContactByID(id)
toProfileModel(parseJSON($response)["result"])
# TODO: change to options
let responseResult = parseJSON($response)["result"]
if responseResult.kind == JNull:
result = nil
else:
result = toProfileModel(parseJSON($response)["result"])
proc blockContact*(self: ContactModel, id: string): string =
var contact = self.getContactByID(id)
@ -41,4 +46,5 @@ proc removeContact*(self: ContactModel, id: string) =
proc isAdded*(self: ContactModel, id: string): bool =
var contact = self.getContactByID(id)
if contact.isNil: return false
contact.systemTags.contains(":contact/added")

View File

@ -1,5 +1,15 @@
import strutils
import profile/profile
import nimcrypto
import strmisc
import json
import strformat
import libstatus/core
import stew/byteutils
import sequtils
import unicode
import algorithm
import libstatus/settings as status_settings
let domain* = ".stateofus.eth"
@ -12,8 +22,64 @@ proc userName*(ensName: string, removeSuffix: bool = false): string =
else:
result = ensName
proc addDomain*(username: string): string =
if username.endsWith(".eth"):
return username
else:
return username & domain
proc userNameOrAlias*(contact: Profile): string =
if(contact.ensName != "" and contact.ensVerified):
result = "@" & userName(contact.ensName, true)
else:
result = contact.alias
proc namehash*(ensName:string): string =
let name = ensName.toLower()
var node:array[32, byte]
node.fill(0)
var parts = name.split(".")
for i in countdown(parts.len - 1,0):
let elem = keccak_256.digest(parts[i]).data
var concatArrays: array[64, byte]
concatArrays[0..31] = node
concatArrays[32..63] = elem
node = keccak_256.digest(concatArrays).data
result = "0x" & node.toHex()
const registry = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"
const resolver_signature = "0x0178b8bf"
proc resolver*(usernameHash: string): string =
let payload = %* [{
"to": registry,
"from": "0x0000000000000000000000000000000000000000",
"data": fmt"{resolver_signature}{userNameHash}"
}, "latest"]
let response = callPrivateRPC("eth_call", payload)
# TODO: error handling
var resolverAddr = response.parseJson["result"].getStr
resolverAddr.removePrefix("0x000000000000000000000000")
result = "0x" & resolverAddr
const pubkey_signature = "0xc8690233" # pubkey(bytes32 node)
proc pubkey*(username: string): string =
var userNameHash = namehash(addDomain(username))
userNameHash.removePrefix("0x")
let ensResolver = resolver(userNameHash)
echo ensResolver
let payload = %* [{
"to": ensResolver,
"from": "0x0000000000000000000000000000000000000000",
"data": fmt"{pubkey_signature}{userNameHash}"
}, "latest"]
let response = callPrivateRPC("eth_call", payload)
# TODO: error handling
var pubkey = response.parseJson["result"].getStr
if pubkey == "0x" or pubkey == "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000":
result = ""
else:
pubkey.removePrefix("0x")
result = "0x04" & pubkey

View File

@ -17,6 +17,7 @@ Rectangle {
property bool showCheckbox: true
property bool isChecked: false
property bool showListSelector: false
property var onItemChecked: (function(pubKey, itemChecked) { console.log(pubKey, itemChecked) })
@ -47,9 +48,29 @@ Rectangle {
anchors.leftMargin: Theme.padding
}
SVGImage {
id: image
visible: showListSelector && !showCheckbox
height: 24
width: 24
anchors.top: accountImage.top
anchors.topMargin: 6
anchors.right: parent.right
anchors.rightMargin: Theme.padding
fillMode: Image.PreserveAspectFit
source: "../../../img/list-next.svg"
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
onItemChecked(pubKey, isChecked)
}
}
}
CheckBox {
id: assetCheck
visible: showCheckbox && !isUser
visible: !showListSelector && showCheckbox && !isUser
anchors.top: accountImage.top
anchors.topMargin: 6
anchors.right: parent.right

View File

@ -3,34 +3,47 @@ import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../../../../imports"
import "../../../../shared"
import "../../Profile/Sections/Contacts/"
import "./"
ModalPopup {
property string validationError: ""
property string pubKey : "";
property string ensUsername : "";
function validate() {
// TODO change this when we support ENS names
if (!Utils.isChatKey(chatKey.text)) {
validationError = "This needs to be a valid chat key"
if (!Utils.isChatKey(chatKey.text) && !Utils.isValidETHNamePrefix(chatKey.text)) {
validationError = "This needs to be a valid chat key or ENS username"
pubKey = "";
ensUsername = "";
} else {
validationError = ""
}
return validationError === ""
}
function doJoin() {
if (chatKey.text !== "") {
if (!validate()) {
return
}
function onKeyReleased(){
ensUsername.text = "";
if (!validate()) return;
chatsModel.joinChat(chatKey.text, Constants.chatTypeOneToOne);
} else if (contactListView.selectedContact.checked) {
chatsModel.joinChat(contactListView.selectedContact.parent.address, Constants.chatTypeOneToOne);
} else {
chatKey.text = chatKey.text.trim();
if(Utils.isChatKey(chatKey.text)){
pubKey = chatKey.text;
return;
}
pubKey = chatsModel.resolveENS(chatKey.text)
if(pubKey == ""){
ensUsername.text = qsTr("User not found");
} else {
ensUsername.text = chatsModel.formatENSUsername(chatKey.text) + " • " + Utils.compactAddress(pubKey, 4)
}
}
function doJoin() {
if (!validate() || pubKey.trim() === "") return;
chatsModel.joinChat(pubKey, Constants.chatTypeOneToOne);
popup.close();
}
@ -39,10 +52,9 @@ ModalPopup {
onOpened: {
chatKey.text = "";
pubKey = "";
ensUsername = "";
chatKey.forceActiveFocus(Qt.MouseFocusReason)
if (contactListView.selectedContact) {
contactListView.selectedContact.checked = false
}
}
Input {
@ -51,15 +63,56 @@ ModalPopup {
Keys.onEnterPressed: doJoin()
Keys.onReturnPressed: doJoin()
validationError: popup.validationError
textField.onEditingFinished: {
validate()
Keys.onReleased: {
onKeyReleased();
}
}
ContactList {
Text {
id: ensUsername
anchors.top: chatKey.bottom
anchors.topMargin: Theme.padding
color: Theme.darkGrey
font.pixelSize: 12
}
Item {
anchors.top: ensUsername.bottom
anchors.topMargin: 32
anchors.fill: parent
ScrollView {
anchors.fill: parent
anchors.topMargin: 50
anchors.top: searchBox.bottom
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: groupMembers.contentHeight > groupMembers.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
ListView {
anchors.fill: parent
spacing: 0
clip: true
id: contactListView
contacts: profileModel.contactList
selectable: true
model: profileModel.contactList
delegate: Contact {
showCheckbox: false
pubKey: model.pubKey
isContact: model.isContact
isUser: model.isUser
name: model.name
address: model.address
identicon: model.identicon
showListSelector: true
onItemChecked: function(pubKey, itemChecked){
chatsModel.joinChat(pubKey, Constants.chatTypeOneToOne);
popup.close()
}
}
}
}
}
footer: Button {
@ -68,7 +121,7 @@ ModalPopup {
anchors.bottom: parent.bottom
anchors.right: parent.right
SVGImage {
source: chatKey.text == "" ? "../../../img/arrow-button-inactive.svg" : "../../../img/arrow-btn-active.svg"
source: pubKey === "" ? "../../../img/arrow-button-inactive.svg" : "../../../img/arrow-btn-active.svg"
width: 50
height: 50
}

3
ui/app/img/list-next.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.46967 5.46967C9.76256 5.17678 10.2374 5.17678 10.5303 5.46967L16.1768 11.1161C16.6649 11.6043 16.6649 12.3957 16.1768 12.8839L10.5303 18.5303C10.2374 18.8232 9.76256 18.8232 9.46967 18.5303C9.17678 18.2374 9.17678 17.7626 9.46967 17.4697L14.5858 12.3536C14.781 12.1583 14.781 11.8417 14.5858 11.6464L9.46967 6.53033C9.17678 6.23744 9.17678 5.76256 9.46967 5.46967Z" fill="#939BA1"/>
</svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@ -15,6 +15,10 @@ QtObject {
return startsWith0x(value) && isHex(value) && value.length === 132
}
function isValidETHNamePrefix(value) {
return !(value.trim() === "" || value.endsWith(".") || value.indexOf("..") > -1)
}
function isAddress(value) {
return startsWith0x(value) && isHex(value) && value.length === 42
}
@ -28,4 +32,11 @@ QtObject {
// Do we support other length than 12?
return value.split(/\s|,/).length === 12
}
function compactAddress(addr, numberOfChars) {
if(addr.length <= 5 + (numberOfChars * 2)){ // 5 represents these chars 0x...
return addr;
}
return addr.substring(0, 2 + numberOfChars) + "..." + addr.substring(addr.length - numberOfChars);
}
}