From e0158daf076e1c613313f08bd3f2a90f4623a594 Mon Sep 17 00:00:00 2001 From: Black Hat Date: Sun, 5 Aug 2018 02:49:21 +0800 Subject: [PATCH 1/4] 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, }; From 085601f6503da3ecc38fe1442351c750e1c0c416 Mon Sep 17 00:00:00 2001 From: Black Hat Date: Sun, 5 Aug 2018 04:14:45 +0800 Subject: [PATCH 2/4] Remove context menu Loader && disable highlight resize animation for ListView(#6). --- qml/component/MessageDelegate.qml | 11 ++---- qml/form/RoomListForm.qml | 64 +++++++++++++++---------------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/qml/component/MessageDelegate.qml b/qml/component/MessageDelegate.qml index 3270c16..0ca8ef6 100644 --- a/qml/component/MessageDelegate.qml +++ b/qml/component/MessageDelegate.qml @@ -24,10 +24,7 @@ Item { MouseArea { anchors.fill: parent - onPressAndHold: { - menuLoader.sourceComponent = menuComponent - menuLoader.item.popup() - } + onPressAndHold: menuComponent.createObject(this) Component { id: menuComponent @@ -57,14 +54,12 @@ Item { text: "Redact" onTriggered: currentRoom.redactEvent(eventId) } + + Component.onCompleted: popup() } } } - Loader { - id: menuLoader - } - Loader { id: delegateLoader diff --git a/qml/form/RoomListForm.qml b/qml/form/RoomListForm.qml index d7363bb..7426b14 100644 --- a/qml/form/RoomListForm.qml +++ b/qml/form/RoomListForm.qml @@ -112,7 +112,6 @@ Item { ] } - ListView { id: listView width: parent.width @@ -125,6 +124,7 @@ Item { opacity: 0.2 } highlightMoveDuration: 250 + highlightResizeDuration: 0 currentIndex: -1 @@ -135,16 +135,13 @@ Item { delegate: ItemDelegate { width: parent.width height: 80 - onClicked: listView.currentIndex = index - onPressAndHold: { - roomListMenu.roomIndex = index - roomListMenu.popup() - } + onPressed: listView.currentIndex = index + onPressAndHold: menuComponent.createObject(this) ToolTip.visible: mini && hovered ToolTip.text: name - contentItem: RowLayout { + contentItem: RowLayout { anchors.fill: parent anchors.margins: 16 spacing: 16 @@ -184,6 +181,33 @@ Item { } } } + + Component { + id: menuComponent + Menu { + id: roomListMenu + + MenuItem { + text: "Favourite" + checkable: true + checked: currentRoom.isFavourite + onTriggered: currentRoom.isFavourite ? currentRoom.removeTag("m.favourite") : currentRoom.addTag("m.favourite", "1") + } + MenuItem { + text: "Deprioritize" + checkable: true + checked: currentRoom.isLowPriority + onTriggered: currentRoom.isLowPriority ? currentRoom.removeTag("m.lowpriority") : currentRoom.addTag("m.lowpriority", "1") + } + MenuSeparator {} + MenuItem { + text: "Leave Room" + onTriggered: matriqueController.forgetRoom(currentRoom.id) + } + + Component.onCompleted: popup() + } + } } section.property: "category" @@ -202,32 +226,6 @@ Item { color: Material.theme == Material.Light ? "#dbdbdb" : "#363636" } } - - Menu { - property int roomIndex: -1 - readonly property int roomProxyIndex: roomListProxyModel.mapToSource(roomIndex) - readonly property var room: roomProxyIndex != -1 ? listModel.roomAt(roomProxyIndex) : null - - id: roomListMenu - - MenuItem { - 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" - 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) - } - } } } } From 2b016131615438649a8715c0fc9e977ad795c931 Mon Sep 17 00:00:00 2001 From: Black Hat Date: Sun, 5 Aug 2018 04:23:49 +0800 Subject: [PATCH 3/4] Add "Copy source". --- qml/component/MessageDelegate.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qml/component/MessageDelegate.qml b/qml/component/MessageDelegate.qml index 0ca8ef6..defc0e5 100644 --- a/qml/component/MessageDelegate.qml +++ b/qml/component/MessageDelegate.qml @@ -35,7 +35,10 @@ Item { text: "Copy" onTriggered: matriqueController.copyToClipboard(plainText) } - + MenuItem { + text: "Copy Source" + onTriggered: matriqueController.copyToClipboard(toolTip) + } MenuItem { visible: isFile height: visible ? undefined : 0 From 5943a32a4b78ddc9594d5a15b73f9d2bea62bb9e Mon Sep 17 00:00:00 2001 From: Black Hat Date: Sun, 5 Aug 2018 14:08:04 +0800 Subject: [PATCH 4/4] Move section delegate to header. Switch from builtin section delegate to custom section delegate. Fixes #4. --- qml/form/RoomForm.qml | 44 +++++++++++++++++++++++++-------------- qml/form/RoomListForm.qml | 1 + 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/qml/form/RoomForm.qml b/qml/form/RoomForm.qml index 3c672fc..6e041fb 100644 --- a/qml/form/RoomForm.qml +++ b/qml/form/RoomForm.qml @@ -101,26 +101,38 @@ Item { room: currentRoom } - delegate: MessageDelegate {} + delegate: Column { + width: parent.width + spacing: 8 - section.property: "section" - section.criteria: ViewSection.FullString - section.delegate: RowLayout { - width: parent.width * 0.6 - anchors.right: parent.right + RowLayout { + readonly property bool sectionVisible: section !== aboveSection - Rectangle { - Layout.fillWidth: true - height:2 - color: Material.accent + width: parent.width * 0.8 + visible: sectionVisible + anchors.horizontalCenter: parent.horizontalCenter + spacing: 8 + + Rectangle { + Layout.fillWidth: true + height:2 + color: Material.accent + } + + Label { + text: section + color: Material.accent + verticalAlignment: Text.AlignVCenter + } + + Rectangle { + Layout.fillWidth: true + height:2 + color: Material.accent + } } - Label { - padding: 4 - text: section - color: Material.accent - verticalAlignment: Text.AlignVCenter - } + MessageDelegate {} } ScrollBar.vertical: messageListViewScrollBar diff --git a/qml/form/RoomListForm.qml b/qml/form/RoomListForm.qml index 7426b14..7f89764 100644 --- a/qml/form/RoomListForm.qml +++ b/qml/form/RoomListForm.qml @@ -31,6 +31,7 @@ Item { id: searchField width: parent.width height: 36 + color: "black" leftPadding: mini ? 4 : 16 topPadding: 0 bottomPadding: 0