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

733 lines
29 KiB
QML

import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import QtGraphicalEffects 1.14
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 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
Pane {
id: root
property bool readOnly
property string publicKey: contactsStore.myPublicKey
property var profileStore
property var contactsStore
property QtObject dirtyValues: null
property bool dirty: false
signal closeRequested()
padding: 0
topPadding: 40
background: StatusDialogBackground {
id: background
}
QtObject {
id: d
property var contactDetails: Utils.getContactDetailsAsJson(root.publicKey)
function reload() {
contactDetails = Utils.getContactDetailsAsJson(root.publicKey)
}
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)
readonly property bool isContact: contactDetails.isContact
readonly property bool isBlocked: contactDetails.isBlocked
readonly property bool isContactRequestSent: contactDetails.isAdded
readonly property bool isContactRequestReceived: contactDetails.hasAddedUs
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
readonly property bool isTrusted: outgoingVerificationStatus === Constants.verificationStatus.trusted ||
incomingVerificationStatus === Constants.verificationStatus.trusted
readonly property bool isVerified: outgoingVerificationStatus === Constants.verificationStatus.verified
readonly property string linkToProfile: {
let user = ""
if (d.isCurrentUser)
user = root.profileStore.preferredName
else
user = contactDetails.name
if (!user)
user = Utils.getCompressedPk(root.publicKey)
return Constants.userLinkPrefix + user
}
readonly property var conns: Connections {
target: root.contactsStore.myContactsModel ?? null
function onItemChanged(pubKey) {
if (pubKey === root.publicKey)
d.reload()
}
}
// FIXME: use myContactsModel for identity verification
readonly property var conns2: Connections {
target: root.contactsStore.receivedContactRequestsModel ?? null
function onItemChanged(pubKey) {
if (pubKey === root.publicKey)
d.reload()
}
}
readonly property var conns3: Connections {
target: root.contactsStore.sentContactRequestsModel ?? null
function onItemChanged(pubKey) {
if (pubKey === root.publicKey)
d.reload()
}
}
readonly property var timer: Timer {
id: timer
}
}
function reload() {
d.reload()
}
Component {
id: btnEditProfileComponent
StatusButton {
objectName: "editProfileButton"
size: StatusButton.Size.Small
text: qsTr("Edit Profile")
enabled: !root.readOnly
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
ColumnLayout {
spacing: Style.current.halfPadding
StatusBaseText {
color: Theme.palette.baseColor1
font.pixelSize: 13
text: qsTr("Respond to contact request")
}
AcceptRejectOptionsButtonsPanel {
menuButton.visible: false
onAcceptClicked: {
root.contactsStore.acceptContactRequest(root.publicKey)
d.reload()
}
onDeclineClicked: {
root.contactsStore.dismissContactRequest(root.publicKey)
d.reload()
}
}
}
}
Component {
id: btnSendContactRequestComponent
StatusButton {
objectName: "profileDialog_sendContactRequestButton"
size: StatusButton.Size.Small
text: qsTr("Send Contact Request")
onClicked: {
Global.openContactRequestPopup(root.publicKey, null)
}
}
}
Component {
id: btnBlockUserComponent
StatusButton {
size: StatusButton.Size.Small
type: StatusBaseButton.Type.Danger
text: qsTr("Block User")
onClicked: Global.blockContactRequested(root.publicKey, d.mainDisplayName)
}
}
Component {
id: btnUnblockUserComponent
StatusButton {
size: StatusButton.Size.Small
text: qsTr("Unblock User")
onClicked: Global.unblockContactRequested(root.publicKey, d.mainDisplayName)
}
}
Component {
id: txtPendingContactRequestComponent
StatusBaseText {
font.pixelSize: 13
font.weight: Font.Medium
color: Theme.palette.baseColor1
verticalAlignment: Text.AlignVCenter
text: qsTr("Contact Request Pending...")
}
}
Component {
id: txtRejectedContactRequestComponent
StatusBaseText {
font.pixelSize: 13
font.weight: Font.Medium
color: Theme.palette.baseColor1
verticalAlignment: Text.AlignVCenter
text: qsTr("Contact Request Rejected")
}
}
Component {
id: btnRespondToIdRequestComponent
StatusButton {
size: StatusButton.Size.Small
text: qsTr("Respond to ID Request")
onClicked: {
Global.openIncomingIDRequestPopup(root.publicKey,
popup => popup.closed.connect(d.reload))
}
}
}
ConfirmationDialog {
id: removeContactConfirmationDialog
header.title: qsTr("Remove contact '%1'").arg(d.mainDisplayName)
confirmationText: qsTr("This will remove the user as a contact. Please confirm.")
onConfirmButtonClicked: {
root.contactsStore.removeContact(root.publicKey)
close()
d.reload()
}
}
ConfirmationDialog {
id: removeVerificationConfirmationDialog
header.title: qsTr("Remove contact verification")
confirmationText: qsTr("This will remove the contact's verified status. Please confirm.")
onConfirmButtonClicked: {
root.contactsStore.removeTrustStatus(root.publicKey)
close()
d.reload()
}
}
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 {
Layout.alignment: Qt.AlignTop
objectName: "ProfileDialog_userImage"
name: root.dirty ? root.dirtyValues.displayName
: d.mainDisplayName
pubkey: root.publicKey
image: root.dirty ? root.dirtyValues.profileLargeImage
: d.contactDetails.largeImage
interactive: false
imageWidth: 80
imageHeight: imageWidth
ensVerified: d.contactDetails.ensVerified
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: 4
Layout.alignment: Qt.AlignTop
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: root.dirty ? root.dirtyValues.displayName
: d.mainDisplayName
}
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
tiny: false
}
}
StatusBaseText {
id: contactSecondaryName
font.pixelSize: 12
color: Theme.palette.baseColor1
text: "(%1)".arg(d.optionalDisplayName)
visible: !!d.userNickName
}
EmojiHash {
objectName: "ProfileDialog_userEmojiHash"
publicKey: root.publicKey
}
}
Loader {
Layout.alignment: Qt.AlignTop
Layout.preferredHeight: menuButton.visible ? menuButton.height : -1
sourceComponent: {
// current user
if (d.isCurrentUser)
return btnEditProfileComponent
// contact request, outgoing, rejected
if (!d.isContact && d.isContactRequestSent && d.outgoingVerificationStatus === Constants.verificationStatus.declined)
return txtRejectedContactRequestComponent
// contact request, outgoing, pending
if (!d.isContact && d.isContactRequestSent)
return txtPendingContactRequestComponent
// contact request, incoming, pending
if (!d.isContact && d.isContactRequestReceived && !d.contactDetails.removed)
return btnAcceptContactRequestComponent
// contact request, incoming, rejected
if (d.isContactRequestSent && d.incomingVerificationStatus === Constants.verificationStatus.declined)
return btnBlockUserComponent
// verified contact request, incoming, pending
if (d.isContact && !d.isTrusted && d.isVerificationRequestReceived)
return btnRespondToIdRequestComponent
// block user
if (!d.isContact && !d.isBlocked &&
(d.contactDetails.trustStatus === Constants.trustStatus.untrustworthy || d.outgoingVerificationStatus === Constants.verificationStatus.declined))
return btnBlockUserComponent
// send contact request
if (!d.isContact && !d.isBlocked && !d.isContactRequestSent)
return btnSendContactRequestComponent
// blocked contact
if (d.isBlocked)
return btnUnblockUserComponent
// send message
if (d.isContact && !d.isBlocked)
return btnSendMessageComponent
console.warn("!!! UNHANDLED CONTACT ACTION BUTTON; PUBKEY", root.publicKey)
}
}
StatusFlatButton {
id: menuButton
Layout.alignment: Qt.AlignTop
Layout.preferredWidth: height
visible: !d.isCurrentUser
size: StatusBaseButton.Size.Small
horizontalPadding: 6
verticalPadding: 6
icon.name: "more"
icon.color: Theme.palette.directColor1
highlighted: moreMenu.opened
onClicked: moreMenu.popup(-moreMenu.width + width, height + 4)
StatusMenu {
id: moreMenu
width: 230
SendContactRequestMenuItem {
enabled: !d.isContact && !d.isBlocked && !d.isContactRequestSent && !d.contactDetails.removed &&
d.contactDetails.trustStatus === Constants.trustStatus.untrustworthy // we have an action button otherwise
onTriggered: {
moreMenu.close()
Global.openContactRequestPopup(root.publicKey, null)
}
}
StatusAction {
text: qsTr("Verify Identity")
icon.name: "checkmark-circle"
enabled: d.isContact && !d.isBlocked &&
d.outgoingVerificationStatus === Constants.verificationStatus.unverified &&
!d.isVerificationRequestReceived
onTriggered: {
moreMenu.close()
Global.openSendIDRequestPopup(root.publicKey,
popup => popup.accepted.connect(d.reload))
}
}
StatusAction {
text: qsTr("ID Request Pending...")
icon.name: "checkmark-circle"
enabled: d.isContact && !d.isBlocked && !d.isTrusted && d.isVerificationRequestSent
onTriggered: {
moreMenu.close()
Global.openOutgoingIDRequestPopup(root.publicKey,
popup => popup.closed.connect(d.reload))
}
}
StatusAction {
text: qsTr("Rename")
icon.name: "edit_pencil"
onTriggered: {
moreMenu.close()
Global.openNicknamePopupRequested(root.publicKey, d.userNickName,
"%1 (%2)".arg(d.optionalDisplayName).arg(Utils.getElidedCompressedPk(root.publicKey)))
}
}
StatusAction {
text: qsTr("Reverse Contact Rejection")
icon.name: "refresh"
enabled: d.contactDetails.removed
onTriggered: {
moreMenu.close()
root.contactsStore.removeContactRequestRejection(root.publicKey)
d.reload()
}
}
StatusAction {
text: qsTr("Copy Link to Profile")
icon.name: "copy"
onTriggered: {
moreMenu.close()
root.profileStore.copyToClipboard(d.linkToProfile)
}
}
StatusAction {
text: qsTr("Unblock User")
icon.name: "remove-circle"
enabled: d.isBlocked
onTriggered: {
moreMenu.close()
Global.unblockContactRequested(root.publicKey, d.mainDisplayName)
}
}
StatusMenuSeparator {}
StatusAction {
text: qsTr("Mark as Untrustworthy")
icon.name: "warning"
type: StatusAction.Type.Danger
enabled: d.contactDetails.trustStatus === Constants.trustStatus.unknown
onTriggered: {
moreMenu.close()
if (d.isContact && !d.isTrusted && d.isVerificationRequestReceived)
root.contactsStore.verifiedUntrustworthy(root.publicKey)
else
root.contactsStore.markUntrustworthy(root.publicKey)
d.reload()
}
}
StatusAction {
text: qsTr("Remove Untrustworthy Mark")
icon.name: "warning"
enabled: d.contactDetails.trustStatus === Constants.trustStatus.untrustworthy
onTriggered: {
moreMenu.close()
root.contactsStore.removeTrustStatus(root.publicKey)
d.reload()
}
}
StatusAction {
text: qsTr("Remove Identity Verification")
icon.name: "warning"
type: StatusAction.Type.Danger
enabled: d.isContact && d.isTrusted
onTriggered: {
moreMenu.close()
removeVerificationConfirmationDialog.open()
}
}
StatusAction {
text: qsTr("Remove Contact")
icon.name: "remove-contact"
type: StatusAction.Type.Danger
enabled: d.isContact && !d.isBlocked
onTriggered: {
moreMenu.close()
removeContactConfirmationDialog.open()
}
}
StatusAction {
text: qsTr("Cancel Contact Request")
icon.name: "cancel"
type: StatusAction.Type.Danger
enabled: !d.isContact && d.isContactRequestSent && !d.contactDetails.removed
onTriggered: {
moreMenu.close()
root.contactsStore.removeContact(root.publicKey)
d.reload()
}
}
StatusAction {
text: qsTr("Block User")
icon.name: "cancel"
type: StatusAction.Type.Danger
enabled: !d.isBlocked
onTriggered: {
moreMenu.close()
Global.blockContactRequested(root.publicKey, d.mainDisplayName)
}
}
}
}
}
StatusDialogDivider {
Layout.fillWidth: true
Layout.leftMargin: -column.anchors.leftMargin
Layout.rightMargin: -column.anchors.rightMargin
Layout.topMargin: -column.spacing
Layout.bottomMargin: -column.spacing
opacity: scrollView.atYBeginning ? 0 : 1
Behavior on opacity { OpacityAnimator {} }
}
StatusScrollView {
id: scrollView
Layout.fillWidth: true
Layout.preferredHeight: contentHeight + topPadding + bottomPadding
Layout.leftMargin: -column.anchors.leftMargin
Layout.rightMargin: -column.anchors.rightMargin
Layout.topMargin: -column.spacing
padding: 0
ColumnLayout {
width: scrollView.width
spacing: 20
ProfileBioSocialsPanel {
Layout.fillWidth: true
Layout.leftMargin: column.anchors.leftMargin + Style.current.halfPadding
Layout.rightMargin: column.anchors.rightMargin + Style.current.halfPadding
bio: root.dirty ? root.dirtyValues.bio
: d.contactDetails.bio
userSocialLinksJson: root.dirty ? root.profileStore.temporarySocialLinksJson
: d.contactDetails.socialLinks
}
GridLayout {
Layout.fillWidth: true
Layout.leftMargin: column.anchors.leftMargin
Layout.rightMargin: column.anchors.rightMargin
flow: GridLayout.TopToBottom
rowSpacing: Style.current.halfPadding
columnSpacing: Style.current.bigPadding
visible: d.isCurrentUser
enabled: visible
columns: 2
rows: 4
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Link to Profile")
font.pixelSize: 13
}
StatusBaseInput {
Layout.fillWidth: true
Layout.preferredHeight: 56
leftPadding: 14
rightPadding: Style.current.halfPadding
topPadding: 0
bottomPadding: 0
placeholder.rightPadding: Style.current.halfPadding
placeholderText: d.linkToProfile
placeholderTextColor: Theme.palette.directColor1
edit.readOnly: true
rightComponent: StatusButton {
id: copyLinkBtn
anchors.verticalCenter: parent.verticalCenter
borderColor: Theme.palette.primaryColor1
size: StatusBaseButton.Size.Tiny
text: qsTr("Copy")
onClicked: {
text = qsTr("Copied")
root.profileStore.copyToClipboard(d.linkToProfile)
d.timer.setTimeout(function() {
copyLinkBtn.text = qsTr("Copy")
}, 2000);
}
}
}
StatusBaseText {
Layout.fillWidth: true
Layout.topMargin: Style.current.smallPadding
text: qsTr("Emoji Hash")
font.pixelSize: 13
}
StatusBaseInput {
Layout.fillWidth: true
Layout.preferredHeight: 56
leftPadding: 14
rightPadding: Style.current.halfPadding
topPadding: 0
bottomPadding: 0
edit.readOnly: true
leftComponent: EmojiHash {
publicKey: root.publicKey
oneRow: !root.readOnly
}
rightComponent: StatusButton {
id: copyHashBtn
anchors.verticalCenter: parent.verticalCenter
borderColor: Theme.palette.primaryColor1
size: StatusBaseButton.Size.Tiny
text: qsTr("Copy")
onClicked: {
root.profileStore.copyToClipboard(Utils.getEmojiHashAsJson(root.publicKey).join("").toString())
text = qsTr("Copied")
d.timer.setTimeout(function() {
copyHashBtn.text = qsTr("Copy")
}, 2000);
}
}
}
Rectangle {
Layout.rowSpan: 4
Layout.fillHeight: true
Layout.preferredWidth: height
Layout.alignment: Qt.AlignCenter
color: "transparent"
border.width: 1
border.color: Theme.palette.baseColor2
radius: Style.current.halfPadding
Image {
anchors.centerIn: parent
asynchronous: true
fillMode: Image.PreserveAspectFit
width: 170
height: width
mipmap: true
smooth: false
source: root.profileStore.getQrCodeSource(root.profileStore.pubkey)
}
}
}
StatusTabBar {
Layout.fillWidth: true
Layout.leftMargin: column.anchors.leftMargin
Layout.rightMargin: column.anchors.rightMargin
bottomPadding: -4
StatusTabButton {
leftPadding: 0
width: implicitWidth
text: qsTr("Assets")
}
StatusTabButton {
width: implicitWidth
text: qsTr("Collectibles")
}
StatusTabButton {
width: implicitWidth
text: qsTr("Communities")
}
StatusTabButton {
width: implicitWidth
text: qsTr("Accounts")
}
}
StatusDialogBackground {
Layout.fillWidth: true
Layout.topMargin: -column.spacing
Layout.preferredHeight: 300
color: Theme.palette.baseColor4
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: parent.radius
color: parent.color
}
StatusBaseText {
anchors.centerIn: parent
color: Theme.palette.baseColor1
text: qsTr("More content to appear here soon...")
}
}
}
}
}
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
}
}
}