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/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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
chatKey.text = chatKey.text.trim();
|
||||
|
||||
chatsModel.joinChat(chatKey.text, Constants.chatTypeOneToOne);
|
||||
} else if (contactListView.selectedContact.checked) {
|
||||
chatsModel.joinChat(contactListView.selectedContact.parent.address, Constants.chatTypeOneToOne);
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: ensUsername
|
||||
anchors.top: chatKey.bottom
|
||||
anchors.topMargin: Theme.padding
|
||||
color: Theme.darkGrey
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
ContactList {
|
||||
id: contactListView
|
||||
contacts: profileModel.contactList
|
||||
selectable: true
|
||||
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
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue