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/status
import ../../status/chat as status_chat import ../../status/chat as status_chat
import ../../status/contacts as status_contacts import ../../status/contacts as status_contacts
import ../../status/ens as status_ens
import ../../status/chat/[chat, message] import ../../status/chat/[chat, message]
import ../../status/libstatus/types import ../../status/libstatus/types
import ../../status/profile/profile import ../../status/profile/profile
@ -228,3 +229,9 @@ QtObject:
proc isEnsVerified*(self: ChatsView, id: string): bool {.slot.} = proc isEnsVerified*(self: ChatsView, id: string): bool {.slot.} =
if id == "": return false if id == "": return false
self.status.contacts.getContactByID(id).ensVerified 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): status.events.once("login") do(a: Args):
var args = AccountArgs(a) var args = AccountArgs(a)
status.startMessenger() status.startMessenger()
chat.init()
wallet.init()
profile.init(args.account) profile.init(args.account)
wallet.init()
chat.init()
var login = login.newController(status) var login = login.newController(status)
var onboarding = onboarding.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 = proc getContactByID*(self: ContactModel, id: string): Profile =
let response = status_contacts.getContactByID(id) 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 = proc blockContact*(self: ContactModel, id: string): string =
var contact = self.getContactByID(id) var contact = self.getContactByID(id)
@ -41,4 +46,5 @@ proc removeContact*(self: ContactModel, id: string) =
proc isAdded*(self: ContactModel, id: string): bool = proc isAdded*(self: ContactModel, id: string): bool =
var contact = self.getContactByID(id) var contact = self.getContactByID(id)
if contact.isNil: return false
contact.systemTags.contains(":contact/added") contact.systemTags.contains(":contact/added")

View File

@ -1,5 +1,15 @@
import strutils import strutils
import profile/profile 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" let domain* = ".stateofus.eth"
@ -12,8 +22,64 @@ proc userName*(ensName: string, removeSuffix: bool = false): string =
else: else:
result = ensName result = ensName
proc addDomain*(username: string): string =
if username.endsWith(".eth"):
return username
else:
return username & domain
proc userNameOrAlias*(contact: Profile): string = proc userNameOrAlias*(contact: Profile): string =
if(contact.ensName != "" and contact.ensVerified): if(contact.ensName != "" and contact.ensVerified):
result = "@" & userName(contact.ensName, true) result = "@" & userName(contact.ensName, true)
else: else:
result = contact.alias 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 showCheckbox: true
property bool isChecked: false property bool isChecked: false
property bool showListSelector: false
property var onItemChecked: (function(pubKey, itemChecked) { console.log(pubKey, itemChecked) }) property var onItemChecked: (function(pubKey, itemChecked) { console.log(pubKey, itemChecked) })
@ -47,9 +48,29 @@ Rectangle {
anchors.leftMargin: Theme.padding 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 { CheckBox {
id: assetCheck id: assetCheck
visible: showCheckbox && !isUser visible: !showListSelector && showCheckbox && !isUser
anchors.top: accountImage.top anchors.top: accountImage.top
anchors.topMargin: 6 anchors.topMargin: 6
anchors.right: parent.right anchors.right: parent.right

View File

@ -3,34 +3,47 @@ import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13 import QtQuick.Layouts 1.13
import "../../../../imports" import "../../../../imports"
import "../../../../shared" import "../../../../shared"
import "../../Profile/Sections/Contacts/"
import "./" import "./"
ModalPopup { ModalPopup {
property string validationError: "" property string validationError: ""
property string pubKey : "";
property string ensUsername : "";
function validate() { function validate() {
// TODO change this when we support ENS names if (!Utils.isChatKey(chatKey.text) && !Utils.isValidETHNamePrefix(chatKey.text)) {
if (!Utils.isChatKey(chatKey.text)) { validationError = "This needs to be a valid chat key or ENS username"
validationError = "This needs to be a valid chat key" pubKey = "";
ensUsername = "";
} else { } else {
validationError = "" validationError = ""
} }
return validationError === "" return validationError === ""
} }
function doJoin() { function onKeyReleased(){
if (chatKey.text !== "") { ensUsername.text = "";
if (!validate()) { if (!validate()) return;
return
} chatKey.text = chatKey.text.trim();
chatsModel.joinChat(chatKey.text, Constants.chatTypeOneToOne); if(Utils.isChatKey(chatKey.text)){
} else if (contactListView.selectedContact.checked) { pubKey = chatKey.text;
chatsModel.joinChat(contactListView.selectedContact.parent.address, Constants.chatTypeOneToOne);
} else {
return; 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(); popup.close();
} }
@ -39,10 +52,9 @@ ModalPopup {
onOpened: { onOpened: {
chatKey.text = ""; chatKey.text = "";
pubKey = "";
ensUsername = "";
chatKey.forceActiveFocus(Qt.MouseFocusReason) chatKey.forceActiveFocus(Qt.MouseFocusReason)
if (contactListView.selectedContact) {
contactListView.selectedContact.checked = false
}
} }
Input { Input {
@ -51,15 +63,56 @@ ModalPopup {
Keys.onEnterPressed: doJoin() Keys.onEnterPressed: doJoin()
Keys.onReturnPressed: doJoin() Keys.onReturnPressed: doJoin()
validationError: popup.validationError validationError: popup.validationError
textField.onEditingFinished: { Keys.onReleased: {
validate() onKeyReleased();
} }
} }
Text {
id: ensUsername
anchors.top: chatKey.bottom
anchors.topMargin: Theme.padding
color: Theme.darkGrey
font.pixelSize: 12
}
ContactList { Item {
id: contactListView anchors.top: ensUsername.bottom
contacts: profileModel.contactList anchors.topMargin: 32
selectable: true 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
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 { footer: Button {
@ -68,7 +121,7 @@ ModalPopup {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
SVGImage { 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 width: 50
height: 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 return startsWith0x(value) && isHex(value) && value.length === 132
} }
function isValidETHNamePrefix(value) {
return !(value.trim() === "" || value.endsWith(".") || value.indexOf("..") > -1)
}
function isAddress(value) { function isAddress(value) {
return startsWith0x(value) && isHex(value) && value.length === 42 return startsWith0x(value) && isHex(value) && value.length === 42
} }
@ -28,4 +32,11 @@ QtObject {
// Do we support other length than 12? // Do we support other length than 12?
return value.split(/\s|,/).length === 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);
}
} }