diff --git a/src/app/chat/view.nim b/src/app/chat/view.nim index d32bb19902..d7d9fab1e7 100644 --- a/src/app/chat/view.nim +++ b/src/app/chat/view.nim @@ -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 \ No newline at end of file + error "Error removing user from the community", msg = e.msg + diff --git a/src/app/profile/core.nim b/src/app/profile/core.nim index bf223ac0d1..3901719487 100644 --- a/src/app/profile/core.nim +++ b/src/app/profile/core.nim @@ -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() diff --git a/src/app/profile/view.nim b/src/app/profile/view.nim index afa4570caf..264bb88f18 100644 --- a/src/app/profile/view.nim +++ b/src/app/profile/view.nim @@ -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) diff --git a/src/app/profile/views/contact_list.nim b/src/app/profile/views/contact_list.nim index e23f69b5a3..69dc7355f0 100644 --- a/src/app/profile/views/contact_list.nim +++ b/src/app/profile/views/contact_list.nim @@ -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) diff --git a/src/app/utilsView/view.nim b/src/app/utilsView/view.nim index 3e44401b6b..2368c60c77 100644 --- a/src/app/utilsView/view.nim +++ b/src/app/utilsView/view.nim @@ -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 \ No newline at end of file + getCurrentNetworkDetails().name diff --git a/ui/app/AppLayouts/Chat/components/Contact.qml b/ui/app/AppLayouts/Chat/components/Contact.qml index 4757fe8320..4862733c86 100644 --- a/ui/app/AppLayouts/Chat/components/Contact.qml +++ b/ui/app/AppLayouts/Chat/components/Contact.qml @@ -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() + } } } diff --git a/ui/app/AppLayouts/Chat/components/PrivateChatPopup.qml b/ui/app/AppLayouts/Chat/components/PrivateChatPopup.qml index 4201e75f09..ac0a4e769e 100644 --- a/ui/app/AppLayouts/Chat/components/PrivateChatPopup.qml +++ b/ui/app/AppLayouts/Chat/components/PrivateChatPopup.qml @@ -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 don’t 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 + } + } /*##^## diff --git a/ui/app/AppLayouts/Chat/components/PrivateChatPopupExistingContacts.qml b/ui/app/AppLayouts/Chat/components/PrivateChatPopupExistingContacts.qml new file mode 100644 index 0000000000..819d51c81a --- /dev/null +++ b/ui/app/AppLayouts/Chat/components/PrivateChatPopupExistingContacts.qml @@ -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) + } + } + } + } + +} + + diff --git a/ui/app/AppLayouts/Chat/components/PrivateChatPopupSearchResults.qml b/ui/app/AppLayouts/Chat/components/PrivateChatPopupSearchResults.qml new file mode 100644 index 0000000000..9c55b110aa --- /dev/null +++ b/ui/app/AppLayouts/Chat/components/PrivateChatPopupSearchResults.qml @@ -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") + } + +} diff --git a/ui/app/img/add-contact.svg b/ui/app/img/add-contact.svg new file mode 100644 index 0000000000..7d8fdc6b4a --- /dev/null +++ b/ui/app/img/add-contact.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/app/img/check-2.svg b/ui/app/img/check-2.svg new file mode 100644 index 0000000000..9c59d0b42b --- /dev/null +++ b/ui/app/img/check-2.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/app/img/close-icon.svg b/ui/app/img/close-icon.svg new file mode 100644 index 0000000000..849b1f8f07 --- /dev/null +++ b/ui/app/img/close-icon.svg @@ -0,0 +1,3 @@ + + +