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 target_compile_definitions(${PROJECT_NAME} PRIVATE
QML_IMPORT_ROOT="${CMAKE_CURRENT_LIST_DIR}" 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( target_link_libraries(
${PROJECT_LIB} PUBLIC Qt5::Core Qt5::Gui Qt5::Quick Qt5::QuickControls2) ${PROJECT_LIB} PUBLIC Qt5::Core Qt5::Gui Qt5::Quick Qt5::QuickControls2)

View File

@ -61,6 +61,10 @@ ListModel {
title: "TokenListView" title: "TokenListView"
section: "Views" section: "Views"
} }
ListElement {
title: "MessageContextMenu"
section: "Views"
}
ListElement { ListElement {
title: "StatusCommunityCard" title: "StatusCommunityCard"
section: "Panels" 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 { sourceComponent: UserListPanel {
usersModel: model usersModel: model
messageContextMenu: null
label: "Some label" label: "Some label"
} }
} }

View File

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

View File

@ -8,16 +8,16 @@ import StatusQ.Components 0.1
import shared 1.0 import shared 1.0
import shared.panels 1.0 import shared.panels 1.0
import shared.status 1.0 import shared.status 1.0
import shared.views.chat 1.0
import utils 1.0 import utils 1.0
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
Item { Item {
id: root id: root
anchors.fill: parent
property var store
property var usersModel property var usersModel
property var messageContextMenu
property string label property string label
StatusBaseText { StatusBaseText {
@ -98,15 +98,13 @@ Item {
ringSettings.ringSpecModel: model.colorHash ringSettings.ringSpecModel: model.colorHash
onClicked: { onClicked: {
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
// Set parent, X & Y positions for the messageContextMenu Global.openMenu(profileContextMenuComponent, this, {
messageContextMenu.parent = this myPublicKey: userProfile.pubKey,
messageContextMenu.isProfile = true selectedUserPublicKey: model.pubKey,
messageContextMenu.myPublicKey = userProfile.pubKey selectedUserDisplayName: title,
messageContextMenu.selectedUserPublicKey = model.pubKey selectedUserIcon: model.icon,
messageContextMenu.selectedUserDisplayName = title })
messageContextMenu.selectedUserIcon = model.icon } else if (mouse.button === Qt.LeftButton) {
messageContextMenu.popup(4, 4)
} else if (mouse.button === Qt.LeftButton && !!messageContextMenu) {
Global.openProfilePopup(model.pubKey); 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 pendingMemberRequestsModel
property var declinedMemberRequestsModel property var declinedMemberRequestsModel
property string communityName property string communityName
property var communityMemberContextMenu
property bool editable: true property bool editable: true
signal membershipRequestsClicked() signal membershipRequestsClicked()
signal userProfileClicked(string id)
signal kickUserClicked(string id) signal kickUserClicked(string id)
signal banUserClicked(string id) signal banUserClicked(string id)
signal unbanUserClicked(string id) signal unbanUserClicked(string id)
@ -120,7 +118,6 @@ SettingsPageLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
onUserProfileClicked: root.userProfileClicked(id)
onKickUserClicked: { onKickUserClicked: {
kickModal.userNameToKick = name kickModal.userNameToKick = name
kickModal.userIdToKick = id kickModal.userIdToKick = id
@ -151,7 +148,6 @@ SettingsPageLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
onUserProfileClicked: root.userProfileClicked(id)
onAcceptRequestToJoin: root.acceptRequestToJoin(id) onAcceptRequestToJoin: root.acceptRequestToJoin(id)
onDeclineRequestToJoin: root.declineRequestToJoin(id) onDeclineRequestToJoin: root.declineRequestToJoin(id)
} }
@ -173,7 +169,6 @@ SettingsPageLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
onUserProfileClicked: root.userProfileClicked(id)
onAcceptRequestToJoin: root.acceptRequestToJoin(id) onAcceptRequestToJoin: root.acceptRequestToJoin(id)
} }
@ -194,7 +189,6 @@ SettingsPageLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
onUserProfileClicked: root.userProfileClicked(id)
onUnbanUserClicked: root.unbanUserClicked(id) onUnbanUserClicked: root.unbanUserClicked(id)
} }
} }

View File

@ -9,6 +9,7 @@ import StatusQ.Components 0.1
import StatusQ.Popups 0.1 import StatusQ.Popups 0.1
import utils 1.0 import utils 1.0
import shared.views.chat 1.0
import shared.controls.chat 1.0 import shared.controls.chat 1.0
import "../../layouts" import "../../layouts"
@ -20,7 +21,6 @@ Item {
property var model property var model
property var communityMemberContextMenu property var communityMemberContextMenu
signal userProfileClicked(string id)
signal kickUserClicked(string id, string name) signal kickUserClicked(string id, string name)
signal banUserClicked(string id, string name) signal banUserClicked(string id, string name)
signal unbanUserClicked(string id) signal unbanUserClicked(string id)
@ -146,18 +146,37 @@ Item {
onClicked: { onClicked: {
if(mouse.button === Qt.RightButton) { if(mouse.button === Qt.RightButton) {
// Set parent, X & Y positions for the messageContextMenu Global.openMenu(memberContextMenuComponent, this, {
root.communityMemberContextMenu.parent = this selectedUserPublicKey: model.pubKey,
root.communityMemberContextMenu.isProfile = true selectedUserDisplayName: userName,
root.communityMemberContextMenu.selectedUserPublicKey = model.pubKey selectedUserIcon: asset.name,
root.communityMemberContextMenu.selectedUserDisplayName = userName })
root.communityMemberContextMenu.selectedUserIcon = asset.name
root.communityMemberContextMenu.popup()
} else { } 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 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1 import StatusQ.Popups.Dialog 0.1
import utils 1.0 import utils 1.0
@ -33,6 +34,15 @@ StatusDialog {
subtitle: root.messageToPin ? qsTr("Unpin a previous message first") subtitle: root.messageToPin ? qsTr("Unpin a previous message first")
: qsTr("%n message(s)", "", pinnedMessageListView.count) : qsTr("%n message(s)", "", pinnedMessageListView.count)
QtObject {
id: d
function jumpToMessage(messageId) {
root.close()
root.messageStore.messageModule.jumpToMessage(messageId)
}
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
id: column id: column
@ -67,7 +77,6 @@ StatusDialog {
rootStore: root.store rootStore: root.store
messageStore: root.messageStore messageStore: root.messageStore
messageContextMenu: msgContextMenu
messageId: model.id messageId: model.id
responseToMessageWithId: model.responseToMessageWithId responseToMessageWithId: model.responseToMessageWithId
@ -120,13 +129,22 @@ StatusDialog {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
z: 55 z: 55
onClicked: { 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 (!!root.messageToPin) {
if (!radio.checked) if (!radio.checked)
radio.checked = true radio.checked = true
} else { } else {
root.close() d.jumpToMessage(model.id)
root.messageStore.messageModule.jumpToMessage(model.id) }
break
} }
} }
} }
@ -160,24 +178,42 @@ StatusDialog {
root.messageToUnpin = checked ? model.id : "" root.messageToUnpin = checked ? model.id : ""
} }
} }
Component {
id: pinnedPopupMessageContextMenuComponent
StatusMenu {
id: messageContextMenu
property string messageId
StatusAction {
text: qsTr("Unpin")
icon.name: "unpin"
onTriggered: {
root.messageStore.unpinMessage(messageContextMenu.messageId)
close()
} }
} }
MessageContextMenuView { StatusAction {
id: msgContextMenu text: qsTr("Jump to")
store: root.store icon.name: "arrow-up"
pinnedPopup: true onTriggered: {
pinnedMessage: true d.jumpToMessage(messageContextMenu.messageId)
onShouldCloseParentPopup: { close()
root.close() }
} }
onUnpinMessage: { onOpened: {
root.messageStore.unpinMessage(messageId) messageItem.setMessageActive(model.id, true)
}
onClosed: {
messageItem.setMessageActive(model.id, false)
destroy()
}
}
} }
onJumpToMessage: {
root.messageStore.messageModule.jumpToMessage(messageId)
} }
} }

View File

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

View File

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

View File

@ -74,53 +74,10 @@ ColumnLayout {
chatSectionModule: root.rootStore.chatCommunitySectionModule chatSectionModule: root.rootStore.chatCommunitySectionModule
} }
Loader { QtObject {
id: contextMenuLoader id: d
active: root.isActiveChannel
asynchronous: true
// FIXME: `MessageContextMenuView` is way too heavy function showReplyArea(messageId) {
// 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)
}
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) let obj = messageStore.getMessageByIdAsJson(messageId)
if (!obj) { if (!obj) {
return return
@ -130,7 +87,6 @@ ColumnLayout {
} }
} }
} }
}
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
@ -146,30 +102,26 @@ ColumnLayout {
chatContentModule: root.chatContentModule chatContentModule: root.chatContentModule
rootStore: root.rootStore rootStore: root.rootStore
contactsStore: root.contactsStore contactsStore: root.contactsStore
messageContextMenu: contextMenuLoader.item
messageStore: root.messageStore messageStore: root.messageStore
emojiPopup: root.emojiPopup emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup stickersPopup: root.stickersPopup
usersStore: root.usersStore usersStore: root.usersStore
stickersLoaded: root.stickersLoaded stickersLoaded: root.stickersLoaded
publicKey: root.chatId chatId: root.chatId
isOneToOne: root.chatType === Constants.chatType.oneToOne isOneToOne: root.chatType === Constants.chatType.oneToOne
isChatBlocked: root.isBlocked || !root.isUserAllowedToSendMessage isChatBlocked: root.isBlocked || !root.isUserAllowedToSendMessage
channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "") channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "")
isActiveChannel: root.isActiveChannel isActiveChannel: root.isActiveChannel
onShowReplyArea: { onShowReplyArea: (messageId, senderId) => {
let obj = messageStore.getMessageByIdAsJson(messageId) d.showReplyArea(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)
}
} }
onOpenStickerPackPopup: { onOpenStickerPackPopup: {
root.openStickerPackPopup(stickerPackId); 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.text: inputAreaLoader.preservedText
textInput.placeholderText: root.chatInputPlaceholder textInput.placeholderText: root.chatInputPlaceholder
messageContextMenu: contextMenuLoader.item
emojiPopup: root.emojiPopup emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup stickersPopup: root.stickersPopup
isContactBlocked: root.isBlocked isContactBlocked: root.isBlocked

View File

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

View File

@ -112,8 +112,9 @@ StatusSectionLayout {
rightPanel: Component { rightPanel: Component {
id: userListComponent id: userListComponent
UserListPanel { UserListPanel {
anchors.fill: parent
store: root.rootStore
label: qsTr("Members") label: qsTr("Members")
messageContextMenu: quickActionMessageOptionsMenu
usersModel: { usersModel: {
let chatContentModule = root.rootStore.currentChatContentModule() let chatContentModule = root.rootStore.currentChatContentModule()
if (!chatContentModule || !chatContentModule.usersModule) { 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 declinedMemberRequestsModel: root.community.declinedMemberRequests
editable: root.community.amISectionAdmin editable: root.community.amISectionAdmin
communityName: root.community.name communityName: root.community.name
communityMemberContextMenu: memberContextMenuView
onUserProfileClicked: Global.openProfilePopup(id)
onKickUserClicked: root.rootStore.removeUserFromCommunity(id) onKickUserClicked: root.rootStore.removeUserFromCommunity(id)
onBanUserClicked: root.rootStore.banUserFromCommunity(id) onBanUserClicked: root.rootStore.banUserFromCommunity(id)
onUnbanUserClicked: root.rootStore.unbanUserFromCommunity(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 { Connections {
target: root.chatCommunitySectionModule target: root.chatCommunitySectionModule
function onOpenNoPermissionsToJoinPopup(communityName: string, userName: string, communityId: string, requestId: string) { function onOpenNoPermissionsToJoinPopup(communityName: string, userName: string, communityId: string, requestId: string) {

View File

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

View File

@ -1,4 +1,5 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Dialogs 1.0
import AppLayouts.Chat.popups 1.0 import AppLayouts.Chat.popups 1.0
import AppLayouts.Profile.popups 1.0 import AppLayouts.Profile.popups 1.0
@ -40,6 +41,8 @@ QtObject {
Global.importCommunityPopupRequested.connect(openImportCommunityPopup) Global.importCommunityPopupRequested.connect(openImportCommunityPopup)
Global.removeContactRequested.connect(openRemoveContactConfirmationPopup) Global.removeContactRequested.connect(openRemoveContactConfirmationPopup)
Global.openPopupRequested.connect(openPopup) Global.openPopupRequested.connect(openPopup)
Global.openDeleteMessagePopup.connect(openDeleteMessagePopup)
Global.openDownloadImageDialog.connect(openDownloadImageDialog)
} }
function openPopup(popupComponent, params = {}, cb = null) { function openPopup(popupComponent, params = {}, cb = null) {
@ -77,9 +80,8 @@ QtObject {
openPopup(downloadPageComponent, popupProperties) openPopup(downloadPageComponent, popupProperties)
} }
function openImagePopup(image, contextMenu) { function openImagePopup(image) {
var popup = imagePopupComponent.createObject(popupParent) var popup = imagePopupComponent.createObject(popupParent)
popup.contextMenu = contextMenu
popup.openPopup(image) 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: [ readonly property list<Component> _components: [
Component { Component {
id: removeContactConfirmationDialog id: removeContactConfirmationDialog
@ -316,17 +333,6 @@ QtObject {
id: imagePopupComponent id: imagePopupComponent
StatusImageModal { StatusImageModal {
id: imagePopup 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() onClosed: destroy()
} }
}, },
@ -461,6 +467,36 @@ QtObject {
DiscordImportProgressDialog { DiscordImportProgressDialog {
store: root.communitiesStore 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: { onClicked: {
if (!!root.store.profileLargeImage) if (!!root.store.profileLargeImage)
imageEditMenu.popup(this, mouse.x, mouse.y); Global.openMenu(editImageMenuComponent, this)
else else
Global.openChangeProfilePicPopup(tempIcon); Global.openChangeProfilePicPopup(tempIcon);
} }
@ -271,8 +271,10 @@ Item {
} }
} }
Component {
id: editImageMenuComponent
StatusMenu { StatusMenu {
id: imageEditMenu
width: 200 width: 200
StatusAction { StatusAction {
@ -298,3 +300,4 @@ Item {
} }
} }
} }
}

View File

@ -14,3 +14,4 @@ MessageBorder 1.0 MessageBorder.qml
EmojiReaction 1.0 EmojiReaction.qml EmojiReaction 1.0 EmojiReaction.qml
ProfileHeader 1.0 ProfileHeader.qml ProfileHeader 1.0 ProfileHeader.qml
VerificationLabel 1.0 VerificationLabel.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 GetSyncCodeInstructionsPopup 1.0 GetSyncCodeInstructionsPopup.qml
NoPermissionsToJoinPopup 1.0 NoPermissionsToJoinPopup.qml NoPermissionsToJoinPopup 1.0 NoPermissionsToJoinPopup.qml
RemoveAccountConfirmationPopup 1.0 RemoveAccountConfirmationPopup.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 imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top // TODO: Remove this proeprty?
property var messageContextMenu
property alias suggestions: suggestionsBox property alias suggestions: suggestionsBox
enum ImageErrorMessageLocation { enum ImageErrorMessageLocation {
@ -1226,7 +1224,9 @@ Rectangle {
Layout.leftMargin: Style.current.halfPadding Layout.leftMargin: Style.current.halfPadding
Layout.rightMargin: Style.current.halfPadding Layout.rightMargin: Style.current.halfPadding
visible: isImage visible: isImage
onImageClicked: Global.openImagePopup(chatImage, messageContextMenu) onImageClicked: {
Global.openImagePopup(chatImage)
}
onImageRemoved: { onImageRemoved: {
if (control.fileUrlsAndSources.length > index && control.fileUrlsAndSources[index]) { if (control.fileUrlsAndSources.length > index && control.fileUrlsAndSources[index]) {
control.fileUrlsAndSources.splice(index, 1) control.fileUrlsAndSources.splice(index, 1)

View File

@ -6,13 +6,12 @@ import QtGraphicalEffects 1.13
import utils 1.0 import utils 1.0
import shared 1.0 import shared 1.0
import shared.views.chat 1.0
Popup { Popup {
id: root id: root
signal clicked(var mouse) property var store
property string imageSource: messageImage.source
property var contextMenu
modal: true modal: true
Overlay.modal: Rectangle { Overlay.modal: Rectangle {
@ -32,7 +31,6 @@ Popup {
const maxHeight = Global.applicationWindow.height - 80 const maxHeight = Global.applicationWindow.height - 80
const maxWidth = Global.applicationWindow.width - 80 const maxWidth = Global.applicationWindow.width - 80
if (image.sourceSize.width >= maxWidth || image.sourceSize.height >= maxHeight) { if (image.sourceSize.width >= maxWidth || image.sourceSize.height >= maxHeight) {
this.width = maxWidth this.width = maxWidth
this.height = maxHeight this.height = maxHeight
@ -44,7 +42,7 @@ Popup {
function openPopup(image) { function openPopup(image) {
setPopupData(image); setPopupData(image);
root.open(); open()
} }
contentItem: AnimatedImage { contentItem: AnimatedImage {
@ -61,7 +59,22 @@ Popup {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: { 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 readonly property alias unfurledImagesCount: d.unfurledImagesCount
property bool isCurrentUser: false property bool isCurrentUser: false
signal imageClicked(var image) signal imageClicked(var image, var mouse, var imageSource)
signal linksLoaded() signal linksLoaded()
spacing: 4 spacing: 4
@ -138,7 +138,12 @@ Column {
isOnline: root.store.mainModuleInst.isOnline isOnline: root.store.mainModuleInst.isOnline
asynchronous: true asynchronous: true
isAnimated: result.contentType ? result.contentType.toLowerCase().endsWith("gif") : false 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 imageAlias.cache: localAnimationEnabled // GIFs can only loop/play properly with cache enabled
Loader { Loader {
width: 45 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.Controls 2.3
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQml.Models 2.3 import QtQml.Models 2.3
import QtQuick.Dialogs 1.0
import StatusQ.Popups 0.1 import StatusQ.Popups 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
@ -20,330 +19,56 @@ StatusMenu {
property var store property var store
property var reactionModel property var reactionModel
property alias emojiContainer: emojiContainer
property string myPublicKey: "" property string myPublicKey: ""
property bool amIChatAdmin: false property bool amIChatAdmin: false
property bool disabledForChat: false property bool disabledForChat: false
property string selectedUserPublicKey: ""
property string selectedUserDisplayName: ""
property string selectedUserIcon: ""
property int chatType: Constants.chatType.unknown property int chatType: Constants.chatType.unknown
property string messageId: "" property string messageId: ""
property string unparsedText: "" property string unparsedText: ""
property string messageSenderId: "" property string messageSenderId: ""
property int messageContentType: Constants.messageContentType.unknownContentType 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 pinMessageAllowedForMembers: false
property bool isDebugEnabled: store && store.isDebugEnabled property bool isDebugEnabled: store && store.isDebugEnabled
property bool isEmoji: false property bool editRestricted: false
property bool isSticker: false
property bool hideEmojiPicker: true
property bool pinnedMessage: false property bool pinnedMessage: false
property bool canPin: false property bool canPin: false
readonly property bool isMyMessage: { readonly property bool isMyMessage: {
return root.messageSenderId !== "" && root.messageSenderId === root.myPublicKey; 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 pinMessage(string messageId)
signal unpinMessage(string messageId) signal unpinMessage(string messageId)
signal pinnedMessagesLimitReached(string messageId) signal pinnedMessagesLimitReached(string messageId)
signal jumpToMessage(string messageId) signal showReplyArea(string messageId, string messageSenderId)
signal shouldCloseParentPopup()
signal createOneToOneChat(string communityId, string chatId, string ensName)
signal showReplyArea()
signal toggleReaction(string messageId, int emojiId) signal toggleReaction(string messageId, int emojiId)
signal deleteMessage(string messageId) signal deleteMessage(string messageId)
signal editClicked(string messageId) signal editClicked(string messageId)
function show(userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam, emojiReactionsModel) { width: Math.max(emojiContainer.visible ? emojiContainer.width : 0, 230)
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)
Item { Item {
id: emojiContainer id: emojiContainer
width: emojiRow.width width: emojiRow.width
height: visible ? emojiRow.height : 0 height: visible ? emojiRow.height : 0
visible: !root.hideEmojiPicker && (root.isEmoji || !root.isProfile) && !root.pinnedPopup && !root.disabledForChat visible: !root.disabledForChat
Row { MessageReactionsRow {
id: emojiRow id: emojiRow
spacing: Style.current.halfPadding reactionsModel: root.reactionModel
leftPadding: Style.current.halfPadding bottomPadding: Style.current.padding
rightPadding: Style.current.halfPadding onToggleReaction: {
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.toggleReaction(root.messageId, emojiId)
root.close() 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 { StatusMenuSeparator {
anchors.bottom: viewProfileAction.top visible: emojiContainer.visible
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)
} }
StatusAction { StatusAction {
@ -351,15 +76,10 @@ StatusMenu {
text: qsTr("Reply to") text: qsTr("Reply to")
icon.name: "chat" icon.name: "chat"
onTriggered: { onTriggered: {
root.showReplyArea() root.showReplyArea(root.messageId, root.messageSenderId)
root.close() root.close()
} }
enabled: (!root.hideEmojiPicker && enabled: !root.disabledForChat
!root.isEmoji &&
!root.isProfile &&
!root.pinnedPopup &&
!root.isRightClickOnImage &&
!root.disabledForChat)
} }
StatusAction { StatusAction {
@ -370,12 +90,7 @@ StatusMenu {
} }
icon.name: "edit" icon.name: "edit"
enabled: root.isMyMessage && enabled: root.isMyMessage &&
!root.hideEmojiPicker && !root.editRestricted &&
!root.isEmoji &&
!root.isSticker &&
!root.isProfile &&
!root.pinnedPopup &&
!root.isRightClickOnImage &&
!root.disabledForChat !root.disabledForChat
} }
@ -403,13 +118,8 @@ StatusMenu {
StatusAction { StatusAction {
id: pinAction id: pinAction
text: { text: root.pinnedMessage ? qsTr("Unpin") : qsTr("Pin")
if (root.pinnedMessage) { icon.name: root.pinnedMessage ? "unpin" : "pin"
return qsTr("Unpin")
}
return qsTr("Pin")
}
onTriggered: { onTriggered: {
if (root.pinnedMessage) { if (root.pinnedMessage) {
root.unpinMessage(root.messageId) root.unpinMessage(root.messageId)
@ -424,14 +134,10 @@ StatusMenu {
root.pinMessage(root.messageId) root.pinMessage(root.messageId)
root.close() root.close()
} }
icon.name: "pin"
enabled: { enabled: {
if (root.isProfile || root.isEmoji || root.isRightClickOnImage || root.disabledForChat) if (root.disabledForChat)
return false return false
if (root.pinnedPopup)
return true
switch (root.chatType) { switch (root.chatType) {
case Constants.chatType.profile: case Constants.chatType.profile:
return false return false
@ -449,10 +155,9 @@ StatusMenu {
StatusMenuSeparator { StatusMenuSeparator {
visible: deleteMessageAction.enabled && visible: deleteMessageAction.enabled &&
(viewProfileAction.enabled || (replyToMenuItem.enabled ||
sendMessageMenuItem.enabled ||
replyToMenuItem.enabled ||
copyMessageMenuItem.enabled || copyMessageMenuItem.enabled ||
copyMessageIdAction ||
editMessageAction.enabled || editMessageAction.enabled ||
pinAction.enabled) pinAction.enabled)
} }
@ -461,72 +166,16 @@ StatusMenu {
id: deleteMessageAction id: deleteMessageAction
enabled: (root.isMyMessage || root.amIChatAdmin) && enabled: (root.isMyMessage || root.amIChatAdmin) &&
!root.disabledForChat && !root.disabledForChat &&
!root.isProfile &&
!root.isEmoji &&
!root.pinnedPopup &&
!root.isRightClickOnImage &&
(root.messageContentType === Constants.messageContentType.messageType || (root.messageContentType === Constants.messageContentType.messageType ||
root.messageContentType === Constants.messageContentType.stickerType || root.messageContentType === Constants.messageContentType.stickerType ||
root.messageContentType === Constants.messageContentType.emojiType || root.messageContentType === Constants.messageContentType.emojiType ||
root.messageContentType === Constants.messageContentType.imageType || root.messageContentType === Constants.messageContentType.imageType ||
root.messageContentType === Constants.messageContentType.audioType) root.messageContentType === Constants.messageContentType.audioType)
text: qsTr("Delete message") text: qsTr("Delete message")
onTriggered: {
if (!localAccountSensitiveSettings.showDeleteMessageWarning) {
deleteMessage(messageId)
}
else {
Global.openPopup(deleteMessageConfirmationDialogComponent)
}
}
icon.name: "delete" icon.name: "delete"
type: StatusAction.Type.Danger type: StatusAction.Type.Danger
}
StatusAction {
id: jumpToAction
enabled: root.pinnedPopup && !root.isProfile
text: qsTr("Jump to")
onTriggered: { 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) root.deleteMessage(messageId)
} }
onClosed: {
destroy()
}
}
} }
} }

View File

@ -22,7 +22,8 @@ Loader {
property var messageStore property var messageStore
property var usersStore property var usersStore
property var contactsStore property var contactsStore
property var messageContextMenu: null property var chatContentModule
property string channelEmoji property string channelEmoji
property bool isActiveChannel: false property bool isActiveChannel: false
@ -34,6 +35,7 @@ Loader {
// without an explicit need to fetch those details via message store/module. // without an explicit need to fetch those details via message store/module.
property bool isChatBlocked: false property bool isChatBlocked: false
property string chatId
property string messageId: "" property string messageId: ""
property string communityId: "" property string communityId: ""
@ -120,67 +122,59 @@ Loader {
readonly property bool isExpired: d.getIsExpired(messageTimestamp, messageOutgoingStatus) readonly property bool isExpired: d.getIsExpired(messageTimestamp, messageOutgoingStatus)
readonly property bool isSending: messageOutgoingStatus === Constants.sending && !isExpired readonly property bool isSending: messageOutgoingStatus === Constants.sending && !isExpired
signal imageClicked(var image) function openProfileContextMenu(sender, mouse, isReply = false) {
if (isReply && !quotedMessageFrom) {
// WARNING: To much arguments here. Create an object argument. // The responseTo message was deleted
property var messageClickHandler: function(sender, point, // so we don't enable to right click the unavailable profile
isProfileClick,
isSticker = false,
isImage = false,
image = null,
isEmoji = false,
hideEmojiPicker = false,
isReply = false,
isRightClickOnImage = false,
imageSource = "") {
if (placeholderMessage || !(root.rootStore.mainModuleInst.activeSection.joined || isProfileClick)) {
return false return false
} }
messageContextMenu.myPublicKey = userProfile.pubKey const params = {
messageContextMenu.amIChatAdmin = root.amIChatAdmin selectedUserPublicKey: isReply ? quotedMessageFrom : root.senderId,
messageContextMenu.pinMessageAllowedForMembers = messageStore.isPinMessageAllowedForMembers selectedUserDisplayName: isReply ? quotedMessageAuthorDetailsDisplayName : root.senderDisplayName,
messageContextMenu.chatType = messageStore.chatType selectedUserIcon: isReply ? quotedMessageAuthorDetailsThumbnailImage : root.senderIcon,
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
} }
// Emoji container is not a menu item of messageContextMenu so checking it separatly Global.openMenu(profileContextMenuComponent, sender, params)
if (messageContextMenu.checkIfEmpty() && !isEmoji) {
return false
} }
messageContextMenu.parent = sender function openMessageContextMenu() {
messageContextMenu.popup(point) if (placeholderMessage || !root.rootStore.mainModuleInst.activeSection.joined)
return true 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,
}
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) signal showReplyArea(string messageId, string author)
@ -237,22 +231,7 @@ Loader {
property string activeMessage property string activeMessage
readonly property bool isMessageActive: d.activeMessage === root.messageId readonly property bool isMessageActive: d.activeMessage === root.messageId
function setMessageActive(messageId, active) { readonly property bool addReactionAllowed: !root.isInPinnedPopup && !root.isChatBlocked
// 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;
}
}
function nextMessageHasHeader() { function nextMessageHasHeader() {
if(!root.nextMessageAsJsonObj) { if(!root.nextMessageAsJsonObj) {
@ -303,15 +282,24 @@ Loader {
return StatusMessage.ContentType.Unknown; 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)
} }
function onImageClicked(image, mouse, imageSource) {
switch (mouse.button) {
Connections { case Qt.LeftButton:
enabled: d.isMessageActive Global.openImagePopup(image)
target: root.messageContextMenu break;
function onClosed() { case Qt.RightButton:
d.setMessageActive(root.messageId, false) Global.openMenu(imageContextMenuComponent, image, { imageSource })
break;
}
} }
} }
@ -498,7 +486,6 @@ Loader {
disableHover: root.disableHover || disableHover: root.disableHover ||
delegate.hideQuickActions || delegate.hideQuickActions ||
(root.chatLogView && root.chatLogView.moving) || (root.chatLogView && root.chatLogView.moving) ||
(root.messageContextMenu && root.messageContextMenu.opened) ||
Global.popupOpened Global.popupOpened
disableEmojis: root.isChatBlocked disableEmojis: root.isChatBlocked
@ -514,15 +501,8 @@ Loader {
onEditCompleted: delegate.editCompletedHandler(newMsgText) onEditCompleted: delegate.editCompletedHandler(newMsgText)
onImageClicked: { onImageClicked: (image, mouse, imageSource) => {
switch (mouse.button) { d.onImageClicked(image, mouse, imageSource)
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;
}
} }
onLinkActivated: { onLinkActivated: {
@ -542,13 +522,11 @@ Loader {
} }
onProfilePictureClicked: { onProfilePictureClicked: {
if (root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), true)) root.openProfileContextMenu(sender, mouse)
d.setMessageActive(root.messageId, true)
} }
onReplyProfileClicked: { onReplyProfileClicked: {
if (root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), true, false, false, null, false, false, true)) root.openProfileContextMenu(sender, mouse, true)
d.setMessageActive(root.messageId, true)
} }
onReplyMessageClicked: { onReplyMessageClicked: {
@ -557,8 +535,7 @@ Loader {
} }
onSenderNameClicked: { onSenderNameClicked: {
if (root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), true)) root.openProfileContextMenu(sender, mouse)
d.setMessageActive(root.messageId, true)
} }
onToggleReactionClicked: { onToggleReactionClicked: {
@ -573,12 +550,8 @@ Loader {
root.messageStore.toggleReaction(root.messageId, emojiId) root.messageStore.toggleReaction(root.messageId, emojiId)
} }
onAddReactionClicked: { onAddReactionClicked: (sender, mouse) => {
if (root.isChatBlocked) d.addReactionClicked(sender, mouse)
return
if (root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), false, false, false, null, true, false))
d.setMessageActive(root.messageId, true)
} }
onStickerClicked: { onStickerClicked: {
@ -592,12 +565,9 @@ Loader {
mouseArea { mouseArea {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
enabled: !root.isChatBlocked && enabled: !root.isChatBlocked &&
!root.placeholderMessage && !root.placeholderMessage
delegate.contentType !== StatusMessage.ContentType.Image
onClicked: { onClicked: {
if (root.messageClickHandler(this, Qt.point(mouse.x, mouse.y), root.openMessageContextMenu()
false, false, false, null, root.isEmoji, false, false, false, ""))
d.setMessageActive(root.messageId, true)
} }
} }
@ -714,7 +684,6 @@ Loader {
usersStore: root.usersStore usersStore: root.usersStore
emojiPopup: root.emojiPopup emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup stickersPopup: root.stickersPopup
messageContextMenu: root.messageContextMenu
chatType: root.messageStore.chatType chatType: root.messageStore.chatType
isEdit: true isEdit: true
@ -736,8 +705,8 @@ Loader {
messageStore: root.messageStore messageStore: root.messageStore
store: root.rootStore store: root.rootStore
isCurrentUser: root.amISender isCurrentUser: root.amISender
onImageClicked: { onImageClicked: (image, mouse, imageSource) => {
root.imageClicked(image); d.onImageClicked(image, mouse, imageSource)
} }
onLinksLoaded: { onLinksLoaded: {
// If there is only one image and no links, hide the message // If there is only one image and no links, hide the message
@ -765,7 +734,7 @@ Loader {
quickActions: [ quickActions: [
Loader { Loader {
active: !root.isInPinnedPopup && delegate.hovered && !delegate.hideQuickActions active: d.addReactionAllowed && delegate.hovered && !delegate.hideQuickActions
visible: active visible: active
sourceComponent: StatusFlatRoundButton { sourceComponent: StatusFlatRoundButton {
width: d.chatButtonSize width: d.chatButtonSize
@ -773,9 +742,8 @@ Loader {
icon.name: "reaction-b" icon.name: "reaction-b"
type: StatusFlatRoundButton.Type.Tertiary type: StatusFlatRoundButton.Type.Tertiary
tooltip.text: qsTr("Add reaction") tooltip.text: qsTr("Add reaction")
onClicked: { onClicked: (mouse) => {
if (root.messageClickHandler(delegate, mapToItem(delegate, mouse.x, mouse.y), false, false, false, null, true, false)) d.addReactionClicked(this, mouse)
d.setMessageActive(root.messageId, true)
} }
} }
}, },
@ -791,9 +759,6 @@ Loader {
tooltip.text: qsTr("Reply") tooltip.text: qsTr("Reply")
onClicked: { onClicked: {
root.showReplyArea(root.messageId, root.senderId) root.showReplyArea(root.messageId, root.senderId)
if (messageContextMenu.closeParentPopup) {
messageContextMenu.closeParentPopup()
}
} }
} }
}, },
@ -886,12 +851,7 @@ Loader {
type: StatusFlatRoundButton.Type.Tertiary type: StatusFlatRoundButton.Type.Tertiary
tooltip.text: qsTr("Delete") tooltip.text: qsTr("Delete")
onClicked: { onClicked: {
if (!localAccountSensitiveSettings.showDeleteMessageWarning) { messageStore.warnAndDeleteMessage(root.messageId)
messageStore.deleteMessage(root.messageId)
}
else {
Global.openPopup(deleteMessageConfirmationDialogComponent)
}
} }
} }
} }
@ -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 { Component {
id: newMessagesMarkerComponent id: newMessagesMarkerComponent
@ -931,4 +868,110 @@ Loader {
timestamp: root.messageTimestamp 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 TransactionBubbleView 1.0 TransactionBubbleView.qml
NewMessagesMarker 1.0 NewMessagesMarker.qml NewMessagesMarker 1.0 NewMessagesMarker.qml
SimplifiedMessageView 1.0 SimplifiedMessageView.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 openDownloadModalRequested(bool available, string version, string url)
signal openChangeProfilePicPopup(var cb) signal openChangeProfilePicPopup(var cb)
signal openBackUpSeedPopup() signal openBackUpSeedPopup()
signal openImagePopup(var image, var contextMenu) signal openImagePopup(var image)
signal openProfilePopupRequested(string publicKey, var parentPopup) signal openProfilePopupRequested(string publicKey, var parentPopup)
signal openEditDisplayNamePopup() signal openEditDisplayNamePopup()
signal openActivityCenterPopupRequested() signal openActivityCenterPopupRequested()
@ -42,6 +42,8 @@ QtObject {
signal openInviteFriendsToCommunityPopup(var community, var communitySectionModule, var cb) signal openInviteFriendsToCommunityPopup(var community, var communitySectionModule, var cb)
signal openIncomingIDRequestPopup(string publicKey, var cb) signal openIncomingIDRequestPopup(string publicKey, var cb)
signal openOutgoingIDRequestPopup(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 contactRenamed(string publicKey)
signal openLink(string link) signal openLink(string link)
@ -77,4 +79,13 @@ QtObject {
function changeAppSectionBySectionType(sectionType, subsection = 0) { function changeAppSectionBySectionType(sectionType, subsection = 0) {
root.appSectionBySectionTypeChanged(sectionType, subsection); 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 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 // Leave this function at the bottom of the file as QT Creator messes up the code color after this
function isPunct(c) { function isPunct(c) {
return /(!|\@|#|\$|%|\^|&|\*|\(|\)|\+|\||-|=|\\|{|}|[|]|"|;|'|<|>|\?|,|\.|\/)/.test(c) return /(!|\@|#|\$|%|\^|&|\*|\(|\)|\+|\||-|=|\\|{|}|[|]|"|;|'|<|>|\?|,|\.|\/)/.test(c)