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