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. 22
      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
}
}
//Text { MessageDelegate {
// property int idx: timelineManager.timeline.idToIndex(replyTo) id: reply
// text: "" + (idx != -1 ? timelineManager.timeline.data(timelineManager.timeline.index(idx, 0), 2) : "nothing") 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
} }
} }

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

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