status-desktop/ui/imports/shared/views/ProfileDialogView.qml

671 lines
27 KiB
QML
Raw Normal View History

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
2022-09-27 21:26:26 +00:00
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
2022-09-27 21:26:26 +00:00
import utils 1.0
import shared.controls 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.controls.chat 1.0
import shared.controls.chat.menuItems 1.0
import shared.views.profile 1.0
import SortFilterProxyModel 0.2
import AppLayouts.Wallet.stores 1.0 as WalletNS
2022-09-27 21:26:26 +00:00
Pane {
id: root
property bool readOnly // inside settings/profile/preview
property bool idVerificationFlowsEnabled: false // disabled temporarily as per https://github.com/status-im/status-desktop/issues/14954
2022-09-27 21:26:26 +00:00
property string publicKey: contactsStore.myPublicKey
readonly property alias isCurrentUser: d.isCurrentUser
2022-09-27 21:26:26 +00:00
property var profileStore
property var contactsStore
property alias sendToAccountEnabled: showcaseView.sendToAccountEnabled
2022-09-27 21:26:26 +00:00
property var dirtyValues: ({})
property bool dirty: false
property var showcaseCommunitiesModel
property var showcaseAccountsModel
property var showcaseCollectiblesModel
property var showcaseSocialLinksModel
property var showcaseAssetsModel
property alias showcaseMaxVisibility: showcaseView.maxVisibility
property alias assetsModel: showcaseView.globalAssetsModel
property alias collectiblesModel: showcaseView.globalCollectiblesModel
2022-09-27 21:26:26 +00:00
signal closeRequested()
padding: 0
topPadding: 32
2022-09-27 21:26:26 +00:00
background: StatusDialogBackground {
id: background
}
QtObject {
id: d
property var contactDetails: Utils.getContactDetailsAsJson(root.publicKey, !isCurrentUser, !isCurrentUser, true)
2022-09-27 21:26:26 +00:00
function reload() {
contactDetails = Utils.getContactDetailsAsJson(root.publicKey, !isCurrentUser, !isCurrentUser, true)
2022-09-27 21:26:26 +00:00
}
readonly property bool isCurrentUser: root.profileStore.pubkey === root.publicKey
readonly property string userDisplayName: contactDetails.displayName
readonly property string userNickName: contactDetails.localNickname
readonly property string prettyEnsName: contactDetails.name
readonly property string aliasName: contactDetails.alias
readonly property string mainDisplayName: ProfileUtils.displayName(userNickName, prettyEnsName, userDisplayName, aliasName)
readonly property string optionalDisplayName: ProfileUtils.displayName("", prettyEnsName, userDisplayName, aliasName)
2022-09-27 21:26:26 +00:00
readonly property bool isContact: contactDetails.isContact
readonly property bool isBlocked: contactDetails.isBlocked
readonly property int contactRequestState: contactDetails.contactRequestState
2022-09-27 21:26:26 +00:00
readonly property int outgoingVerificationStatus: contactDetails.verificationStatus
readonly property int incomingVerificationStatus: contactDetails.incomingVerificationStatus
readonly property bool isVerificationRequestSent:
outgoingVerificationStatus !== Constants.verificationStatus.unverified &&
outgoingVerificationStatus !== Constants.verificationStatus.verified &&
outgoingVerificationStatus !== Constants.verificationStatus.trusted
readonly property bool isVerificationRequestReceived: incomingVerificationStatus === Constants.verificationStatus.verifying ||
incomingVerificationStatus === Constants.verificationStatus.verified
2022-09-27 21:26:26 +00:00
readonly property bool isTrusted: outgoingVerificationStatus === Constants.verificationStatus.trusted ||
incomingVerificationStatus === Constants.verificationStatus.trusted
readonly property bool isLocallyTrusted: contactDetails.trustStatus === Constants.trustStatus.trusted
2022-09-27 21:26:26 +00:00
readonly property string linkToProfile: root.contactsStore.getLinkToProfile(root.publicKey)
2022-09-27 21:26:26 +00:00
readonly property var conns: Connections {
2023-02-28 14:54:10 +00:00
target: root.contactsStore.myContactsModel ?? null
function onItemChanged(pubKey) {
if (pubKey === root.publicKey)
d.reload()
2022-09-27 21:26:26 +00:00
}
}
// FIXME: use myContactsModel for identity verification
readonly property var conns2: Connections {
2023-02-28 14:54:10 +00:00
target: root.contactsStore.receivedContactRequestsModel ?? null
function onItemChanged(pubKey) {
if (pubKey === root.publicKey)
d.reload()
2022-09-27 21:26:26 +00:00
}
}
readonly property var conns3: Connections {
2023-02-28 14:54:10 +00:00
target: root.contactsStore.sentContactRequestsModel ?? null
function onItemChanged(pubKey) {
if (pubKey === root.publicKey)
d.reload()
}
}
2022-09-27 21:26:26 +00:00
}
function reload() {
d.reload()
}
onDirtyChanged: {
if (!dirty)
d.reload()
}
2022-09-27 21:26:26 +00:00
Component {
id: btnEditProfileComponent
StatusButton {
objectName: "editProfileButton"
2022-09-27 21:26:26 +00:00
size: StatusButton.Size.Small
text: qsTr("Edit Profile")
interactive: !root.readOnly
tooltip.text: interactive ? "" : qsTr("Not available in preview mode")
2022-09-27 21:26:26 +00:00
onClicked: {
Global.changeAppSectionBySectionType(Constants.appSection.profile)
root.closeRequested()
}
}
}
Component {
id: btnSendMessageComponent
StatusButton {
size: StatusButton.Size.Small
text: qsTr("Send Message")
onClicked: {
root.contactsStore.joinPrivateChat(root.publicKey)
root.closeRequested()
}
}
}
Component {
id: btnAcceptContactRequestComponent
StatusButton {
objectName: "profileDialog_reviewContactRequestButton"
size: StatusButton.Size.Small
text: qsTr("Review contact request")
onClicked: Global.openReviewContactRequestPopup(root.publicKey, d.contactDetails,
popup => popup.closed.connect(d.reload))
2022-09-27 21:26:26 +00:00
}
}
Component {
id: btnSendContactRequestComponent
StatusButton {
objectName: "profileDialog_sendContactRequestButton"
2022-09-27 21:26:26 +00:00
size: StatusButton.Size.Small
text: qsTr("Send contact request")
onClicked: Global.openContactRequestPopup(root.publicKey, d.contactDetails,
popup => popup.accepted.connect(d.reload))
2022-09-27 21:26:26 +00:00
}
}
Component {
id: btnBlockUserComponent
StatusButton {
size: StatusButton.Size.Small
type: StatusBaseButton.Type.Danger
text: qsTr("Block user")
onClicked: Global.blockContactRequested(root.publicKey, d.contactDetails)
2022-09-27 21:26:26 +00:00
}
}
Component {
id: btnUnblockUserComponent
StatusButton {
size: StatusButton.Size.Small
text: qsTr("Unblock user")
onClicked: Global.unblockContactRequested(root.publicKey, d.contactDetails)
2022-09-27 21:26:26 +00:00
}
}
Component {
id: txtPendingContactRequestComponent
RowLayout {
StatusIcon {
icon: "history"
width: 16
height: width
color: Theme.palette.baseColor1
}
StatusBaseText {
font.pixelSize: 13
font.weight: Font.Medium
color: Theme.palette.baseColor1
verticalAlignment: Text.AlignVCenter
text: qsTr("Contact Request Pending")
}
2022-09-27 21:26:26 +00:00
}
}
Component {
id: btnReplyToIdRequestComponent
StatusFlatButton {
2022-09-27 21:26:26 +00:00
size: StatusButton.Size.Small
text: qsTr("Reply to ID verification request")
objectName: "respondToIDRequest_StatusItem"
icon.name: "checkmark-circle"
onClicked: Global.openIncomingIDRequestPopup(root.publicKey, d.contactDetails,
popup => popup.closed.connect(d.reload))
2022-09-27 21:26:26 +00:00
}
}
Component {
id: btnRequestIDVerification
StatusFlatButton {
size: StatusButton.Size.Small
text: qsTr("Request ID verification")
objectName: "requestIDVerification_StatusItem"
icon.name: "checkmark-circle"
onClicked: Global.openSendIDRequestPopup(root.publicKey, d.contactDetails,
popup => popup.accepted.connect(d.reload))
}
}
Component {
id: btnReviewIDVerificationReply
StatusFlatButton {
size: StatusButton.Size.Small
text: d.incomingVerificationStatus !== Constants.verificationStatus.verified ? qsTr("ID verification pending")
: qsTr("Review ID verification reply")
icon.name: d.incomingVerificationStatus !== Constants.verificationStatus.verified ? "history" : "checkmark-circle"
onClicked: Global.openOutgoingIDRequestPopup(root.publicKey, d.contactDetails,
popup => popup.closed.connect(d.reload))
}
}
Component {
id: btnShareProfile
StatusFlatButton {
size: StatusButton.Size.Small
text: qsTr("Share Profile")
onClicked: Global.openPopup(shareProfileCmp)
2022-09-27 21:26:26 +00:00
}
}
Component {
id: shareProfileCmp
ShareProfileDialog {
destroyOnClose: true
title: d.isCurrentUser ? qsTr("Share your profile") : qsTr("%1's profile").arg(StatusQUtils.Emoji.parse(d.mainDisplayName))
publicKey: root.publicKey
linkToProfile: d.linkToProfile
qrCode: root.profileStore.getQrCodeSource(linkToProfile)
displayName: userImage.name
largeImage: userImage.image
}
}
2022-09-27 21:26:26 +00:00
ColumnLayout {
id: column
spacing: 20
anchors {
fill: parent
leftMargin: Style.current.bigPadding
rightMargin: Style.current.bigPadding
}
RowLayout {
Layout.fillWidth: true
spacing: Style.current.halfPadding
UserImage {
id: userImage
2022-09-27 21:26:26 +00:00
Layout.alignment: Qt.AlignTop
objectName: "ProfileDialog_userImage"
name: root.dirty ? root.dirtyValues.displayName
: d.mainDisplayName
2022-09-27 21:26:26 +00:00
pubkey: root.publicKey
image: root.dirty ? root.dirtyValues.profileLargeImage
: Utils.addTimestampToURL(d.contactDetails.largeImage)
2022-09-27 21:26:26 +00:00
interactive: false
imageWidth: 90
2022-09-27 21:26:26 +00:00
imageHeight: imageWidth
ensVerified: d.contactDetails.ensVerified
Binding on onlineStatus {
value: d.contactDetails.onlineStatus
when: !d.isCurrentUser
}
2022-09-27 21:26:26 +00:00
}
Item { Layout.fillWidth: true }
// secondary action button
Loader {
Layout.alignment: Qt.AlignTop
Layout.preferredHeight: menuButton.visible ? menuButton.height : -1
active: root.idVerificationFlowsEnabled
sourceComponent: {
if (d.isCurrentUser && !root.readOnly)
return btnShareProfile
if (d.isContact && !(d.isTrusted || d.isLocallyTrusted) && !d.isBlocked) {
if (d.isVerificationRequestSent)
return btnReviewIDVerificationReply
else if (d.isVerificationRequestReceived)
return btnReplyToIdRequestComponent
else if (d.outgoingVerificationStatus === Constants.verificationStatus.unverified)
return btnRequestIDVerification
}
}
2022-09-27 21:26:26 +00:00
}
// primary action button
2022-09-27 21:26:26 +00:00
Loader {
Layout.alignment: Qt.AlignTop
Layout.preferredHeight: menuButton.visible ? menuButton.height : -1
2022-09-27 21:26:26 +00:00
sourceComponent: {
// current user
if (d.isCurrentUser)
return btnEditProfileComponent
// blocked user
if (d.isBlocked)
return btnUnblockUserComponent
// accept incoming CR
if (d.contactRequestState === Constants.ContactRequestState.Received)
return btnAcceptContactRequestComponent
// mutual contact
if (d.isContact || d.contactRequestState === Constants.ContactRequestState.Mutual)
return btnSendMessageComponent
// depend on contactRequestState
switch (d.contactRequestState) {
case Constants.ContactRequestState.Sent:
2022-09-27 21:26:26 +00:00
return txtPendingContactRequestComponent
case Constants.ContactRequestState.Received:
break // handled above
case Constants.ContactRequestState.Mutual: {
if (d.outgoingVerificationStatus === Constants.verificationStatus.declined) {
return btnBlockUserComponent
}
break
}
case Constants.ContactRequestState.None:
case Constants.ContactRequestState.Dismissed:
2022-09-27 21:26:26 +00:00
return btnSendContactRequestComponent
default:
console.warn("!!! UNHANDLED CONTACT ACTION BUTTON; PUBKEY", root.publicKey)
return null
}
2022-09-27 21:26:26 +00:00
}
}
StatusFlatButton {
id: menuButton
Layout.alignment: Qt.AlignTop
Layout.preferredWidth: height
visible: !d.isCurrentUser
horizontalPadding: 6
verticalPadding: 6
icon.name: "more"
icon.color: Theme.palette.directColor1
highlighted: moreMenu.opened
onClicked: moreMenu.popup(-moreMenu.width + width, height + 4)
2022-12-01 16:58:37 +00:00
StatusMenu {
2022-09-27 21:26:26 +00:00
id: moreMenu
2022-09-27 21:26:26 +00:00
SendContactRequestMenuItem {
enabled: !d.isContact && !d.isBlocked && d.contactRequestState !== Constants.ContactRequestState.Sent &&
2022-09-27 21:26:26 +00:00
d.contactDetails.trustStatus === Constants.trustStatus.untrustworthy // we have an action button otherwise
onTriggered: {
Global.openContactRequestPopup(root.publicKey, d.contactDetails, null)
2022-09-27 21:26:26 +00:00
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
text: qsTr("Mark as ID verified")
2022-09-27 21:26:26 +00:00
icon.name: "checkmark-circle"
enabled: root.idVerificationFlowsEnabled && d.isContact && !d.isBlocked && !(d.isTrusted || d.isLocallyTrusted)
onTriggered: Global.openMarkAsIDVerifiedPopup(root.publicKey, d.contactDetails,
popup => popup.accepted.connect(d.reload))
2022-09-27 21:26:26 +00:00
}
2022-12-01 16:58:37 +00:00
StatusAction {
text: d.userNickName ? qsTr("Edit nickname") : qsTr("Add nickname")
2022-09-27 21:26:26 +00:00
icon.name: "edit_pencil"
onTriggered: {
Global.openNicknamePopupRequested(root.publicKey, d.contactDetails,
popup => popup.closed.connect(d.reload))
2022-09-27 21:26:26 +00:00
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
text: qsTr("Show QR code")
icon.name: "qr"
enabled: !d.isCurrentUser
onTriggered: {
Global.openPopup(shareProfileCmp)
}
}
StatusAction {
text: qsTr("Copy link to profile")
2022-09-27 21:26:26 +00:00
icon.name: "copy"
onTriggered: {
root.profileStore.copyToClipboard(d.linkToProfile)
}
}
StatusMenuSeparator {}
StatusAction {
text: qsTr("Remove ID verification")
icon.name: "delete"
type: StatusAction.Type.Danger
enabled: root.idVerificationFlowsEnabled && d.isContact && (d.isTrusted || d.isLocallyTrusted)
onTriggered: Global.openRemoveIDVerificationDialog(root.publicKey, d.contactDetails,
popup => popup.accepted.connect(d.reload))
}
StatusAction {
text: qsTr("Remove nickname")
icon.name: "delete"
type: StatusAction.Type.Danger
enabled: !d.isCurrentUser && !!d.contactDetails.localNickname
onTriggered: root.contactsStore.changeContactNickname(root.publicKey, "", d.optionalDisplayName, true)
}
2022-12-01 16:58:37 +00:00
StatusAction {
text: qsTr("Mark as untrusted")
2022-09-27 21:26:26 +00:00
icon.name: "warning"
2022-12-01 16:58:37 +00:00
type: StatusAction.Type.Danger
enabled: d.contactDetails.trustStatus !== Constants.trustStatus.untrustworthy && !d.isBlocked
2022-09-27 21:26:26 +00:00
onTriggered: {
Global.markAsUntrustedRequested(root.publicKey, d.contactDetails)
2022-09-27 21:26:26 +00:00
}
}
StatusAction {
text: qsTr("Cancel ID verification request")
icon.name: "delete"
type: StatusAction.Type.Danger
enabled: root.idVerificationFlowsEnabled && d.isContact && !d.isBlocked && d.isVerificationRequestSent
onTriggered: root.contactsStore.cancelVerificationRequest(root.publicKey)
}
2022-12-01 16:58:37 +00:00
StatusAction {
text: qsTr("Remove untrusted mark")
2022-09-27 21:26:26 +00:00
icon.name: "warning"
type: StatusAction.Type.Danger
enabled: d.contactDetails.trustStatus === Constants.trustStatus.untrustworthy && !d.isBlocked
2022-09-27 21:26:26 +00:00
onTriggered: {
root.contactsStore.removeTrustStatus(root.publicKey)
d.reload()
2022-09-27 21:26:26 +00:00
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
text: qsTr("Remove contact")
2022-09-27 21:26:26 +00:00
icon.name: "remove-contact"
2022-12-01 16:58:37 +00:00
type: StatusAction.Type.Danger
enabled: d.isContact && !d.isBlocked && d.contactRequestState !== Constants.ContactRequestState.Sent
2022-09-27 21:26:26 +00:00
onTriggered: {
Global.removeContactRequested(root.publicKey, d.contactDetails)
2022-09-27 21:26:26 +00:00
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
text: qsTr("Block user")
2022-09-27 21:26:26 +00:00
icon.name: "cancel"
2022-12-01 16:58:37 +00:00
type: StatusAction.Type.Danger
2022-09-27 21:26:26 +00:00
enabled: !d.isBlocked
onTriggered: {
Global.blockContactRequested(root.publicKey, d.contactDetails)
2022-09-27 21:26:26 +00:00
}
}
}
}
}
ColumnLayout {
2022-09-27 21:26:26 +00:00
Layout.fillWidth: true
spacing: 4
Item {
id: contactRow
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
StatusBaseText {
id: contactName
anchors.left: parent.left
width: Math.min(implicitWidth, contactRow.width - verificationIcons.width - verificationIcons.anchors.leftMargin)
objectName: "ProfileDialog_displayName"
font.bold: true
font.pixelSize: 22
elide: Text.ElideRight
text: StatusQUtils.Emoji.parse(root.dirty ? root.dirtyValues.displayName : d.mainDisplayName, StatusQUtils.Emoji.size.middle)
}
StatusContactVerificationIcons {
id: verificationIcons
anchors.left: contactName.right
anchors.leftMargin: Style.current.halfPadding
anchors.verticalCenter: contactName.verticalCenter
objectName: "ProfileDialog_userVerificationIcons"
visible: !d.isCurrentUser
isContact: d.isContact
trustIndicator: d.contactDetails.trustStatus
isBlocked: d.isBlocked
tiny: false
}
}
RowLayout {
spacing: Style.current.halfPadding
StatusBaseText {
id: contactSecondaryName
color: Theme.palette.baseColor1
text: StatusQUtils.Emoji.parse(d.optionalDisplayName)
visible: !!d.userNickName
}
Rectangle {
Layout.preferredWidth: 4
Layout.preferredHeight: 4
radius: width/2
color: Theme.palette.baseColor1
visible: contactSecondaryName.visible
}
StatusBaseText {
color: Theme.palette.baseColor1
text: Utils.getElidedCompressedPk(root.publicKey)
HoverHandler {
id: keyHoverHandler
}
StatusToolTip {
text: Utils.getCompressedPk(root.publicKey)
visible: keyHoverHandler.hovered
}
}
CopyButton {
Layout.leftMargin: -4
Layout.preferredWidth: 16
Layout.preferredHeight: 16
textToCopy: Utils.getCompressedPk(root.publicKey)
StatusToolTip {
text: qsTr("Copy Chat Key")
visible: parent.hovered
}
}
}
StatusScrollView {
id: bioScrollView
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
Layout.maximumHeight: 120
contentWidth: availableWidth
Layout.topMargin: Style.current.halfPadding
padding: 0
rightPadding: Style.current.padding
visible: !!bioText.text
StatusBaseText {
id: bioText
width: bioScrollView.availableWidth
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
text: root.dirty ? root.dirtyValues.bio.trim() : d.contactDetails.bio.trim()
}
}
EmojiHash {
Layout.topMargin: Style.current.halfPadding
objectName: "ProfileDialog_userEmojiHash"
publicKey: root.publicKey
oneRow: true
}
2022-09-27 21:26:26 +00:00
}
StatusScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
2022-09-27 21:26:26 +00:00
Layout.leftMargin: -column.anchors.leftMargin
Layout.rightMargin: -column.anchors.rightMargin
padding: 0
contentWidth: availableWidth
2022-09-27 21:26:26 +00:00
ColumnLayout {
width: scrollView.availableWidth
2022-09-27 21:26:26 +00:00
spacing: 20
StatusTabBar {
id: showcaseTabBar
2022-09-27 21:26:26 +00:00
Layout.fillWidth: true
Layout.leftMargin: column.anchors.leftMargin
Layout.rightMargin: column.anchors.rightMargin
bottomPadding: -4
StatusTabButton {
leftPadding: 0
2022-09-27 21:26:26 +00:00
width: implicitWidth
text: qsTr("Communities")
2022-09-27 21:26:26 +00:00
}
StatusTabButton {
width: implicitWidth
text: qsTr("Accounts")
2022-09-27 21:26:26 +00:00
}
StatusTabButton {
width: implicitWidth
text: qsTr("Collectibles")
2022-09-27 21:26:26 +00:00
}
// StatusTabButton {
// width: implicitWidth
// text: qsTr("Assets")
// }
StatusTabButton {
width: implicitWidth
text: qsTr("Web")
}
2022-09-27 21:26:26 +00:00
}
// Profile Showcase
ProfileShowcaseView {
id: showcaseView
2022-09-27 21:26:26 +00:00
Layout.fillWidth: true
Layout.topMargin: -column.spacing
Layout.preferredHeight: 300
currentTabIndex: showcaseTabBar.currentIndex
mainDisplayName: root.dirty ? root.dirtyValues.displayName
: d.mainDisplayName
readOnly: root.readOnly
communitiesModel: root.showcaseCommunitiesModel
accountsModel: root.showcaseAccountsModel
collectiblesModel: root.showcaseCollectiblesModel
socialLinksModel: root.showcaseSocialLinksModel
// assetsModel: root.showcaseAssetsModel
2022-09-27 21:26:26 +00:00
walletStore: WalletNS.RootStore
onCloseRequested: root.closeRequested()
onCopyToClipboard: root.profileStore.copyToClipboard(text)
2022-09-27 21:26:26 +00:00
}
}
}
}
layer.enabled: !root.readOnly // profile preview has its own layer.effect
layer.effect: OpacityMask {
maskSource: Rectangle {
anchors.centerIn: parent
width: column.width
height: column.height
radius: background.radius
}
}
}