refactor: redesign PrivateChatPopup to match new designs

This commit introduces the new design and behaviour of the modal
that opens up when starting a new 1-on-1 chat.

Main changes include:

- New search UI/UX functionality of users and ENS resolutions
- Composed view of existing contacts and contacts to be searched
- Ability to add contacts from within the modal

Closes: #1747
This commit is contained in:
Pascal Precht 2021-02-08 13:21:23 +01:00 committed by Iuri Matias
parent 0c65551b45
commit 8977ba4931
12 changed files with 330 additions and 85 deletions

View File

@ -324,6 +324,9 @@ QtObject:
return
self.messageList[id].clear(not channel.isNil and channel.chatType != ChatType.Profile)
self.messagesCleared()
proc isAddedContact*(self: ChatsView, id: string): bool {.slot.} =
result = self.status.contacts.isAdded(id)
proc pushMessages*(self:ChatsView, messages: var seq[Message]) =
for msg in messages.mitems:
@ -351,7 +354,7 @@ QtObject:
self.newMessagePushed()
if not channel.muted:
let isAddedContact = channel.chatType.isOneToOne and self.status.contacts.isAdded(channel.id)
let isAddedContact = channel.chatType.isOneToOne and self.isAddedContact(channel.id)
self.messageNotificationPushed(
msg.chatId,
escape_html(msg.text),
@ -848,4 +851,5 @@ QtObject:
self.status.chat.removeUserFromCommunity(self.activeCommunity.id(), pubKey)
self.activeCommunity.removeMember(pubKey)
except Exception as e:
error "Error removing user from the community", msg = e.msg
error "Error removing user from the community", msg = e.msg

View File

@ -94,6 +94,7 @@ proc init*(self: ProfileController, account: Account) =
self.status.events.on("contactAdded") do(e: Args):
let contacts = self.status.contacts.getContacts()
self.view.contacts.setContactList(contacts)
self.view.contactsChanged()
self.status.events.on("contactBlocked") do(e: Args):
let contacts = self.status.contacts.getContacts()

View File

@ -195,11 +195,14 @@ QtObject:
self.mutedChatsListChanged()
self.mutedContactsListChanged()
proc contactsChanged*(self: ProfileView) {.signal.}
proc getContacts*(self: ProfileView): QVariant {.slot.} =
newQVariant(self.contacts)
QtProperty[QVariant] contacts:
read = getContacts
notify = contactsChanged
proc getDevices*(self: ProfileView): QVariant {.slot.} =
newQVariant(self.devices)

View File

@ -38,7 +38,7 @@ QtObject:
method rowCount(self: ContactList, index: QModelIndex = nil): int =
return self.contacts.len
proc userName(self: ContactList, pubKey: string, defaultValue: string = ""): string {.slot.} =
proc userName*(self: ContactList, pubKey: string, defaultValue: string = ""): string {.slot.} =
for contact in self.contacts:
if(contact.id != pubKey): continue
return ens.userNameOrAlias(contact)

View File

@ -91,8 +91,11 @@ QtObject:
weiValue = fromHex(Stuint[256], weiValue).toString()
return status_utils.wei2Eth(weiValue, decimals)
proc generateAlias*(self: UtilsView, pk: string): string {.slot.} =
result = status_accounts.generateAlias(pk)
proc generateIdenticon*(self: UtilsView, pk: string): string {.slot.} =
result = status_accounts.generateIdenticon(pk)
proc getNetworkName*(self: UtilsView): string {.slot.} =
getCurrentNetworkDetails().name
getCurrentNetworkDetails().name

View File

@ -22,6 +22,8 @@ Rectangle {
property bool isHovered: false
property var onItemChecked: (function(pubKey, itemChecked) { console.log(pubKey, itemChecked) })
property var onContactClicked
id: root
visible: isVisible && (isContact || isUser)
height: visible ? 64 : 0
@ -85,6 +87,11 @@ Rectangle {
hoverEnabled: root.clickable || root.showCheckbox
onEntered: root.isHovered = true
onExited: root.isHovered = false
onClicked: assetCheck.clicked()
onClicked: {
if (typeof root.onContactClicked !== "function") {
return assetCheck.clicked()
}
root.onContactClicked()
}
}
}

View File

@ -12,11 +12,10 @@ ModalPopup {
property string pubKey : "";
property string ensUsername : "";
property bool loading: false;
function validate() {
if (!Utils.isChatKey(chatKey.text) && !Utils.isValidETHNamePrefix(chatKey.text)) {
validationError = "This needs to be a valid chat key or ENS username";
validationError = qsTr("Enter a valid chat key or ENS username");
pubKey = ""
ensUsername.text = "";
} else if (profileModel.profile.pubKey === chatKey.text) {
validationError = qsTr("Can't chat with yourself");
@ -27,32 +26,45 @@ ModalPopup {
}
property var resolveENS: Backpressure.debounce(popup, 500, function (ensName){
noContactsRect.visible = false
searchResults.loading = true
searchResults.showProfileNotFoundMessage = false
chatsModel.resolveENS(ensName)
loading = true
});
function onKeyReleased(){
searchResults.pubKey = ""
if (!validate()) {
searchResults.showProfileNotFoundMessage = false
noContactsRect.visible = false
return;
}
chatKey.text = chatKey.text.trim();
if(Utils.isChatKey(chatKey.text)){
if (Utils.isChatKey(chatKey.text)){
pubKey = chatKey.text;
ensUsername.text = "";
if (!profileModel.contacts.isAdded(pubKey)) {
searchResults.username = utilsModel.generateAlias(pubKey)
searchResults.userAlias = Utils.compactAddress(pubKey, 4)
searchResults.pubKey = pubKey
}
noContactsRect.visible = false
return;
}
Qt.callLater(resolveENS, chatKey.text)
}
function doJoin() {
if (!validate() || pubKey.trim() === "" || validationError !== "") return;
if(Utils.isChatKey(chatKey.text)){
chatsModel.joinChat(pubKey, Constants.chatTypeOneToOne);
function validateAndJoin(pk, ensName) {
if (!validate() || pk.trim() === "" || validationError !== "") return;
doJoin(pk, ensName)
}
function doJoin(pk, ensName) {
if(Utils.isChatKey(pk)){
chatsModel.joinChat(pk, Constants.chatTypeOneToOne);
} else {
chatsModel.joinChatWithENS(pubKey, chatKey.text);
chatsModel.joinChatWithENS(pk, ensName);
}
popup.close();
@ -67,107 +79,111 @@ ModalPopup {
pubKey = "";
ensUsername.text = "";
chatKey.forceActiveFocus(Qt.MouseFocusReason)
noContactsRect.visible = !profileModel.contacts.list.hasAddedContacts()
existingContacts.visible = profileModel.contacts.list.hasAddedContacts()
noContactsRect.visible = !existingContacts.visible
}
Input {
id: chatKey
//% "Enter ENS username or chat key"
placeholderText: qsTrId("enter-contact-code")
Keys.onEnterPressed: doJoin()
Keys.onReturnPressed: doJoin()
validationError: popup.validationError
Keys.onEnterPressed: validateAndJoin(popup.pubKey, chatKey.text)
Keys.onReturnPressed: validateAndJoin(popup.pubKey, chatKey.text)
Keys.onReleased: {
onKeyReleased();
}
textField.anchors.rightMargin: clearBtn.width + Style.current.padding + 2
Connections {
target: chatsModel
onEnsWasResolved: {
if(chatKey.text == ""){
ensUsername.text == "";
ensUsername.text = "";
pubKey = "";
} else if(resolvedPubKey == ""){
//% "User not found"
ensUsername.text = qsTrId("user-not-found");
pubKey = "";
ensUsername.text = "";
searchResults.pubKey = pubKey = "";
searchResults.showProfileNotFoundMessage = true
} else {
if (profileModel.profile.pubKey === resolvedPubKey) {
validationError = qsTr("Can't chat with yourself");
popup.validationError = qsTr("Can't chat with yourself");
} else {
ensUsername.text = chatsModel.formatENSUsername(chatKey.text) + " • " + Utils.compactAddress(resolvedPubKey, 4)
pubKey = resolvedPubKey;
searchResults.username = chatsModel.formatENSUsername(chatKey.text)
let userAlias = utilsModel.generateAlias(resolvedPubKey)
userAlias = userAlias.length > 20 ? userAlias.substring(0, 19) + "..." : userAlias
searchResults.userAlias = userAlias + " • " + Utils.compactAddress(resolvedPubKey, 4)
searchResults.pubKey = pubKey = resolvedPubKey;
}
searchResults.showProfileNotFoundMessage = false
}
loading = false;
searchResults.loading = false;
noContactsRect.visible = pubKey === "" && ensUsername.text === "" && !profileModel.contacts.list.hasAddedContacts() && !profileNotFoundMessage.visible
}
}
StatusIconButton {
id: clearBtn
icon.name: "close-icon"
type: "secondary"
visible: chatKey.text !== ""
icon.width: 14
icon.height: 14
width: 14
height: 14
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
onClicked: {
chatKey.text = ""
chatKey.forceActiveFocus(Qt.MouseFocusReason)
searchResults.showProfileNotFoundMessage = false
searchResults.pubKey = popup.pubKey = ""
noContactsRect.visible = false
}
}
}
StyledText {
id: ensUsername
id: validationErrorMessage
text: popup.validationError
visible: popup.validationError !== ""
font.pixelSize: 13
color: Style.current.danger
anchors.top: chatKey.bottom
anchors.topMargin: Style.current.padding
color: Style.current.darkGrey
font.pixelSize: 12
anchors.topMargin: Style.current.smallPadding
anchors.horizontalCenter: parent.horizontalCenter
}
Item {
anchors.top: ensUsername.bottom
anchors.topMargin: 90
anchors.fill: parent
ScrollView {
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: contactListView.contentHeight > contactListView.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
ListView {
anchors.fill: parent
spacing: 0
clip: true
id: contactListView
model: profileModel.contacts.list
delegate: Contact {
showCheckbox: false
pubKey: model.pubKey
isContact: model.isContact
isUser: false
name: model.name
address: model.address
identicon: model.thumbnailImage || model.identicon
onItemChecked: function(pubKey, itemChecked){
chatsModel.joinChat(pubKey, Constants.chatTypeOneToOne);
popup.close()
}
visible: model.isContact && (chatKey.text === "" ||
model.name.toLowerCase().includes(chatKey.text.toLowerCase()) ||
model.address.toLowerCase().includes(chatKey.text.toLowerCase()))
}
}
NoFriendsRectangle {
id: noContactsRect
visible: profileModel.contacts.addedContacts.rowCount() === 0
text: qsTr("You dont have any contacts yet. Invite your friends to start chatting.")
width: parent.width
anchors.verticalCenter: parent.verticalCenter
}
PrivateChatPopupExistingContacts {
id: existingContacts
anchors.topMargin: this.height > 0 ? Style.current.xlPadding : 0
anchors.top: chatKey.bottom
filterText: chatKey.text
onContactClicked: function (contact) {
doJoin(contact.pubKey, profileModel.contacts.addedContacts.userName(contact.pubKey, contact.name))
}
expanded: !searchResults.loading && popup.pubKey === "" && !searchResults.showProfileNotFoundMessage
}
footer: StatusButton {
anchors.right: parent.right
id: submitBtn
state: loading ? "pending" : "default"
text: qsTr("Start chat")
enabled: pubKey !== ""
onClicked : doJoin()
PrivateChatPopupSearchResults {
id: searchResults
anchors.top: existingContacts.visible ? existingContacts.bottom : chatKey.bottom
anchors.topMargin: Style.current.padding
hasExistingContacts: existingContacts.visible
loading: false
onResultClicked: validateAndJoin(popup.pubKey, chatKey.text)
onAddToContactsButtonClicked: profileModel.contacts.addContact(popup.pubKey)
}
NoFriendsRectangle {
id: noContactsRect
anchors.top: chatKey.bottom
anchors.topMargin: Style.current.xlPadding * 3
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
}
/*##^##

View File

@ -0,0 +1,56 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
import "./"
Item {
id: root
anchors.left: parent.left
anchors.right: parent.right
property string filterText: ""
property bool expanded: true
signal contactClicked(var contact)
function matchesAlias(name, filter) {
let parts = name.split(" ")
return parts.some(p => p.startsWith(filter))
}
height: Math.min(contactListView.contentHeight, (expanded ? 320 : 192))
ScrollView {
anchors.fill: parent
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: contactListView.contentHeight > contactListView.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
ListView {
anchors.fill: parent
spacing: 0
clip: true
id: contactListView
model: profileModel.contacts.list
delegate: Contact {
showCheckbox: false
pubKey: model.pubKey
isContact: model.isContact
isUser: false
name: model.name
address: model.address
identicon: model.thumbnailImage || model.identicon
visible: model.isContact && (root.filterText === "" ||
root.matchesAlias(model.name.toLowerCase(), root.filterText.toLowerCase()) ||
model.name.toLowerCase().includes(root.filterText.toLowerCase()) ||
model.address.toLowerCase().includes(root.filterText.toLowerCase()))
onContactClicked: function () {
root.contactClicked(model)
}
}
}
}
}

View File

@ -0,0 +1,144 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
import "./"
Item {
id: root
height: 64
property bool hasExistingContacts: false
property bool showProfileNotFoundMessage: false
property bool loading: false
property string username: ""
property string userAlias: ""
property string pubKey: ""
signal resultClicked(string pubKey)
signal addToContactsButtonClicked(string pubKey)
width: parent.width
StyledText {
id: nonContactsLabel
text: qsTr("Non contacts")
anchors.top: parent.top
color: Style.current.secondaryText
font.pixelSize: 15
visible: root.hasExistingContacts && (root.loading || root.pubKey !== "" || root.showProfileNotFoundMessage)
}
Loader {
active: root.loading
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
sourceComponent: Component {
LoadingAnimation {
width: 18
height: 18
}
}
}
Rectangle {
id: foundContact
property bool hovered: false
anchors.top: nonContactsLabel.visible ? nonContactsLabel.bottom : parent.top
color: hovered ? Style.current.backgroundHover : Style.current.background
radius: Style.current.radius
width: parent.width
height: 64
visible: root.pubKey !== "" && !root.loading
StatusImageIdenticon {
id: contactIdenticon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
source: utilsModel.generateIdenticon(root.pubKey)
}
StyledText {
id: ensUsername
font.pixelSize: 17
color: Style.current.textColor
anchors.top: contactIdenticon.top
anchors.left: contactIdenticon.right
anchors.leftMargin: Style.current.padding
text: root.username
}
StyledText {
id: contactAlias
font.pixelSize: 15
color: Style.current.secondaryText
anchors.top: ensUsername.bottom
anchors.topMargin: 2
anchors.left: ensUsername.left
text: root.userAlias
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
hoverEnabled: true
onEntered: foundContact.hovered = true
onExited: foundContact.hovered = false
onClicked: root.resultClicked(root.pubKey)
}
StatusIconButton {
id: addContactBtn
icon.name: "add-contact"
highlightedBackgroundColor: Utils.setColorAlpha(Style.current.buttonHoveredBackgroundColor, 0.2)
iconColor: Style.current.primary
icon.width: 24
icon.height: 24
width: 32
height: 32
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
visible: !chatsModel.isAddedContact(root.pubKey) && !checkIcon.visible
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
foundContact.hovered = true
}
onExited: {
foundContact.hovered = false
}
onClicked: {
root.addToContactsButtonClicked(root.pubKey)
mouse.accepted = false
}
}
}
SVGImage {
id: checkIcon
source: "../../../../app/img/check-2.svg"
width: 19
height: 19
anchors.right: parent.right
anchors.rightMargin: Style.current.smallPadding * 2
anchors.verticalCenter: parent.verticalCenter
visible: foundContact.hovered && chatsModel.isAddedContact(root.pubKey)
}
}
StyledText {
id: profileNotFoundMessage
color: Style.current.darkGrey
visible: root.showProfileNotFoundMessage
font.pixelSize: 15
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
text: qsTr("No profile found")
}
}

View File

@ -0,0 +1,5 @@
<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="M17 8C17 10.7614 14.7614 13 12 13C9.23858 13 7 10.7614 7 8C7 5.23858 9.23858 3 12 3C14.7614 3 17 5.23858 17 8ZM15.5 8C15.5 9.933 13.933 11.5 12 11.5C10.067 11.5 8.5 9.933 8.5 8C8.5 6.067 10.067 4.5 12 4.5C13.933 4.5 15.5 6.067 15.5 8Z" fill="#4360DF"/>
<path d="M12.1099 17.5007C12.5288 17.506 12.8997 17.2015 12.9345 16.7839C12.9685 16.3762 12.6689 16.0138 12.2599 16.0033C12.1735 16.0011 12.0868 16 11.9998 16C8.95932 16 6.23574 17.357 4.40167 19.4984C4.16181 19.7784 4.19319 20.1933 4.45392 20.454C4.77772 20.7779 5.31374 20.7312 5.61624 20.3874C7.17395 18.6171 9.45645 17.5 11.9998 17.5C12.0366 17.5 12.0732 17.5002 12.1099 17.5007Z" fill="#4360DF"/>
<path d="M15.25 16.75C15.25 16.3358 15.5858 16 16 16H17.25C17.5261 16 17.75 15.7761 17.75 15.5V14.25C17.75 13.8358 18.0858 13.5 18.5 13.5C18.9142 13.5 19.25 13.8358 19.25 14.25V15.5C19.25 15.7761 19.4739 16 19.75 16H21C21.4142 16 21.75 16.3358 21.75 16.75C21.75 17.1642 21.4142 17.5 21 17.5H19.75C19.4739 17.5 19.25 17.7239 19.25 18V19.25C19.25 19.6642 18.9142 20 18.5 20C18.0858 20 17.75 19.6642 17.75 19.25V18C17.75 17.7239 17.5261 17.5 17.25 17.5H16C15.5858 17.5 15.25 17.1642 15.25 16.75Z" fill="#4360DF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

3
ui/app/img/check-2.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.416 0.376042C19.7607 0.605806 19.8538 1.07146 19.624 1.4161L7.62404 19.4161C7.4994 19.6031 7.2975 19.7243 7.0739 19.7464C6.8503 19.7686 6.62855 19.6893 6.46967 19.5304L0.46967 13.5304C0.176777 13.2375 0.176777 12.7626 0.46967 12.4698C0.762563 12.1769 1.23744 12.1769 1.53033 12.4698L6.88343 17.8229L18.376 0.584055C18.6057 0.239408 19.0714 0.146278 19.416 0.376042Z" fill="#4EBC60"/>
</svg>

After

Width:  |  Height:  |  Size: 540 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM10.442 4.44194L7.88393 7L10.442 9.55806L9.55811 10.4419L7.00005 7.88389L4.44199 10.4419L3.55811 9.55806L6.11616 7L3.55811 4.44195L4.44199 3.55806L7.00005 6.11612L9.55811 3.55806L10.442 4.44194Z" fill="#8E8E93"/>
</svg>

After

Width:  |  Height:  |  Size: 469 B