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.left: parent.left
anchors.right: parent.right anchors.right: parent.right
height: Math.max(contentItem.height, 16) //height: Math.max(model.replyTo ? reply.height + contentItem.height + 4 : contentItem.height, 16)
Column { Column {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignTop 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 { modelData: chat.model.getDump(model.replyTo)
// property int idx: timelineManager.timeline.idToIndex(replyTo) }
// text: "" + (idx != -1 ? timelineManager.timeline.data(timelineManager.timeline.index(idx, 0), 2) : "nothing") }
//}
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 { MessageDelegate {
id: contentItem id: contentItem
width: parent.width width: parent.width
height: childrenRect.height
modelData: model
} }
} }

@ -31,7 +31,7 @@ Rectangle {
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: timelineManager.timeline.saveMedia(model.id) onClicked: timelineManager.timeline.saveMedia(model.data.id)
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
} }
@ -40,14 +40,14 @@ Rectangle {
Text { Text {
Layout.fillWidth: true Layout.fillWidth: true
text: model.body text: model.data.body
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight elide: Text.ElideRight
color: colors.text color: colors.text
} }
Text { Text {
Layout.fillWidth: true Layout.fillWidth: true
text: model.filesize text: model.data.filesize
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight elide: Text.ElideRight
color: colors.text color: colors.text

@ -3,26 +3,26 @@ import QtQuick 2.6
import im.nheko 1.0 import im.nheko 1.0
Item { Item {
property double tempWidth: Math.min(parent ? parent.width : undefined, model.width) property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width)
property double tempHeight: tempWidth * model.proportionalHeight property double tempHeight: tempWidth * model.data.proportionalHeight
property bool tooHigh: tempHeight > chat.height - 40 property bool tooHigh: tempHeight > chat.height - 40
height: tooHigh ? chat.height - 40 : tempHeight height: tooHigh ? chat.height - 40 : tempHeight
width: tooHigh ? (chat.height - 40) / model.proportionalHeight : tempWidth width: tooHigh ? (chat.height - 40) / model.data.proportionalHeight : tempWidth
Image { Image {
id: img id: img
anchors.fill: parent anchors.fill: parent
source: model.url.replace("mxc://", "image://MxcImage/") source: model.data.url.replace("mxc://", "image://MxcImage/")
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
MouseArea { MouseArea {
enabled: model.type == MtxEvent.ImageMessage enabled: model.data.type == MtxEvent.ImageMessage
anchors.fill: parent 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 QtQuick 2.6
import im.nheko 1.0 import im.nheko 1.0
DelegateChooser { Item {
//role: "type" //< not supported in our custom implementation, have to use roleValue // Workaround to have an assignable global property
roleValue: model.type Item {
id: model
DelegateChoice { property var data;
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 property alias modelData: model.data
PlayableMediaMessage {}
} height: chooser.childrenRect.height
DelegateChoice {
roleValue: MtxEvent.AudioMessage DelegateChooser {
PlayableMediaMessage {} id: chooser
} //role: "type" //< not supported in our custom implementation, have to use roleValue
DelegateChoice { roleValue: model.data.type
roleValue: MtxEvent.Redacted anchors.fill: parent
Pill {
text: qsTr("redacted") DelegateChoice {
roleValue: MtxEvent.TextMessage
TextMessage {}
} }
} DelegateChoice {
DelegateChoice { roleValue: MtxEvent.NoticeMessage
roleValue: MtxEvent.Encryption NoticeMessage {}
Pill {
text: qsTr("Encryption enabled")
} }
} DelegateChoice {
DelegateChoice { roleValue: MtxEvent.EmoteMessage
roleValue: MtxEvent.Name TextMessage {}
NoticeMessage {
notice: model.roomName ? qsTr("room name changed to: %1").arg(model.roomName) : qsTr("removed room name")
} }
} DelegateChoice {
DelegateChoice { roleValue: MtxEvent.ImageMessage
roleValue: MtxEvent.Topic ImageMessage {}
NoticeMessage { }
notice: model.roomTopic ? qsTr("topic changed to: %1").arg(model.roomTopic) : qsTr("removed topic") 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 ".." import ".."
MatrixText { 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 text: notice
width: parent ? parent.width : undefined width: parent ? parent.width : undefined
font.italic: true font.italic: true

@ -1,7 +1,7 @@
import ".." import ".."
MatrixText { MatrixText {
text: qsTr("unimplemented event: ") + model.type text: qsTr("unimplemented event: ") + model.data.type
width: parent ? parent.width : undefined width: parent ? parent.width : undefined
color: inactiveColors.text color: inactiveColors.text
} }

@ -19,12 +19,12 @@ Rectangle {
Rectangle { Rectangle {
id: videoContainer id: videoContainer
visible: model.type == MtxEvent.VideoMessage visible: model.data.type == MtxEvent.VideoMessage
width: Math.min(parent.width, model.width ? model.width : 400) // some media has 0 as size... width: Math.min(parent.width, model.data.width ? model.data.width : 400) // some media has 0 as size...
height: width*model.proportionalHeight height: width*model.data.proportionalHeight
Image { Image {
anchors.fill: parent anchors.fill: parent
source: model.thumbnailUrl.replace("mxc://", "image://MxcImage/") source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/")
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
@ -97,7 +97,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
switch (button.state) { switch (button.state) {
case "": timelineManager.timeline.cacheMedia(model.id); break; case "": timelineManager.timeline.cacheMedia(model.data.id); break;
case "stopped": case "stopped":
media.play(); console.log("play"); media.play(); console.log("play");
button.state = "playing" button.state = "playing"
@ -120,7 +120,7 @@ Rectangle {
Connections { Connections {
target: timelineManager.timeline target: timelineManager.timeline
onMediaCached: { onMediaCached: {
if (mxcUrl == model.url) { if (mxcUrl == model.data.url) {
media.source = "file://" + cacheUrl media.source = "file://" + cacheUrl
button.state = "stopped" button.state = "stopped"
console.log("media loaded: " + mxcUrl + " at " + cacheUrl) console.log("media loaded: " + mxcUrl + " at " + cacheUrl)
@ -145,14 +145,14 @@ Rectangle {
Text { Text {
Layout.fillWidth: true Layout.fillWidth: true
text: model.body text: model.data.body
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight elide: Text.ElideRight
color: colors.text color: colors.text
} }
Text { Text {
Layout.fillWidth: true Layout.fillWidth: true
text: model.filesize text: model.data.filesize
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight elide: Text.ElideRight
color: colors.text color: colors.text

@ -1,6 +1,6 @@
import ".." import ".."
MatrixText { 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 width: parent ? parent.width : undefined
} }

@ -212,6 +212,14 @@ TimelineModel::rowCount(const QModelIndex &parent) const
return (int)this->eventOrder.size(); 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 QVariant
TimelineModel::data(const QModelIndex &index, int role) const TimelineModel::data(const QModelIndex &index, int role) const
{ {
@ -263,11 +271,13 @@ TimelineModel::data(const QModelIndex &index, int role) const
return QVariant(toRoomEventType(event)); return QVariant(toRoomEventType(event));
case Body: case Body:
return QVariant(utils::replaceEmoji(QString::fromStdString(body(event)))); return QVariant(utils::replaceEmoji(QString::fromStdString(body(event))));
case FormattedBody: case FormattedBody: {
const static QRegularExpression replyFallback(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
return QVariant( return QVariant(
utils::replaceEmoji(utils::linkifyMessage(formattedBodyWithFallback(event))) utils::replaceEmoji(utils::linkifyMessage(formattedBodyWithFallback(event)))
.remove("<mx-reply>") .remove(replyFallback));
.remove("</mx-reply>")); }
case Url: case Url:
return QVariant(QString::fromStdString(url(event))); return QVariant(QString::fromStdString(url(event)));
case ThumbnailUrl: case ThumbnailUrl:

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

Loading…
Cancel
Save