fix(Profile flow): Adding/changing/removing a nickname

- introduce a shared `CommonContactDialog.qml` component, to be used in
all profile/contact related flows
- make `NicknamePopup.qml` use the common dialog
- adjust the menu item text and add a new "Remove nickname" item
- remove unused `DisplayNamePopup.qml`
- emit toasts when the nickname changes

Fixes #13513
This commit is contained in:
Lukáš Tinkl 2024-02-14 13:52:54 +01:00 committed by Lukáš Tinkl
parent a2b8c3d35c
commit 275d4fb349
11 changed files with 254 additions and 170 deletions

View File

@ -116,8 +116,9 @@ SplitView {
popupParent: root
rootStore: QtObject {
property var contactStore: QtObject {
function changeContactNickname(publicKey, newNickname) {
logs.logEvent("rootStore::contactStore::changeContactNickname", ["publicKey", "newNickname"], arguments)
function changeContactNickname(publicKey, newNickname, displayName, isEdit) {
logs.logEvent("rootStore::contactsStore::changeContactNickname", ["publicKey", "newNickname", "displayName", "isEdit"], arguments)
localNickname.text = newNickname
}
function blockContact(publicKey) {
@ -175,7 +176,7 @@ SplitView {
logs.logEvent("profileStore::copyToClipboard", ["text"], arguments)
}
function requestProfileShowcase(publicKey) {
console.warn("STUB: requestProfileShowcase(publicKey)")
logs.logEvent("profileStore::requestProfileShowcase", ["publicKey"], arguments)
}
readonly property var profileShowcaseCommunitiesModel: CommunitiesModel {}
@ -222,6 +223,11 @@ SplitView {
function getLinkToProfile(publicKey) {
return Constants.userLinkPrefix + publicKey
}
function changeContactNickname(publicKey, newNickname, displayName, isEdit) {
logs.logEvent("contactsStore::changeContactNickname", ["publicKey", "newNickname", "displayName", "isEdit"], arguments)
localNickname.text = newNickname
}
}
walletStore: QtObject {
@ -281,12 +287,13 @@ SplitView {
Label { text: "localNickname:" }
TextField {
id: localNickname
text: "MockNickname"
text: "Nick"
placeholderText: "Local Nickname"
}
Label { text: "displayName:" }
TextField {
id: displayName
text: "Alex Pella"
placeholderText: "Display Name"
}
}

View File

@ -61,8 +61,21 @@ QtObject {
root.contactsModule.removeContact(pubKey)
}
function changeContactNickname(pubKey, nickname) {
function changeContactNickname(pubKey, nickname, displayName, isEdit) {
root.contactsModule.changeContactNickname(pubKey, nickname)
let message = ""
if (nickname === "") { // removed nickname
message = qsTr("Nickname for %1 removed").arg(displayName)
} else {
if (isEdit)
message = qsTr("Nickname for %1 changed").arg(displayName) // changed nickname
else
message = qsTr("Nickname for %1 added").arg(displayName) // added a new nickname
}
if (!!message) {
Global.displaySuccessToastMessage(message)
}
}
function sendContactRequest(pubKey, message) {

View File

@ -60,7 +60,6 @@ QtObject {
Global.unblockContactRequested.connect(openUnblockContactPopup)
Global.openChangeProfilePicPopup.connect(openChangeProfilePicPopup)
Global.openBackUpSeedPopup.connect(openBackUpSeedPopup)
Global.openEditDisplayNamePopup.connect(openEditDisplayNamePopup)
Global.openPinnedMessagesPopupRequested.connect(openPinnedMessagesPopup)
Global.openCommunityProfilePopupRequested.connect(openCommunityProfilePopup)
Global.createCommunityPopupRequested.connect(openCreateCommunityPopup)
@ -133,8 +132,8 @@ QtObject {
openPopup(profilePopupComponent, {publicKey: publicKey, parentPopup: parentPopup}, cb)
}
function openNicknamePopup(publicKey: string, nickname: string, subtitle: string) {
openPopup(nicknamePopupComponent, {publicKey: publicKey, nickname: nickname, "headerSettings.subTitle": subtitle})
function openNicknamePopup(publicKey: string, contactDetails) {
openPopup(nicknamePopupComponent, {publicKey, contactDetails})
}
function openBlockContactPopup(publicKey: string, contactName: string) {
@ -154,10 +153,6 @@ QtObject {
openPopup(backupSeedModalComponent)
}
function openEditDisplayNamePopup() {
openPopup(displayNamePopupComponent)
}
function openCommunityProfilePopup(store, community, communitySectionModule) {
openPopup(communityProfilePopup, { store: store, community: community, communitySectionModule: communitySectionModule})
}
@ -431,15 +426,6 @@ QtObject {
}
},
// FIXME remove, unused
Component {
id: displayNamePopupComponent
DisplayNamePopup {
profileStore: rootStore.profileSectionStore.profileStore
onClosed: destroy()
}
},
Component {
id: downloadPageComponent
DownloadPage {
@ -527,11 +513,14 @@ QtObject {
NicknamePopup {
onEditDone: {
if (nickname !== newNickname) {
rootStore.contactStore.changeContactNickname(publicKey, newNickname)
Global.contactRenamed(publicKey)
rootStore.contactStore.changeContactNickname(publicKey, newNickname, originalDisplayName, !!nickname)
}
close()
}
onRemoveNicknameRequested: {
rootStore.contactStore.changeContactNickname(publicKey, "", originalDisplayName, true)
close()
}
onClosed: destroy()
}
},

View File

@ -0,0 +1,120 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Popups.Dialog 0.1
import utils 1.0
import shared.controls 1.0
import shared.controls.chat 1.0
StatusDialog {
id: root
required property string publicKey
required property var contactDetails
default property alias content: contentLayout.children
readonly property string originalDisplayName: d.optionalDisplayName
width: 480
horizontalPadding: 16
verticalPadding: 20
QtObject {
id: d
readonly property string mainDisplayName: ProfileUtils.displayName(contactDetails.localNickname, contactDetails.name,
contactDetails.displayName, contactDetails.alias)
readonly property string optionalDisplayName: ProfileUtils.displayName("", contactDetails.name, contactDetails.displayName, contactDetails.alias)
}
contentItem: ColumnLayout {
RowLayout {
Layout.fillWidth: true
spacing: Style.current.padding
UserImage {
name: d.mainDisplayName
pubkey: root.publicKey
image: Utils.addTimestampToURL(contactDetails.largeImage)
interactive: false
imageWidth: 60
imageHeight: 60
ensVerified: contactDetails.ensVerified
onlineStatus: contactDetails.onlineStatus
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
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)
font.bold: true
font.pixelSize: 17
elide: Text.ElideRight
text: d.mainDisplayName
}
StatusContactVerificationIcons {
id: verificationIcons
anchors.left: contactName.right
anchors.leftMargin: Style.current.halfPadding
anchors.verticalCenter: contactName.verticalCenter
isContact: contactDetails.isContact
trustIndicator: contactDetails.trustStatus
isBlocked: contactDetails.isBlocked
tiny: false
}
}
RowLayout {
spacing: Style.current.halfPadding
StatusBaseText {
id: contactSecondaryName
color: Theme.palette.baseColor1
font.pixelSize: 13
text: d.optionalDisplayName
visible: !!contactDetails.localNickname
}
Rectangle {
Layout.preferredWidth: 4
Layout.preferredHeight: 4
radius: width/2
color: Theme.palette.baseColor1
visible: contactSecondaryName.visible
}
StatusBaseText {
font.pixelSize: 13
color: Theme.palette.baseColor1
text: Utils.getElidedCompressedPk(root.publicKey)
}
}
EmojiHash {
Layout.topMargin: 4
publicKey: root.publicKey
oneRow: true
}
}
}
StatusDialogDivider {
Layout.fillWidth: true
Layout.topMargin: 15
Layout.bottomMargin: 15
}
ColumnLayout {
id: contentLayout
}
}
}

View File

@ -1,52 +0,0 @@
import QtQuick 2.12
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQml.Models 2.3
import utils 1.0
import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.controls 1.0
import StatusQ.Controls 0.1
import StatusQ.Popups 0.1
import StatusQ.Controls.Validators 0.1
StatusModal {
id: root
property var profileStore
width: 420
height: 250
closePolicy: Popup.NoAutoClose
headerSettings.title: qsTr("Edit")
contentItem: Item {
StatusInput {
id: displayNameInput
input.edit.objectName: "DisplayNamePopup_displayNameInput"
width: parent.width - Style.current.padding
anchors.top: parent.top
anchors.topMargin: Style.current.padding
anchors.horizontalCenter: parent.horizontalCenter
placeholderText: qsTr("Display Name")
input.text: root.profileStore.displayName
validators: Constants.validators.displayName
}
}
rightButtons: [
StatusButton {
id: okBtn
objectName: "DisplayNamePopup_okButton"
text: qsTr("OK")
enabled: !!displayNameInput.text && displayNameInput.valid
onClicked: {
root.profileStore.setDisplayName(displayNameInput.text)
root.close()
}
}
]
onOpened: { displayNameInput.input.edit.forceActiveFocus() }
}

View File

@ -1,85 +1,84 @@
import QtQuick 2.14
import utils 1.0
import shared.controls 1.0
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1
StatusModal {
id: popup
import shared.controls 1.0
width: 400
height: 340
anchors.centerIn: parent
CommonContactDialog {
id: root
headerSettings.title: qsTr("Nickname")
headerSettings.subTitleElide: Text.ElideMiddle
/*required*/ property string publicKey
property string nickname
readonly property int nicknameLength: nicknameInput.text.length
readonly property int maxNicknameLength: 32
readonly property string nickname: contactDetails.localNickname
signal editDone(string newNickname)
signal removeNicknameRequested()
title: d.editMode ? qsTr("Edit nickname") : qsTr("Add nickname")
onOpened: {
nicknameInput.input.edit.forceActiveFocus()
}
contentItem: Item {
width: popup.width
height: childrenRect.height
readonly property var d: QtObject {
id: d
readonly property bool editMode: root.nickname !== ""
readonly property int maxNicknameLength: 32
}
Column {
anchors.top: parent.top
anchors.topMargin: 16
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 32
spacing: 16
StatusBaseText {
id: descriptionText
text: qsTr("Nicknames help you identify others in Status. Only you can see the nicknames youve added")
wrapMode: Text.WordWrap
color: Theme.palette.baseColor1
width: parent.width
StatusInput {
Layout.fillWidth: true
id: nicknameInput
label: qsTr("Nickname")
input.clearable: true
text: root.nickname
charLimit: d.maxNicknameLength
validationMode: StatusInput.ValidationMode.IgnoreInvalidInput
validators: [
StatusValidator {
validatorObj: RXValidator { regularExpression: /^[\w\d_ -]*$/u }
validate: (value) => validatorObj.test(value)
}
StatusInput {
id: nicknameInput
placeholderText: qsTr("Nickname")
input.clearable: true
width: parent.width
text: popup.nickname
charLimit: maxNicknameLength
validationMode: StatusInput.ValidationMode.IgnoreInvalidInput
validators: [
StatusValidator {
validatorObj: RXValidator { regularExpression: /^[\w\d_ -]*$/u }
validate: (value) => validatorObj.test(value)
}
]
Keys.onReleased: {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
editDone(nicknameInput.text)
}
}
]
Keys.onReleased: {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.editDone(nicknameInput.text)
}
}
}
rightButtons: [
StatusButton {
id: doneBtn
text: qsTr("Done")
enabled: nicknameInput.valid
onClicked: editDone(nicknameInput.text)
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Nicknames help you identify others and are only visible to you")
wrapMode: Text.WordWrap
color: Theme.palette.baseColor1
font.pixelSize: Theme.tertiaryTextFontSize
}
footer: StatusDialogFooter {
rightButtons: ObjectModel {
StatusFlatButton {
visible: !d.editMode
text: qsTr("Cancel")
onClicked: root.close()
}
StatusFlatButton {
visible: d.editMode
borderColor: "transparent"
type: StatusBaseButton.Type.Danger
text: qsTr("Remove nickname")
onClicked: root.removeNicknameRequested()
}
StatusButton {
enabled: root.nickname !== nicknameInput.text && nicknameInput.valid
text: d.editMode ? qsTr("Change nickname") : qsTr("Add nickname")
onClicked: root.editDone(nicknameInput.text)
}
}
]
}
}

View File

@ -6,6 +6,7 @@ import QtQml.Models 2.3
import utils 1.0
import shared.controls.chat 1.0
import shared.panels 1.0
import shared.controls.chat.menuItems 1.0
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
@ -28,16 +29,23 @@ StatusMenu {
StatusMenuSeparator {
}
StatusAction {
ViewProfileMenuItem {
objectName: "userStatusViewMyProfileAction"
text: qsTr("View My Profile")
icon.name: "profile"
onTriggered: {
Global.openProfilePopup(root.store.userProfileInst.pubKey)
root.close()
}
}
StatusAction {
text: qsTr("Copy link to profile")
icon.name: "copy"
onTriggered: {
Utils.copyToClipboard(root.store.contactStore.getLinkToProfile(root.store.userProfileInst.pubKey))
root.close()
}
}
StatusMenuSeparator {
}

View File

@ -1,4 +1,5 @@
ChatCommandsPopup 1.0 ChatCommandsPopup.qml
CommonContactDialog 1.0 CommonContactDialog.qml
BlockContactConfirmationDialog 1.0 BlockContactConfirmationDialog.qml
SettingsDirtyToastMessage 1.0 SettingsDirtyToastMessage.qml
ConfirmationDialog 1.0 ConfirmationDialog.qml
@ -16,7 +17,6 @@ UserStatusContextMenu 1.0 UserStatusContextMenu.qml
ProfileDialog 1.0 ProfileDialog.qml
ImageCropWorkflow 1.0 ImageCropWorkflow.qml
ImportCommunityPopup 1.0 ImportCommunityPopup.qml
DisplayNamePopup 1.0 DisplayNamePopup.qml
SendContactRequestModal 1.0 SendContactRequestModal.qml
GetSyncCodeInstructionsPopup 1.0 GetSyncCodeInstructionsPopup.qml
NoPermissionsToJoinPopup 1.0 NoPermissionsToJoinPopup.qml

View File

@ -111,15 +111,6 @@ Pane {
d.reload()
}
}
readonly property var conns4: Connections {
target: Global
function onContactRenamed(pubKey) {
if (pubKey === root.publicKey)
d.reload()
}
}
}
function reload() {
@ -395,7 +386,6 @@ Pane {
enabled: !d.isContact && !d.isBlocked && d.contactRequestState !== Constants.ContactRequestState.Sent &&
d.contactDetails.trustStatus === Constants.trustStatus.untrustworthy // we have an action button otherwise
onTriggered: {
moreMenu.close()
Global.openContactRequestPopup(root.publicKey, null)
}
}
@ -406,7 +396,6 @@ Pane {
d.outgoingVerificationStatus === Constants.verificationStatus.unverified &&
!d.isVerificationRequestReceived
onTriggered: {
moreMenu.close()
Global.openSendIDRequestPopup(root.publicKey,
popup => popup.accepted.connect(d.reload))
}
@ -417,7 +406,6 @@ Pane {
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))
}
@ -426,9 +414,7 @@ Pane {
text: d.userNickName ? qsTr("Edit nickname") : qsTr("Add nickname")
icon.name: "edit_pencil"
onTriggered: {
moreMenu.close()
Global.openNicknamePopupRequested(root.publicKey, d.userNickName,
"%1 (%2)".arg(d.optionalDisplayName).arg(Utils.getElidedCompressedPk(root.publicKey)))
Global.openNicknamePopupRequested(root.publicKey, d.contactDetails)
}
}
StatusAction {
@ -436,7 +422,6 @@ Pane {
icon.name: "qr"
enabled: !d.isCurrentUser
onTriggered: {
moreMenu.close()
Global.openPopup(shareProfileCmp)
}
}
@ -444,19 +429,23 @@ Pane {
text: qsTr("Copy link to profile")
icon.name: "copy"
onTriggered: {
moreMenu.close()
root.profileStore.copyToClipboard(d.linkToProfile)
}
}
StatusMenuSeparator {}
// TODO Remove nickname
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)
}
StatusAction {
text: qsTr("Unblock user")
icon.name: "cancel"
type: StatusAction.Type.Danger
enabled: d.isBlocked
onTriggered: {
moreMenu.close()
Global.unblockContactRequested(root.publicKey, d.mainDisplayName)
}
}
@ -466,7 +455,6 @@ Pane {
type: StatusAction.Type.Danger
enabled: d.contactDetails.trustStatus === Constants.trustStatus.unknown && !d.isBlocked
onTriggered: {
moreMenu.close()
if (d.isContact && !d.isTrusted && d.isVerificationRequestReceived)
root.contactsStore.verifiedUntrustworthy(root.publicKey)
else
@ -480,7 +468,6 @@ Pane {
type: StatusAction.Type.Danger
enabled: d.contactDetails.trustStatus === Constants.trustStatus.untrustworthy && !d.isBlocked
onTriggered: {
moreMenu.close()
root.contactsStore.removeTrustStatus(root.publicKey)
d.reload()
}
@ -491,7 +478,6 @@ Pane {
type: StatusAction.Type.Danger
enabled: d.isContact && d.isTrusted
onTriggered: {
moreMenu.close()
removeVerificationConfirmationDialog.open()
}
}
@ -501,8 +487,7 @@ Pane {
type: StatusAction.Type.Danger
enabled: d.isContact && !d.isBlocked && d.contactRequestState !== Constants.ContactRequestState.Sent
onTriggered: {
Global.removeContactRequested(d.mainDisplayName, root.publicKey);
moreMenu.close();
Global.removeContactRequested(d.mainDisplayName, root.publicKey)
}
}
StatusAction {
@ -511,7 +496,6 @@ Pane {
type: StatusAction.Type.Danger
enabled: !d.isBlocked
onTriggered: {
moreMenu.close()
Global.blockContactRequested(root.publicKey, d.mainDisplayName)
}
}

View File

@ -194,8 +194,7 @@ StatusMenu {
icon.name: "edit_pencil"
enabled: !root.isMe && !root.isBridgedAccount
onTriggered: {
Global.openNicknamePopupRequested(root.selectedUserPublicKey, contactDetails.localNickname,
"%1 (%2)".arg(root.selectedUserDisplayName).arg(Utils.getElidedCompressedPk(root.selectedUserPublicKey)))
Global.openNicknamePopupRequested(root.selectedUserPublicKey, root.contactDetails)
root.close()
}
}
@ -206,6 +205,14 @@ StatusMenu {
|| removeUntrustworthyMarkMenuItem.enabled
}
StatusAction {
text: qsTr("Remove nickname")
icon.name: "delete"
type: StatusAction.Type.Danger
enabled: !root.isMe && !!contactDetails.localNickname
onTriggered: root.store.contactsStore.changeContactNickname(root.selectedUserPublicKey, "", root.selectedUserDisplayName, true)
}
StatusAction {
id: unblockAction
objectName: "unblock_StatusItem"

View File

@ -31,13 +31,12 @@ QtObject {
signal openPopupRequested(var popupComponent, var params)
signal closePopupRequested()
signal openNicknamePopupRequested(string publicKey, string nickname, string subtitle)
signal openNicknamePopupRequested(string publicKey, var contactDetails)
signal openDownloadModalRequested(bool available, string version, string url)
signal openChangeProfilePicPopup(var cb)
signal openBackUpSeedPopup()
signal openImagePopup(var image, string url)
signal openProfilePopupRequested(string publicKey, var parentPopup, var cb)
signal openEditDisplayNamePopup()
signal openActivityCenterPopupRequested()
signal openSendIDRequestPopup(string publicKey, var cb)
signal openContactRequestPopup(string publicKey, var cb)
@ -49,7 +48,6 @@ QtObject {
signal openDownloadImageDialog(string imageSource)
signal openExportControlNodePopup(var community)
signal openImportControlNodePopup(var community)
signal contactRenamed(string publicKey)
signal openTransferOwnershipPopup(string communityId,
string communityName,
string communityLogo,
@ -131,4 +129,15 @@ QtObject {
menu.popup()
return menu
}
function displaySuccessToastMessage(title: string, subTitle = "") {
root.displayToastMessage(
title,
subTitle,
"checkmark-circle",
false,
Constants.ephemeralNotificationType.success,
""
)
}
}