From e0158daf076e1c613313f08bd3f2a90f4623a594 Mon Sep 17 00:00:00 2001 From: Black Hat Date: Sun, 5 Aug 2018 02:49:21 +0800 Subject: [PATCH] Add universal context menu for message bubbles. Also some minor changes. --- qml/component/DownloadableContent.qml | 1 - qml/component/ImageBubble.qml | 17 +---- qml/component/MessageDelegate.qml | 42 +++++++++-- qml/form/RoomListForm.qml | 9 ++- src/controller.cpp | 5 ++ src/controller.h | 7 +- src/messageeventmodel.cpp | 101 ++++++++++++++++++++++++++ src/messageeventmodel.h | 1 + 8 files changed, 159 insertions(+), 24 deletions(-) diff --git a/qml/component/DownloadableContent.qml b/qml/component/DownloadableContent.qml index 71200ec..495f22b 100644 --- a/qml/component/DownloadableContent.qml +++ b/qml/component/DownloadableContent.qml @@ -22,7 +22,6 @@ Item { selectFolder: true onAccepted: currentRoom.downloadFile(eventId, folder + "/" + currentRoom.fileNameToDownload(eventId)) - } onDownloadedChanged: downloaded && openOnFinished ? openSavedFile() : {} diff --git a/qml/component/ImageBubble.qml b/qml/component/ImageBubble.qml index 9b0b4ec..5fd8868 100644 --- a/qml/component/ImageBubble.qml +++ b/qml/component/ImageBubble.qml @@ -3,6 +3,9 @@ import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.2 AvatarContainer { + readonly property var downloadAndOpen: downloadable.downloadAndOpen + readonly property var saveFileAs: downloadable.saveFileAs + Rectangle { id: messageRect @@ -33,20 +36,6 @@ AvatarContainer { ToolTip.text: content.body onClicked: downloadable.downloadAndOpen() - onPressAndHold: messageImageMenu.popup() - } - - Menu { - id: messageImageMenu - - MenuItem { - text: "View" - onTriggered: downloadable.downloadAndOpen() - } - MenuItem { - text: "Save as..." - onTriggered: downloadable.saveFileAs() - } } } } diff --git a/qml/component/MessageDelegate.qml b/qml/component/MessageDelegate.qml index e77210a..3270c16 100644 --- a/qml/component/MessageDelegate.qml +++ b/qml/component/MessageDelegate.qml @@ -24,17 +24,47 @@ Item { MouseArea { anchors.fill: parent - onPressAndHold: messageContextMenu.popup() + onPressAndHold: { + menuLoader.sourceComponent = menuComponent + menuLoader.item.popup() + } - Menu { - id: messageContextMenu - MenuItem { - text: "Redact" - onTriggered: currentRoom.redactEvent(eventId) + Component { + id: menuComponent + Menu { + id: messageContextMenu + + MenuItem { + text: "Copy" + onTriggered: matriqueController.copyToClipboard(plainText) + } + + MenuItem { + visible: isFile + height: visible ? undefined : 0 + text: "Open Externally" + onTriggered: delegateLoader.item.downloadAndOpen() + } + MenuItem { + visible: isFile + height: visible ? undefined : 0 + text: "Save As" + onTriggered: delegateLoader.item.saveFileAs() + } + MenuItem { + visible: sentByMe + height: visible ? undefined : 0 + text: "Redact" + onTriggered: currentRoom.redactEvent(eventId) + } } } } + Loader { + id: menuLoader + } + Loader { id: delegateLoader diff --git a/qml/form/RoomListForm.qml b/qml/form/RoomListForm.qml index bb4b76f..d7363bb 100644 --- a/qml/form/RoomListForm.qml +++ b/qml/form/RoomListForm.qml @@ -211,13 +211,18 @@ Item { id: roomListMenu MenuItem { - text: "Favourite" + (roomListMenu.room && roomListMenu.room.isFavourite ? " \u2713" : "") + text: "Favourite" + checkable: true + checked: roomListMenu.room && roomListMenu.room.isFavourite onTriggered: roomListMenu.room.isFavourite ? roomListMenu.room.removeTag("m.favourite") : roomListMenu.room.addTag("m.favourite", "1") } MenuItem { - text: "Deprioritize" + (roomListMenu.room && roomListMenu.room.isLowPriority ? " \u2713" : "") + text: "Deprioritize" + checkable: true + checked: roomListMenu.room && roomListMenu.room.isLowPriority onTriggered: roomListMenu.room.isLowPriority ? roomListMenu.room.removeTag("m.lowpriority") : roomListMenu.room.addTag("m.lowpriority", "1") } + MenuSeparator {} MenuItem { text: "Leave Room" onTriggered: matriqueController.forgetRoom(roomListMenu.room.id) diff --git a/src/controller.cpp b/src/controller.cpp index 87aab58..7e0b472 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -8,6 +8,7 @@ #include "csapi/joining.h" #include "csapi/leaving.h" +#include #include #include #include @@ -120,3 +121,7 @@ void Controller::createRoom(const QString& name, const QString& topic) { void Controller::createDirectChat(const QString& userID) { m_connection->requestDirectChat(userID); } + +void Controller::copyToClipboard(const QString& text) { + m_clipboard->setText(text); +} diff --git a/src/controller.h b/src/controller.h index 563c010..1d78006 100644 --- a/src/controller.h +++ b/src/controller.h @@ -1,11 +1,13 @@ #ifndef CONTROLLER_H #define CONTROLLER_H -#include #include "connection.h" #include "roomlistmodel.h" #include "user.h" +#include +#include + using namespace QMatrixClient; class Controller : public QObject { @@ -81,6 +83,8 @@ class Controller : public QObject { } private: + QClipboard* m_clipboard = QApplication::clipboard(); + void connected(); void resync(); void reconnect(); @@ -101,6 +105,7 @@ class Controller : public QObject { void joinRoom(const QString& alias); void createRoom(const QString& name, const QString& topic); void createDirectChat(const QString& userID); + void copyToClipboard(const QString& text); }; #endif // CONTROLLER_H diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp index 8316cd0..22f813c 100644 --- a/src/messageeventmodel.cpp +++ b/src/messageeventmodel.cpp @@ -28,6 +28,7 @@ QHash MessageEventModel::roleNames() const { roles[SpecialMarksRole] = "marks"; roles[LongOperationRole] = "progressInfo"; roles[EventResolvedTypeRole] = "eventResolvedType"; + roles[PlainTextRole] = "plainText"; return roles; } @@ -298,6 +299,106 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const { tr("Unknown Event")); } + if (role == PlainTextRole) { + if (evt.isRedacted()) { + auto reason = evt.redactedBecause()->reason(); + if (reason.isEmpty()) return tr("Redacted"); + + return tr("Redacted: %1").arg(evt.redactedBecause()->reason()); + } + + return visit( + evt, + [this](const RoomMessageEvent& e) { + using namespace MessageEventContent; + + if (e.hasFileContent()) { + auto fileCaption = e.content()->fileInfo()->originalName; + if (fileCaption.isEmpty()) + fileCaption = e.plainBody(); + if (fileCaption.isEmpty()) return tr("a file"); + } + return e.plainBody(); + }, + [this](const RoomMemberEvent& e) { + // FIXME: Rewind to the name that was at the time of this event + QString subjectName = m_currentRoom->roomMembername(e.userId()); + // The below code assumes senderName output in AuthorRole + switch (e.membership()) { + case MembershipType::Invite: + if (e.repeatsState()) + return tr("reinvited %1 to the room").arg(subjectName); + FALLTHROUGH; + case MembershipType::Join: { + if (e.repeatsState()) return tr("joined the room (repeated)"); + if (!e.prevContent() || + e.membership() != e.prevContent()->membership) { + return e.membership() == MembershipType::Invite + ? tr("invited %1 to the room").arg(subjectName) + : tr("joined the room"); + } + QString text{}; + if (e.displayName() != e.prevContent()->displayName) { + if (e.displayName().isEmpty()) + text = tr("cleared the display name"); + else + text = + tr("changed the display name to %1").arg(e.displayName()); + } + if (e.avatarUrl() != e.prevContent()->avatarUrl) { + if (!text.isEmpty()) text += " and "; + if (e.avatarUrl().isEmpty()) + text += tr("cleared the avatar"); + else + text += tr("updated the avatar"); + } + return text; + } + case MembershipType::Leave: + if (e.prevContent() && + e.prevContent()->membership == MembershipType::Ban) { + return (e.senderId() != e.userId()) + ? tr("unbanned %1").arg(subjectName) + : tr("self-unbanned"); + } + return (e.senderId() != e.userId()) + ? tr("has put %1 out of the room").arg(subjectName) + : tr("left the room"); + case MembershipType::Ban: + return (e.senderId() != e.userId()) + ? tr("banned %1 from the room").arg(subjectName) + : tr("self-banned from the room"); + case MembershipType::Knock: + return tr("knocked"); + default:; + } + return tr("made something unknown"); + }, + [](const RoomAliasesEvent& e) { + return tr("set aliases to: %1").arg(e.aliases().join(", ")); + }, + [](const RoomCanonicalAliasEvent& e) { + return (e.alias().isEmpty()) + ? tr("cleared the room main alias") + : tr("set the room main alias to: %1").arg(e.alias()); + }, + [](const RoomNameEvent& e) { + return (e.name().isEmpty()) + ? tr("cleared the room name") + : tr("set the room name to: %1").arg(e.name()); + }, + [](const RoomTopicEvent& e) { + return (e.topic().isEmpty()) + ? tr("cleared the topic") + : tr("set the topic to: %1").arg(e.topic()); + }, + [](const RoomAvatarEvent&) { return tr("changed the room avatar"); }, + [](const EncryptionEvent&) { + return tr("activated End-to-End Encryption"); + }, + tr("Unknown Event")); + } + if (role == Qt::ToolTipRole) { return evt.originalJson(); } diff --git a/src/messageeventmodel.h b/src/messageeventmodel.h index 1e83fb4..c79a6c8 100644 --- a/src/messageeventmodel.h +++ b/src/messageeventmodel.h @@ -25,6 +25,7 @@ class MessageEventModel : public QAbstractListModel { ReadMarkerRole, SpecialMarksRole, LongOperationRole, + PlainTextRole, // For debugging EventResolvedTypeRole, };