From 2b3dc3d8b9d1108c3f6ad226ad65060bd3999033 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 11 Jan 2020 14:07:51 +0100 Subject: [PATCH] Implement fancy reply rendering This currently assumes the event, that is replied to, is already fetched. If it isn't, it will render an empty reply. In the future we should fetch replies before rendering them. --- resources/qml/TimelineRow.qml | 61 ++++++++- resources/qml/delegates/FileMessage.qml | 6 +- resources/qml/delegates/ImageMessage.qml | 12 +- resources/qml/delegates/MessageDelegate.qml | 128 ++++++++++-------- resources/qml/delegates/NoticeMessage.qml | 2 +- resources/qml/delegates/Placeholder.qml | 2 +- .../qml/delegates/PlayableMediaMessage.qml | 16 +-- resources/qml/delegates/TextMessage.qml | 2 +- src/timeline/TimelineModel.cpp | 16 ++- src/timeline/TimelineModel.h | 1 + 10 files changed, 159 insertions(+), 87 deletions(-) diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 2c2ed02a..86780413 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -14,23 +14,70 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right - height: Math.max(contentItem.height, 16) + //height: Math.max(model.replyTo ? reply.height + contentItem.height + 4 : contentItem.height, 16) Column { Layout.fillWidth: true Layout.alignment: Qt.AlignTop + spacing: 4 - //property var replyTo: model.replyTo + // fancy reply, if this is a reply + Rectangle { + visible: model.replyTo + width: parent.width + height: replyContainer.height - //Text { - // property int idx: timelineManager.timeline.idToIndex(replyTo) - // text: "" + (idx != -1 ? timelineManager.timeline.data(timelineManager.timeline.index(idx, 0), 2) : "nothing") - //} + Rectangle { + id: colorLine + height: replyContainer.height + width: 4 + color: chat.model.userColor(reply.modelData.userId, colors.window) + } + + Column { + id: replyContainer + anchors.left: colorLine.right + anchors.leftMargin: 4 + width: parent.width - 8 + + + Text { + id: userName + text: chat.model.escapeEmoji(reply.modelData.userName) + color: chat.model.userColor(reply.modelData.userId, colors.window) + textFormat: Text.RichText + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(reply.modelData.userId) + cursorShape: Qt.PointingHandCursor + } + } + + MessageDelegate { + id: reply + width: parent.width + + modelData: chat.model.getDump(model.replyTo) + } + } + + color: { var col = chat.model.userColor(reply.modelData.userId, colors.window); col.a = 0.2; return col } + + MouseArea { + anchors.fill: parent + onClicked: chat.positionViewAtIndex(chat.model.idToIndex(model.replyTo), ListView.Contain) + cursorShape: Qt.PointingHandCursor + } + } + + // actual message content MessageDelegate { id: contentItem width: parent.width - height: childrenRect.height + + modelData: model } } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 2c911c5e..9a5300bb 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -31,7 +31,7 @@ Rectangle { } MouseArea { anchors.fill: parent - onClicked: timelineManager.timeline.saveMedia(model.id) + onClicked: timelineManager.timeline.saveMedia(model.data.id) cursorShape: Qt.PointingHandCursor } } @@ -40,14 +40,14 @@ Rectangle { Text { Layout.fillWidth: true - text: model.body + text: model.data.body textFormat: Text.PlainText elide: Text.ElideRight color: colors.text } Text { Layout.fillWidth: true - text: model.filesize + text: model.data.filesize textFormat: Text.PlainText elide: Text.ElideRight color: colors.text diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 15ce29b7..3393f043 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -3,26 +3,26 @@ import QtQuick 2.6 import im.nheko 1.0 Item { - property double tempWidth: Math.min(parent ? parent.width : undefined, model.width) - property double tempHeight: tempWidth * model.proportionalHeight + property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width) + property double tempHeight: tempWidth * model.data.proportionalHeight property bool tooHigh: tempHeight > chat.height - 40 height: tooHigh ? chat.height - 40 : tempHeight - width: tooHigh ? (chat.height - 40) / model.proportionalHeight : tempWidth + width: tooHigh ? (chat.height - 40) / model.data.proportionalHeight : tempWidth Image { id: img anchors.fill: parent - source: model.url.replace("mxc://", "image://MxcImage/") + source: model.data.url.replace("mxc://", "image://MxcImage/") asynchronous: true fillMode: Image.PreserveAspectFit MouseArea { - enabled: model.type == MtxEvent.ImageMessage + enabled: model.data.type == MtxEvent.ImageMessage anchors.fill: parent - onClicked: timelineManager.openImageOverlay(model.url, model.id) + onClicked: timelineManager.openImageOverlay(model.data.url, model.data.id) } } } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 20ec71e5..1716d2d4 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -1,67 +1,81 @@ import QtQuick 2.6 import im.nheko 1.0 -DelegateChooser { - //role: "type" //< not supported in our custom implementation, have to use roleValue - roleValue: model.type +Item { + // Workaround to have an assignable global property + Item { + id: model + property var data; + } + + property alias modelData: model.data - DelegateChoice { - roleValue: MtxEvent.TextMessage - TextMessage {} - } - DelegateChoice { - roleValue: MtxEvent.NoticeMessage - NoticeMessage {} - } - DelegateChoice { - roleValue: MtxEvent.EmoteMessage - TextMessage {} - } - DelegateChoice { - roleValue: MtxEvent.ImageMessage - ImageMessage {} - } - DelegateChoice { - roleValue: MtxEvent.Sticker - ImageMessage {} - } - DelegateChoice { - roleValue: MtxEvent.FileMessage - FileMessage {} - } - DelegateChoice { - roleValue: MtxEvent.VideoMessage - PlayableMediaMessage {} - } - DelegateChoice { - roleValue: MtxEvent.AudioMessage - PlayableMediaMessage {} - } - DelegateChoice { - roleValue: MtxEvent.Redacted - Pill { - text: qsTr("redacted") + height: chooser.childrenRect.height + + DelegateChooser { + id: chooser + //role: "type" //< not supported in our custom implementation, have to use roleValue + roleValue: model.data.type + anchors.fill: parent + + DelegateChoice { + roleValue: MtxEvent.TextMessage + TextMessage {} } - } - DelegateChoice { - roleValue: MtxEvent.Encryption - Pill { - text: qsTr("Encryption enabled") + DelegateChoice { + roleValue: MtxEvent.NoticeMessage + NoticeMessage {} } - } - DelegateChoice { - roleValue: MtxEvent.Name - NoticeMessage { - notice: model.roomName ? qsTr("room name changed to: %1").arg(model.roomName) : qsTr("removed room name") + DelegateChoice { + roleValue: MtxEvent.EmoteMessage + TextMessage {} } - } - DelegateChoice { - roleValue: MtxEvent.Topic - NoticeMessage { - notice: model.roomTopic ? qsTr("topic changed to: %1").arg(model.roomTopic) : qsTr("removed topic") + DelegateChoice { + roleValue: MtxEvent.ImageMessage + ImageMessage {} + } + DelegateChoice { + roleValue: MtxEvent.Sticker + ImageMessage {} + } + DelegateChoice { + roleValue: MtxEvent.FileMessage + FileMessage {} + } + DelegateChoice { + roleValue: MtxEvent.VideoMessage + PlayableMediaMessage {} + } + DelegateChoice { + roleValue: MtxEvent.AudioMessage + PlayableMediaMessage {} + } + DelegateChoice { + roleValue: MtxEvent.Redacted + Pill { + text: qsTr("redacted") + } + } + DelegateChoice { + roleValue: MtxEvent.Encryption + Pill { + text: qsTr("Encryption enabled") + } + } + DelegateChoice { + roleValue: MtxEvent.Name + NoticeMessage { + notice: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name") + } + } + DelegateChoice { + roleValue: MtxEvent.Topic + NoticeMessage { + notice: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic") + } + } + DelegateChoice { + Placeholder {} } - } - DelegateChoice { - Placeholder {} } } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index f7467eca..34132bcf 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -1,7 +1,7 @@ import ".." MatrixText { - property string notice: model.formattedBody.replace("
", "
")
+	property string notice: model.data.formattedBody.replace("
", "
")
 	text: notice
 	width: parent ? parent.width : undefined
 	font.italic: true
diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml
index 4c0e68c3..36d7b2bc 100644
--- a/resources/qml/delegates/Placeholder.qml
+++ b/resources/qml/delegates/Placeholder.qml
@@ -1,7 +1,7 @@
 import ".."
 
 MatrixText {
-	text: qsTr("unimplemented event: ") + model.type
+	text: qsTr("unimplemented event: ") + model.data.type
 	width: parent ? parent.width : undefined
 	color: inactiveColors.text
 }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index b3275462..ebf7487c 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -19,12 +19,12 @@ Rectangle {
 
 		Rectangle {
 			id: videoContainer
-			visible: model.type == MtxEvent.VideoMessage
-			width: Math.min(parent.width, model.width ? model.width : 400) // some media has 0 as size...
-			height: width*model.proportionalHeight
+			visible: model.data.type == MtxEvent.VideoMessage
+			width: Math.min(parent.width, model.data.width ? model.data.width : 400) // some media has 0 as size...
+			height: width*model.data.proportionalHeight
 			Image {
 				anchors.fill: parent
-				source: model.thumbnailUrl.replace("mxc://", "image://MxcImage/")
+				source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/")
 				asynchronous: true
 				fillMode: Image.PreserveAspectFit
 
@@ -97,7 +97,7 @@ Rectangle {
 					anchors.fill: parent
 					onClicked: {
 						switch (button.state) {
-							case "": timelineManager.timeline.cacheMedia(model.id); break;
+							case "": timelineManager.timeline.cacheMedia(model.data.id); break;
 							case "stopped":
 							media.play(); console.log("play");
 							button.state = "playing"
@@ -120,7 +120,7 @@ Rectangle {
 				Connections {
 					target: timelineManager.timeline
 					onMediaCached: {
-						if (mxcUrl == model.url) {
+						if (mxcUrl == model.data.url) {
 							media.source = "file://" + cacheUrl
 							button.state = "stopped"
 							console.log("media loaded: " + mxcUrl + " at " + cacheUrl)
@@ -145,14 +145,14 @@ Rectangle {
 
 				Text {
 					Layout.fillWidth: true
-					text: model.body
+					text: model.data.body
 					textFormat: Text.PlainText
 					elide: Text.ElideRight
 					color: colors.text
 				}
 				Text {
 					Layout.fillWidth: true
-					text: model.filesize
+					text: model.data.filesize
 					textFormat: Text.PlainText
 					elide: Text.ElideRight
 					color: colors.text
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index f984b32f..92ba560b 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -1,6 +1,6 @@
 import ".."
 
 MatrixText {
-	text: model.formattedBody.replace("
", "
")
+	text: model.data.formattedBody.replace("
", "
")
 	width: parent ? parent.width : undefined
 }
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 0e7f5259..41d864bd 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -212,6 +212,14 @@ TimelineModel::rowCount(const QModelIndex &parent) const
         return (int)this->eventOrder.size();
 }
 
+QVariantMap
+TimelineModel::getDump(QString eventId) const
+{
+        if (events.contains(eventId))
+                return data(index(idToIndex(eventId), 0), Dump).toMap();
+        return {};
+}
+
 QVariant
 TimelineModel::data(const QModelIndex &index, int role) const
 {
@@ -263,11 +271,13 @@ TimelineModel::data(const QModelIndex &index, int role) const
                 return QVariant(toRoomEventType(event));
         case Body:
                 return QVariant(utils::replaceEmoji(QString::fromStdString(body(event))));
-        case FormattedBody:
+        case FormattedBody: {
+                const static QRegularExpression replyFallback(
+                  ".*", QRegularExpression::DotMatchesEverythingOption);
                 return QVariant(
                   utils::replaceEmoji(utils::linkifyMessage(formattedBodyWithFallback(event)))
-                    .remove("")
-                    .remove(""));
+                    .remove(replyFallback));
+        }
         case Url:
                 return QVariant(QString::fromStdString(url(event)));
         case ThumbnailUrl:
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 0f18f7ef..61dd6b69 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -181,6 +181,7 @@ public slots:
         void setCurrentIndex(int index);
         int currentIndex() const { return idToIndex(currentId); }
         void markEventsAsRead(const std::vector &event_ids);
+        QVariantMap getDump(QString eventId) const;
 
 private slots:
         // Add old events at the top of the timeline.