Message menu rework

Rework the file menu so that it no longer relies on having a reference to the media delegate to manage a download for either opening externally or copying to clipboard. This allows the menus to be moved out of the delegates and maximize components and have them accessed through RoomManager. This reduces duplication and reduces the number of components in an already heavy delegate.
This commit is contained in:
James Graham
2023-09-15 13:57:40 +00:00
parent 14cdd096cf
commit 33c0cae64c
14 changed files with 305 additions and 103 deletions

View File

@@ -39,6 +39,7 @@
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h>
#include "clipboard.h"
#include "controller.h"
#include "eventhandler.h"
#include "events/joinrulesevent.h"
@@ -47,6 +48,7 @@
#include "neochatconfig.h"
#include "notificationsmanager.h"
#include "texthandler.h"
#include "urlhelper.h"
#include "utils.h"
#include <KConfig>
@@ -1344,6 +1346,62 @@ QByteArray NeoChatRoom::getEventJsonSource(const QString &eventId)
return {};
}
void NeoChatRoom::openEventMediaExternally(const QString &eventId)
{
const auto evtIt = findInTimeline(eventId);
if (evtIt != messageEvents().rend() && is<RoomMessageEvent>(**evtIt)) {
const auto event = evtIt->viewAs<RoomMessageEvent>();
if (event->hasFileContent()) {
const auto transferInfo = fileTransferInfo(eventId);
if (transferInfo.completed()) {
UrlHelper helper;
helper.openUrl(transferInfo.localPath);
} else {
downloadFile(eventId,
QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
+ event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
connect(this, &Room::fileTransferCompleted, this, [this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
Q_UNUSED(localFile);
Q_UNUSED(fileMetadata);
if (id == eventId) {
auto transferInfo = fileTransferInfo(eventId);
UrlHelper helper;
helper.openUrl(transferInfo.localPath);
}
});
}
}
}
}
void NeoChatRoom::copyEventMedia(const QString &eventId)
{
const auto evtIt = findInTimeline(eventId);
if (evtIt != messageEvents().rend() && is<RoomMessageEvent>(**evtIt)) {
const auto event = evtIt->viewAs<RoomMessageEvent>();
if (event->hasFileContent()) {
const auto transferInfo = fileTransferInfo(eventId);
if (transferInfo.completed()) {
Clipboard clipboard;
clipboard.setImage(transferInfo.localPath);
} else {
downloadFile(eventId,
QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
+ event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
connect(this, &Room::fileTransferCompleted, this, [this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
Q_UNUSED(localFile);
Q_UNUSED(fileMetadata);
if (id == eventId) {
auto transferInfo = fileTransferInfo(eventId);
Clipboard clipboard;
clipboard.setImage(transferInfo.localPath);
}
});
}
}
}
}
QString NeoChatRoom::chatBoxText() const
{
return m_chatBoxText;

View File

@@ -553,6 +553,20 @@ public:
Q_INVOKABLE QByteArray getEventJsonSource(const QString &eventId);
/**
* @brief Open the media for the given event in an appropriate external app.
*
* Will do nothing if the event has no media.
*/
Q_INVOKABLE void openEventMediaExternally(const QString &eventId);
/**
* @brief Copy the media for the given event to the clipboard.
*
* Will do nothing if the event has no media.
*/
Q_INVOKABLE void copyEventMedia(const QString &eventId);
[[nodiscard]] bool readMarkerLoaded() const;
/**

View File

@@ -25,6 +25,8 @@ Components.AlbumMaximizeComponent {
readonly property var currentTime: model.data(model.index(content.currentIndex, 0), MessageEventModel.TimeRole)
readonly property var currentDelegateType: model.data(model.index(content.currentIndex, 0), MessageEventModel.DelegateTypeRole)
readonly property string currentPlainText: model.data(model.index(content.currentIndex, 0), MessageEventModel.PlainText)
readonly property var currentMimeType: model.data(model.index(content.currentIndex, 0), MessageEventModel.MimeTypeRole)
@@ -84,28 +86,26 @@ Components.AlbumMaximizeComponent {
}
}
}
onItemRightClicked: {
const contextMenu = fileDelegateContextMenu.createObject(parent, {
author: root.currentAuthor,
eventId: root.currentEventId,
file: parent,
mimeType: root.currentMimeType,
progressInfo: root.currentProgressInfo,
plainText: root.currentPlainText,
connection: root.currentRoom.connection
});
contextMenu.closeFullscreen.connect(root.close)
contextMenu.open();
}
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId,
root.currentAuthor,
root.currentDelegateType,
root.currentPlainText,
"",
"",
root.currentMimeType,
root.currentProgressInfo)
onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(root.currentEventId)
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
Connections {
target: RoomManager
function onCloseFullScreen() {
root.close()
}
}
Component {

View File

@@ -36,7 +36,7 @@ TimelineContainer {
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
onDownloadedChanged: audio.play()
onOpenContextMenu: openFileContext(root)
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
innerObject: ColumnLayout {
Layout.fillWidth: true

View File

@@ -43,7 +43,7 @@ TimelineContainer {
openSavedFile();
}
onOpenContextMenu: openFileContext(root)
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
function saveFileAs() {
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)

View File

@@ -53,7 +53,7 @@ TimelineContainer {
*/
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
onOpenContextMenu: openFileContext(root)
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
innerObject: Item {
id: imageContainer

View File

@@ -36,7 +36,7 @@ TimelineContainer {
*/
required property bool showLinkPreview
onOpenContextMenu: openMessageContext(label.selectedText)
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, display, label.selectedText)
innerObject: ColumnLayout {
Layout.maximumWidth: root.contentMaxWidth

View File

@@ -589,44 +589,6 @@ ColumnLayout {
return (yoff + height > 0 && yoff < ListView.view.height)
}
Component {
id: messageDelegateContextMenu
MessageDelegateContextMenu {}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
}
/// Open message context dialog for file and videos
function openFileContext(file) {
const contextMenu = fileDelegateContextMenu.createObject(root, {
author: root.author,
eventId: root.eventId,
file: file,
progressInfo: root.progressInfo,
plainText: root.plainText,
htmlText: root.display,
connection: root.connection,
});
contextMenu.open();
}
/// Open context menu for normal message
function openMessageContext(selectedText) {
const contextMenu = messageDelegateContextMenu.createObject(root, {
selectedText: selectedText,
author: root.author,
eventId: root.eventId,
eventType: root.delegateType,
plainText: root.plainText,
htmlText: root.display,
connection: root.connection,
});
contextMenu.open();
}
function setHoverActionsToDelegate() {
if (ListView.view.setHoverActionsToDelegate) {
ListView.view.setHoverActionsToDelegate(root)

View File

@@ -54,7 +54,7 @@ TimelineContainer {
*/
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
onOpenContextMenu: openFileContext(vid)
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
onDownloadedChanged: {
if (downloaded) {

View File

@@ -9,36 +9,40 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/**
* @brief The menu for media messages.
*
* This component just overloads the actions and nested actions of the base menu
* to what is required for a media item.
*
* @sa MessageDelegateContextMenu
*/
MessageDelegateContextMenu {
id: root
signal closeFullscreen
/**
* @brief The MIME type of the media.
*/
property string mimeType
required property var file
/**
* @brief Progress info when downloading files.
*
* @sa Quotient::FileTransferInfo
*/
required property var progressInfo
/**
* @brief The main list of menu item actions.
*
* Each action will be instantiated as a single line in the menu.
*/
property list<Kirigami.Action> actions: [
Kirigami.Action {
text: i18n("Open Externally")
icon.name: "document-open"
onTriggered: {
if (file.downloaded) {
if (!UrlHelper.openUrl(progressInfo.localPath)) {
UrlHelper.openUrl(progressInfo.localDir);
}
} else {
file.onDownloadedChanged.connect(function() {
if (!UrlHelper.openUrl(progressInfo.localPath)) {
UrlHelper.openUrl(progressInfo.localDir);
}
});
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
}
currentRoom.openEventMediaExternally(root.eventId)
}
},
Kirigami.Action {
@@ -56,21 +60,14 @@ MessageDelegateContextMenu {
onTriggered: {
currentRoom.chatBoxReplyId = eventId
currentRoom.chatBoxEditId = ""
root.closeFullscreen()
RoomManager.requestFullScreenClose()
}
},
Kirigami.Action {
text: i18n("Copy")
icon.name: "edit-copy"
onTriggered: {
if(file.downloaded) {
Clipboard.setImage(progressInfo.localPath)
} else {
file.onDownloadedChanged.connect(function() {
Clipboard.setImage(progressInfo.localPath)
});
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
}
currentRoom.copyEventMedia(root.eventId)
}
},
Kirigami.Action {
@@ -99,12 +96,17 @@ MessageDelegateContextMenu {
}
]
/**
* @brief The list of menu item actions that have sub-actions.
*
* Each action will be instantiated as a single line that opens a sub menu.
*/
property list<Kirigami.Action> nestedActions: [
ShareAction {
id: shareAction
inputData: {
'urls': [],
'mimeType': [root.mimeType ? root.mimeType : root.file.mediaINfo.mimeType]
'mimeType': [root.mimeType]
}
property string filename: StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId);
@@ -114,11 +116,12 @@ MessageDelegateContextMenu {
Component.onCompleted: {
shareAction.inputData = {
urls: [filename],
mimeType: [root.mimeType ? root.mimeType : root.file.mediaINfo.mimeType]
mimeType: [root.mimeType]
};
}
}
]
Component {
id: saveAsDialog
FileDialog {

View File

@@ -10,20 +10,82 @@ import org.kde.kirigamiaddons.labs.components 1.0 as KirigamiComponents
import org.kde.neochat 1.0
/**
* @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
required property var author
required property string eventId
property var eventType
property string selectedText: ""
required property string plainText
property string htmlText: undefined
/**
* @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 delegateType
/**
* @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: ""
/**
* @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<Kirigami.Action> nestedActions
/**
* @brief The main list of menu item actions.
*
* Each action will be instantiated as a single line in the menu.
*/
property list<Kirigami.Action> actions: [
Kirigami.Action {
text: i18n("Edit")
@@ -32,7 +94,7 @@ Loader {
currentRoom.chatBoxEditId = eventId;
currentRoom.chatBoxReplyId = "";
}
visible: author.id === root.connection.localUserId && (root.eventType === DelegateType.Emote || root.eventType === DelegateType.Message)
visible: author.isLocalUser && (root.delegateType === DelegateType.Emote || root.delegateType === DelegateType.Message)
},
Kirigami.Action {
text: i18n("Reply")
@@ -53,13 +115,13 @@ Loader {
width: Kirigami.Units.gridUnit * 25
})
page.chosen.connect(function(targetRoomId) {
root.connection.room(targetRoomId).postHtmlMessage(root.plainText, root.htmlText ? root.htmlText : root.plainText)
root.connection.room(targetRoomId).postHtmlMessage(root.plainText, root.htmlText.length > 0 ? root.htmlText : root.plainText)
page.closeDialog()
})
}
},
Kirigami.Action {
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
visible: author.isLocalUser || currentRoom.canSendState("redact")
text: i18n("Remove")
icon.name: "edit-delete-remove"
icon.color: "red"
@@ -71,12 +133,12 @@ Loader {
Kirigami.Action {
text: i18n("Copy")
icon.name: "edit-copy"
onTriggered: Clipboard.saveText(root.selectedText === "" ? root.plainText : root.selectedText)
onTriggered: Clipboard.saveText(root.selectedText.length > 0 ? root.plainText : root.selectedText)
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
visible: author.id !== currentRoom.localUser.id
visible: author.isLocalUser
onTriggered: applicationWindow().pageStack.pushDialogLayer("qrc:/ReportSheet.qml", {room: currentRoom, eventId: eventId}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
@@ -116,12 +178,10 @@ Loader {
icon.name: modelData.icon.name
onTriggered: modelData.trigger()
}
onObjectAdded: {
menuItem.insertItem(0, object)
}
onObjectAdded: (index, object) => {menuItem.insertItem(0, object)}
}
}
onObjectAdded: {
onObjectAdded: (index, object) => {
object.visible = false;
menu.addMenu(object)
}
@@ -130,7 +190,6 @@ Loader {
Repeater {
model: root.actions
QQC2.MenuItem {
id: menuItem
visible: modelData.visible
action: modelData
onClicked: root.item.close();
@@ -147,7 +206,7 @@ Loader {
Instantiator {
model: WebShortcutModel {
id: webshortcutmodel
selectedText: root.selectedText ? root.selectedText : root.plainText
selectedText: root.selectedText.length > 0 ? root.selectedText : root.plainText
onOpenUrl: RoomManager.visitNonMatrix(url)
}
delegate: QQC2.MenuItem {

View File

@@ -208,6 +208,30 @@ Kirigami.Page {
width: Kirigami.Units.gridUnit * 25
});
}
function onShowMessageMenu(eventId, author, delegateType, plainText, htmlText, selectedText) {
const contextMenu = messageDelegateContextMenu.createObject(root, {
selectedText: selectedText,
author: author,
eventId: eventId,
delegateType: delegateType,
plainText: plainText,
htmlText: htmlText
});
contextMenu.open();
}
function onShowFileMenu(eventId, author, delegateType, plainText, mimeType, progressInfo) {
const contextMenu = fileDelegateContextMenu.createObject(root, {
author: author,
eventId: eventId,
delegateType: delegateType,
plainText: plainText,
mimeType: mimeType,
progressInfo: progressInfo
});
contextMenu.open();
}
}
function showUserDetail(user) {
@@ -221,4 +245,18 @@ Kirigami.Page {
id: userDetailDialog
UserDetailDialog {}
}
Component {
id: messageDelegateContextMenu
MessageDelegateContextMenu {
connection: root.connection
}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {
connection: root.connection
}
}
}

View File

@@ -5,6 +5,7 @@
#include "roommanager.h"
#include "controller.h"
#include "enums/delegatetype.h"
#include "models/messageeventmodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
@@ -103,11 +104,34 @@ void RoomManager::maximizeMedia(int index)
Q_EMIT showMaximizedMedia(index);
}
void RoomManager::requestFullScreenClose()
{
Q_EMIT closeFullScreen();
}
void RoomManager::viewEventSource(const QString &eventId)
{
Q_EMIT showEventSource(eventId);
}
void RoomManager::viewEventMenu(const QString &eventId,
const QVariantMap &author,
DelegateType::Type delegateType,
const QString &plainText,
const QString &htmlText,
const QString &selectedText,
const QString &mimeType,
const FileTransferInfo &progressInfo)
{
if (delegateType == DelegateType::Image || delegateType == DelegateType::Video || delegateType == DelegateType::Audio
|| delegateType == DelegateType::File) {
Q_EMIT showFileMenu(eventId, author, delegateType, plainText, mimeType, progressInfo);
return;
}
Q_EMIT showMessageMenu(eventId, author, delegateType, plainText, htmlText, selectedText);
}
bool RoomManager::hasOpenRoom() const
{
return m_currentRoom != nullptr;

View File

@@ -6,9 +6,11 @@
#include <KConfig>
#include <KConfigGroup>
#include <QObject>
#include <Quotient/room.h>
#include <Quotient/uriresolver.h>
#include "chatdocumenthandler.h"
#include "enums/delegatetype.h"
#include "models/mediamessagefiltermodel.h"
#include "models/messageeventmodel.h"
#include "models/messagefiltermodel.h"
@@ -195,11 +197,28 @@ public:
*/
Q_INVOKABLE void maximizeMedia(int index);
/**
* @brief Request that any full screen overlay currently open closes.
*/
Q_INVOKABLE void requestFullScreenClose();
/**
* @brief Show the JSON source for the given event Matrix ID
*/
Q_INVOKABLE void viewEventSource(const QString &eventId);
/**
* @brief Show a conterxt menu for the given event.
*/
Q_INVOKABLE void viewEventMenu(const QString &eventId,
const QVariantMap &author,
DelegateType::Type delegateType,
const QString &plainText,
const QString &htmlText = {},
const QString &selectedText = {},
const QString &mimeType = {},
const FileTransferInfo &progressInfo = {});
/**
* @brief Call this when the current used connection is dropped.
*/
@@ -266,11 +285,36 @@ Q_SIGNALS:
*/
void showMaximizedMedia(int index);
/**
* @brief Request that any full screen overlay closes.
*/
void closeFullScreen();
/**
* @brief Request the JSON source for the given event ID is shown.
*/
void showEventSource(const QString &eventId);
/**
* @brief Request to show a menu for the given event.
*/
void showMessageMenu(const QString &eventId,
const QVariantMap &author,
DelegateType::Type delegateType,
const QString &plainText,
const QString &htmlText,
const QString &selectedText);
/**
* @brief Request to show a menu for the given media event.
*/
void showFileMenu(const QString &eventId,
const QVariantMap &author,
DelegateType::Type delegateType,
const QString &plainText,
const QString &mimeType,
const FileTransferInfo &progressInfo);
/**
* @brief Show the direct chat confirmation dialog.
*