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

748 lines
29 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
2022-09-27 21:26:26 +00:00
property string publicKey: contactsStore.myPublicKey
property var profileStore
property var contactsStore
property var walletStore: WalletNS.RootStore
property var communitiesModel
2022-09-27 21:26:26 +00:00
property QtObject dirtyValues: null
property bool dirty: false
2022-09-27 21:26:26 +00:00
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)
2022-09-27 21:26:26 +00:00
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
2022-09-27 21:26:26 +00:00
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
2022-09-27 21:26:26 +00:00
else
user = contactDetails.name
if (!user)
user = Utils.getCompressedPk(root.publicKey)
2022-09-27 21:26:26 +00:00
return Constants.userLinkPrefix + user
}
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()
}
}
readonly property var conns4: Connections {
target: Global
function onContactRenamed(pubKey) {
if (pubKey === root.publicKey)
d.reload()
}
}
readonly property var timer: Timer {
id: timer
}
2022-09-27 21:26:26 +00:00
}
function reload() {
d.reload()
}
Component {
id: btnEditProfileComponent
StatusButton {
objectName: "editProfileButton"
2022-09-27 21:26:26 +00:00
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: btnRevertContactRequestRejectionComponent
StatusButton {
size: StatusButton.Size.Small
text: qsTr("Reverse Contact Rejection")
icon.name: "refresh"
icon.width: 16
icon.height: 16
onClicked: {
root.contactsStore.removeContactRequestRejection(root.publicKey)
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, null)
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.mainDisplayName)
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.mainDisplayName)
2022-09-27 21:26:26 +00:00
}
}
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))
2022-09-27 21:26:26 +00:00
}
}
}
ConfirmationDialog {
id: removeContactConfirmationDialog
header.title: qsTr("Remove contact '%1'").arg(d.mainDisplayName)
2022-09-27 21:26:26 +00:00
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
2022-09-27 21:26:26 +00:00
pubkey: root.publicKey
image: root.dirty ? root.dirtyValues.profileLargeImage
: d.contactDetails.largeImage
2022-09-27 21:26:26 +00:00
interactive: false
imageWidth: 80
imageHeight: imageWidth
ensVerified: d.contactDetails.ensVerified
2022-09-27 21:26:26 +00:00
}
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
2022-09-27 21:26:26 +00:00
}
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
2022-09-27 21:26:26 +00:00
}
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
2022-09-27 21:26:26 +00:00
// contact request, outgoing, pending
if (!d.isContact && d.isContactRequestSent)
return txtPendingContactRequestComponent
// contact request, incoming, pending
if (!d.isContact && d.isContactRequestReceived) {
if (d.contactDetails.removed) {
return btnRevertContactRequestRejectionComponent
} else {
return btnAcceptContactRequestComponent
}
}
2022-09-27 21:26:26 +00:00
// 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)
2022-12-01 16:58:37 +00:00
StatusMenu {
2022-09-27 21:26:26 +00:00
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)
2022-09-27 21:26:26 +00:00
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
2022-09-27 21:26:26 +00:00
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))
2022-09-27 21:26:26 +00:00
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
2022-09-27 21:26:26 +00:00
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))
2022-09-27 21:26:26 +00:00
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
2022-09-27 21:26:26 +00:00
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)))
2022-09-27 21:26:26 +00:00
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
2022-09-27 21:26:26 +00:00
text: qsTr("Copy Link to Profile")
icon.name: "copy"
onTriggered: {
moreMenu.close()
root.profileStore.copyToClipboard(d.linkToProfile)
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
2022-09-27 21:26:26 +00:00
text: qsTr("Unblock User")
icon.name: "remove-circle"
enabled: d.isBlocked
onTriggered: {
moreMenu.close()
Global.unblockContactRequested(root.publicKey, d.mainDisplayName)
2022-09-27 21:26:26 +00:00
}
}
StatusMenuSeparator {}
2022-12-01 16:58:37 +00:00
StatusAction {
2022-09-27 21:26:26 +00:00
text: qsTr("Mark as Untrustworthy")
icon.name: "warning"
2022-12-01 16:58:37 +00:00
type: StatusAction.Type.Danger
2022-09-27 21:26:26 +00:00
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()
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
2022-09-27 21:26:26 +00:00
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()
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
2022-09-27 21:26:26 +00:00
text: qsTr("Remove Identity Verification")
icon.name: "warning"
2022-12-01 16:58:37 +00:00
type: StatusAction.Type.Danger
2022-09-27 21:26:26 +00:00
enabled: d.isContact && d.isTrusted
onTriggered: {
moreMenu.close()
removeVerificationConfirmationDialog.open()
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
2022-09-27 21:26:26 +00:00
text: qsTr("Remove Contact")
icon.name: "remove-contact"
2022-12-01 16:58:37 +00:00
type: StatusAction.Type.Danger
2022-09-27 21:26:26 +00:00
enabled: d.isContact && !d.isBlocked
onTriggered: {
moreMenu.close()
removeContactConfirmationDialog.open()
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
2022-09-27 21:26:26 +00:00
text: qsTr("Block User")
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: {
moreMenu.close()
Global.blockContactRequested(root.publicKey, d.mainDisplayName)
2022-09-27 21:26:26 +00:00
}
}
}
}
}
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
implicitWidth: contentWidth + leftPadding + rightPadding
implicitHeight: contentHeight + topPadding + bottomPadding
2022-09-27 21:26:26 +00:00
Layout.fillWidth: true
Layout.fillHeight: true
2022-09-27 21:26:26 +00:00
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.readOnly ? root.profileStore.temporarySocialLinksJson
: d.contactDetails.socialLinks
2022-09-27 21:26:26 +00:00
}
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: Style.current.padding
2022-09-27 21:26:26 +00:00
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
2022-09-27 21:26:26 +00:00
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);
2022-09-27 21:26:26 +00:00
}
}
}
StatusBaseText {
Layout.fillWidth: true
Layout.topMargin: Style.current.smallPadding
text: qsTr("Emoji Hash")
font.pixelSize: 13
}
StatusBaseInput {
Layout.fillWidth: true
Layout.preferredHeight: 56
leftPadding: Style.current.padding
2022-09-27 21:26:26 +00:00
rightPadding: Style.current.halfPadding
topPadding: 0
bottomPadding: 0
edit.readOnly: true
leftComponent: EmojiHash {
publicKey: root.publicKey
oneRow: !root.readOnly
}
rightComponent: StatusButton {
id: copyHashBtn
2022-09-27 21:26:26 +00:00
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);
2022-09-27 21:26:26 +00:00
}
}
}
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(Utils.getCompressedPk(root.profileStore.pubkey))
2022-09-27 21:26:26 +00:00
}
}
}
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 {
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 {
leftPadding: 0
2022-09-27 21:26:26 +00:00
width: implicitWidth
text: qsTr("Assets")
2022-09-27 21:26:26 +00:00
}
}
// Profile Showcase
ProfileShowcaseView {
2022-09-27 21:26:26 +00:00
Layout.fillWidth: true
Layout.topMargin: -column.spacing
Layout.preferredHeight: 300
currentTabIndex: showcaseTabBar.currentIndex
isCurrentUser: d.isCurrentUser
mainDisplayName: d.mainDisplayName
readOnly: root.readOnly
profileStore: root.profileStore
walletStore: root.walletStore
communitiesModel: root.communitiesModel
2022-09-27 21:26:26 +00:00
onCloseRequested: root.closeRequested()
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
}
}
}