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.
This commit is contained in:
Nicolas Werner 2020-01-11 14:07:51 +01:00
parent b130b85df8
commit 2b3dc3d8b9
10 changed files with 159 additions and 87 deletions

View File

@ -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
//Text { Rectangle {
// property int idx: timelineManager.timeline.idToIndex(replyTo) id: colorLine
// text: "" + (idx != -1 ? timelineManager.timeline.data(timelineManager.timeline.index(idx, 0), 2) : "nothing") height: replyContainer.height
//} width: 4
color: chat.model.userColor(reply.modelData.userId, colors.window)
}
Column {
id: replyContainer
anchors.left: colorLine.right
anchors.leftMargin: 4
width: parent.width - 8
Text {
id: userName
text: chat.model.escapeEmoji(reply.modelData.userName)
color: chat.model.userColor(reply.modelData.userId, colors.window)
textFormat: Text.RichText
MouseArea {
anchors.fill: parent
onClicked: chat.model.openUserProfile(reply.modelData.userId)
cursorShape: Qt.PointingHandCursor
}
}
MessageDelegate {
id: reply
width: parent.width
modelData: chat.model.getDump(model.replyTo)
}
}
color: { var col = chat.model.userColor(reply.modelData.userId, colors.window); col.a = 0.2; return col }
MouseArea {
anchors.fill: parent
onClicked: chat.positionViewAtIndex(chat.model.idToIndex(model.replyTo), ListView.Contain)
cursorShape: Qt.PointingHandCursor
}
}
// actual message content
MessageDelegate { MessageDelegate {
id: contentItem id: contentItem
width: parent.width width: parent.width
height: childrenRect.height
modelData: model
} }
} }

View File

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

View File

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

View File

@ -1,9 +1,22 @@
import QtQuick 2.6 import QtQuick 2.6
import im.nheko 1.0 import im.nheko 1.0
DelegateChooser { Item {
// Workaround to have an assignable global property
Item {
id: model
property var data;
}
property alias modelData: model.data
height: chooser.childrenRect.height
DelegateChooser {
id: chooser
//role: "type" //< not supported in our custom implementation, have to use roleValue //role: "type" //< not supported in our custom implementation, have to use roleValue
roleValue: model.type roleValue: model.data.type
anchors.fill: parent
DelegateChoice { DelegateChoice {
roleValue: MtxEvent.TextMessage roleValue: MtxEvent.TextMessage
@ -52,16 +65,17 @@ DelegateChooser {
DelegateChoice { DelegateChoice {
roleValue: MtxEvent.Name roleValue: MtxEvent.Name
NoticeMessage { NoticeMessage {
notice: model.roomName ? qsTr("room name changed to: %1").arg(model.roomName) : qsTr("removed room name") notice: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name")
} }
} }
DelegateChoice { DelegateChoice {
roleValue: MtxEvent.Topic roleValue: MtxEvent.Topic
NoticeMessage { NoticeMessage {
notice: model.roomTopic ? qsTr("topic changed to: %1").arg(model.roomTopic) : qsTr("removed topic") notice: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic")
} }
} }
DelegateChoice { DelegateChoice {
Placeholder {} Placeholder {}
} }
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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