fix(MessageContextMenu): Cleanup. Separate menu for profile. (#10729)

This commit is contained in:
Igor Sirotin 2023-05-19 19:07:50 +03:00 committed by GitHub
parent f7e75208a5
commit 5ff4b5a435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 986 additions and 740 deletions

View File

@ -67,7 +67,9 @@ add_executable(
target_compile_definitions(${PROJECT_NAME} PRIVATE
QML_IMPORT_ROOT="${CMAKE_CURRENT_LIST_DIR}"
STATUSQ_MODULE_IMPORT_PATH="${STATUSQ_MODULE_IMPORT_PATH}")
STATUSQ_MODULE_IMPORT_PATH="${STATUSQ_MODULE_IMPORT_PATH}"
$<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>
)
target_link_libraries(
${PROJECT_LIB} PUBLIC Qt5::Core Qt5::Gui Qt5::Quick Qt5::QuickControls2)

View File

@ -61,6 +61,10 @@ ListModel {
title: "TokenListView"
section: "Views"
}
ListElement {
title: "MessageContextMenu"
section: "Views"
}
ListElement {
title: "StatusCommunityCard"
section: "Panels"

View File

@ -0,0 +1,131 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import AppLayouts.Chat.views.communities 1.0
import Storybook 1.0
import Models 1.0
import utils 1.0
import shared.views.chat 1.0
SplitView {
QtObject {
id: d
}
Logs { id: logs }
SplitView {
orientation: Qt.Vertical
SplitView.fillWidth: true
Rectangle {
SplitView.fillWidth: true
SplitView.fillHeight: true
color: Theme.palette.statusAppLayout.rightPanelBackgroundColor
clip: true
RowLayout {
anchors.centerIn: parent
Button {
text: "Message context menu"
onClicked: {
menu1.createObject(this).popup()
}
}
Button {
text: "Message context menu (hide disabled items)"
onClicked: {
menu2.createObject(this).popup()
}
}
Button {
text: "Profile context menu"
onClicked: {
menu3.createObject(this).popup()
}
}
Button {
text: "Profile context menu (hide disabled items)"
onClicked: {
menu4.createObject(this).popup()
}
}
}
Component {
id: menu1
MessageContextMenuView {
anchors.centerIn: parent
hideDisabledItems: false
onClosed: {
destroy()
}
}
}
Component {
id: menu2
MessageContextMenuView {
anchors.centerIn: parent
hideDisabledItems: true
onClosed: {
destroy()
}
}
}
Component {
id: menu3
ProfileContextMenu {
anchors.centerIn: parent
hideDisabledItems: false
onClosed: {
destroy()
}
}
}
Component {
id: menu4
ProfileContextMenu {
anchors.centerIn: parent
hideDisabledItems: true
onClosed: {
destroy()
}
}
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 150
logsView.logText: logs.logText
}
}
Pane {
SplitView.minimumWidth: 300
SplitView.preferredWidth: 300
ScrollView {
anchors.fill: parent
ColumnLayout {
spacing: 16
}
}
}
}

View File

@ -66,7 +66,6 @@ SplitView {
sourceComponent: UserListPanel {
usersModel: model
messageContextMenu: null
label: "Some label"
}
}

View File

@ -71,7 +71,7 @@ Menu {
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
topPadding: 8
bottomPadding: 8
bottomMargin: 16
margins: 16
onOpened: {
if (typeof openHandler === "function") {

View File

@ -8,16 +8,16 @@ import StatusQ.Components 0.1
import shared 1.0
import shared.panels 1.0
import shared.status 1.0
import shared.views.chat 1.0
import utils 1.0
import SortFilterProxyModel 0.2
Item {
id: root
anchors.fill: parent
property var store
property var usersModel
property var messageContextMenu
property string label
StatusBaseText {
@ -98,15 +98,13 @@ Item {
ringSettings.ringSpecModel: model.colorHash
onClicked: {
if (mouse.button === Qt.RightButton) {
// Set parent, X & Y positions for the messageContextMenu
messageContextMenu.parent = this
messageContextMenu.isProfile = true
messageContextMenu.myPublicKey = userProfile.pubKey
messageContextMenu.selectedUserPublicKey = model.pubKey
messageContextMenu.selectedUserDisplayName = title
messageContextMenu.selectedUserIcon = model.icon
messageContextMenu.popup(4, 4)
} else if (mouse.button === Qt.LeftButton && !!messageContextMenu) {
Global.openMenu(profileContextMenuComponent, this, {
myPublicKey: userProfile.pubKey,
selectedUserPublicKey: model.pubKey,
selectedUserDisplayName: title,
selectedUserIcon: model.icon,
})
} else if (mouse.button === Qt.LeftButton) {
Global.openProfilePopup(model.pubKey);
}
}
@ -136,4 +134,23 @@ Item {
}
}
}
Component {
id: profileContextMenuComponent
ProfileContextMenu {
store: root.store
margins: 8
onOpenProfileClicked: {
Global.openProfilePopup(publicKey, null)
}
onCreateOneToOneChat: {
Global.changeAppSectionBySectionType(Constants.appSection.chat)
root.store.chatCommunitySectionModule.createOneToOneChat(communityId, chatId, ensName)
}
onClosed: {
destroy()
}
}
}
}

View File

@ -21,12 +21,10 @@ SettingsPageLayout {
property var pendingMemberRequestsModel
property var declinedMemberRequestsModel
property string communityName
property var communityMemberContextMenu
property bool editable: true
signal membershipRequestsClicked()
signal userProfileClicked(string id)
signal kickUserClicked(string id)
signal banUserClicked(string id)
signal unbanUserClicked(string id)
@ -120,7 +118,6 @@ SettingsPageLayout {
Layout.fillWidth: true
Layout.fillHeight: true
onUserProfileClicked: root.userProfileClicked(id)
onKickUserClicked: {
kickModal.userNameToKick = name
kickModal.userIdToKick = id
@ -151,7 +148,6 @@ SettingsPageLayout {
Layout.fillWidth: true
Layout.fillHeight: true
onUserProfileClicked: root.userProfileClicked(id)
onAcceptRequestToJoin: root.acceptRequestToJoin(id)
onDeclineRequestToJoin: root.declineRequestToJoin(id)
}
@ -173,7 +169,6 @@ SettingsPageLayout {
Layout.fillWidth: true
Layout.fillHeight: true
onUserProfileClicked: root.userProfileClicked(id)
onAcceptRequestToJoin: root.acceptRequestToJoin(id)
}
@ -194,7 +189,6 @@ SettingsPageLayout {
Layout.fillWidth: true
Layout.fillHeight: true
onUserProfileClicked: root.userProfileClicked(id)
onUnbanUserClicked: root.unbanUserClicked(id)
}
}

View File

@ -9,6 +9,7 @@ import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import utils 1.0
import shared.views.chat 1.0
import shared.controls.chat 1.0
import "../../layouts"
@ -20,7 +21,6 @@ Item {
property var model
property var communityMemberContextMenu
signal userProfileClicked(string id)
signal kickUserClicked(string id, string name)
signal banUserClicked(string id, string name)
signal unbanUserClicked(string id)
@ -146,18 +146,37 @@ Item {
onClicked: {
if(mouse.button === Qt.RightButton) {
// Set parent, X & Y positions for the messageContextMenu
root.communityMemberContextMenu.parent = this
root.communityMemberContextMenu.isProfile = true
root.communityMemberContextMenu.selectedUserPublicKey = model.pubKey
root.communityMemberContextMenu.selectedUserDisplayName = userName
root.communityMemberContextMenu.selectedUserIcon = asset.name
root.communityMemberContextMenu.popup()
Global.openMenu(memberContextMenuComponent, this, {
selectedUserPublicKey: model.pubKey,
selectedUserDisplayName: userName,
selectedUserIcon: asset.name,
})
} else {
root.userProfileClicked(model.pubKey)
Global.openProfilePopup(model.pubKey)
}
}
}
}
}
Component {
id: memberContextMenuComponent
ProfileContextMenu {
id: memberContextMenuView
store: root.rootStore
myPublicKey: root.rootStore.myPublicKey()
onOpenProfileClicked: {
Global.openProfilePopup(publicKey, null)
}
onCreateOneToOneChat: {
Global.changeAppSectionBySectionType(Constants.appSection.chat)
root.rootStore.chatCommunitySectionModule.createOneToOneChat(communityId, chatId, ensName)
}
onClosed: {
destroy()
}
}
}
}

View File

@ -7,6 +7,7 @@ import QtGraphicalEffects 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1
import utils 1.0
@ -33,6 +34,15 @@ StatusDialog {
subtitle: root.messageToPin ? qsTr("Unpin a previous message first")
: qsTr("%n message(s)", "", pinnedMessageListView.count)
QtObject {
id: d
function jumpToMessage(messageId) {
root.close()
root.messageStore.messageModule.jumpToMessage(messageId)
}
}
contentItem: ColumnLayout {
id: column
@ -67,7 +77,6 @@ StatusDialog {
rootStore: root.store
messageStore: root.messageStore
messageContextMenu: msgContextMenu
messageId: model.id
responseToMessageWithId: model.responseToMessageWithId
@ -120,14 +129,23 @@ StatusDialog {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
z: 55
onClicked: {
if (!!root.messageToPin) {
if (!radio.checked)
radio.checked = true
} else {
root.close()
root.messageStore.messageModule.jumpToMessage(model.id)
}
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
switch (mouse.button) {
case Qt.RightButton:
Global.openMenu(pinnedPopupMessageContextMenuComponent, this, {
messageId: messageItem.messageId,
})
break
case Qt.LeftButton:
if (!!root.messageToPin) {
if (!radio.checked)
radio.checked = true
} else {
d.jumpToMessage(model.id)
}
break
}
}
}
@ -160,24 +178,42 @@ StatusDialog {
root.messageToUnpin = checked ? model.id : ""
}
}
}
}
MessageContextMenuView {
id: msgContextMenu
store: root.store
pinnedPopup: true
pinnedMessage: true
onShouldCloseParentPopup: {
root.close()
}
Component {
id: pinnedPopupMessageContextMenuComponent
onUnpinMessage: {
root.messageStore.unpinMessage(messageId)
}
StatusMenu {
id: messageContextMenu
onJumpToMessage: {
root.messageStore.messageModule.jumpToMessage(messageId)
property string messageId
StatusAction {
text: qsTr("Unpin")
icon.name: "unpin"
onTriggered: {
root.messageStore.unpinMessage(messageContextMenu.messageId)
close()
}
}
StatusAction {
text: qsTr("Jump to")
icon.name: "arrow-up"
onTriggered: {
d.jumpToMessage(messageContextMenu.messageId)
close()
}
}
onOpened: {
messageItem.setMessageActive(model.id, true)
}
onClosed: {
messageItem.setMessageActive(model.id, false)
destroy()
}
}
}
}
}

View File

@ -124,6 +124,13 @@ QtObject {
messageModule.deleteMessage(messageId)
}
function warnAndDeleteMessage(messageId) {
if (localAccountSensitiveSettings.showDeleteMessageWarning)
Global.openDeleteMessagePopup(messageId, this)
else
deleteMessage(messageId)
}
function setEditModeOn(messageId) {
if(!messageModule)
return

View File

@ -209,18 +209,11 @@ QtObject {
stickersModuleInst.send(channelId, hash, replyTo, pack, url)
}
// TODO: This seems to be better in Utils.qml
function copyToClipboard(text) {
globalUtilsInst.copyToClipboard(text)
}
function copyImageToClipboardByUrl(content) {
globalUtilsInst.copyImageToClipboardByUrl(content)
}
function downloadImageByUrl(url, path) {
globalUtilsInst.downloadImageByUrl(url, path)
}
function isCurrentUser(pubkey) {
return userProfileInst.pubKey === pubkey
}

View File

@ -74,60 +74,16 @@ ColumnLayout {
chatSectionModule: root.rootStore.chatCommunitySectionModule
}
Loader {
id: contextMenuLoader
active: root.isActiveChannel
asynchronous: true
QtObject {
id: d
// FIXME: `MessageContextMenuView` is way too heavy
// see: https://github.com/status-im/status-desktop/pull/10343#issuecomment-1515103756
sourceComponent: MessageContextMenuView {
store: root.rootStore
reactionModel: root.rootStore.emojiReactionsModel
disabledForChat: !root.isUserAllowedToSendMessage
onPinMessage: {
messageStore.pinMessage(messageId)
function showReplyArea(messageId) {
let obj = messageStore.getMessageByIdAsJson(messageId)
if (!obj) {
return
}
onUnpinMessage: {
messageStore.unpinMessage(messageId)
}
onPinnedMessagesLimitReached: {
if(!chatContentModule) {
console.warn("error on open pinned messages limit reached from message context menu - chat content module is not set")
return
}
Global.openPinnedMessagesPopupRequested(rootStore, messageStore, chatContentModule.pinnedMessagesModel, messageId, root.chatId)
}
onToggleReaction: {
messageStore.toggleReaction(messageId, emojiId)
}
onOpenProfileClicked: {
Global.openProfilePopup(publicKey, null)
}
onDeleteMessage: {
messageStore.deleteMessage(messageId)
}
onEditClicked: messageStore.setEditModeOn(messageId)
onCreateOneToOneChat: {
Global.changeAppSectionBySectionType(Constants.appSection.chat)
root.rootStore.chatCommunitySectionModule.createOneToOneChat("", chatId, ensName)
}
onShowReplyArea: {
let obj = messageStore.getMessageByIdAsJson(messageId)
if (!obj) {
return
}
if (inputAreaLoader.item) {
inputAreaLoader.item.chatInput.showReplyArea(messageId, obj.senderDisplayName, obj.messageText, obj.contentType, obj.messageImage, obj.albumMessageImages, obj.albumImagesCount, obj.sticker)
}
if (inputAreaLoader.item) {
inputAreaLoader.item.chatInput.showReplyArea(messageId, obj.senderDisplayName, obj.messageText, obj.contentType, obj.messageImage, obj.albumMessageImages, obj.albumImagesCount, obj.sticker)
}
}
}
@ -146,30 +102,26 @@ ColumnLayout {
chatContentModule: root.chatContentModule
rootStore: root.rootStore
contactsStore: root.contactsStore
messageContextMenu: contextMenuLoader.item
messageStore: root.messageStore
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup
usersStore: root.usersStore
stickersLoaded: root.stickersLoaded
publicKey: root.chatId
chatId: root.chatId
isOneToOne: root.chatType === Constants.chatType.oneToOne
isChatBlocked: root.isBlocked || !root.isUserAllowedToSendMessage
channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "")
isActiveChannel: root.isActiveChannel
onShowReplyArea: {
let obj = messageStore.getMessageByIdAsJson(messageId)
if (!obj) {
return
}
if (inputAreaLoader.item) {
inputAreaLoader.item.chatInput.showReplyArea(messageId, obj.senderDisplayName, obj.messageText, obj.contentType, obj.messageImage, obj.albumMessageImages, obj.albumImagesCount, obj.sticker)
}
onShowReplyArea: (messageId, senderId) => {
d.showReplyArea(messageId)
}
onOpenStickerPackPopup: {
root.openStickerPackPopup(stickerPackId);
}
onEditModeChanged: if (!editModeOn && inputAreaLoader.item) inputAreaLoader.item.chatInput.forceInputActiveFocus()
onEditModeChanged: {
if (!editModeOn && inputAreaLoader.item)
inputAreaLoader.item.chatInput.forceInputActiveFocus()
}
}
}
@ -214,7 +166,6 @@ ColumnLayout {
textInput.text: inputAreaLoader.preservedText
textInput.placeholderText: root.chatInputPlaceholder
messageContextMenu: contextMenuLoader.item
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup
isContactBlocked: root.isBlocked

View File

@ -33,15 +33,13 @@ Item {
property var emojiPopup
property var stickersPopup
property string publicKey: ""
property string chatId: ""
property bool stickersLoaded: false
property alias chatLogView: chatLogView
property bool isChatBlocked: false
property bool isOneToOne: false
property bool isActiveChannel: false
property var messageContextMenu
signal openStickerPackPopup(string stickerPackId)
signal showReplyArea(string messageId, string author)
signal editModeChanged(bool editModeOn)
@ -250,11 +248,12 @@ Item {
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup
chatLogView: ListView.view
chatContentModule: root.chatContentModule
isActiveChannel: root.isActiveChannel
isChatBlocked: root.isChatBlocked
messageContextMenu: root.messageContextMenu
chatId: root.chatId
messageId: model.id
communityId: model.communityId
responseToMessageWithId: model.responseToMessageWithId
@ -322,8 +321,6 @@ Item {
root.showReplyArea(messageId, author)
}
onImageClicked: Global.openImagePopup(image, messageContextMenu)
stickersLoaded: root.stickersLoaded
onVisibleChanged: {
@ -364,7 +361,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Send Contact Request")
onClicked: {
Global.openContactRequestPopup(root.publicKey, null)
Global.openContactRequestPopup(root.chatId, null)
}
}
}
@ -379,14 +376,14 @@ Item {
text: qsTr("Reject Contact Request")
type: StatusBaseButton.Type.Danger
onClicked: {
root.contactsStore.dismissContactRequest(root.publicKey, "")
root.contactsStore.dismissContactRequest(root.chatId, "")
}
}
StatusButton {
text: qsTr("Accept Contact Request")
onClicked: {
root.contactsStore.acceptContactRequest(root.publicKey, "")
root.contactsStore.acceptContactRequest(root.chatId, "")
}
}
}

View File

@ -112,8 +112,9 @@ StatusSectionLayout {
rightPanel: Component {
id: userListComponent
UserListPanel {
anchors.fill: parent
store: root.rootStore
label: qsTr("Members")
messageContextMenu: quickActionMessageOptionsMenu
usersModel: {
let chatContentModule = root.rootStore.currentChatContentModule()
if (!chatContentModule || !chatContentModule.usersModule) {
@ -166,17 +167,4 @@ StatusSectionLayout {
}
}
}
MessageContextMenuView {
id: quickActionMessageOptionsMenu
store: root.rootStore
onOpenProfileClicked: {
Global.openProfilePopup(publicKey, null)
}
onCreateOneToOneChat: {
Global.changeAppSectionBySectionType(Constants.appSection.chat)
root.rootStore.chatCommunitySectionModule.createOneToOneChat(communityId, chatId, ensName)
}
}
}

View File

@ -221,9 +221,7 @@ StatusSectionLayout {
declinedMemberRequestsModel: root.community.declinedMemberRequests
editable: root.community.amISectionAdmin
communityName: root.community.name
communityMemberContextMenu: memberContextMenuView
onUserProfileClicked: Global.openProfilePopup(id)
onKickUserClicked: root.rootStore.removeUserFromCommunity(id)
onBanUserClicked: root.rootStore.banUserFromCommunity(id)
onUnbanUserClicked: root.rootStore.unbanUserFromCommunity(id)
@ -485,22 +483,6 @@ StatusSectionLayout {
}
}
MessageContextMenuView {
id: memberContextMenuView
store: root.rootStore
isProfile: true
amIChatAdmin: root.rootStore.amIChatAdmin()
myPublicKey: root.rootStore.myPublicKey()
onOpenProfileClicked: {
Global.openProfilePopup(publicKey, null)
}
onCreateOneToOneChat: {
Global.changeAppSectionBySectionType(Constants.appSection.chat)
root.rootStore.chatCommunitySectionModule.createOneToOneChat(communityId, chatId, ensName)
}
}
Connections {
target: root.chatCommunitySectionModule
function onOpenNoPermissionsToJoinPopup(communityName: string, userName: string, communityId: string, requestId: string) {

View File

@ -34,10 +34,11 @@ SettingsContentBase {
}
function openContextMenu(publicKey, name, icon) {
contactContextMenu.selectedUserPublicKey = publicKey
contactContextMenu.selectedUserDisplayName = name
contactContextMenu.selectedUserIcon = icon
contactContextMenu.popup()
Global.openMenu(contactContextMenuComponent, this, {
selectedUserPublicKey: publicKey,
selectedUserDisplayName: name,
selectedUserIcon: icon,
})
}
Item {
@ -46,17 +47,22 @@ SettingsContentBase {
height: (searchBox.height + contactsTabBar.height
+ stackLayout.height + (2 * Style.current.bigPadding))
MessageContextMenuView {
id: contactContextMenu
store: ({contactsStore: root.contactsStore})
isProfile: true
Component {
id: contactContextMenuComponent
onOpenProfileClicked: function (pubkey) {
Global.openProfilePopup(pubkey, null)
}
ProfileContextMenu {
id: contactContextMenu
store: ({contactsStore: root.contactsStore})
onCreateOneToOneChat: function (communityId, chatId, ensName) {
root.contactsStore.joinPrivateChat(chatId)
onOpenProfileClicked: function (pubkey) {
Global.openProfilePopup(pubkey, null)
}
onCreateOneToOneChat: function (communityId, chatId, ensName) {
root.contactsStore.joinPrivateChat(chatId)
}
onClosed: {
destroy()
}
}
}

View File

@ -1,4 +1,5 @@
import QtQuick 2.15
import QtQuick.Dialogs 1.0
import AppLayouts.Chat.popups 1.0
import AppLayouts.Profile.popups 1.0
@ -40,6 +41,8 @@ QtObject {
Global.importCommunityPopupRequested.connect(openImportCommunityPopup)
Global.removeContactRequested.connect(openRemoveContactConfirmationPopup)
Global.openPopupRequested.connect(openPopup)
Global.openDeleteMessagePopup.connect(openDeleteMessagePopup)
Global.openDownloadImageDialog.connect(openDownloadImageDialog)
}
function openPopup(popupComponent, params = {}, cb = null) {
@ -77,9 +80,8 @@ QtObject {
openPopup(downloadPageComponent, popupProperties)
}
function openImagePopup(image, contextMenu) {
function openImagePopup(image) {
var popup = imagePopupComponent.createObject(popupParent)
popup.contextMenu = contextMenu
popup.openPopup(image)
}
@ -206,6 +208,21 @@ QtObject {
})
}
function openDeleteMessagePopup(messageId, messageStore) {
openPopup(deleteMessageConfirmationDialogComponent,
{
messageId,
messageStore
})
}
function openDownloadImageDialog(imageSource) {
// We don't use `openPopup`, because there's no `FileDialog::closed` signal.
// And multiple file dialogs are (almost) ok
const popup = downloadImageDialogComponent.createObject(popupParent, { imageSource })
popup.open()
}
readonly property list<Component> _components: [
Component {
id: removeContactConfirmationDialog
@ -316,17 +333,6 @@ QtObject {
id: imagePopupComponent
StatusImageModal {
id: imagePopup
onClicked: {
if (mouse.button === Qt.LeftButton) {
imagePopup.close()
} else if(mouse.button === Qt.RightButton) {
contextMenu.imageSource = imagePopup.imageSource
contextMenu.hideEmojiPicker = true
contextMenu.isRightClickOnImage = true
contextMenu.parent = imagePopup.contentItem
contextMenu.show()
}
}
onClosed: destroy()
}
},
@ -461,6 +467,36 @@ QtObject {
DiscordImportProgressDialog {
store: root.communitiesStore
}
},
Component {
id: deleteMessageConfirmationDialogComponent
DeleteMessageConfirmationPopup {
onClosed: destroy()
}
},
Component {
id: downloadImageDialogComponent
FileDialog {
property string imageSource
title: qsTr("Please choose a directory")
selectFolder: true
selectExisting: true
selectMultiple: false
modality: Qt.NonModal
onAccepted: {
Utils.downloadImageByUrl(imageSource, fileUrl)
destroy()
}
onRejected: {
destroy()
}
Component.onCompleted: {
open()
}
}
}
]
}

View File

@ -0,0 +1,29 @@
import QtQuick 2.15
import utils 1.0
import shared 1.0
Row {
id: root
property var reactionsModel
property var messageReactionsModel: [] // TODO: https://github.com/status-im/status-desktop/issues/10703
signal toggleReaction(int emojiId)
spacing: Style.current.halfPadding
leftPadding: Style.current.halfPadding
rightPadding: Style.current.halfPadding
Repeater {
model: root.reactionsModel
delegate: EmojiReaction {
source: Style.svg(filename)
emojiId: model.emojiId
// reactedByUser: !!root.messageReactionsModel[emojiId]
onCloseModal: {
root.toggleReaction(emojiId)
}
}
}
}

View File

@ -138,7 +138,7 @@ Item {
onClicked: {
if (!!root.store.profileLargeImage)
imageEditMenu.popup(this, mouse.x, mouse.y);
Global.openMenu(editImageMenuComponent, this)
else
Global.openChangeProfilePicPopup(tempIcon);
}
@ -271,30 +271,33 @@ Item {
}
}
StatusMenu {
id: imageEditMenu
width: 200
Component {
id: editImageMenuComponent
StatusAction {
text: qsTr("Select different image")
assetSettings.name: "image"
onTriggered: Global.openChangeProfilePicPopup(editButton.tempIcon)
}
StatusMenu {
width: 200
StatusAction {
text: qsTr("Use an NFT")
assetSettings.name: "nft-profile"
onTriggered: Global.openChangeProfilePicPopup(editButton.tempIcon)
enabled: false // TODO enable this with the profile showcase
}
StatusAction {
text: qsTr("Select different image")
assetSettings.name: "image"
onTriggered: Global.openChangeProfilePicPopup(editButton.tempIcon)
}
StatusMenuSeparator {}
StatusAction {
text: qsTr("Use an NFT")
assetSettings.name: "nft-profile"
onTriggered: Global.openChangeProfilePicPopup(editButton.tempIcon)
enabled: false // TODO enable this with the profile showcase
}
StatusAction {
text: qsTr("Remove image")
type: StatusAction.Danger
assetSettings.name: "delete"
onTriggered: root.icon = ""
StatusMenuSeparator {}
StatusAction {
text: qsTr("Remove image")
type: StatusAction.Danger
assetSettings.name: "delete"
onTriggered: root.icon = ""
}
}
}
}

View File

@ -14,3 +14,4 @@ MessageBorder 1.0 MessageBorder.qml
EmojiReaction 1.0 EmojiReaction.qml
ProfileHeader 1.0 ProfileHeader.qml
VerificationLabel 1.0 VerificationLabel.qml
MessageReactionsRow 1.0 MessageReactionsRow.qml

View File

@ -0,0 +1,26 @@
import QtQuick 2.15
ConfirmationDialog {
id: root
property var messageStore
property string messageId
header.title: qsTr("Confirm deleting this message")
confirmationText: qsTr("Are you sure you want to delete this message? Be aware that other clients are not guaranteed to delete the message as well.")
height: 260
checkbox.visible: true
confirmButtonObjectName: "chatButtonsPanelConfirmDeleteMessageButton"
executeConfirm: () => {
if (checkbox.checked) {
localAccountSensitiveSettings.showDeleteMessageWarning = false
}
close()
messageStore.deleteMessage(messageId)
}
onClosed: {
destroy()
}
}

View File

@ -27,3 +27,4 @@ AccountsModalHeader 1.0 AccountsModalHeader.qml
GetSyncCodeInstructionsPopup 1.0 GetSyncCodeInstructionsPopup.qml
NoPermissionsToJoinPopup 1.0 NoPermissionsToJoinPopup.qml
RemoveAccountConfirmationPopup 1.0 RemoveAccountConfirmationPopup.qml
DeleteMessageConfirmationPopup 1.0 DeleteMessageConfirmationPopup.qml

View File

@ -63,8 +63,6 @@ Rectangle {
property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top // TODO: Remove this proeprty?
property var messageContextMenu
property alias suggestions: suggestionsBox
enum ImageErrorMessageLocation {
@ -1226,7 +1224,9 @@ Rectangle {
Layout.leftMargin: Style.current.halfPadding
Layout.rightMargin: Style.current.halfPadding
visible: isImage
onImageClicked: Global.openImagePopup(chatImage, messageContextMenu)
onImageClicked: {
Global.openImagePopup(chatImage)
}
onImageRemoved: {
if (control.fileUrlsAndSources.length > index && control.fileUrlsAndSources[index]) {
control.fileUrlsAndSources.splice(index, 1)

View File

@ -6,13 +6,12 @@ import QtGraphicalEffects 1.13
import utils 1.0
import shared 1.0
import shared.views.chat 1.0
Popup {
id: root
signal clicked(var mouse)
property string imageSource: messageImage.source
property var contextMenu
property var store
modal: true
Overlay.modal: Rectangle {
@ -32,7 +31,6 @@ Popup {
const maxHeight = Global.applicationWindow.height - 80
const maxWidth = Global.applicationWindow.width - 80
if (image.sourceSize.width >= maxWidth || image.sourceSize.height >= maxHeight) {
this.width = maxWidth
this.height = maxHeight
@ -44,7 +42,7 @@ Popup {
function openPopup(image) {
setPopupData(image);
root.open();
open()
}
contentItem: AnimatedImage {
@ -61,7 +59,22 @@ Popup {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
root.clicked(mouse)
if (mouse.button === Qt.LeftButton)
root.close()
if (mouse.button === Qt.RightButton)
Global.openMenu(imageContextMenu,
messageImage,
{ imageSource: messageImage.source })
}
}
}
Component {
id: imageContextMenu
ImageContextMenu {
onClosed: {
destroy()
}
}
}

View File

@ -0,0 +1,29 @@
import StatusQ.Popups 0.1
import utils 1.0
StatusMenu {
id: root
property string imageSource
StatusAction {
text: root.imageSource.endsWith(".gif") ? qsTr("Copy GIF")
: qsTr("Copy image")
icon.name: "copy"
enabled: !!root.imageSource
onTriggered: {
Utils.copyImageToClipboardByUrl(root.imageSource)
}
}
StatusAction {
text: root.imageSource.endsWith(".gif") ? qsTr("Download GIF")
: qsTr("Download image")
icon.name: "download"
enabled: !!root.imageSource
onTriggered: {
Global.openDownloadImageDialog(root.imageSource)
}
}
}

View File

@ -25,7 +25,7 @@ Column {
readonly property alias unfurledImagesCount: d.unfurledImagesCount
property bool isCurrentUser: false
signal imageClicked(var image)
signal imageClicked(var image, var mouse, var imageSource)
signal linksLoaded()
spacing: 4
@ -138,7 +138,12 @@ Column {
isOnline: root.store.mainModuleInst.isOnline
asynchronous: true
isAnimated: result.contentType ? result.contentType.toLowerCase().endsWith("gif") : false
onClicked: isAnimated && !playing ? localAnimationEnabled = true : root.imageClicked(linkImage.imageAlias)
onClicked: {
if (isAnimated && !playing)
localAnimationEnabled = true
else
root.imageClicked(linkImage.imageAlias, mouse, source)
}
imageAlias.cache: localAnimationEnabled // GIFs can only loop/play properly with cache enabled
Loader {
width: 45

View File

@ -0,0 +1,22 @@
import StatusQ.Popups 0.1
import shared.controls.chat 1.0
StatusMenu {
id: root
property alias reactionsModel: emojiRow.reactionsModel
property alias messageReactionsModel: emojiRow.messageReactionsModel
signal toggleReaction(int emojiId)
width: emojiRow.width
MessageReactionsRow {
id: emojiRow
onToggleReaction: {
root.toggleReaction(emojiId)
root.close()
}
}
}

View File

@ -2,7 +2,6 @@ import QtQuick 2.12
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQml.Models 2.3
import QtQuick.Dialogs 1.0
import StatusQ.Popups 0.1
import StatusQ.Components 0.1
@ -20,330 +19,56 @@ StatusMenu {
property var store
property var reactionModel
property alias emojiContainer: emojiContainer
property string myPublicKey: ""
property bool amIChatAdmin: false
property bool disabledForChat: false
property string selectedUserPublicKey: ""
property string selectedUserDisplayName: ""
property string selectedUserIcon: ""
property int chatType: Constants.chatType.unknown
property string messageId: ""
property string unparsedText: ""
property string messageSenderId: ""
property int messageContentType: Constants.messageContentType.unknownContentType
property string imageSource: ""
property bool isProfile: false
property bool isRightClickOnImage: false
property bool pinnedPopup: false
property bool pinMessageAllowedForMembers: false
property bool isDebugEnabled: store && store.isDebugEnabled
property bool isEmoji: false
property bool isSticker: false
property bool hideEmojiPicker: true
property bool editRestricted: false
property bool pinnedMessage: false
property bool canPin: false
readonly property bool isMyMessage: {
return root.messageSenderId !== "" && root.messageSenderId === root.myPublicKey;
}
readonly property bool isMe: {
return root.selectedUserPublicKey === root.store.contactsStore.myPublicKey;
}
readonly property var contactDetails: {
if (root.selectedUserPublicKey === "" || isMe) {
return {}
}
return Utils.getContactDetailsAsJson(root.selectedUserPublicKey);
}
readonly property bool isContact: {
return root.selectedUserPublicKey !== "" && !!contactDetails.isContact
}
readonly property bool isBlockedContact: (!!contactDetails && contactDetails.isBlocked) || false
readonly property int outgoingVerificationStatus: {
if (root.selectedUserPublicKey === "" || root.isMe || !root.isContact) {
return 0
}
return contactDetails.verificationStatus
}
readonly property int incomingVerificationStatus: {
if (root.selectedUserPublicKey === "" || root.isMe || !root.isContact) {
return 0
}
return contactDetails.incomingVerificationStatus
}
readonly property bool hasPendingContactRequest: {
return !root.isMe && root.selectedUserPublicKey !== "" &&
root.store.contactsStore.hasPendingContactRequest(root.selectedUserPublicKey);
}
readonly property bool hasActiveReceivedVerificationRequestFrom: {
if (!root.selectedUserPublicKey || root.isMe || !root.isContact) {
return false
}
return contactDetails.incomingVerificationStatus === Constants.verificationStatus.verifying ||
contactDetails.incomingVerificationStatus === Constants.verificationStatus.verified
}
readonly property bool isVerificationRequestSent: {
if (!root.selectedUserPublicKey || root.isMe || !root.isContact) {
return false
}
return root.outgoingVerificationStatus !== Constants.verificationStatus.unverified &&
root.outgoingVerificationStatus !== Constants.verificationStatus.verified &&
root.outgoingVerificationStatus !== Constants.verificationStatus.trusted
}
readonly property bool isTrusted: {
if (!root.selectedUserPublicKey || root.isMe || !root.isContact) {
return false
}
return root.outgoingVerificationStatus === Constants.verificationStatus.trusted ||
root.incomingVerificationStatus === Constants.verificationStatus.trusted
}
readonly property bool userTrustIsUnknown: contactDetails && contactDetails.trustStatus === Constants.trustStatus.unknown
readonly property bool userIsUntrustworthy: contactDetails && contactDetails.trustStatus === Constants.trustStatus.untrustworthy
property var emojiReactionsReactedByUser: []
signal openProfileClicked(string publicKey)
signal pinMessage(string messageId)
signal unpinMessage(string messageId)
signal pinnedMessagesLimitReached(string messageId)
signal jumpToMessage(string messageId)
signal shouldCloseParentPopup()
signal createOneToOneChat(string communityId, string chatId, string ensName)
signal showReplyArea()
signal showReplyArea(string messageId, string messageSenderId)
signal toggleReaction(string messageId, int emojiId)
signal deleteMessage(string messageId)
signal editClicked(string messageId)
function show(userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam, emojiReactionsModel) {
let newEmojiReactions = []
if (!!emojiReactionsModel) {
emojiReactionsModel.forEach(function (emojiReaction) {
newEmojiReactions[emojiReaction.emojiId] = emojiReaction.currentUserReacted
})
}
root.emojiReactionsReactedByUser = newEmojiReactions;
popup()
}
onClosed: {
// Reset selectedUserPublicKey so that associated properties get recalculated on re-open
selectedUserPublicKey = ""
}
width: Math.max(emojiContainer.visible ? emojiContainer.width : 0,
(root.isRightClickOnImage && !root.pinnedPopup) ? 176 : 230)
width: Math.max(emojiContainer.visible ? emojiContainer.width : 0, 230)
Item {
id: emojiContainer
width: emojiRow.width
height: visible ? emojiRow.height : 0
visible: !root.hideEmojiPicker && (root.isEmoji || !root.isProfile) && !root.pinnedPopup && !root.disabledForChat
visible: !root.disabledForChat
Row {
MessageReactionsRow {
id: emojiRow
spacing: Style.current.halfPadding
leftPadding: Style.current.halfPadding
rightPadding: Style.current.halfPadding
bottomPadding: root.isEmoji ? 0 : Style.current.padding
Repeater {
model: root.reactionModel
delegate: EmojiReaction {
source: Style.svg(filename)
emojiId: model.emojiId
reactedByUser: !!root.emojiReactionsReactedByUser[model.emojiId]
onCloseModal: {
root.toggleReaction(root.messageId, emojiId)
root.close()
}
}
reactionsModel: root.reactionModel
bottomPadding: Style.current.padding
onToggleReaction: {
root.toggleReaction(root.messageId, emojiId)
close()
}
}
}
ProfileHeader {
visible: root.isProfile
width: parent.width
height: visible ? implicitHeight : 0
displayNameVisible: false
displayNamePlusIconsVisible: true
editButtonVisible: false
displayName: root.selectedUserDisplayName
pubkey: root.selectedUserPublicKey
icon: root.selectedUserIcon
trustStatus: contactDetails && contactDetails.trustStatus ? contactDetails.trustStatus
: Constants.trustStatus.unknown
isContact: root.isContact
isCurrentUser: root.isMe
userIsEnsVerified: (!!contactDetails && contactDetails.ensVerified) || false
}
Item {
visible: root.isProfile
height: visible ? root.topPadding : 0
}
StatusMenuSeparator {
anchors.bottom: viewProfileAction.top
visible: !root.isEmoji && !root.hideEmojiPicker && !pinnedPopup
}
StatusAction {
id: copyImageAction
text: (root.imageSource.endsWith(".gif")) ? qsTr("Copy GIF") : qsTr("Copy image")
onTriggered: {
if (root.imageSource) {
root.store.copyImageToClipboardByUrl(root.imageSource)
}
root.close()
}
icon.name: "copy"
enabled: root.isRightClickOnImage && !root.pinnedPopup
}
StatusAction {
id: downloadImageAction
text: (root.imageSource.endsWith(".gif")) ? qsTr("Download GIF") : qsTr("Download image")
onTriggered: {
fileDialog.open()
root.close()
}
icon.name: "download"
enabled: root.isRightClickOnImage && !root.pinnedPopup
}
ViewProfileMenuItem {
id: viewProfileAction
enabled: root.isProfile && !root.pinnedPopup
onTriggered: {
root.openProfileClicked(root.selectedUserPublicKey)
root.close()
}
}
SendMessageMenuItem {
id: sendMessageMenuItem
enabled: root.isProfile && root.isContact && !root.isBlockedContact
onTriggered: {
root.createOneToOneChat("", root.selectedUserPublicKey, "")
root.close()
}
}
SendContactRequestMenuItem {
id: sendContactRequestMenuItem
enabled: root.isProfile && !root.isMe && !root.isContact
&& !root.isBlockedContact && !root.hasPendingContactRequest
onTriggered: {
Global.openContactRequestPopup(root.selectedUserPublicKey, null)
root.close()
}
}
StatusAction {
id: verifyIdentityAction
text: qsTr("Verify Identity")
icon.name: "checkmark-circle"
enabled: root.isProfile && !root.isMe && root.isContact
&& !root.isBlockedContact
&& root.outgoingVerificationStatus === Constants.verificationStatus.unverified
&& !root.hasActiveReceivedVerificationRequestFrom
onTriggered: {
Global.openSendIDRequestPopup(root.selectedUserPublicKey, null)
root.close()
}
}
StatusAction {
id: pendingIdentityAction
text: isVerificationRequestSent ||
root.incomingVerificationStatus === Constants.verificationStatus.verified ?
qsTr("ID Request Pending....") :
qsTr("Respond to ID Request...")
icon.name: "checkmark-circle"
enabled: root.isProfile && !root.isMe && root.isContact
&& !root.isBlockedContact && !root.isTrusted
&& (root.hasActiveReceivedVerificationRequestFrom
|| root.isVerificationRequestSent)
onTriggered: {
if (hasActiveReceivedVerificationRequestFrom) {
Global.openIncomingIDRequestPopup(root.selectedUserPublicKey, null)
} else if (root.isVerificationRequestSent) {
Global.openOutgoingIDRequestPopup(root.selectedUserPublicKey, null)
}
root.close()
}
}
StatusAction {
id: renameAction
text: qsTr("Rename")
icon.name: "edit_pencil"
enabled: root.isProfile && !root.isMe
onTriggered: {
Global.openNicknamePopupRequested(root.selectedUserPublicKey, contactDetails.localNickname,
"%1 (%2)".arg(root.selectedUserDisplayName).arg(Utils.getElidedCompressedPk(root.selectedUserPublicKey)))
root.close()
}
}
StatusAction {
id: unblockAction
text: qsTr("Unblock User")
icon.name: "remove-circle"
enabled: root.isProfile && !root.isMe && root.isBlockedContact
onTriggered: Global.unblockContactRequested(root.selectedUserPublicKey, root.selectedUserDisplayName)
}
StatusMenuSeparator {
visible: blockMenuItem.enabled || markUntrustworthyMenuItem.enabled || removeUntrustworthyMarkMenuItem.enabled
}
StatusAction {
id: markUntrustworthyMenuItem
text: qsTr("Mark as Untrustworthy")
icon.name: "warning"
type: StatusAction.Type.Danger
enabled: root.isProfile && !root.isMe && root.userTrustIsUnknown
onTriggered: root.store.contactsStore.markUntrustworthy(root.selectedUserPublicKey)
}
StatusAction {
id: removeUntrustworthyMarkMenuItem
text: qsTr("Remove Untrustworthy Mark")
icon.name: "warning"
enabled: root.isProfile && !root.isMe && root.userIsUntrustworthy
onTriggered: root.store.contactsStore.removeTrustStatus(root.selectedUserPublicKey)
}
StatusAction {
text: qsTr("Remove Contact")
icon.name: "remove-contact"
type: StatusAction.Type.Danger
enabled: root.isContact && !root.isBlockedContact && !root.hasPendingContactRequest
onTriggered: {
Global.removeContactRequested(root.selectedUserDisplayName, root.selectedUserPublicKey);
root.close();
}
}
StatusAction {
id: blockMenuItem
text: qsTr("Block User")
icon.name: "cancel"
type: StatusAction.Type.Danger
enabled: root.isProfile && !root.isMe && !root.isBlockedContact
onTriggered: Global.blockContactRequested(root.selectedUserPublicKey, root.selectedUserDisplayName)
visible: emojiContainer.visible
}
StatusAction {
@ -351,15 +76,10 @@ StatusMenu {
text: qsTr("Reply to")
icon.name: "chat"
onTriggered: {
root.showReplyArea()
root.showReplyArea(root.messageId, root.messageSenderId)
root.close()
}
enabled: (!root.hideEmojiPicker &&
!root.isEmoji &&
!root.isProfile &&
!root.pinnedPopup &&
!root.isRightClickOnImage &&
!root.disabledForChat)
enabled: !root.disabledForChat
}
StatusAction {
@ -370,12 +90,7 @@ StatusMenu {
}
icon.name: "edit"
enabled: root.isMyMessage &&
!root.hideEmojiPicker &&
!root.isEmoji &&
!root.isSticker &&
!root.isProfile &&
!root.pinnedPopup &&
!root.isRightClickOnImage &&
!root.editRestricted &&
!root.disabledForChat
}
@ -403,13 +118,8 @@ StatusMenu {
StatusAction {
id: pinAction
text: {
if (root.pinnedMessage) {
return qsTr("Unpin")
}
return qsTr("Pin")
}
text: root.pinnedMessage ? qsTr("Unpin") : qsTr("Pin")
icon.name: root.pinnedMessage ? "unpin" : "pin"
onTriggered: {
if (root.pinnedMessage) {
root.unpinMessage(root.messageId)
@ -424,14 +134,10 @@ StatusMenu {
root.pinMessage(root.messageId)
root.close()
}
icon.name: "pin"
enabled: {
if (root.isProfile || root.isEmoji || root.isRightClickOnImage || root.disabledForChat)
if (root.disabledForChat)
return false
if (root.pinnedPopup)
return true
switch (root.chatType) {
case Constants.chatType.profile:
return false
@ -449,10 +155,9 @@ StatusMenu {
StatusMenuSeparator {
visible: deleteMessageAction.enabled &&
(viewProfileAction.enabled ||
sendMessageMenuItem.enabled ||
replyToMenuItem.enabled ||
(replyToMenuItem.enabled ||
copyMessageMenuItem.enabled ||
copyMessageIdAction ||
editMessageAction.enabled ||
pinAction.enabled)
}
@ -461,72 +166,16 @@ StatusMenu {
id: deleteMessageAction
enabled: (root.isMyMessage || root.amIChatAdmin) &&
!root.disabledForChat &&
!root.isProfile &&
!root.isEmoji &&
!root.pinnedPopup &&
!root.isRightClickOnImage &&
(root.messageContentType === Constants.messageContentType.messageType ||
root.messageContentType === Constants.messageContentType.stickerType ||
root.messageContentType === Constants.messageContentType.emojiType ||
root.messageContentType === Constants.messageContentType.imageType ||
root.messageContentType === Constants.messageContentType.audioType)
text: qsTr("Delete message")
onTriggered: {
if (!localAccountSensitiveSettings.showDeleteMessageWarning) {
deleteMessage(messageId)
}
else {
Global.openPopup(deleteMessageConfirmationDialogComponent)
}
}
icon.name: "delete"
type: StatusAction.Type.Danger
}
StatusAction {
id: jumpToAction
enabled: root.pinnedPopup && !root.isProfile
text: qsTr("Jump to")
onTriggered: {
root.jumpToMessage(root.messageId)
root.close()
root.shouldCloseParentPopup()
}
icon.name: "arrow-up"
}
FileDialog {
id: fileDialog
title: qsTr("Please choose a directory")
selectFolder: true
selectExisting: true
selectMultiple: false
modality: Qt.NonModal
onAccepted: {
if (root.imageSource) {
root.store.downloadImageByUrl(root.imageSource, fileDialog.fileUrl)
}
}
}
Component {
id: deleteMessageConfirmationDialogComponent
ConfirmationDialog {
header.title: qsTr("Confirm deleting this message")
confirmationText: qsTr("Are you sure you want to delete this message? Be aware that other clients are not guaranteed to delete the message as well.")
height: 260
checkbox.visible: true
executeConfirm: function () {
if (checkbox.checked) {
localAccountSensitiveSettings.showDeleteMessageWarning = false
}
close()
root.deleteMessage(messageId)
}
onClosed: {
destroy()
}
root.deleteMessage(messageId)
}
}
}

View File

@ -22,7 +22,8 @@ Loader {
property var messageStore
property var usersStore
property var contactsStore
property var messageContextMenu: null
property var chatContentModule
property string channelEmoji
property bool isActiveChannel: false
@ -34,6 +35,7 @@ Loader {
// without an explicit need to fetch those details via message store/module.
property bool isChatBlocked: false
property string chatId
property string messageId: ""
property string communityId: ""
@ -120,67 +122,59 @@ Loader {
readonly property bool isExpired: d.getIsExpired(messageTimestamp, messageOutgoingStatus)
readonly property bool isSending: messageOutgoingStatus === Constants.sending && !isExpired
signal imageClicked(var image)
// WARNING: To much arguments here. Create an object argument.
property var messageClickHandler: function(sender, point,
isProfileClick,
isSticker = false,
isImage = false,
image = null,
isEmoji = false,
hideEmojiPicker = false,
isReply = false,
isRightClickOnImage = false,
imageSource = "") {
if (placeholderMessage || !(root.rootStore.mainModuleInst.activeSection.joined || isProfileClick)) {
function openProfileContextMenu(sender, mouse, isReply = false) {
if (isReply && !quotedMessageFrom) {
// The responseTo message was deleted
// so we don't enable to right click the unavailable profile
return false
}
messageContextMenu.myPublicKey = userProfile.pubKey
messageContextMenu.amIChatAdmin = root.amIChatAdmin
messageContextMenu.pinMessageAllowedForMembers = messageStore.isPinMessageAllowedForMembers
messageContextMenu.chatType = messageStore.chatType
messageContextMenu.messageId = root.messageId
messageContextMenu.unparsedText = root.unparsedText
messageContextMenu.messageSenderId = root.senderId
messageContextMenu.messageContentType = root.messageContentType
messageContextMenu.pinnedMessage = root.pinnedMessage
messageContextMenu.canPin = !!root.messageStore && root.messageStore.getNumberOfPinnedMessages() < Constants.maxNumberOfPins
messageContextMenu.selectedUserPublicKey = root.senderId
messageContextMenu.selectedUserDisplayName = root.senderDisplayName
messageContextMenu.selectedUserIcon = root.senderIcon
messageContextMenu.imageSource = imageSource
messageContextMenu.isProfile = !!isProfileClick
messageContextMenu.isRightClickOnImage = isRightClickOnImage
messageContextMenu.isEmoji = isEmoji
messageContextMenu.isSticker = isSticker
messageContextMenu.hideEmojiPicker = hideEmojiPicker
if (isReply) {
if (!quotedMessageFrom) {
// The responseTo message was deleted so we don't eneble to right click the unaviable profile
return false
}
messageContextMenu.messageSenderId = quotedMessageFrom
messageContextMenu.selectedUserPublicKey = quotedMessageFrom
messageContextMenu.selectedUserDisplayName = quotedMessageAuthorDetailsDisplayName
messageContextMenu.selectedUserIcon = quotedMessageAuthorDetailsThumbnailImage
const params = {
selectedUserPublicKey: isReply ? quotedMessageFrom : root.senderId,
selectedUserDisplayName: isReply ? quotedMessageAuthorDetailsDisplayName : root.senderDisplayName,
selectedUserIcon: isReply ? quotedMessageAuthorDetailsThumbnailImage : root.senderIcon,
}
// Emoji container is not a menu item of messageContextMenu so checking it separatly
if (messageContextMenu.checkIfEmpty() && !isEmoji) {
return false
Global.openMenu(profileContextMenuComponent, sender, params)
}
function openMessageContextMenu() {
if (placeholderMessage || !root.rootStore.mainModuleInst.activeSection.joined)
return
const params = {
myPublicKey: userProfile.pubKey,
amIChatAdmin: root.amIChatAdmin,
pinMessageAllowedForMembers: messageStore.isPinMessageAllowedForMembers,
chatType: messageStore.chatType,
messageId: root.messageId,
unparsedText: root.unparsedText,
messageSenderId: root.senderId,
messageContentType: root.messageContentType,
pinnedMessage: root.pinnedMessage,
canPin: !!root.messageStore && root.messageStore.getNumberOfPinnedMessages() < Constants.maxNumberOfPins,
editRestricted: root.isSticker || root.isImage,
}
messageContextMenu.parent = sender
messageContextMenu.popup(point)
return true
Global.openMenu(messageContextMenuComponent, this, params)
}
function setMessageActive(messageId, active) {
// TODO: Is argument messageId actually needed?
// It was probably used with dynamic scoping,
// but not this method can be moved to private `d`.
// Probably that it was done this way, because `MessageView` is reused as delegate.
if (active) {
d.activeMessage = messageId;
return;
}
if (d.activeMessage === messageId) {
d.activeMessage = "";
return;
}
}
signal showReplyArea(string messageId, string author)
@ -237,22 +231,7 @@ Loader {
property string activeMessage
readonly property bool isMessageActive: d.activeMessage === root.messageId
function setMessageActive(messageId, active) {
// TODO: Is argument messageId actually needed?
// It was probably used with dynamic scoping,
// but not this method can be moved to private `d`.
// Probably that it was done this way, because `MessageView` is reused as delegate.
if (active) {
d.activeMessage = messageId;
return;
}
if (d.activeMessage === messageId) {
d.activeMessage = "";
return;
}
}
readonly property bool addReactionAllowed: !root.isInPinnedPopup && !root.isChatBlocked
function nextMessageHasHeader() {
if(!root.nextMessageAsJsonObj) {
@ -303,15 +282,24 @@ Loader {
return StatusMessage.ContentType.Unknown;
}
}
}
function addReactionClicked(mouseArea, mouse) {
if (!d.addReactionAllowed)
return
// Don't use mouseArea as parent, as it will be destroyed right after opening menu
const point = mouseArea.mapToItem(root, mouse.x, mouse.y)
Global.openMenu(addReactionContextMenu, root, {}, point)
}
Connections {
enabled: d.isMessageActive
target: root.messageContextMenu
function onClosed() {
d.setMessageActive(root.messageId, false)
function onImageClicked(image, mouse, imageSource) {
switch (mouse.button) {
case Qt.LeftButton:
Global.openImagePopup(image)
break;
case Qt.RightButton:
Global.openMenu(imageContextMenuComponent, image, { imageSource })
break;
}
}
}
@ -498,7 +486,6 @@ Loader {
disableHover: root.disableHover ||
delegate.hideQuickActions ||
(root.chatLogView && root.chatLogView.moving) ||
(root.messageContextMenu && root.messageContextMenu.opened) ||
Global.popupOpened
disableEmojis: root.isChatBlocked
@ -514,15 +501,8 @@ Loader {
onEditCompleted: delegate.editCompletedHandler(newMsgText)
onImageClicked: {
switch (mouse.button) {
case Qt.LeftButton:
root.imageClicked(image, mouse);
break;
case Qt.RightButton:
root.messageClickHandler(image, Qt.point(mouse.x, mouse.y), false, false, true, image, false, true, false, true, imageSource)
break;
}
onImageClicked: (image, mouse, imageSource) => {
d.onImageClicked(image, mouse, imageSource)
}
onLinkActivated: {
@ -542,13 +522,11 @@ Loader {
}
onProfilePictureClicked: {
if (root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), true))
d.setMessageActive(root.messageId, true)
root.openProfileContextMenu(sender, mouse)
}
onReplyProfileClicked: {
if (root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), true, false, false, null, false, false, true))
d.setMessageActive(root.messageId, true)
root.openProfileContextMenu(sender, mouse, true)
}
onReplyMessageClicked: {
@ -557,8 +535,7 @@ Loader {
}
onSenderNameClicked: {
if (root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), true))
d.setMessageActive(root.messageId, true)
root.openProfileContextMenu(sender, mouse)
}
onToggleReactionClicked: {
@ -573,12 +550,8 @@ Loader {
root.messageStore.toggleReaction(root.messageId, emojiId)
}
onAddReactionClicked: {
if (root.isChatBlocked)
return
if (root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), false, false, false, null, true, false))
d.setMessageActive(root.messageId, true)
onAddReactionClicked: (sender, mouse) => {
d.addReactionClicked(sender, mouse)
}
onStickerClicked: {
@ -592,12 +565,9 @@ Loader {
mouseArea {
acceptedButtons: Qt.RightButton
enabled: !root.isChatBlocked &&
!root.placeholderMessage &&
delegate.contentType !== StatusMessage.ContentType.Image
!root.placeholderMessage
onClicked: {
if (root.messageClickHandler(this, Qt.point(mouse.x, mouse.y),
false, false, false, null, root.isEmoji, false, false, false, ""))
d.setMessageActive(root.messageId, true)
root.openMessageContextMenu()
}
}
@ -714,7 +684,6 @@ Loader {
usersStore: root.usersStore
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup
messageContextMenu: root.messageContextMenu
chatType: root.messageStore.chatType
isEdit: true
@ -736,8 +705,8 @@ Loader {
messageStore: root.messageStore
store: root.rootStore
isCurrentUser: root.amISender
onImageClicked: {
root.imageClicked(image);
onImageClicked: (image, mouse, imageSource) => {
d.onImageClicked(image, mouse, imageSource)
}
onLinksLoaded: {
// If there is only one image and no links, hide the message
@ -765,7 +734,7 @@ Loader {
quickActions: [
Loader {
active: !root.isInPinnedPopup && delegate.hovered && !delegate.hideQuickActions
active: d.addReactionAllowed && delegate.hovered && !delegate.hideQuickActions
visible: active
sourceComponent: StatusFlatRoundButton {
width: d.chatButtonSize
@ -773,9 +742,8 @@ Loader {
icon.name: "reaction-b"
type: StatusFlatRoundButton.Type.Tertiary
tooltip.text: qsTr("Add reaction")
onClicked: {
if (root.messageClickHandler(delegate, mapToItem(delegate, mouse.x, mouse.y), false, false, false, null, true, false))
d.setMessageActive(root.messageId, true)
onClicked: (mouse) => {
d.addReactionClicked(this, mouse)
}
}
},
@ -791,9 +759,6 @@ Loader {
tooltip.text: qsTr("Reply")
onClicked: {
root.showReplyArea(root.messageId, root.senderId)
if (messageContextMenu.closeParentPopup) {
messageContextMenu.closeParentPopup()
}
}
}
},
@ -886,12 +851,7 @@ Loader {
type: StatusFlatRoundButton.Type.Tertiary
tooltip.text: qsTr("Delete")
onClicked: {
if (!localAccountSensitiveSettings.showDeleteMessageWarning) {
messageStore.deleteMessage(root.messageId)
}
else {
Global.openPopup(deleteMessageConfirmationDialogComponent)
}
messageStore.warnAndDeleteMessage(root.messageId)
}
}
}
@ -900,29 +860,6 @@ Loader {
}
}
Component {
id: deleteMessageConfirmationDialogComponent
ConfirmationDialog {
confirmButtonObjectName: "chatButtonsPanelConfirmDeleteMessageButton"
header.title: qsTr("Confirm deleting this message")
confirmationText: qsTr("Are you sure you want to delete this message? Be aware that other clients are not guaranteed to delete the message as well.")
height: 260
checkbox.visible: true
executeConfirm: function () {
if (checkbox.checked) {
localAccountSensitiveSettings.showDeleteMessageWarning = false
}
close()
messageStore.deleteMessage(root.messageId)
}
onClosed: {
destroy()
}
}
}
Component {
id: newMessagesMarkerComponent
@ -931,4 +868,110 @@ Loader {
timestamp: root.messageTimestamp
}
}
Component {
id: addReactionContextMenu
MessageAddReactionContextMenu {
reactionsModel: root.rootStore.emojiReactionsModel
onToggleReaction: (emojiId) => {
root.messageStore.toggleReaction(root.messageId, emojiId)
}
onOpened: {
root.setMessageActive(root.messageId, true)
}
onClosed: {
root.setMessageActive(root.messageId, false)
destroy()
}
}
}
Component {
id: imageContextMenuComponent
ImageContextMenu {
onClosed: {
destroy()
}
}
}
Component {
id: profileContextMenuComponent
ProfileContextMenu {
store: root.rootStore
onOpenProfileClicked: (publicKey) => {
Global.openProfilePopup(publicKey, null)
}
onCreateOneToOneChat: (communityId, chatId, ensName) => {
Global.changeAppSectionBySectionType(Constants.appSection.chat)
root.rootStore.chatCommunitySectionModule.createOneToOneChat("", chatId, ensName)
}
onOpened: {
root.setMessageActive(root.messageId, true)
}
onClosed: {
root.setMessageActive(root.messageId, false)
destroy()
}
}
}
Component {
id: messageContextMenuComponent
MessageContextMenuView {
store: root.rootStore
reactionModel: root.rootStore.emojiReactionsModel
disabledForChat: !root.rootStore.isUserAllowedToSendMessage
onPinMessage: (messageId) => {
root.messageStore.pinMessage(messageId)
}
onUnpinMessage: (messageId) => {
root.messageStore.unpinMessage(messageId)
}
onPinnedMessagesLimitReached: (messageId) => {
if (!root.chatContentModule) {
console.warn("error on open pinned messages limit reached from message context menu - chat content module is not set")
return
}
Global.openPinnedMessagesPopupRequested(root.rootStore,
root.messageStore,
root.chatContentModule.pinnedMessagesModel,
messageId,
root.chatId)
}
onToggleReaction: (messageId, emojiId) => {
root.messageStore.toggleReaction(messageId, emojiId)
}
onDeleteMessage: (messageId) => {
root.messageStore.warnAndDeleteMessage(messageId)
}
onEditClicked: (messageId) => {
root.messageStore.setEditModeOn(messageId)
}
onShowReplyArea: (messageId, senderId) => {
root.showReplyArea(messageId, senderId)
}
onOpened: {
root.setMessageActive(model.id, true)
}
onClosed: {
root.setMessageActive(model.id, false)
destroy()
}
}
}
}

View File

@ -0,0 +1,241 @@
import QtQuick 2.12
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQml.Models 2.3
import StatusQ.Popups 0.1
import StatusQ.Components 0.1
import utils 1.0
import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.status 1.0
import shared.controls.chat 1.0
import shared.controls.chat.menuItems 1.0
StatusMenu {
id: root
property var store
property string myPublicKey: ""
property string selectedUserPublicKey: ""
property string selectedUserDisplayName: ""
property string selectedUserIcon: ""
readonly property bool isMe: {
return root.selectedUserPublicKey === root.store.contactsStore.myPublicKey;
}
readonly property var contactDetails: {
if (root.selectedUserPublicKey === "" || isMe) {
return {}
}
return Utils.getContactDetailsAsJson(root.selectedUserPublicKey);
}
readonly property bool isContact: {
return root.selectedUserPublicKey !== "" && !!contactDetails.isContact
}
readonly property bool isBlockedContact: (!!contactDetails && contactDetails.isBlocked) || false
readonly property int outgoingVerificationStatus: {
if (root.selectedUserPublicKey === "" || root.isMe || !root.isContact) {
return 0
}
return contactDetails.verificationStatus
}
readonly property int incomingVerificationStatus: {
if (root.selectedUserPublicKey === "" || root.isMe || !root.isContact) {
return 0
}
return contactDetails.incomingVerificationStatus
}
readonly property bool hasPendingContactRequest: {
return !root.isMe && root.selectedUserPublicKey !== "" &&
root.store.contactsStore.hasPendingContactRequest(root.selectedUserPublicKey);
}
readonly property bool hasActiveReceivedVerificationRequestFrom: {
if (!root.selectedUserPublicKey || root.isMe || !root.isContact) {
return false
}
return contactDetails.incomingVerificationStatus === Constants.verificationStatus.verifying ||
contactDetails.incomingVerificationStatus === Constants.verificationStatus.verified
}
readonly property bool isVerificationRequestSent: {
if (!root.selectedUserPublicKey || root.isMe || !root.isContact) {
return false
}
return root.outgoingVerificationStatus !== Constants.verificationStatus.unverified &&
root.outgoingVerificationStatus !== Constants.verificationStatus.verified &&
root.outgoingVerificationStatus !== Constants.verificationStatus.trusted
}
readonly property bool isTrusted: {
if (!root.selectedUserPublicKey || root.isMe || !root.isContact) {
return false
}
return root.outgoingVerificationStatus === Constants.verificationStatus.trusted ||
root.incomingVerificationStatus === Constants.verificationStatus.trusted
}
readonly property bool userTrustIsUnknown: contactDetails && contactDetails.trustStatus === Constants.trustStatus.unknown
readonly property bool userIsUntrustworthy: contactDetails && contactDetails.trustStatus === Constants.trustStatus.untrustworthy
signal openProfileClicked(string publicKey)
signal createOneToOneChat(string communityId, string chatId, string ensName)
onClosed: {
// Reset selectedUserPublicKey so that associated properties get recalculated on re-open
selectedUserPublicKey = ""
}
width: 230
ProfileHeader {
width: parent.width
height: visible ? implicitHeight : 0
displayNameVisible: false
displayNamePlusIconsVisible: true
editButtonVisible: false
displayName: root.selectedUserDisplayName
pubkey: root.selectedUserPublicKey
icon: root.selectedUserIcon
trustStatus: contactDetails && contactDetails.trustStatus ? contactDetails.trustStatus
: Constants.trustStatus.unknown
isContact: root.isContact
isCurrentUser: root.isMe
userIsEnsVerified: (!!contactDetails && contactDetails.ensVerified) || false
}
StatusMenuSeparator {
topPadding: root.topPadding
}
ViewProfileMenuItem {
id: viewProfileAction
onTriggered: {
root.openProfileClicked(root.selectedUserPublicKey)
root.close()
}
}
SendMessageMenuItem {
id: sendMessageMenuItem
enabled: root.isContact && !root.isBlockedContact
onTriggered: {
root.createOneToOneChat("", root.selectedUserPublicKey, "")
root.close()
}
}
SendContactRequestMenuItem {
id: sendContactRequestMenuItem
enabled: !root.isMe && !root.isContact
&& !root.isBlockedContact && !root.hasPendingContactRequest
onTriggered: {
Global.openContactRequestPopup(root.selectedUserPublicKey, null)
root.close()
}
}
StatusAction {
id: verifyIdentityAction
text: qsTr("Verify Identity")
icon.name: "checkmark-circle"
enabled: !root.isMe && root.isContact
&& !root.isBlockedContact
&& root.outgoingVerificationStatus === Constants.verificationStatus.unverified
&& !root.hasActiveReceivedVerificationRequestFrom
onTriggered: {
Global.openSendIDRequestPopup(root.selectedUserPublicKey, null)
root.close()
}
}
StatusAction {
id: pendingIdentityAction
text: isVerificationRequestSent ||
root.incomingVerificationStatus === Constants.verificationStatus.verified ?
qsTr("ID Request Pending....") :
qsTr("Respond to ID Request...")
icon.name: "checkmark-circle"
enabled: !root.isMe && root.isContact
&& !root.isBlockedContact && !root.isTrusted
&& (root.hasActiveReceivedVerificationRequestFrom
|| root.isVerificationRequestSent)
onTriggered: {
if (hasActiveReceivedVerificationRequestFrom) {
Global.openIncomingIDRequestPopup(root.selectedUserPublicKey, null)
} else if (root.isVerificationRequestSent) {
Global.openOutgoingIDRequestPopup(root.selectedUserPublicKey, null)
}
root.close()
}
}
StatusAction {
id: renameAction
text: qsTr("Rename")
icon.name: "edit_pencil"
enabled: !root.isMe
onTriggered: {
Global.openNicknamePopupRequested(root.selectedUserPublicKey, contactDetails.localNickname,
"%1 (%2)".arg(root.selectedUserDisplayName).arg(Utils.getElidedCompressedPk(root.selectedUserPublicKey)))
root.close()
}
}
StatusAction {
id: unblockAction
text: qsTr("Unblock User")
icon.name: "remove-circle"
enabled: !root.isMe && root.isBlockedContact
onTriggered: Global.unblockContactRequested(root.selectedUserPublicKey, root.selectedUserDisplayName)
}
StatusMenuSeparator {
visible: blockMenuItem.enabled
|| markUntrustworthyMenuItem.enabled
|| removeUntrustworthyMarkMenuItem.enabled
}
StatusAction {
id: markUntrustworthyMenuItem
text: qsTr("Mark as Untrustworthy")
icon.name: "warning"
type: StatusAction.Type.Danger
enabled: !root.isMe && root.userTrustIsUnknown
onTriggered: root.store.contactsStore.markUntrustworthy(root.selectedUserPublicKey)
}
StatusAction {
id: removeUntrustworthyMarkMenuItem
text: qsTr("Remove Untrustworthy Mark")
icon.name: "warning"
enabled: !root.isMe && root.userIsUntrustworthy
onTriggered: root.store.contactsStore.removeTrustStatus(root.selectedUserPublicKey)
}
StatusAction {
text: qsTr("Remove Contact")
icon.name: "remove-contact"
type: StatusAction.Type.Danger
enabled: root.isContact && !root.isBlockedContact && !root.hasPendingContactRequest
onTriggered: {
Global.removeContactRequested(root.selectedUserDisplayName, root.selectedUserPublicKey)
root.close()
}
}
StatusAction {
id: blockMenuItem
text: qsTr("Block User")
icon.name: "cancel"
type: StatusAction.Type.Danger
enabled: !root.isMe && !root.isBlockedContact
onTriggered: Global.blockContactRequested(root.selectedUserPublicKey, root.selectedUserDisplayName)
}
}

View File

@ -10,3 +10,6 @@ ProfileHeaderContextMenuView 1.0 ProfileHeaderContextMenuView.qml
TransactionBubbleView 1.0 TransactionBubbleView.qml
NewMessagesMarker 1.0 NewMessagesMarker.qml
SimplifiedMessageView 1.0 SimplifiedMessageView.qml
ImageContextMenu 1.0 ImageContextMenu.qml
ProfileContextMenu 1.0 ProfileContextMenu.qml
MessageAddReactionContextMenu 1.0 MessageAddReactionContextMenu.qml

View File

@ -32,7 +32,7 @@ QtObject {
signal openDownloadModalRequested(bool available, string version, string url)
signal openChangeProfilePicPopup(var cb)
signal openBackUpSeedPopup()
signal openImagePopup(var image, var contextMenu)
signal openImagePopup(var image)
signal openProfilePopupRequested(string publicKey, var parentPopup)
signal openEditDisplayNamePopup()
signal openActivityCenterPopupRequested()
@ -42,6 +42,8 @@ QtObject {
signal openInviteFriendsToCommunityPopup(var community, var communitySectionModule, var cb)
signal openIncomingIDRequestPopup(string publicKey, var cb)
signal openOutgoingIDRequestPopup(string publicKey, var cb)
signal openDeleteMessagePopup(string messageId, var messageStore)
signal openDownloadImageDialog(string imageSource)
signal contactRenamed(string publicKey)
signal openLink(string link)
@ -77,4 +79,13 @@ QtObject {
function changeAppSectionBySectionType(sectionType, subsection = 0) {
root.appSectionBySectionTypeChanged(sectionType, subsection);
}
function openMenu(menuComponent, menuParent, params = {}, point = undefined) {
const menu = menuComponent.createObject(menuParent, params)
if (point)
menu.popup(point)
else
menu.popup()
return menu
}
}

View File

@ -678,6 +678,14 @@ QtObject {
return text
}
function copyImageToClipboardByUrl(content) {
globalUtilsInst.copyImageToClipboardByUrl(content)
}
function downloadImageByUrl(url, path) {
globalUtilsInst.downloadImageByUrl(url, path)
}
// Leave this function at the bottom of the file as QT Creator messes up the code color after this
function isPunct(c) {
return /(!|\@|#|\$|%|\^|&|\*|\(|\)|\+|\||-|=|\\|{|}|[|]|"|;|'|<|>|\?|,|\.|\/)/.test(c)