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.
remotes/origin/0.7.0-dev
Nicolas Werner 5 years ago
parent b130b85df8
commit 2b3dc3d8b9
  1. 61
      resources/qml/TimelineRow.qml
  2. 6
      resources/qml/delegates/FileMessage.qml
  3. 12
      resources/qml/delegates/ImageMessage.qml
  4. 128
      resources/qml/delegates/MessageDelegate.qml
  5. 2
      resources/qml/delegates/NoticeMessage.qml
  6. 2
      resources/qml/delegates/Placeholder.qml
  7. 16
      resources/qml/delegates/PlayableMediaMessage.qml
  8. 2
      resources/qml/delegates/TextMessage.qml
  9. 16
      src/timeline/TimelineModel.cpp
  10. 1
      src/timeline/TimelineModel.h

@ -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
}
}

@ -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

@ -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)
}
}
}

@ -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 {}
}
}

@ -1,7 +1,7 @@
import ".."
MatrixText {
property string notice: model.formattedBody.replace("<pre>", "<pre style='white-space: pre-wrap'>")
property string notice: model.data.formattedBody.replace("<pre>", "<pre style='white-space: pre-wrap'>")
text: notice
width: parent ? parent.width : undefined
font.italic: true

@ -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
}

@ -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

@ -1,6 +1,6 @@
import ".."
MatrixText {
text: model.formattedBody.replace("<pre>", "<pre style='white-space: pre-wrap'>")
text: model.data.formattedBody.replace("<pre>", "<pre style='white-space: pre-wrap'>")
width: parent ? parent.width : undefined
}

@ -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(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
return QVariant(
utils::replaceEmoji(utils::linkifyMessage(formattedBodyWithFallback(event)))
.remove("<mx-reply>")
.remove("</mx-reply>"));
.remove(replyFallback));
}
case Url:
return QVariant(QString::fromStdString(url(event)));
case ThumbnailUrl:

@ -181,6 +181,7 @@ public slots:
void setCurrentIndex(int index);
int currentIndex() const { return idToIndex(currentId); }
void markEventsAsRead(const std::vector<QString> &event_ids);
QVariantMap getDump(QString eventId) const;
private slots:
// Add old events at the top of the timeline.

Loading…
Cancel
Save