From 8e96217ae6a5d95dfd636da7784fd6817568f66e Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 19 Jan 2024 17:16:00 -0500 Subject: [PATCH] Separate MessageDelegateContextMenu to its own base component This is the first step in separation, so we can focus more on the actions in this menu, and it's not tangled up in how the context menu is shown and displayed. --- src/CMakeLists.txt | 2 + src/qml/DelegateContextMenu.qml | 363 +++++++++++++++++++++++++ src/qml/FileDelegateContextMenu.qml | 18 +- src/qml/MessageDelegateContextMenu.qml | 354 +----------------------- src/qml/RoomPage.qml | 1 - 5 files changed, 382 insertions(+), 356 deletions(-) create mode 100644 src/qml/DelegateContextMenu.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6b136861c..6879eebc4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -321,6 +321,8 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN qml/LoadComponent.qml qml/RecommendedSpaceDialog.qml qml/RoomTreeSection.qml + qml/DelegateContextMenu.qml + qml/ShareDialog.qml RESOURCES qml/confetti.png qml/glowdot.png diff --git a/src/qml/DelegateContextMenu.qml b/src/qml/DelegateContextMenu.qml new file mode 100644 index 000000000..c5a189642 --- /dev/null +++ b/src/qml/DelegateContextMenu.qml @@ -0,0 +1,363 @@ +// SPDX-FileCopyrightText: 2019 Black Hat +// SPDX-FileCopyrightText: 2020 Carl Schwan +// SPDX-License-Identifier: GPL-3.0-only + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.components as KirigamiComponents +import org.kde.kirigamiaddons.formcard as FormCard + +import org.kde.neochat +import org.kde.neochat.config + +/** + * @brief The base menu for most message types. + * + * This menu supports showing a list of actions to be shown for a particular event + * delegate in a message timeline. The menu supports both desktop and mobile menus + * with different visuals appropriate to the platform. + * + * The menu supports both a list of main actions and the ability to define sub menus + * using the nested action parameter. + * + * For event types that need alternate actions this class can be used as a base and + * the actions and nested actions can be overwritten to show the alternate items. + */ +Loader { + id: root + + /** + * @brief The current connection for the account accessing the event. + */ + required property NeoChatConnection connection + + /** + * @brief The matrix ID of the message event. + */ + required property string eventId + + /** + * @brief The message author. + * + * This should consist of the following: + * - id - The matrix ID of the author. + * - isLocalUser - Whether the author is the local user. + * - avatarSource - The mxc URL for the author's avatar in the current room. + * - avatarMediaId - The media ID of the author's avatar. + * - avatarUrl - The mxc URL for the author's avatar. + * - displayName - The display name of the author. + * - display - The name of the author. + * - color - The color for the author. + * - object - The Quotient::User object for the author. + * + * @sa Quotient::User + */ + required property var author + + /** + * @brief The display text of the message as plain text. + */ + required property string plainText + + /** + * @brief The text the user currently has selected. + */ + property string selectedText: "" + + /** + * @brief The list of menu item actions that have sub-actions. + * + * Each action will be instantiated as a single line that open a sub menu. + */ + property list nestedActions + + /** + * @brief The main list of menu item actions. + * + * Each action will be instantiated as a single line in the menu. + */ + property list actions + + /** + * Some common actions shared between menus + */ + component ViewSourceAction: Kirigami.Action { + text: i18n("View Source") + icon.name: "code-context" + onTriggered: RoomManager.viewEventSource(root.eventId) + } + + component RemoveMessageAction: Kirigami.Action { + visible: author.isLocalUser || currentRoom.canSendState("redact") + text: i18n("Remove") + icon.name: "edit-delete-remove" + icon.color: "red" + onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/RemoveSheet.qml", {room: currentRoom, eventId: eventId}, { + title: i18nc("@title", "Remove Message"), + width: Kirigami.Units.gridUnit * 25 + }) + } + + component ReplyMessageAction: Kirigami.Action { + text: i18n("Reply") + icon.name: "mail-replied-symbolic" + onTriggered: { + currentRoom.mainCache.replyId = eventId; + currentRoom.editCache.editId = ""; + RoomManager.requestFullScreenClose() + } + } + + component ReportMessageAction: Kirigami.Action { + text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report") + icon.name: "dialog-warning-symbolic" + visible: !author.isLocalUser + onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ReportSheet.qml", {room: currentRoom, eventId: eventId}, { + title: i18nc("@title", "Report Message"), + width: Kirigami.Units.gridUnit * 25 + }) + } + + Component { + id: regularMenu + + QQC2.Menu { + id: menu + Instantiator { + model: root.nestedActions + delegate: QQC2.Menu { + id: menuItem + visible: modelData.visible + title: modelData.text + + Instantiator { + model: modelData.children + delegate: QQC2.MenuItem { + text: modelData.text + icon.name: modelData.icon.name + onTriggered: modelData.trigger() + } + onObjectAdded: (index, object) => {menuItem.insertItem(0, object)} + } + } + onObjectAdded: (index, object) => { + object.visible = false; + menu.addMenu(object) + } + } + + Repeater { + model: root.actions + QQC2.MenuItem { + visible: modelData.visible + action: modelData + onClicked: root.item.close(); + } + } + QQC2.Menu { + id: webshortcutmenu + title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText) + property bool isVisible: webshortcutmodel.enabled + Component.onCompleted: { + webshortcutmenu.parent.visible = isVisible + } + onIsVisibleChanged: webshortcutmenu.parent.visible = isVisible + Instantiator { + model: WebShortcutModel { + id: webshortcutmodel + selectedText: root.selectedText.length > 0 ? root.selectedText : root.plainText + onOpenUrl: RoomManager.resolveResource(url) + } + delegate: QQC2.MenuItem { + text: model.display + icon.name: model.decoration + onTriggered: webshortcutmodel.trigger(model.edit) + } + onObjectAdded: (index, object) => webshortcutmenu.insertItem(0, object) + } + QQC2.MenuSeparator {} + QQC2.MenuItem { + text: i18n("Configure Web Shortcuts...") + icon.name: "configure" + visible: !Controller.isFlatpak + onTriggered: webshortcutmodel.configureWebShortcuts() + } + } + } + } + Component { + id: mobileMenu + + Kirigami.OverlayDrawer { + id: drawer + height: stackView.implicitHeight + edge: Qt.BottomEdge + padding: 0 + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + topPadding: 0 + + parent: applicationWindow().overlay + + QQC2.StackView { + id: stackView + width: parent.width + implicitHeight: currentItem.implicitHeight + + Component { + id: nestedActionsComponent + ColumnLayout { + id: actionLayout + property string title: "" + property list actions + width: parent.width + spacing: 0 + RowLayout { + QQC2.ToolButton { + icon.name: 'draw-arrow-back' + onClicked: stackView.pop() + } + Kirigami.Heading { + level: 3 + Layout.fillWidth: true + text: actionLayout.title + wrapMode: Text.WordWrap + } + } + Repeater { + id: listViewAction + model: actionLayout.actions + + FormCard.FormButtonDelegate { + icon.name: modelData.icon.name + icon.color: modelData.icon.color ?? undefined + enabled: modelData.enabled + visible: modelData.visible + text: modelData.text + onClicked: { + modelData.triggered() + root.item.close(); + } + } + } + } + } + initialItem: ColumnLayout { + id: popupContent + width: parent.width + spacing: 0 + RowLayout { + id: headerLayout + Layout.fillWidth: true + Layout.margins: Kirigami.Units.largeSpacing + spacing: Kirigami.Units.largeSpacing + KirigamiComponents.Avatar { + id: avatar + source: author.avatarSource + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + Layout.alignment: Qt.AlignTop + } + ColumnLayout { + Layout.fillWidth: true + Kirigami.Heading { + level: 3 + Layout.fillWidth: true + text: currentRoom.htmlSafeMemberName(author.id) + wrapMode: Text.WordWrap + } + QQC2.Label { + text: plainText + Layout.fillWidth: true + wrapMode: Text.WordWrap + + onLinkActivated: RoomManager.resolveResource(link, "join"); + } + } + } + Kirigami.Separator { + Layout.fillWidth: true + } + RowLayout { + spacing: 0 + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5 + Repeater { + model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"] + delegate: QQC2.ItemDelegate { + Layout.fillWidth: true + Layout.fillHeight: true + + contentItem: Kirigami.Heading { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + font.family: "emoji" + text: modelData + } + + onClicked: { + currentRoom.toggleReaction(eventId, modelData); + root.item.close(); + } + } + } + } + Kirigami.Separator { + Layout.fillWidth: true + } + Repeater { + id: listViewAction + model: root.actions + + FormCard.FormButtonDelegate { + icon.name: modelData.icon.name + icon.color: modelData.icon.color ?? undefined + enabled: modelData.enabled + visible: modelData.visible + text: modelData.text + onClicked: { + modelData.triggered() + root.item.close(); + } + } + } + + Repeater { + model: root.nestedActions + + FormCard.FormButtonDelegate { + action: modelData + visible: modelData.visible + onClicked: { + stackView.push(nestedActionsComponent, { + title: modelData.text, + actions: modelData.children + }); + } + } + } + } + } + } + } + + asynchronous: true + sourceComponent: Kirigami.Settings.isMobile ? mobileMenu : regularMenu + + function open() { + active = true; + } + + onStatusChanged: if (status == Loader.Ready) { + if (Kirigami.Settings.isMobile) { + item.open(); + } else { + item.popup(); + } + } +} + diff --git a/src/qml/FileDelegateContextMenu.qml b/src/qml/FileDelegateContextMenu.qml index b0fed700a..753e339eb 100644 --- a/src/qml/FileDelegateContextMenu.qml +++ b/src/qml/FileDelegateContextMenu.qml @@ -16,9 +16,9 @@ import org.kde.neochat.config * This component just overloads the actions and nested actions of the base menu * to what is required for a media item. * - * @sa MessageDelegateContextMenu + * @sa DelegateContextMenu */ -MessageDelegateContextMenu { +DelegateContextMenu { id: root /** @@ -55,15 +55,7 @@ MessageDelegateContextMenu { dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId); } }, - Kirigami.Action { - text: i18n("Reply") - icon.name: "mail-replied-symbolic" - onTriggered: { - currentRoom.mainCache.replyId = eventId; - currentRoom.editCache.editId = ""; - RoomManager.requestFullScreenClose(); - } - }, + DelegateContextMenu.ReplyMessageAction {}, Kirigami.Action { text: i18n("Copy") icon.name: "edit-copy" @@ -100,7 +92,9 @@ MessageDelegateContextMenu { text: i18n("View Source") icon.name: "code-context" onTriggered: RoomManager.viewEventSource(root.eventId) - } + }, + DelegateContextMenu.ReportMessageAction {}, + DelegateContextMenu.ViewSourceAction {} ] /** diff --git a/src/qml/MessageDelegateContextMenu.qml b/src/qml/MessageDelegateContextMenu.qml index 90f8bef7b..4665eb1e6 100644 --- a/src/qml/MessageDelegateContextMenu.qml +++ b/src/qml/MessageDelegateContextMenu.qml @@ -13,82 +13,27 @@ import org.kde.neochat import org.kde.neochat.config /** - * @brief The base menu for most message types. + * @brief The menu for normal messages. * - * This menu supports showing a list of actions to be shown for a particular event - * delegate in a message timeline. The menu supports both desktop and mobile menus - * with different visuals appropriate to the platform. + * This component just overloads the actions and nested actions of the base menu + * to what is required for a message item. * - * The menu supports both a list of main actions and the ability to define sub menus - * using the nested action parameter. - * - * For event types that need alternate actions this class can be used as a base and - * the actions and nested actions can be overwritten to show the alternate items. + * @sa DelegateContextMenu */ -Loader { +DelegateContextMenu { id: root - /** - * @brief The curent connection for the account accessing the event. - */ - required property NeoChatConnection connection - - /** - * @brief The matrix ID of the message event. - */ - required property string eventId - - /** - * @brief The message author. - * - * This should consist of the following: - * - id - The matrix ID of the author. - * - isLocalUser - Whether the author is the local user. - * - avatarSource - The mxc URL for the author's avatar in the current room. - * - avatarMediaId - The media ID of the author's avatar. - * - avatarUrl - The mxc URL for the author's avatar. - * - displayName - The display name of the author. - * - display - The name of the author. - * - color - The color for the author. - * - object - The Quotient::User object for the author. - * - * @sa Quotient::User - */ - required property var author - /** * @brief The delegate type of the message. */ required property int messageComponentType - /** - * @brief The display text of the message as plain text. - */ - required property string plainText - /** * @brief The display text of the message as rich text. */ - property string htmlText: "" + required property string htmlText - /** - * @brief The text the user currently has selected. - */ - property string selectedText: "" - - /** - * @brief The list of menu item actions that have sub-actions. - * - * Each action will be instantiated as a single line that open a sub menu. - */ - property list nestedActions - - /** - * @brief The main list of menu item actions. - * - * Each action will be instantiated as a single line in the menu. - */ - property list actions: [ + actions: [ Kirigami.Action { text: i18n("Edit") icon.name: "document-edit" @@ -98,14 +43,7 @@ Loader { } visible: author.isLocalUser && (root.messageComponentType === MessageComponentType.Emote || root.messageComponentType === MessageComponentType.Message) }, - Kirigami.Action { - text: i18n("Reply") - icon.name: "mail-replied-symbolic" - onTriggered: { - currentRoom.mainCache.replyId = eventId; - currentRoom.editCache.editId = ""; - } - }, + DelegateContextMenu.ReplyMessageAction {}, Kirigami.Action { text: i18nc("@action:inmenu As in 'Forward this message'", "Forward") icon.name: "mail-forward-symbolic" @@ -122,42 +60,14 @@ Loader { }); } }, - Kirigami.Action { - visible: author.isLocalUser || currentRoom.canSendState("redact") - text: i18n("Remove") - icon.name: "edit-delete-remove" - icon.color: "red" - onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/RemoveSheet.qml", { - room: currentRoom, - eventId: eventId - }, { - title: i18nc("@title", "Remove Message"), - width: Kirigami.Units.gridUnit * 25 - }) - }, + DelegateContextMenu.RemoveMessageAction {}, Kirigami.Action { text: i18n("Copy") icon.name: "edit-copy" onTriggered: Clipboard.saveText(root.selectedText.length > 0 ? root.selectedText : root.plainText) }, - Kirigami.Action { - text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report") - icon.name: "dialog-warning-symbolic" - visible: !author.isLocalUser - onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ReportSheet.qml", { - room: currentRoom, - eventId: eventId - }, { - title: i18nc("@title", "Report Message"), - width: Kirigami.Units.gridUnit * 25 - }) - }, - Kirigami.Action { - visible: Config.developerTools - text: i18n("View Source") - icon.name: "code-context" - onTriggered: RoomManager.viewEventSource(root.eventId) - }, + DelegateContextMenu.ReportMessageAction {}, + DelegateContextMenu.ViewSourceAction {}, Kirigami.Action { text: i18n("Copy Link") icon.name: "edit-copy" @@ -166,246 +76,4 @@ Loader { } } ] - - Component { - id: regularMenu - - QQC2.Menu { - id: menu - Instantiator { - model: root.nestedActions - delegate: QQC2.Menu { - id: menuItem - visible: modelData.visible - title: modelData.text - - Instantiator { - model: modelData.children - delegate: QQC2.MenuItem { - text: modelData.text - icon.name: modelData.icon.name - onTriggered: modelData.trigger() - } - onObjectAdded: (index, object) => { - menuItem.insertItem(0, object); - } - } - } - onObjectAdded: (index, object) => { - object.visible = false; - menu.addMenu(object); - } - } - - Repeater { - model: root.actions - QQC2.MenuItem { - visible: modelData.visible - action: modelData - onClicked: root.item.close() - } - } - QQC2.Menu { - id: webshortcutmenu - title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText) - property bool isVisible: webshortcutmodel.enabled - Component.onCompleted: { - webshortcutmenu.parent.visible = isVisible; - } - onIsVisibleChanged: webshortcutmenu.parent.visible = isVisible - Instantiator { - model: WebShortcutModel { - id: webshortcutmodel - selectedText: root.selectedText.length > 0 ? root.selectedText : root.plainText - onOpenUrl: RoomManager.resolveResource(url) - } - delegate: QQC2.MenuItem { - text: model.display - icon.name: model.decoration - onTriggered: webshortcutmodel.trigger(model.edit) - } - onObjectAdded: (index, object) => webshortcutmenu.insertItem(0, object) - } - QQC2.MenuSeparator {} - QQC2.MenuItem { - text: i18n("Configure Web Shortcuts...") - icon.name: "configure" - visible: !Controller.isFlatpak - onTriggered: webshortcutmodel.configureWebShortcuts() - } - } - } - } - Component { - id: mobileMenu - - Kirigami.OverlayDrawer { - id: drawer - height: stackView.implicitHeight - edge: Qt.BottomEdge - padding: 0 - leftPadding: 0 - rightPadding: 0 - bottomPadding: 0 - topPadding: 0 - - parent: applicationWindow().overlay - - QQC2.StackView { - id: stackView - width: parent.width - implicitHeight: currentItem.implicitHeight - - Component { - id: nestedActionsComponent - ColumnLayout { - id: actionLayout - property string title: "" - property list actions - width: parent.width - spacing: 0 - RowLayout { - QQC2.ToolButton { - icon.name: 'draw-arrow-back' - onClicked: stackView.pop() - } - Kirigami.Heading { - level: 3 - Layout.fillWidth: true - text: actionLayout.title - wrapMode: Text.WordWrap - } - } - Repeater { - id: listViewAction - model: actionLayout.actions - - FormCard.FormButtonDelegate { - icon.name: modelData.icon.name - icon.color: modelData.icon.color ?? undefined - enabled: modelData.enabled - visible: modelData.visible - text: modelData.text - onClicked: { - modelData.triggered(); - root.item.close(); - } - } - } - } - } - initialItem: ColumnLayout { - id: popupContent - width: parent.width - spacing: 0 - RowLayout { - id: headerLayout - Layout.fillWidth: true - Layout.margins: Kirigami.Units.largeSpacing - spacing: Kirigami.Units.largeSpacing - KirigamiComponents.Avatar { - id: avatar - source: author.avatarSource - Layout.preferredWidth: Kirigami.Units.gridUnit * 2 - Layout.preferredHeight: Kirigami.Units.gridUnit * 2 - Layout.alignment: Qt.AlignTop - } - ColumnLayout { - Layout.fillWidth: true - Kirigami.Heading { - level: 3 - Layout.fillWidth: true - text: currentRoom.htmlSafeMemberName(author.id) - wrapMode: Text.WordWrap - } - QQC2.Label { - text: plainText - Layout.fillWidth: true - wrapMode: Text.WordWrap - - onLinkActivated: RoomManager.resolveResource(link, "join") - } - } - } - Kirigami.Separator { - Layout.fillWidth: true - } - RowLayout { - spacing: 0 - Layout.fillWidth: true - Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5 - Repeater { - model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"] - delegate: QQC2.ItemDelegate { - Layout.fillWidth: true - Layout.fillHeight: true - - contentItem: Kirigami.Heading { - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - font.family: "emoji" - text: modelData - } - - onClicked: { - currentRoom.toggleReaction(eventId, modelData); - root.item.close(); - } - } - } - } - Kirigami.Separator { - Layout.fillWidth: true - } - Repeater { - id: listViewAction - model: root.actions - - FormCard.FormButtonDelegate { - icon.name: modelData.icon.name - icon.color: modelData.icon.color ?? undefined - enabled: modelData.enabled - visible: modelData.visible - text: modelData.text - onClicked: { - modelData.triggered(); - root.item.close(); - } - } - } - - Repeater { - model: root.nestedActions - - FormCard.FormButtonDelegate { - action: modelData - visible: modelData.visible - onClicked: { - stackView.push(nestedActionsComponent, { - title: modelData.text, - actions: modelData.children - }); - } - } - } - } - } - } - } - - asynchronous: true - sourceComponent: Kirigami.Settings.isMobile ? mobileMenu : regularMenu - - function open() { - active = true; - } - - onStatusChanged: if (status == Loader.Ready) { - if (Kirigami.Settings.isMobile) { - item.open(); - } else { - item.popup(); - } - } } diff --git a/src/qml/RoomPage.qml b/src/qml/RoomPage.qml index 10a3acb52..83af98df0 100644 --- a/src/qml/RoomPage.qml +++ b/src/qml/RoomPage.qml @@ -267,7 +267,6 @@ Kirigami.Page { const contextMenu = fileDelegateContextMenu.createObject(root, { author: author, eventId: eventId, - messageComponentType: messageComponentType, plainText: plainText, mimeType: mimeType, progressInfo: progressInfo