feat: start chats with ENS usernames
This commit is contained in:
parent
4fe19e8130
commit
961139e778
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 |
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue