From a8166462adc8ffd8d6c5d2a9a50e5cde5c810588 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 4 Oct 2019 01:10:46 +0200 Subject: File messages (qml) --- resources/qml/delegates/FileMessage.qml | 57 +++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 resources/qml/delegates/FileMessage.qml (limited to 'resources/qml/delegates/FileMessage.qml') diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml new file mode 100644 index 00000000..3099acaa --- /dev/null +++ b/resources/qml/delegates/FileMessage.qml @@ -0,0 +1,57 @@ +import QtQuick 2.6 + +Row { +Rectangle { + radius: 10 + color: colors.dark + height: row.height + width: row.width + + Row { + id: row + + spacing: 15 + padding: 12 + + Rectangle { + color: colors.light + radius: 22 + height: 44 + width: 44 + Image { + id: img + anchors.centerIn: parent + + source: "qrc:/icons/icons/ui/arrow-pointing-down.png" + fillMode: Image.Pad + + } + MouseArea { + anchors.fill: parent + onClicked: timelineManager.saveMedia(eventData.url, eventData.filename, eventData.mimetype, eventData.type) + cursorShape: Qt.PointingHandCursor + } + } + Column { + TextEdit { + text: eventData.body + textFormat: TextEdit.PlainText + readOnly: true + wrapMode: Text.Wrap + selectByMouse: true + color: colors.text + } + TextEdit { + text: eventData.filesize + textFormat: TextEdit.PlainText + readOnly: true + wrapMode: Text.Wrap + selectByMouse: true + color: colors.text + } + } + } +} +Rectangle { +} +} -- cgit 1.5.1 From ea98d7b2aed5d5085d0ce1833568ec93ff813b0f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 5 Oct 2019 23:11:20 +0200 Subject: Add simple audio message widget --- resources/qml/TimelineView.qml | 1 + resources/qml/delegates/AudioMessage.qml | 98 ++++++++++++++++++++++++++++++++ resources/qml/delegates/FileMessage.qml | 38 ++++++------- resources/res.qrc | 1 + src/timeline2/TimelineViewManager.cpp | 59 +++++++++++++++++++ src/timeline2/TimelineViewManager.h | 2 + 6 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 resources/qml/delegates/AudioMessage.qml (limited to 'resources/qml/delegates/FileMessage.qml') diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index c4750ddf..c25e6543 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -113,6 +113,7 @@ Rectangle { case MtxEvent.ImageMessage: return "delegates/ImageMessage.qml" case MtxEvent.FileMessage: return "delegates/FileMessage.qml" //case MtxEvent.VideoMessage: return "delegates/VideoMessage.qml" + case MtxEvent.AudioMessage: return "delegates/AudioMessage.qml" case MtxEvent.Redacted: return "delegates/Redacted.qml" default: return "delegates/placeholder.qml" } diff --git a/resources/qml/delegates/AudioMessage.qml b/resources/qml/delegates/AudioMessage.qml new file mode 100644 index 00000000..f36d22b9 --- /dev/null +++ b/resources/qml/delegates/AudioMessage.qml @@ -0,0 +1,98 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.6 +import QtMultimedia 5.12 + +Rectangle { + radius: 10 + color: colors.dark + height: row.height + 24 + width: parent.width + + RowLayout { + id: row + + anchors.centerIn: parent + width: parent.width - 24 + + spacing: 15 + + Rectangle { + id: button + color: colors.light + radius: 22 + height: 44 + width: 44 + Image { + id: img + anchors.centerIn: parent + + source: "qrc:/icons/icons/ui/arrow-pointing-down.png" + fillMode: Image.Pad + + } + MouseArea { + anchors.fill: parent + onClicked: { + switch (button.state) { + case "": timelineManager.cacheMedia(eventData.url, eventData.mimetype); break; + case "stopped": + audio.play(); console.log("play"); + button.state = "playing" + break + case "playing": + audio.pause(); console.log("pause"); + button.state = "stopped" + break + } + } + cursorShape: Qt.PointingHandCursor + } + MediaPlayer { + id: audio + onError: console.log(errorString) + } + + Connections { + target: timelineManager + onMediaCached: { + if (mxcUrl == eventData.url) { + audio.source = "file://" + cacheUrl + button.state = "stopped" + console.log("media loaded: " + mxcUrl + " at " + cacheUrl) + } + console.log("media cached: " + mxcUrl + " at " + cacheUrl) + } + } + + states: [ + State { + name: "stopped" + PropertyChanges { target: img; source: "qrc:/icons/icons/ui/play-sign.png" } + }, + State { + name: "playing" + PropertyChanges { target: img; source: "qrc:/icons/icons/ui/pause-symbol.png" } + } + ] + } + ColumnLayout { + id: col + + Text { + Layout.fillWidth: true + text: eventData.body + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + Text { + Layout.fillWidth: true + text: eventData.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + } + } +} + diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 3099acaa..27cd6403 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -1,19 +1,22 @@ import QtQuick 2.6 +import QtQuick.Layouts 1.6 -Row { Rectangle { radius: 10 color: colors.dark - height: row.height - width: row.width + height: row.height + 24 + width: parent.width - Row { + RowLayout { id: row + anchors.centerIn: parent + width: parent.width - 24 + spacing: 15 - padding: 12 Rectangle { + id: button color: colors.light radius: 22 height: 44 @@ -32,26 +35,23 @@ Rectangle { cursorShape: Qt.PointingHandCursor } } - Column { - TextEdit { + ColumnLayout { + id: col + + Text { + Layout.fillWidth: true text: eventData.body - textFormat: TextEdit.PlainText - readOnly: true - wrapMode: Text.Wrap - selectByMouse: true + textFormat: Text.PlainText + elide: Text.ElideRight color: colors.text } - TextEdit { + Text { + Layout.fillWidth: true text: eventData.filesize - textFormat: TextEdit.PlainText - readOnly: true - wrapMode: Text.Wrap - selectByMouse: true + textFormat: Text.PlainText + elide: Text.ElideRight color: colors.text } } } } -Rectangle { -} -} diff --git a/resources/res.qrc b/resources/res.qrc index c865200c..1caf378e 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -122,6 +122,7 @@ qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml + qml/delegates/AudioMessage.qml qml/delegates/FileMessage.qml qml/delegates/Redacted.qml qml/delegates/placeholder.qml diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 74e851c4..29c52ac9 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "Logging.h" #include "MxcImageProvider.h" @@ -143,6 +144,64 @@ TimelineViewManager::saveMedia(QString mxcUrl, }); } +void +TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) +{ + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString(mxcUrl).remove("mxc://")) + .arg(suffix)); + if (QDir::cleanPath(filename.path()) != filename.path()) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } + + QDir().mkpath(filename.path()); + + if (filename.isReadable()) { + emit mediaCached(mxcUrl, filename.filePath()); + return; + } + + http::client()->download( + url, + [this, mxcUrl, filename, url](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(data.data(), data.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + + emit mediaCached(mxcUrl, filename.filePath()); + }); +} + void TimelineViewManager::updateReadReceipts(const QString &room_id, const std::vector &event_ids) diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h index a8fcf7ce..6a6d3c6b 100644 --- a/src/timeline2/TimelineViewManager.h +++ b/src/timeline2/TimelineViewManager.h @@ -42,6 +42,7 @@ public: QString originalFilename, QString mimeType, qml_mtx_events::EventType eventType) const; + Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType); // Qml can only pass enum as int Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString originalFilename, @@ -63,6 +64,7 @@ signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); + void mediaCached(QString mxcUrl, QString cacheUrl); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); -- cgit 1.5.1 From 489165d579d6b32d902427a5744971d97ea5be03 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 6 Oct 2019 12:12:29 +0200 Subject: Lower requirement on QtQuick.Layouts version --- resources/qml/TimelineView.qml | 2 +- resources/qml/delegates/FileMessage.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'resources/qml/delegates/FileMessage.qml') diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 65933124..4782c1f1 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -1,6 +1,6 @@ import QtQuick 2.6 import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.5 +import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 27cd6403..6dd552ab 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -1,5 +1,5 @@ import QtQuick 2.6 -import QtQuick.Layouts 1.6 +import QtQuick.Layouts 1.2 Rectangle { radius: 10 diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 645ca102..5a5a2162 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -1,5 +1,5 @@ import QtQuick 2.6 -import QtQuick.Layouts 1.6 +import QtQuick.Layouts 1.2 import QtQuick.Controls 2.1 import QtMultimedia 5.6 -- cgit 1.5.1 From b9076c5c4d1beb7ff4cb4a2db8e6eb4e7f5b0dcd Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 8 Oct 2019 20:55:09 +0200 Subject: Try out DelegateChooser requires Qt5.12+ --- resources/qml/TimelineView.qml | 176 +++++------------------ resources/qml/delegates/FileMessage.qml | 6 +- resources/qml/delegates/ImageMessage.qml | 8 +- resources/qml/delegates/NoticeMessage.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 10 +- resources/qml/delegates/TextMessage.qml | 2 +- resources/qml/delegates/TimelineRow.qml | 139 ++++++++++++++++++ resources/qml/delegates/placeholder.qml | 2 +- resources/res.qrc | 1 + 9 files changed, 189 insertions(+), 157 deletions(-) create mode 100644 resources/qml/delegates/TimelineRow.qml (limited to 'resources/qml/delegates/FileMessage.qml') diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 4782c1f1..0642b13a 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -3,9 +3,12 @@ import QtQuick.Controls 2.1 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 +import Qt.labs.qmlmodels 1.0 import com.github.nheko 1.0 +import "./delegates" + Rectangle { anchors.fill: parent @@ -77,158 +80,47 @@ Rectangle { onMovementEnded: updatePosition() spacing: 4 - delegate: RowLayout { - anchors.leftMargin: avatarSize + 4 - anchors.left: parent.left - anchors.right: parent.right - anchors.rightMargin: scrollbar.width - - function isFullyVisible() { - return (y - chat.contentY - 1) + height < chat.height + delegate: DelegateChooser { + role: "type" + DelegateChoice { + roleValue: MtxEvent.TextMessage + TimelineRow { view: chat; TextMessage { id: kid } } } - function getIndex() { - return index; + DelegateChoice { + roleValue: MtxEvent.NoticeMessage + TimelineRow { view: chat; NoticeMessage { id: kid } } } - - Loader { - id: loader - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - height: item.height - - source: switch(model.type) { - //case MtxEvent.Aliases: return "delegates/Aliases.qml" - //case MtxEvent.Avatar: return "delegates/Avatar.qml" - //case MtxEvent.CanonicalAlias: return "delegates/CanonicalAlias.qml" - //case MtxEvent.Create: return "delegates/Create.qml" - //case MtxEvent.GuestAccess: return "delegates/GuestAccess.qml" - //case MtxEvent.HistoryVisibility: return "delegates/HistoryVisibility.qml" - //case MtxEvent.JoinRules: return "delegates/JoinRules.qml" - //case MtxEvent.Member: return "delegates/Member.qml" - //case MtxEvent.Name: return "delegates/Name.qml" - //case MtxEvent.PowerLevels: return "delegates/PowerLevels.qml" - //case MtxEvent.Topic: return "delegates/Topic.qml" - case MtxEvent.NoticeMessage: return "delegates/NoticeMessage.qml" - case MtxEvent.TextMessage: return "delegates/TextMessage.qml" - case MtxEvent.EmoteMessage: return "delegates/TextMessage.qml" - case MtxEvent.ImageMessage: return "delegates/ImageMessage.qml" - case MtxEvent.Sticker: return "delegates/ImageMessage.qml" - case MtxEvent.FileMessage: return "delegates/FileMessage.qml" - case MtxEvent.VideoMessage: return "delegates/PlayableMediaMessage.qml" - case MtxEvent.AudioMessage: return "delegates/PlayableMediaMessage.qml" - case MtxEvent.Redacted: return "delegates/Redacted.qml" - default: return "delegates/placeholder.qml" - } - property variant eventData: model + DelegateChoice { + roleValue: MtxEvent.EmoteMessage + TimelineRow { view: chat; TextMessage { id: kid } } } - - StatusIndicator { - state: model.state - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 + DelegateChoice { + roleValue: MtxEvent.ImageMessage + TimelineRow { view: chat; ImageMessage { id: kid } } } - - EncryptionIndicator { - visible: model.isEncrypted - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 + DelegateChoice { + roleValue: MtxEvent.Sticker + TimelineRow { view: chat; ImageMessage { id: kid } } } - - Button { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - id: replyButton - flat: true - Layout.preferredHeight: 16 - ToolTip.visible: hovered - ToolTip.text: qsTr("Reply") - - // disable background, because we don't want a border on hover - background: Item { - } - - Image { - id: replyButtonImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: "qrc:/icons/icons/ui/mail-reply.png" - } - ColorOverlay { - anchors.fill: replyButtonImg - source: replyButtonImg - color: replyButton.hovered ? colors.highlight : colors.buttonText - } - - onClicked: chat.model.replyAction(model.id) + DelegateChoice { + roleValue: MtxEvent.FileMessage + TimelineRow { view: chat; FileMessage { id: kid } } } - Button { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - id: optionsButton - flat: true - Layout.preferredHeight: 16 - ToolTip.visible: hovered - ToolTip.text: qsTr("Options") - - // disable background, because we don't want a border on hover - background: Item { - } - - Image { - id: optionsButtonImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: "qrc:/icons/icons/ui/vertical-ellipsis.png" - } - ColorOverlay { - anchors.fill: optionsButtonImg - source: optionsButtonImg - color: optionsButton.hovered ? colors.highlight : colors.buttonText - } - - onClicked: contextMenu.open() - - Menu { - y: optionsButton.height - id: contextMenu - - MenuItem { - text: qsTr("Read receipts") - onTriggered: chat.model.readReceiptsAction(model.id) - } - MenuItem { - text: qsTr("Mark as read") - } - MenuItem { - text: qsTr("View raw message") - onTriggered: chat.model.viewRawMessage(model.id) - } - MenuItem { - text: qsTr("Redact message") - onTriggered: chat.model.redactEvent(model.id) - } - MenuItem { - visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage || model.type == MtxEvent.Sticker - text: qsTr("Save as") - onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) - } - } + DelegateChoice { + roleValue: MtxEvent.VideoMessage + TimelineRow { view: chat; PlayableMediaMessage { id: kid } } } - - Text { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - text: model.timestamp.toLocaleTimeString("HH:mm") - color: inactiveColors.text - - ToolTip.visible: ma.containsMouse - ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) - - MouseArea{ - id: ma - anchors.fill: parent - hoverEnabled: true - } + DelegateChoice { + roleValue: MtxEvent.AudioMessage + TimelineRow { view: chat; PlayableMediaMessage { id: kid } } + } + DelegateChoice { + roleValue: MtxEvent.Redacted + TimelineRow { view: chat; Redacted { id: kid } } } } + section { property: "section" delegate: Column { diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 6dd552ab..ad2c695d 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -31,7 +31,7 @@ Rectangle { } MouseArea { anchors.fill: parent - onClicked: timelineManager.saveMedia(eventData.url, eventData.filename, eventData.mimetype, eventData.type) + onClicked: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) cursorShape: Qt.PointingHandCursor } } @@ -40,14 +40,14 @@ Rectangle { Text { Layout.fillWidth: true - text: eventData.body + text: model.body textFormat: Text.PlainText elide: Text.ElideRight color: colors.text } Text { Layout.fillWidth: true - text: eventData.filesize + text: model.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 2ed41a17..70d2debe 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -4,20 +4,20 @@ import com.github.nheko 1.0 Item { width: 300 - height: 300 * eventData.proportionalHeight + height: 300 * model.proportionalHeight Image { id: img anchors.fill: parent - source: eventData.url.replace("mxc://", "image://MxcImage/") + source: model.url.replace("mxc://", "image://MxcImage/") asynchronous: true fillMode: Image.PreserveAspectFit MouseArea { - enabled: eventData.type == MtxEvent.ImageMessage + enabled: model.type == MtxEvent.ImageMessage anchors.fill: parent - onClicked: timelineManager.openImageOverlay(eventData.url, eventData.filename, eventData.mimetype, eventData.type) + onClicked: timelineManager.openImageOverlay(model.url, model.filename, model.mimetype, model.type) } } } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index 5f04d235..b916d65a 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -1,7 +1,7 @@ import QtQuick 2.5 TextEdit { - text: eventData.formattedBody + text: model.formattedBody textFormat: TextEdit.RichText readOnly: true wrapMode: Text.Wrap diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 2385c750..c716d21d 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -17,7 +17,7 @@ Rectangle { anchors.centerIn: parent VideoOutput { - visible: eventData.type == MtxEvent.VideoMessage + visible: model.type == MtxEvent.VideoMessage Layout.maximumHeight: 300 Layout.minimumHeight: 300 Layout.maximumWidth: 500 @@ -85,7 +85,7 @@ Rectangle { anchors.fill: parent onClicked: { switch (button.state) { - case "": timelineManager.cacheMedia(eventData.url, eventData.mimetype); break; + case "": timelineManager.cacheMedia(model.url, model.mimetype); break; case "stopped": media.play(); console.log("play"); button.state = "playing" @@ -107,7 +107,7 @@ Rectangle { Connections { target: timelineManager onMediaCached: { - if (mxcUrl == eventData.url) { + if (mxcUrl == model.url) { media.source = "file://" + cacheUrl button.state = "stopped" console.log("media loaded: " + mxcUrl + " at " + cacheUrl) @@ -132,14 +132,14 @@ Rectangle { Text { Layout.fillWidth: true - text: eventData.body + text: model.body textFormat: Text.PlainText elide: Text.ElideRight color: colors.text } Text { Layout.fillWidth: true - text: eventData.filesize + text: model.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 f7dba618..3a3492ed 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -1,7 +1,7 @@ import QtQuick 2.5 TextEdit { - text: eventData.formattedBody + text: model.formattedBody textFormat: TextEdit.RichText readOnly: true wrapMode: Text.Wrap diff --git a/resources/qml/delegates/TimelineRow.qml b/resources/qml/delegates/TimelineRow.qml new file mode 100644 index 00000000..28a2ec8c --- /dev/null +++ b/resources/qml/delegates/TimelineRow.qml @@ -0,0 +1,139 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtQuick.Window 2.2 + +import com.github.nheko 1.0 + +import ".." + +RowLayout { + property var view: undefined + default property alias data: contentItem.data + + height: kid.height // TODO: fix this, we shouldn't need to give the child of contentItem this id! + anchors.leftMargin: avatarSize + 4 + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: scrollbar.width + + function isFullyVisible() { + return (y - view.contentY - 1) + height < view.height + } + function getIndex() { + return index; + } + + Item { + id: contentItem + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + } + + StatusIndicator { + state: model.state + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } + + EncryptionIndicator { + visible: model.isEncrypted + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } + + Button { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + id: replyButton + flat: true + Layout.preferredHeight: 16 + ToolTip.visible: hovered + ToolTip.text: qsTr("Reply") + + // disable background, because we don't want a border on hover + background: Item { + } + + Image { + id: replyButtonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "qrc:/icons/icons/ui/mail-reply.png" + } + ColorOverlay { + anchors.fill: replyButtonImg + source: replyButtonImg + color: replyButton.hovered ? colors.highlight : colors.buttonText + } + + onClicked: view.model.replyAction(model.id) + } + Button { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + id: optionsButton + flat: true + Layout.preferredHeight: 16 + ToolTip.visible: hovered + ToolTip.text: qsTr("Options") + + // disable background, because we don't want a border on hover + background: Item { + } + + Image { + id: optionsButtonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "qrc:/icons/icons/ui/vertical-ellipsis.png" + } + ColorOverlay { + anchors.fill: optionsButtonImg + source: optionsButtonImg + color: optionsButton.hovered ? colors.highlight : colors.buttonText + } + + onClicked: contextMenu.open() + + Menu { + y: optionsButton.height + id: contextMenu + + MenuItem { + text: qsTr("Read receipts") + onTriggered: view.model.readReceiptsAction(model.id) + } + MenuItem { + text: qsTr("Mark as read") + } + MenuItem { + text: qsTr("View raw message") + onTriggered: view.model.viewRawMessage(model.id) + } + MenuItem { + text: qsTr("Redact message") + onTriggered: view.model.redactEvent(model.id) + } + MenuItem { + visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage || model.type == MtxEvent.Sticker + text: qsTr("Save as") + onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) + } + } + } + + Text { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + text: model.timestamp.toLocaleTimeString("HH:mm") + color: inactiveColors.text + + ToolTip.visible: ma.containsMouse + ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) + + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + } +} diff --git a/resources/qml/delegates/placeholder.qml b/resources/qml/delegates/placeholder.qml index e64bc368..462af2db 100644 --- a/resources/qml/delegates/placeholder.qml +++ b/resources/qml/delegates/placeholder.qml @@ -2,7 +2,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 Label { - text: qsTr("unimplemented event: ") + eventData.type + text: qsTr("unimplemented event: ") + model.type textFormat: Text.PlainText wrapMode: Text.Wrap width: parent.width diff --git a/resources/res.qrc b/resources/res.qrc index 16bab4e4..2e0f89ce 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -119,6 +119,7 @@ qml/Avatar.qml qml/StatusIndicator.qml qml/EncryptionIndicator.qml + qml/delegates/TimelineRow.qml qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml -- cgit 1.5.1 From 2055c75f8ba710e8950a55aa3c41a9cec9f26ad7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 27 Oct 2019 22:01:40 +0100 Subject: Organize qml files a bit --- CMakeLists.txt | 1 + cmake/Translations.cmake | 7 +- resources/qml/Avatar.qml | 1 + resources/qml/RowDelegateChooser.qml | 51 -------- resources/qml/TimelineRow.qml | 144 +++++++++++++++++++++ resources/qml/TimelineView.qml | 3 +- resources/qml/delegates/FileMessage.qml | 2 +- resources/qml/delegates/ImageMessage.qml | 2 +- resources/qml/delegates/MessageDelegate.qml | 50 ++++++++ resources/qml/delegates/NoticeMessage.qml | 2 +- resources/qml/delegates/Placeholder.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 2 +- resources/qml/delegates/TextMessage.qml | 2 +- resources/qml/delegates/TimelineRow.qml | 151 ----------------------- resources/res.qrc | 4 +- 15 files changed, 211 insertions(+), 213 deletions(-) delete mode 100644 resources/qml/RowDelegateChooser.qml create mode 100644 resources/qml/TimelineRow.qml create mode 100644 resources/qml/delegates/MessageDelegate.qml delete mode 100644 resources/qml/delegates/TimelineRow.qml (limited to 'resources/qml/delegates/FileMessage.qml') diff --git a/CMakeLists.txt b/CMakeLists.txt index 368b754a..ae9a5e46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,7 @@ include(LMDB) # Discover Qt dependencies. # find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 REQUIRED) +find_package(Qt5QuickCompiler) find_package(Qt5DBus) if (APPLE) diff --git a/cmake/Translations.cmake b/cmake/Translations.cmake index 8ca91883..bdd8ecbc 100644 --- a/cmake/Translations.cmake +++ b/cmake/Translations.cmake @@ -21,4 +21,9 @@ if(NOT EXISTS ${_qrc}) endif() qt5_add_resources(LANG_QRC ${_qrc}) -qt5_add_resources(QRC resources/res.qrc) +#qt5_add_resources(QRC resources/res.qrc) +if(Qt5QuickCompiler_FOUND) + qtquick_compiler_add_resources(QRC resources/res.qrc) +else() + qt5_add_resources(QRC resources/res.qrc) +endif() diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 9d7b54fe..131e6b46 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -30,6 +30,7 @@ Rectangle { id: img anchors.fill: parent asynchronous: true + fillMode: Image.PreserveAspectCrop layer.enabled: true layer.effect: OpacityMask { diff --git a/resources/qml/RowDelegateChooser.qml b/resources/qml/RowDelegateChooser.qml deleted file mode 100644 index bacd970a..00000000 --- a/resources/qml/RowDelegateChooser.qml +++ /dev/null @@ -1,51 +0,0 @@ -import QtQuick 2.6 -import Qt.labs.qmlmodels 1.0 -import com.github.nheko 1.0 - -import "./delegates" - -DelegateChooser { - //role: "type" //< not supported in our custom implementation, have to use roleValue - width: chat.width - roleValue: model.type - - DelegateChoice { - roleValue: MtxEvent.TextMessage - TimelineRow { view: chat; TextMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.NoticeMessage - TimelineRow { view: chat; NoticeMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.EmoteMessage - TimelineRow { view: chat; TextMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.ImageMessage - TimelineRow { view: chat; ImageMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.Sticker - TimelineRow { view: chat; ImageMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.FileMessage - TimelineRow { view: chat; FileMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.VideoMessage - TimelineRow { view: chat; PlayableMediaMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.AudioMessage - TimelineRow { view: chat; PlayableMediaMessage { id: kid } } - } - DelegateChoice { - roleValue: MtxEvent.Redacted - TimelineRow { view: chat; Redacted { id: kid } } - } - DelegateChoice { - TimelineRow { view: chat; Placeholder { id: kid } } - } -} diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml new file mode 100644 index 00000000..fdc2ec95 --- /dev/null +++ b/resources/qml/TimelineRow.qml @@ -0,0 +1,144 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtQuick.Window 2.2 + +import com.github.nheko 1.0 + +import "./delegates" + +RowLayout { + property var view: chat + + anchors.leftMargin: avatarSize + 4 + anchors.rightMargin: scrollbar.width + anchors.left: parent.left + anchors.right: parent.right + + height: contentItem.height + + MessageDelegate { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + id: contentItem + } + + StatusIndicator { + state: model.state + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } + + EncryptionIndicator { + visible: model.isEncrypted + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + } + + Button { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + id: replyButton + flat: true + Layout.preferredHeight: 16 + + ToolTip { + visible: replyButton.hovered + text: qsTr("Reply") + palette: colors + } + + // disable background, because we don't want a border on hover + background: Item { + } + + Image { + id: replyButtonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "qrc:/icons/icons/ui/mail-reply.png" + } + ColorOverlay { + anchors.fill: replyButtonImg + source: replyButtonImg + color: replyButton.hovered ? colors.highlight : colors.buttonText + } + + onClicked: view.model.replyAction(model.id) + } + Button { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + id: optionsButton + flat: true + Layout.preferredHeight: 16 + + ToolTip { + visible: optionsButton.hovered + text: qsTr("Options") + palette: colors + } + + // disable background, because we don't want a border on hover + background: Item { + } + + Image { + id: optionsButtonImg + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "qrc:/icons/icons/ui/vertical-ellipsis.png" + } + ColorOverlay { + anchors.fill: optionsButtonImg + source: optionsButtonImg + color: optionsButton.hovered ? colors.highlight : colors.buttonText + } + + onClicked: contextMenu.open() + + Menu { + y: optionsButton.height + id: contextMenu + palette: colors + + MenuItem { + text: qsTr("Read receipts") + onTriggered: view.model.readReceiptsAction(model.id) + } + MenuItem { + text: qsTr("Mark as read") + } + MenuItem { + text: qsTr("View raw message") + onTriggered: view.model.viewRawMessage(model.id) + } + MenuItem { + text: qsTr("Redact message") + onTriggered: view.model.redactEvent(model.id) + } + MenuItem { + visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage || model.type == MtxEvent.Sticker + text: qsTr("Save as") + onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) + } + } + } + + Text { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + text: model.timestamp.toLocaleTimeString("HH:mm") + color: inactiveColors.text + + MouseArea{ + id: ma + anchors.fill: parent + hoverEnabled: true + } + + ToolTip { + visible: ma.containsMouse + text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) + palette: colors + } + } +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 046f7800..e5c1bda6 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -90,14 +90,13 @@ Rectangle { onMovementEnded: updatePosition() spacing: 4 - delegate: RowDelegateChooser { + delegate: TimelineRow { function isFullyVisible() { return height > 1 && (y - chat.contentY - 1) + height < chat.height } function getIndex() { return index; } - } section { diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index ad2c695d..f4cf3f15 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -5,7 +5,7 @@ Rectangle { radius: 10 color: colors.dark height: row.height + 24 - width: parent.width + width: parent ? parent.width : undefined RowLayout { id: row diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index f1e95e3d..802ef721 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -3,7 +3,7 @@ import QtQuick 2.6 import com.github.nheko 1.0 Item { - width: Math.min(parent.width, model.width) + width: Math.min(parent ? parent.width : undefined, model.width) height: width * model.proportionalHeight Image { diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml new file mode 100644 index 00000000..3d342a02 --- /dev/null +++ b/resources/qml/delegates/MessageDelegate.qml @@ -0,0 +1,50 @@ +import QtQuick 2.6 +import Qt.labs.qmlmodels 1.0 +import com.github.nheko 1.0 + +DelegateChooser { + //role: "type" //< not supported in our custom implementation, have to use roleValue + roleValue: model.type + + width: parent.width + + 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 + Redacted {} + } + DelegateChoice { + Placeholder {} + } +} diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index b916d65a..59e051be 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -5,7 +5,7 @@ TextEdit { textFormat: TextEdit.RichText readOnly: true wrapMode: Text.Wrap - width: parent.width + width: parent ? parent.width : undefined selectByMouse: true font.italic: true color: inactiveColors.text diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml index 462af2db..171bf18d 100644 --- a/resources/qml/delegates/Placeholder.qml +++ b/resources/qml/delegates/Placeholder.qml @@ -5,6 +5,6 @@ Label { text: qsTr("unimplemented event: ") + model.type textFormat: Text.PlainText wrapMode: Text.Wrap - width: parent.width + width: parent ? parent.width : undefined color: inactiveColors.text } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 3a518617..68b09f7b 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -10,7 +10,7 @@ Rectangle { radius: 10 color: colors.dark height: content.height + 24 - width: parent.width + width: parent ? parent.width : undefined Column { id: content diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 3a3492ed..713be868 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -5,7 +5,7 @@ TextEdit { textFormat: TextEdit.RichText readOnly: true wrapMode: Text.Wrap - width: parent.width + width: parent ? parent.width : undefined selectByMouse: true color: colors.text } diff --git a/resources/qml/delegates/TimelineRow.qml b/resources/qml/delegates/TimelineRow.qml deleted file mode 100644 index 3019deb1..00000000 --- a/resources/qml/delegates/TimelineRow.qml +++ /dev/null @@ -1,151 +0,0 @@ -import QtQuick 2.6 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.2 -import QtGraphicalEffects 1.0 -import QtQuick.Window 2.2 - -import com.github.nheko 1.0 - -import ".." - -RowLayout { - property var view: undefined - default property alias data: contentItem.data - - height: kid.height // TODO: fix this, we shouldn't need to give the child of contentItem this id! - anchors.leftMargin: avatarSize + 4 - anchors.left: parent.left - anchors.right: parent.right - anchors.rightMargin: scrollbar.width - - function isFullyVisible() { - return (y - view.contentY - 1) + height < view.height - } - function getIndex() { - return index; - } - - Item { - id: contentItem - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - } - - StatusIndicator { - state: model.state - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - } - - EncryptionIndicator { - visible: model.isEncrypted - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - } - - Button { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - id: replyButton - flat: true - Layout.preferredHeight: 16 - - ToolTip { - visible: replyButton.hovered - text: qsTr("Reply") - palette: colors - } - - // disable background, because we don't want a border on hover - background: Item { - } - - Image { - id: replyButtonImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: "qrc:/icons/icons/ui/mail-reply.png" - } - ColorOverlay { - anchors.fill: replyButtonImg - source: replyButtonImg - color: replyButton.hovered ? colors.highlight : colors.buttonText - } - - onClicked: view.model.replyAction(model.id) - } - Button { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - id: optionsButton - flat: true - Layout.preferredHeight: 16 - - ToolTip { - visible: optionsButton.hovered - text: qsTr("Options") - palette: colors - } - - // disable background, because we don't want a border on hover - background: Item { - } - - Image { - id: optionsButtonImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: "qrc:/icons/icons/ui/vertical-ellipsis.png" - } - ColorOverlay { - anchors.fill: optionsButtonImg - source: optionsButtonImg - color: optionsButton.hovered ? colors.highlight : colors.buttonText - } - - onClicked: contextMenu.open() - - Menu { - y: optionsButton.height - id: contextMenu - palette: colors - - MenuItem { - text: qsTr("Read receipts") - onTriggered: view.model.readReceiptsAction(model.id) - } - MenuItem { - text: qsTr("Mark as read") - } - MenuItem { - text: qsTr("View raw message") - onTriggered: view.model.viewRawMessage(model.id) - } - MenuItem { - text: qsTr("Redact message") - onTriggered: view.model.redactEvent(model.id) - } - MenuItem { - visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage || model.type == MtxEvent.Sticker - text: qsTr("Save as") - onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) - } - } - } - - Text { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - text: model.timestamp.toLocaleTimeString("HH:mm") - color: inactiveColors.text - - MouseArea{ - id: ma - anchors.fill: parent - hoverEnabled: true - } - - ToolTip { - visible: ma.containsMouse - text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) - palette: colors - } - } -} diff --git a/resources/res.qrc b/resources/res.qrc index 4816ffad..86b1364c 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -116,11 +116,11 @@ qml/TimelineView.qml - qml/RowDelegateChooser.qml qml/Avatar.qml qml/StatusIndicator.qml qml/EncryptionIndicator.qml - qml/delegates/TimelineRow.qml + qml/TimelineRow.qml + qml/delegates/MessageDelegate.qml qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml qml/delegates/ImageMessage.qml -- cgit 1.5.1 From b8f6e4ce6462f074c34a8b7a286cbabe0e2897aa Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Dec 2019 02:26:41 +0100 Subject: Add encrypted file download --- deps/CMakeLists.txt | 4 +- resources/qml/TimelineRow.qml | 2 +- resources/qml/delegates/FileMessage.qml | 2 +- resources/qml/delegates/ImageMessage.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 4 +- src/timeline/TimelineModel.cpp | 184 +++++++++++++++++++++++ src/timeline/TimelineModel.h | 3 + src/timeline/TimelineViewManager.cpp | 154 ++----------------- src/timeline/TimelineViewManager.h | 27 +--- 9 files changed, 210 insertions(+), 172 deletions(-) (limited to 'resources/qml/delegates/FileMessage.qml') diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index d0a715e0..c5932ab7 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -46,10 +46,10 @@ set(BOOST_SHA256 set( MTXCLIENT_URL - https://github.com/Nheko-Reborn/mtxclient/archive/6eee767cc25a9db9f125843e584656cde1ebb6c5.tar.gz + https://github.com/Nheko-Reborn/mtxclient/archive/f719236b08d373d9508f2467bbfc6dfa953b1f8d.zip ) set(MTXCLIENT_HASH - 72fe77da4fed98b3cf069299f66092c820c900359a27ec26070175f9ad208a03) + 0660756c16cf297e02b0b29c07a59fc851723cc65f305893ae7238e6dd2e41c8) set( TWEENY_URL https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 4917e893..2c2ed02a 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -97,7 +97,7 @@ RowLayout { MenuItem { visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage || model.type == MtxEvent.Sticker text: qsTr("Save as") - onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) + onTriggered: timelineManager.timeline.saveMedia(model.id) } } } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index f4cf3f15..2c911c5e 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -31,7 +31,7 @@ Rectangle { } MouseArea { anchors.fill: parent - onClicked: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) + onClicked: timelineManager.timeline.saveMedia(model.id) cursorShape: Qt.PointingHandCursor } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index a1a06012..1b6e5729 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -17,7 +17,7 @@ Item { MouseArea { enabled: model.type == MtxEvent.ImageMessage anchors.fill: parent - onClicked: timelineManager.openImageOverlay(model.url, model.filename, model.mimetype, model.type) + onClicked: timelineManager.openImageOverlay(model.url, model.id) } } } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 3b987545..d0d4d7cb 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -97,7 +97,7 @@ Rectangle { anchors.fill: parent onClicked: { switch (button.state) { - case "": timelineManager.cacheMedia(model.url, model.mimetype); break; + case "": timelineManager.timeline.cacheMedia(model.id); break; case "stopped": media.play(); console.log("play"); button.state = "playing" @@ -118,7 +118,7 @@ Rectangle { } Connections { - target: timelineManager + target: timelineManager.timeline onMediaCached: { if (mxcUrl == model.url) { media.source = "file://" + cacheUrl diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index b904dfd7..f606b603 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -3,11 +3,15 @@ #include #include +#include +#include #include +#include #include "ChatPage.h" #include "Logging.h" #include "MainWindow.h" +#include "MxcImageProvider.h" #include "Olm.h" #include "TimelineViewManager.h" #include "Utils.h" @@ -88,17 +92,42 @@ eventFormattedBody(const mtx::events::RoomEvent &e) } } +template +boost::optional +eventEncryptionInfo(const mtx::events::Event &) +{ + return boost::none; +} + +template +auto +eventEncryptionInfo(const mtx::events::RoomEvent &e) -> std::enable_if_t< + std::is_same>::value, + boost::optional> +{ + return e.content.file; +} + template QString eventUrl(const mtx::events::Event &) { return ""; } + +QString +eventUrl(const mtx::events::StateEvent &e) +{ + return QString::fromStdString(e.content.url); +} + template auto eventUrl(const mtx::events::RoomEvent &e) -> std::enable_if_t::value, QString> { + if (e.content.file) + return QString::fromStdString(e.content.file->url); return QString::fromStdString(e.content.url); } @@ -1342,3 +1371,158 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) if (!isProcessingPending) emit nextPendingMessage(); } + +void +TimelineModel::saveMedia(QString eventId) const +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = + boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event); + QString originalFilename = + boost::apply_visitor([](const auto &e) -> QString { return eventFilename(e); }, event); + QString mimeType = + boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event); + + using EncF = boost::optional; + EncF encryptionInfo = + boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event); + + qml_mtx_events::EventType eventType = boost::apply_visitor( + [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, event); + + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); + } + + QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + + auto filename = QFileDialog::getSaveFileName( + manager_->getWidget(), dialogTitle, originalFilename, filterString); + + if (filename.isEmpty()) + return; + + const auto url = mxcUrl.toStdString(); + + http::client()->download( + url, + [filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), (int)temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); +} + +void +TimelineModel::cacheMedia(QString eventId) +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = + boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event); + QString mimeType = + boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event); + + using EncF = boost::optional; + EncF encryptionInfo = + boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event); + + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString(mxcUrl).remove("mxc://")) + .arg(suffix)); + if (QDir::cleanPath(filename.path()) != filename.path()) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } + + QDir().mkpath(filename.path()); + + if (filename.isReadable()) { + emit mediaCached(mxcUrl, filename.filePath()); + return; + } + + http::client()->download( + url, + [this, mxcUrl, filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + + emit mediaCached(mxcUrl, filename.filePath()); + }); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index e7842b99..f52091e6 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -159,6 +159,8 @@ public: Q_INVOKABLE void redactEvent(QString id); Q_INVOKABLE int idToIndex(QString id) const; Q_INVOKABLE QString indexToId(int index) const; + Q_INVOKABLE void cacheMedia(QString eventId); + Q_INVOKABLE void saveMedia(QString eventId) const; void addEvents(const mtx::responses::Timeline &events); template @@ -185,6 +187,7 @@ signals: void eventRedacted(QString id); void nextPendingMessage(); void newMessageToSend(mtx::events::collections::TimelineEvents event); + void mediaCached(QString mxcUrl, QString cacheUrl); private: DecryptionResult decryptEvent( diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 2a88c882..6430a426 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -1,11 +1,8 @@ #include "TimelineViewManager.h" -#include #include -#include #include #include -#include #include "ChatPage.h" #include "ColorImageProvider.h" @@ -124,146 +121,24 @@ TimelineViewManager::setHistoryView(const QString &room_id) } void -TimelineViewManager::openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const +TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { QQuickImageResponse *imgResponse = imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); - connect(imgResponse, - &QQuickImageResponse::finished, - this, - [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() { - if (!imgResponse->errorString().isEmpty()) { - nhlog::ui()->error("Error when retrieving image for overlay: {}", - imgResponse->errorString().toStdString()); - return; - } - auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - - auto imgDialog = new dialogs::ImageOverlay(pixmap); - imgDialog->show(); - connect(imgDialog, - &dialogs::ImageOverlay::saving, - this, - [this, mxcUrl, originalFilename, mimeType, eventType]() { - saveMedia(mxcUrl, originalFilename, mimeType, eventType); - }); - }); -} - -void -TimelineViewManager::saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const -{ - QString dialogTitle; - if (eventType == qml_mtx_events::EventType::ImageMessage) { - dialogTitle = tr("Save image"); - } else if (eventType == qml_mtx_events::EventType::VideoMessage) { - dialogTitle = tr("Save video"); - } else if (eventType == qml_mtx_events::EventType::AudioMessage) { - dialogTitle = tr("Save audio"); - } else { - dialogTitle = tr("Save file"); - } - - QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); - - auto filename = - QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString); - - if (filename.isEmpty()) - return; - - const auto url = mxcUrl.toStdString(); - - http::client()->download( - url, - [filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), (int)data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - }); -} - -void -TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) -{ - // If the message is a link to a non mxcUrl, don't download it - if (!mxcUrl.startsWith("mxc://")) { - emit mediaCached(mxcUrl, mxcUrl); - return; - } - - QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); - - const auto url = mxcUrl.toStdString(); - QFileInfo filename(QString("%1/media_cache/%2.%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString(mxcUrl).remove("mxc://")) - .arg(suffix)); - if (QDir::cleanPath(filename.path()) != filename.path()) { - nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); - return; - } - - QDir().mkpath(filename.path()); - - if (filename.isReadable()) { - emit mediaCached(mxcUrl, filename.filePath()); - return; - } + connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, imgResponse]() { + if (!imgResponse->errorString().isEmpty()) { + nhlog::ui()->error("Error when retrieving image for overlay: {}", + imgResponse->errorString().toStdString()); + return; + } + auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - http::client()->download( - url, - [this, mxcUrl, filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename.filePath()); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - - emit mediaCached(mxcUrl, filename.filePath()); - }); + auto imgDialog = new dialogs::ImageOverlay(pixmap); + imgDialog->show(); + connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() { + timeline_->saveMedia(eventId); + }); + }); } void @@ -401,3 +276,4 @@ TimelineViewManager::queueVideoMessage(const QString &roomid, video.url = url.toStdString(); models.value(roomid)->sendMessage(video); } + diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 0bc58e68..1cb0de44 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -35,38 +35,13 @@ public: Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } - void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType); - // Qml can only pass enum as int - Q_INVOKABLE void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - openImageOverlay( - mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } - Q_INVOKABLE void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } + Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); void initialSyncChanged(bool isInitialSync); - void mediaCached(QString mxcUrl, QString cacheUrl); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); -- cgit 1.5.1 From 2b3dc3d8b9d1108c3f6ad226ad65060bd3999033 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 11 Jan 2020 14:07:51 +0100 Subject: 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 +- resources/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(-) (limited to 'resources/qml/delegates/FileMessage.qml') 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 + + 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 - //Text { - // property int idx: timelineManager.timeline.idToIndex(replyTo) - // text: "" + (idx != -1 ? timelineManager.timeline.data(timelineManager.timeline.index(idx, 0), 2) : "nothing") - //} + 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 - - 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 {} +Item { + // Workaround to have an assignable global property + Item { + id: model + property var data; } - DelegateChoice { - roleValue: MtxEvent.VideoMessage - PlayableMediaMessage {} - } - DelegateChoice { - roleValue: MtxEvent.AudioMessage - PlayableMediaMessage {} - } - DelegateChoice { - roleValue: MtxEvent.Redacted - Pill { - text: qsTr("redacted") + + property alias modelData: model.data + + 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.
-- 
cgit 1.5.1


From d330020fddb22dc7cfddd298ef00e3f3829aa691 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Mon, 3 Feb 2020 19:21:03 +0100
Subject: Improve styling a bit

---
 resources/qml/Avatar.qml                         |  2 +-
 resources/qml/TimelineView.qml                   |  6 +++---
 resources/qml/delegates/FileMessage.qml          |  2 +-
 resources/qml/delegates/Pill.qml                 |  2 +-
 resources/qml/delegates/PlayableMediaMessage.qml | 11 ++++++-----
 src/timeline/TimelineViewManager.cpp             |  6 +++---
 6 files changed, 15 insertions(+), 14 deletions(-)

(limited to 'resources/qml/delegates/FileMessage.qml')

diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index 25fff7f3..f19cd09f 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -48,5 +48,5 @@ Rectangle {
 			}
 		}
 	}
-	color: colors.dark
+	color: colors.base
 }
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 69d8d29e..6f5eb42b 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -196,7 +196,7 @@ Item {
 						horizontalAlignment: Text.AlignHCenter
 						background: Rectangle {
 							radius: parent.height / 2
-							color: colors.dark
+							color: colors.base
 						}
 					}
 					Row {
@@ -242,7 +242,7 @@ Item {
 			anchors.bottom: parent.bottom
 			z: 3
 
-			color: colors.window
+			color: "transparent"
 
 			Column {
 				id: footerContent
@@ -270,7 +270,7 @@ Item {
 					visible: timelineManager.replyingEvent && chat.model
 					// Height of child, plus margins, plus border
 					height: replyPreview.height + 10
-					color: colors.dark
+					color: colors.base
 
 
 					Reply {
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index 9a5300bb..2fe0a490 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -3,7 +3,7 @@ import QtQuick.Layouts 1.2
 
 Rectangle {
 	radius: 10
-	color: colors.dark
+	color: colors.base
 	height: row.height + 24
 	width: parent ? parent.width : undefined
 
diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml
index 53a9684e..b19d9a54 100644
--- a/resources/qml/delegates/Pill.qml
+++ b/resources/qml/delegates/Pill.qml
@@ -9,6 +9,6 @@ Label {
 	width: contentWidth * 1.2
 	background: Rectangle {
 		radius: parent.height / 2
-		color: colors.dark
+		color: colors.base
 	}
 }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 50202098..a4096864 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -8,7 +8,7 @@ import im.nheko 1.0
 Rectangle {
 	id: bg
 	radius: 10
-	color: colors.dark
+	color: colors.base
 	height: content.height + 24
 	width: parent ? parent.width : undefined
 
@@ -83,15 +83,16 @@ Rectangle {
 
 			Rectangle {
 				id: button
-				color: colors.light
+				color: colors.window
 				radius: 22
 				height: 44
 				width: 44
 				Image {
 					id: img
 					anchors.centerIn: parent
+					z: 3
 
-					source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+colors.dark
+					source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+colors.text
 					fillMode: Image.Pad
 
 				}
@@ -134,11 +135,11 @@ Rectangle {
 				states: [
 					State {
 						name: "stopped"
-						PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/play-sign.png?"+colors.dark }
+						PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/play-sign.png?"+colors.text }
 					},
 					State {
 						name: "playing"
-						PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+colors.dark }
+						PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+colors.text }
 					}
 				]
 			}
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 33441a62..37c56217 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -22,11 +22,11 @@ TimelineViewManager::updateColorPalette()
                 QPalette lightActive(/*windowText*/ QColor("#333"),
                                      /*button*/ QColor("#333"),
                                      /*light*/ QColor(),
-                                     /*dark*/ QColor(220, 220, 220, 120),
+                                     /*dark*/ QColor(220, 220, 220),
                                      /*mid*/ QColor(),
                                      /*text*/ QColor("#333"),
                                      /*bright_text*/ QColor(),
-                                     /*base*/ QColor("white"),
+                                     /*base*/ QColor(220, 220, 220),
                                      /*window*/ QColor("white"));
                 lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color());
                 lightActive.setColor(QPalette::ToolTipText, lightActive.text().color());
@@ -40,7 +40,7 @@ TimelineViewManager::updateColorPalette()
                                     /*mid*/ QColor(),
                                     /*text*/ QColor("#caccd1"),
                                     /*bright_text*/ QColor(),
-                                    /*base*/ QColor("#202228"),
+                                    /*base*/ QColor("#2d3139"),
                                     /*window*/ QColor("#202228"));
                 darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9"));
                 darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color());
-- 
cgit 1.5.1