diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index eee3879c..127b59c2 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -50,9 +50,24 @@ Popup { Reply { id: replyPreview - modelData: room ? room.getDump(mid, "") : { + property var modelData: room ? room.getDump(mid, "") : { } + userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window) + blurhash: modelData.blurhash ?? "" + body: modelData.body ?? "" + formattedBody: modelData.formattedBody ?? "" + eventId: modelData.eventId ?? "" + filename: modelData.filename ?? "" + filesize: modelData.filesize ?? "" + proportionalHeight: modelData.proportionalHeight ?? 1 + type: modelData.type ?? MtxEvent.UnknownMessage + typeString: modelData.typeString ?? "" + url: modelData.url ?? "" + originalWidth: modelData.originalWidth ?? 0 + isOnlyEmoji: modelData.isOnlyEmoji ?? false + userId: modelData.userId ?? "" + userName: modelData.userName ?? "" } MatrixTextField { diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 260bc9da..9e01ef9a 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -7,8 +7,8 @@ import "./emoji" import "./ui" import Qt.labs.platform 1.1 as Platform import QtGraphicalEffects 1.0 -import QtQuick 2.12 -import QtQuick.Controls 2.3 +import QtQuick 2.15 +import QtQuick.Controls 2.15 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 import im.nheko 1.0 @@ -25,6 +25,9 @@ ScrollView { property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2 model: room + // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107 + //onModelChanged: if (room) room.sendReset() + //reuseItems: true boundsBehavior: Flickable.StopAtBounds pixelAligned: true spacing: 4 @@ -84,7 +87,7 @@ ScrollView { ToolTip.text: qsTr("Edit") onClicked: { if (row.model.isEditable) - chat.model.editAction(row.model.id); + chat.model.editAction(row.model.eventId); } } @@ -98,7 +101,7 @@ ScrollView { ToolTip.visible: hovered ToolTip.text: qsTr("React") emojiPicker: emojiPopup - event_id: row.model ? row.model.id : "" + event_id: row.model ? row.model.eventId : "" } ImageButton { @@ -110,7 +113,7 @@ ScrollView { image: ":/icons/icons/ui/mail-reply.png" ToolTip.visible: hovered ToolTip.text: qsTr("Reply") - onClicked: chat.model.replyAction(row.model.id) + onClicked: chat.model.replyAction(row.model.eventId) } ImageButton { @@ -121,7 +124,7 @@ ScrollView { image: ":/icons/icons/ui/vertical-ellipsis.png" ToolTip.visible: hovered ToolTip.text: qsTr("Options") - onClicked: messageContextMenu.show(row.model.id, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton) + onClicked: messageContextMenu.show(row.model.eventId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton) } } @@ -212,16 +215,16 @@ ScrollView { topPadding: 4 bottomPadding: 4 spacing: 8 - visible: modelData && (modelData.previousMessageUserId !== modelData.userId || modelData.previousMessageDay !== modelData.day) + visible: (previousMessageUserId !== userId || previousMessageDay !== day) width: parentWidth - height: ((modelData && modelData.previousMessageDay !== modelData.day) ? dateBubble.height + 8 + userName.height : userName.height) + 8 + height: ((previousMessageDay !== day) ? dateBubble.height + 8 + userName.height : userName.height) + 8 Label { id: dateBubble anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - visible: modelData && modelData.previousMessageDay !== modelData.day - text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : "" + visible: previousMessageDay !== day + text: chat.model.formatDateSeparator(timestamp) color: Nheko.colors.text height: Math.round(fontMetrics.height * 1.4) width: contentWidth * 1.2 @@ -236,7 +239,7 @@ ScrollView { } Row { - height: userName.height + height: userName_.height spacing: 8 Avatar { @@ -244,10 +247,10 @@ ScrollView { width: Nheko.avatarSize height: Nheko.avatarSize - url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "" - displayName: modelData ? modelData.userName : "" - userid: modelData ? modelData.userId : "" - onClicked: chat.model.openUserProfile(modelData.userId) + url: chat.model.avatarUrl(userId).replace("mxc://", "image://MxcImage/") + displayName: userName + userid: userId + onClicked: chat.model.openUserProfile(userId) ToolTip.visible: avatarHover.hovered ToolTip.text: userid @@ -260,22 +263,22 @@ ScrollView { Connections { target: chat.model onRoomAvatarUrlChanged: { - messageUserAvatar.url = modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""; + messageUserAvatar.url = chat.model.avatarUrl(userId).replace("mxc://", "image://MxcImage/"); } onScrollToIndex: chat.positionViewAtIndex(index, ListView.Visible) } Label { - id: userName + id: userName_ - text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : "" - color: TimelineManager.userColor(modelData ? modelData.userId : "", Nheko.colors.window) + text: TimelineManager.escapeEmoji(userName) + color: TimelineManager.userColor(userId, Nheko.colors.window) textFormat: Text.RichText ToolTip.visible: displayNameHover.hovered - ToolTip.text: modelData ? modelData.userId : "" + ToolTip.text: userId TapHandler { - onSingleTapped: chat.model.openUserProfile(modelData.userId) + onSingleTapped: chat.model.openUserProfile(userId) } CursorShape { @@ -291,7 +294,7 @@ ScrollView { Label { color: Nheko.colors.buttonText - text: modelData ? TimelineManager.userStatus(modelData.userId) : "" + text: TimelineManager.userStatus(userId) textFormat: Text.PlainText elide: Text.ElideRight width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - Nheko.avatarSize @@ -307,7 +310,35 @@ ScrollView { delegate: Item { id: wrapper - property bool scrolledToThis: model.id === chat.model.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) + required property double proportionalHeight + required property int type + required property string typeString + required property int originalWidth + required property string blurhash + required property string body + required property string formattedBody + required property string eventId + required property string filename + required property string filesize + required property string url + required property string thumbnailUrl + required property bool isOnlyEmoji + required property bool isSender + required property bool isEncrypted + required property bool isEditable + required property bool isEdited + required property string replyTo + required property string userId + required property var reactions + required property int trustlevel + required property var timestamp + required property int status + required property int index + required property string previousMessageUserId + required property string day + required property string previousMessageDay + required property string userName + property bool scrolledToThis: eventId === chat.model.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined width: chat.delegateMaxWidth @@ -362,10 +393,15 @@ ScrollView { Loader { id: section - property var modelData: model property int parentWidth: parent.width + property string userId: wrapper.userId + property string previousMessageUserId: wrapper.previousMessageUserId + property string day: wrapper.day + property string previousMessageDay: wrapper.previousMessageDay + property string userName: wrapper.userName + property var timestamp: wrapper.timestamp - active: model.previousMessageUserId !== undefined && model.previousMessageUserId !== model.userId || model.previousMessageDay !== model.day + active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day //asynchronous: true sourceComponent: sectionHeader visible: status == Loader.Ready @@ -376,6 +412,30 @@ ScrollView { property alias hovered: hoverHandler.hovered + proportionalHeight: wrapper.proportionalHeight + type: chat.model, wrapper.type + typeString: wrapper.typeString + originalWidth: wrapper.originalWidth + blurhash: wrapper.blurhash + body: wrapper.body + formattedBody: wrapper.formattedBody + eventId: chat.model, wrapper.eventId + filename: wrapper.filename + filesize: wrapper.filesize + url: wrapper.url + thumbnailUrl: wrapper.thumbnailUrl + isOnlyEmoji: wrapper.isOnlyEmoji + isSender: wrapper.isSender + isEncrypted: wrapper.isEncrypted + isEditable: wrapper.isEditable + isEdited: wrapper.isEdited + replyTo: wrapper.replyTo + userId: wrapper.userId + userName: wrapper.userName + reactions: wrapper.reactions + trustlevel: wrapper.trustlevel + timestamp: wrapper.timestamp + status: wrapper.status y: section.visible && section.active ? section.y + section.height : 0 HoverHandler { @@ -386,7 +446,7 @@ ScrollView { if (hovered) { if (!messageActionHover.hovered) { messageActions.attached = timelinerow; - messageActions.model = model; + messageActions.model = timelinerow; } } } diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index 0de68fe8..54b4f20c 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -21,15 +21,30 @@ Rectangle { Reply { id: replyPreview + property var modelData: room ? room.getDump(room.reply, room.id) : { + } + visible: room && room.reply anchors.left: parent.left anchors.leftMargin: 2 * 22 + 3 * 16 anchors.right: closeReplyButton.left anchors.rightMargin: 2 * 22 + 3 * 16 anchors.bottom: parent.bottom - modelData: room ? room.getDump(room.reply, room.id) : { - } userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window) + blurhash: modelData.blurhash ?? "" + body: modelData.body ?? "" + formattedBody: modelData.formattedBody ?? "" + eventId: modelData.eventId ?? "" + filename: modelData.filename ?? "" + filesize: modelData.filesize ?? "" + proportionalHeight: modelData.proportionalHeight ?? 1 + type: modelData.type ?? MtxEvent.UnknownMessage + typeString: modelData.typeString ?? "" + url: modelData.url ?? "" + originalWidth: modelData.originalWidth ?? 0 + isOnlyEmoji: modelData.isOnlyEmoji ?? false + userId: modelData.userId ?? "" + userName: modelData.userName ?? "" } ImageButton { diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 739cc007..7e471d69 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -9,14 +9,17 @@ import im.nheko 1.0 ImageButton { id: indicator + required property int status + required property string eventId + width: 16 height: 16 hoverEnabled: true - changeColorOnHover: (model.state == MtxEvent.Read) - cursor: (model.state == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor - ToolTip.visible: hovered && model.state != MtxEvent.Empty + changeColorOnHover: (status == MtxEvent.Read) + cursor: (status == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor + ToolTip.visible: hovered && status != MtxEvent.Empty ToolTip.text: { - switch (model.state) { + switch (status) { case MtxEvent.Failed: return qsTr("Failed"); case MtxEvent.Sent: @@ -30,12 +33,12 @@ ImageButton { } } onClicked: { - if (model.state == MtxEvent.Read) - room.readReceiptsAction(model.id); + if (status == MtxEvent.Read) + room.readReceiptsAction(eventId); } image: { - switch (model.state) { + switch (status) { case MtxEvent.Failed: return ":/icons/icons/ui/remove-symbol.png"; case MtxEvent.Sent: diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index e66dd2de..c1cdaf9b 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -11,6 +11,33 @@ import QtQuick.Window 2.2 import im.nheko 1.0 Item { + id: r + + required property double proportionalHeight + required property int type + required property string typeString + required property int originalWidth + required property string blurhash + required property string body + required property string formattedBody + required property string eventId + required property string filename + required property string filesize + required property string url + required property string thumbnailUrl + required property bool isOnlyEmoji + required property bool isSender + required property bool isEncrypted + required property bool isEditable + required property bool isEdited + required property string replyTo + required property string userId + required property string userName + required property var reactions + required property int trustlevel + required property var timestamp + required property int status + anchors.left: parent.left anchors.right: parent.right height: row.height @@ -28,19 +55,21 @@ Item { TapHandler { acceptedButtons: Qt.RightButton - onSingleTapped: messageContextMenu.show(model.id, model.type, model.isSender, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) + onSingleTapped: messageContextMenu.show(eventId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) gesturePolicy: TapHandler.ReleaseWithinBounds } TapHandler { - onLongPressed: messageContextMenu.show(model.id, model.type, model.isSender, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) - onDoubleTapped: chat.model.reply = model.id + onLongPressed: messageContextMenu.show(eventId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) + onDoubleTapped: chat.model.reply = eventId gesturePolicy: TapHandler.ReleaseWithinBounds } RowLayout { id: row + property var replyData: chat.model.getDump(replyTo, eventId) + anchors.rightMargin: 1 anchors.leftMargin: Nheko.avatarSize + 16 anchors.left: parent.left @@ -55,9 +84,23 @@ Item { // fancy reply, if this is a reply Reply { - visible: model.replyTo - modelData: chat.model.getDump(model.replyTo, model.id) - userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.base) + visible: replyTo + userColor: TimelineManager.userColor(userId, Nheko.colors.base) + blurhash: row.replyData.blurhash ?? "" + body: row.replyData.body ?? "" + formattedBody: row.replyData.formattedBody ?? "" + eventId: row.replyData.eventId ?? "" + filename: row.replyData.filename ?? "" + filesize: row.replyData.filesize ?? "" + proportionalHeight: row.replyData.proportionalHeight ?? 1 + type: row.replyData.type ?? MtxEvent.UnknownMessage + typeString: row.replyData.typeString ?? "" + url: row.replyData.url ?? "" + originalWidth: row.replyData.originalWidth ?? 0 + isOnlyEmoji: row.replyData.isOnlyEmoji ?? false + userId: row.replyData.userId ?? "" + userName: row.replyData.userName ?? "" + thumbnailUrl: row.replyData.thumbnailUrl ?? "" } // actual message content @@ -65,14 +108,29 @@ Item { id: contentItem width: parent.width - modelData: model + blurhash: r.blurhash + body: r.body + formattedBody: r.formattedBody + eventId: r.eventId + filename: r.filename + filesize: r.filesize + proportionalHeight: r.proportionalHeight + type: r.type + typeString: r.typeString ?? "" + url: r.url + thumbnailUrl: r.thumbnailUrl + originalWidth: r.originalWidth + isOnlyEmoji: r.isOnlyEmoji + userId: r.userId + userName: r.userName + isReply: false } Reactions { id: reactionRow - reactions: model.reactions - eventId: model.id + reactions: r.reactions + eventId: r.eventId } } @@ -81,19 +139,21 @@ Item { Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredHeight: 16 width: 16 + status: r.status + eventId: r.eventId } EncryptionIndicator { visible: room.isEncrypted - encrypted: model.isEncrypted - trust: model.trustlevel + encrypted: isEncrypted + trust: trustlevel Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredHeight: 16 Layout.preferredWidth: 16 } Image { - visible: model.isEdited || model.id == chat.model.edit + visible: isEdited || eventId == chat.model.edit Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredHeight: 16 Layout.preferredWidth: 16 @@ -101,7 +161,7 @@ Item { width: 16 sourceSize.width: 16 sourceSize.height: 16 - source: "image://colorimage/:/icons/icons/ui/edit.png?" + ((model.id == chat.model.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText) + source: "image://colorimage/:/icons/icons/ui/edit.png?" + ((eventId == chat.model.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText) ToolTip.visible: editHovered.hovered ToolTip.text: qsTr("Edited") @@ -113,11 +173,11 @@ Item { Label { Layout.alignment: Qt.AlignRight | Qt.AlignTop - text: model.timestamp.toLocaleTimeString(Locale.ShortFormat) + text: timestamp.toLocaleTimeString(Locale.ShortFormat) width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) color: Nheko.inactiveColors.text ToolTip.visible: ma.hovered - ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) + ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate) HoverHandler { id: ma diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 0392c73a..4f2a2836 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -7,6 +7,10 @@ import QtQuick.Layouts 1.2 import im.nheko 1.0 Item { + required property string eventId + required property string filename + required property string filesize + height: row.height + 24 width: parent ? parent.width : undefined @@ -34,7 +38,7 @@ Item { } TapHandler { - onSingleTapped: room.saveMedia(model.data.id) + onSingleTapped: room.saveMedia(eventId) gesturePolicy: TapHandler.ReleaseWithinBounds } @@ -49,20 +53,20 @@ Item { id: col Text { - id: filename + id: filename_ Layout.fillWidth: true - text: model.data.filename + text: filename textFormat: Text.PlainText elide: Text.ElideRight color: Nheko.colors.text } Text { - id: filesize + id: filesize_ Layout.fillWidth: true - text: model.data.filesize + text: filesize textFormat: Text.PlainText elide: Text.ElideRight color: Nheko.colors.text @@ -77,7 +81,7 @@ Item { z: -1 radius: 10 height: row.height + 24 - width: 44 + 24 + 24 + Math.max(Math.min(filesize.width, filesize.implicitWidth), Math.min(filename.width, filename.implicitWidth)) + width: 44 + 24 + 24 + Math.max(Math.min(filesize_.width, filesize_.implicitWidth), Math.min(filename_.width, filename_.implicitWidth)) } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index ce8e779c..b432018c 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -6,20 +6,28 @@ import QtQuick 2.12 import im.nheko 1.0 Item { - property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) - property double tempHeight: tempWidth * model.data.proportionalHeight - property double divisor: model.isReply ? 5 : 3 + required property int type + required property int originalWidth + required property double proportionalHeight + required property string url + required property string blurhash + required property string body + required property string filename + required property bool isReply + property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 200 : originalWidth) + property double tempHeight: tempWidth * proportionalHeight + property double divisor: isReply ? 5 : 3 property bool tooHigh: tempHeight > timelineView.height / divisor height: Math.round(tooHigh ? timelineView.height / divisor : tempHeight) - width: Math.round(tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth) + width: Math.round(tooHigh ? (timelineView.height / divisor) / proportionalHeight : tempWidth) Image { - id: blurhash + id: blurhash_ anchors.fill: parent visible: img.status != Image.Ready - source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + Nheko.colors.buttonText) + source: blurhash ? ("image://blurhash/" + blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + Nheko.colors.buttonText) asynchronous: true fillMode: Image.PreserveAspectFit sourceSize.width: parent.width @@ -30,16 +38,16 @@ Item { id: img anchors.fill: parent - source: model.data.url.replace("mxc://", "image://MxcImage/") + source: url.replace("mxc://", "image://MxcImage/") asynchronous: true fillMode: Image.PreserveAspectFit smooth: true mipmap: true TapHandler { - enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready + enabled: type == MtxEvent.ImageMessage && img.status == Image.Ready onSingleTapped: { - TimelineManager.openImageOverlay(model.data.url, model.data.id); + TimelineManager.openImageOverlay(url, room.data.eventId); eventPoint.accepted = true; } gesturePolicy: TapHandler.ReleaseWithinBounds @@ -73,7 +81,7 @@ Item { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter // See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530 - text: model.data.filename ? model.data.filename : model.data.body + text: filename ? filename : body color: Nheko.colors.text } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 1befcec3..4b32751c 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -6,32 +6,41 @@ import QtQuick 2.6 import im.nheko 1.0 Item { - property alias modelData: model.data - property alias isReply: model.isReply + id: d + + required property bool isReply property alias child: chooser.child property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width + required property double proportionalHeight + required property int type + required property string typeString + required property int originalWidth + required property string blurhash + required property string body + required property string formattedBody + required property string eventId + required property string filename + required property string filesize + required property string url + required property string thumbnailUrl + required property bool isOnlyEmoji + required property string userId + required property string userName height: chooser.childrenRect.height - // Workaround to have an assignable global property - Item { - id: model - - property var data - property bool isReply: false - } - DelegateChooser { id: chooser //role: "type" //< not supported in our custom implementation, have to use roleValue - roleValue: model.data.type + roleValue: type anchors.fill: parent DelegateChoice { roleValue: MtxEvent.UnknownMessage Placeholder { + typeString: d.typeString text: "Unretrieved event" } @@ -41,6 +50,10 @@ Item { roleValue: MtxEvent.TextMessage TextMessage { + formatted: d.formattedBody + body: d.body + isOnlyEmoji: d.isOnlyEmoji + isReply: d.isReply } } @@ -49,6 +62,10 @@ Item { roleValue: MtxEvent.NoticeMessage NoticeMessage { + formatted: d.formattedBody + body: d.body + isOnlyEmoji: d.isOnlyEmoji + isReply: d.isReply } } @@ -57,8 +74,11 @@ Item { roleValue: MtxEvent.EmoteMessage NoticeMessage { - formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody - color: TimelineManager.userColor(modelData.userId, Nheko.colors.window) + formatted: TimelineManager.escapeEmoji(d.userName) + " " + d.formattedBody + color: TimelineManager.userColor(d.userId, Nheko.colors.window) + body: d.body + isOnlyEmoji: d.isOnlyEmoji + isReply: d.isReply } } @@ -67,6 +87,14 @@ Item { roleValue: MtxEvent.ImageMessage ImageMessage { + type: d.type + originalWidth: d.originalWidth + proportionalHeight: d.proportionalHeight + url: d.url + blurhash: d.blurhash + body: d.body + filename: d.filename + isReply: d.isReply } } @@ -75,6 +103,14 @@ Item { roleValue: MtxEvent.Sticker ImageMessage { + type: d.type + originalWidth: d.originalWidth + proportionalHeight: d.proportionalHeight + url: d.url + blurhash: d.blurhash + body: d.body + filename: d.filename + isReply: d.isReply } } @@ -83,6 +119,9 @@ Item { roleValue: MtxEvent.FileMessage FileMessage { + eventId: d.eventId + filename: d.filename + filesize: d.filesize } } @@ -91,6 +130,14 @@ Item { roleValue: MtxEvent.VideoMessage PlayableMediaMessage { + proportionalHeight: d.proportionalHeight + type: d.type + originalWidth: d.originalWidth + thumbnailUrl: d.thumbnailUrl + eventId: d.eventId + url: d.url + body: d.body + filesize: d.filesize } } @@ -99,6 +146,14 @@ Item { roleValue: MtxEvent.AudioMessage PlayableMediaMessage { + proportionalHeight: d.proportionalHeight + type: d.type + originalWidth: d.originalWidth + thumbnailUrl: d.thumbnailUrl + eventId: d.eventId + url: d.url + body: d.body + filesize: d.filesize } } @@ -134,7 +189,10 @@ Item { roleValue: MtxEvent.Name NoticeMessage { - text: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name") + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name") } } @@ -143,7 +201,10 @@ Item { roleValue: MtxEvent.Topic NoticeMessage { - text: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic") + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic") } } @@ -152,7 +213,10 @@ Item { roleValue: MtxEvent.Avatar NoticeMessage { - text: qsTr("%1 changed the room avatar").arg(model.data.userName) + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: qsTr("%1 changed the room avatar").arg(d.userName) } } @@ -161,7 +225,10 @@ Item { roleValue: MtxEvent.RoomCreate NoticeMessage { - text: qsTr("%1 created and configured room: %2").arg(model.data.userName).arg(model.data.roomId) + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(model.data.roomId) } } @@ -170,14 +237,17 @@ Item { roleValue: MtxEvent.CallInvite NoticeMessage { - text: { + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: { switch (model.data.callType) { case "voice": - return qsTr("%1 placed a voice call.").arg(model.data.userName); + return qsTr("%1 placed a voice call.").arg(d.userName); case "video": - return qsTr("%1 placed a video call.").arg(model.data.userName); + return qsTr("%1 placed a video call.").arg(d.userName); default: - return qsTr("%1 placed a call.").arg(model.data.userName); + return qsTr("%1 placed a call.").arg(d.userName); } } } @@ -188,7 +258,10 @@ Item { roleValue: MtxEvent.CallAnswer NoticeMessage { - text: qsTr("%1 answered the call.").arg(model.data.userName) + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: qsTr("%1 answered the call.").arg(d.userName) } } @@ -197,7 +270,10 @@ Item { roleValue: MtxEvent.CallHangUp NoticeMessage { - text: qsTr("%1 ended the call.").arg(model.data.userName) + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: qsTr("%1 ended the call.").arg(d.userName) } } @@ -206,7 +282,10 @@ Item { roleValue: MtxEvent.CallCandidates NoticeMessage { - text: qsTr("Negotiating call...") + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: qsTr("Negotiating call...") } } @@ -216,7 +295,10 @@ Item { roleValue: MtxEvent.PowerLevels NoticeMessage { - text: room.formatPowerLevelEvent(model.data.id) + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: room.formatPowerLevelEvent(d.eventId) } } @@ -225,7 +307,10 @@ Item { roleValue: MtxEvent.RoomJoinRules NoticeMessage { - text: room.formatJoinRuleEvent(model.data.id) + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: room.formatJoinRuleEvent(d.eventId) } } @@ -234,7 +319,10 @@ Item { roleValue: MtxEvent.RoomHistoryVisibility NoticeMessage { - text: room.formatHistoryVisibilityEvent(model.data.id) + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: room.formatHistoryVisibilityEvent(d.eventId) } } @@ -243,7 +331,10 @@ Item { roleValue: MtxEvent.RoomGuestAccess NoticeMessage { - text: room.formatGuestAccessEvent(model.data.id) + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: room.formatGuestAccessEvent(d.eventId) } } @@ -252,7 +343,10 @@ Item { roleValue: MtxEvent.Member NoticeMessage { - text: room.formatMemberEvent(model.data.id) + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: room.formatMemberEvent(d.eventId) } } @@ -261,7 +355,10 @@ Item { roleValue: MtxEvent.KeyVerificationRequest NoticeMessage { - text: "KeyVerificationRequest" + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: "KeyVerificationRequest" } } @@ -270,7 +367,10 @@ Item { roleValue: MtxEvent.KeyVerificationStart NoticeMessage { - text: "KeyVerificationStart" + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: "KeyVerificationStart" } } @@ -279,7 +379,10 @@ Item { roleValue: MtxEvent.KeyVerificationReady NoticeMessage { - text: "KeyVerificationReady" + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: "KeyVerificationReady" } } @@ -288,7 +391,10 @@ Item { roleValue: MtxEvent.KeyVerificationCancel NoticeMessage { - text: "KeyVerificationCancel" + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: "KeyVerificationCancel" } } @@ -297,7 +403,10 @@ Item { roleValue: MtxEvent.KeyVerificationKey NoticeMessage { - text: "KeyVerificationKey" + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: "KeyVerificationKey" } } @@ -306,7 +415,10 @@ Item { roleValue: MtxEvent.KeyVerificationMac NoticeMessage { - text: "KeyVerificationMac" + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: "KeyVerificationMac" } } @@ -315,7 +427,10 @@ Item { roleValue: MtxEvent.KeyVerificationDone NoticeMessage { - text: "KeyVerificationDone" + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: "KeyVerificationDone" } } @@ -324,7 +439,10 @@ Item { roleValue: MtxEvent.KeyVerificationDone NoticeMessage { - text: "KeyVerificationDone" + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: "KeyVerificationDone" } } @@ -333,13 +451,17 @@ Item { roleValue: MtxEvent.KeyVerificationAccept NoticeMessage { - text: "KeyVerificationAccept" + body: formatted + isOnlyEmoji: false + isReply: d.isReply + formatted: "KeyVerificationAccept" } } DelegateChoice { Placeholder { + typeString: d.typeString } } diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml index c4fc6cc3..692fe4a9 100644 --- a/resources/qml/delegates/Placeholder.qml +++ b/resources/qml/delegates/Placeholder.qml @@ -6,7 +6,9 @@ import ".." import im.nheko 1.0 MatrixText { - text: qsTr("unimplemented event: ") + model.data.typeString + required property string typeString + + text: qsTr("unimplemented event: ") + typeString width: parent ? parent.width : undefined color: Nheko.inactiveColors.text } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 83864db9..fd764d52 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -12,10 +12,21 @@ import im.nheko 1.0 Rectangle { id: bg + required property double proportionalHeight + required property int type + required property int originalWidth + required property string thumbnailUrl + required property string eventId + required property string url + required property string body + required property string filesize + radius: 10 color: Nheko.colors.alternateBase height: Math.round(content.height + 24) width: parent ? parent.width : undefined + ListView.onPooled: height = 4 + ListView.onReused: height = Math.round(content.height + 24) Column { id: content @@ -26,18 +37,18 @@ Rectangle { Rectangle { id: videoContainer - property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width) - property double tempHeight: tempWidth * model.data.proportionalHeight - property double divisor: model.isReply ? 4 : 2 + property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 400 : originalWidth) + property double tempHeight: tempWidth * proportionalHeight + property double divisor: isReply ? 4 : 2 property bool tooHigh: tempHeight > timelineView.height / divisor - visible: model.data.type == MtxEvent.VideoMessage + visible: type == MtxEvent.VideoMessage height: tooHigh ? timelineView.height / divisor : tempHeight - width: tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth + width: tooHigh ? (timelineView.height / divisor) / proportionalHeight : tempWidth Image { anchors.fill: parent - source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/") + source: thumbnailUrl.replace("mxc://", "image://MxcImage/") asynchronous: true fillMode: Image.PreserveAspectFit @@ -121,7 +132,7 @@ Rectangle { onClicked: { switch (button.state) { case "": - room.cacheMedia(model.data.id); + room.cacheMedia(eventId); break; case "stopped": media.play(); @@ -176,7 +187,7 @@ Rectangle { Connections { target: room onMediaCached: { - if (mxcUrl == model.data.url) { + if (mxcUrl == url) { media.source = cacheUrl; button.state = "stopped"; console.log("media loaded: " + mxcUrl + " at " + cacheUrl); @@ -192,14 +203,14 @@ Rectangle { Text { Layout.fillWidth: true - text: model.data.body + text: body elide: Text.ElideRight color: Nheko.colors.text } Text { Layout.fillWidth: true - text: model.data.filesize + text: filesize textFormat: Text.PlainText elide: Text.ElideRight color: Nheko.colors.text diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index b5090529..08f13955 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -9,16 +9,30 @@ import QtQuick.Window 2.2 import im.nheko 1.0 Item { - id: replyComponent + id: r - property alias modelData: reply.modelData property color userColor: "red" + property double proportionalHeight + property int type + property string typeString + property int originalWidth + property string blurhash + property string body + property string formattedBody + property string eventId + property string filename + property string filesize + property string url + property bool isOnlyEmoji + property string userId + property string userName + property string thumbnailUrl width: parent.width height: replyContainer.height TapHandler { - onSingleTapped: chat.model.showEvent(modelData.id) + onSingleTapped: chat.model.showEvent(eventId) gesturePolicy: TapHandler.ReleaseWithinBounds } @@ -33,7 +47,7 @@ Item { anchors.top: replyContainer.top anchors.bottom: replyContainer.bottom width: 4 - color: TimelineManager.userColor(reply.modelData.userId, Nheko.colors.window) + color: TimelineManager.userColor(userId, Nheko.colors.window) } Column { @@ -44,14 +58,14 @@ Item { width: parent.width - 8 Text { - id: userName + id: userName_ - text: TimelineManager.escapeEmoji(reply.modelData.userName) - color: replyComponent.userColor + text: TimelineManager.escapeEmoji(userName) + color: r.userColor textFormat: Text.RichText TapHandler { - onSingleTapped: chat.model.openUserProfile(reply.modelData.userId) + onSingleTapped: chat.model.openUserProfile(userId) gesturePolicy: TapHandler.ReleaseWithinBounds } @@ -60,6 +74,21 @@ Item { MessageDelegate { id: reply + blurhash: r.blurhash + body: r.body + formattedBody: r.formattedBody + eventId: r.eventId + filename: r.filename + filesize: r.filesize + proportionalHeight: r.proportionalHeight + type: r.type + typeString: r.typeString ?? "" + url: r.url + thumbnailUrl: r.thumbnailUrl + originalWidth: r.originalWidth + isOnlyEmoji: r.isOnlyEmoji + userId: r.userId + userName: r.userName enabled: false width: parent.width isReply: true @@ -72,7 +101,7 @@ Item { z: -1 height: replyContainer.height - width: Math.min(Math.max(reply.implicitWidth, userName.implicitWidth) + 8 + 4, parent.width) + width: Math.min(Math.max(reply.implicitWidth, userName_.implicitWidth) + 8 + 4, parent.width) color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.1) } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index cd46f8ca..58aa99ca 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -6,8 +6,11 @@ import ".." import im.nheko 1.0 MatrixText { - property string formatted: model.data.formattedBody - property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body + required property string body + required property bool isOnlyEmoji + required property bool isReply + required property string formatted + property string copyText: selectedText ? getText(selectionStart, selectionEnd) : body // table border-collapse doesn't seem to work text: " @@ -31,5 +34,5 @@ MatrixText { height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined clip: isReply selectByMouse: !Settings.mobileMode && !isReply - font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize + font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize } diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml index 3106c382..d44c5edf 100644 --- a/resources/qml/voip/ActiveCallBar.qml +++ b/resources/qml/voip/ActiveCallBar.qml @@ -35,7 +35,7 @@ Rectangle { height: Nheko.avatarSize url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") displayName: CallManager.callParty - onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id) + onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) } Label { diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml index 2d8e3040..f6c1ecde 100644 --- a/resources/qml/voip/CallInviteBar.qml +++ b/resources/qml/voip/CallInviteBar.qml @@ -42,7 +42,7 @@ Rectangle { height: Nheko.avatarSize url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") displayName: CallManager.callParty - onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id) + onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) } Label { diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml index 97e39e02..5f564853 100644 --- a/resources/qml/voip/PlaceCall.qml +++ b/resources/qml/voip/PlaceCall.qml @@ -79,7 +79,7 @@ Popup { height: Nheko.avatarSize url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") displayName: room.roomName - onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id) + onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) } Button { diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 4cb97e07..ab11f99b 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -427,11 +427,11 @@ TimelineModel::roleNames() const {Filename, "filename"}, {Filesize, "filesize"}, {MimeType, "mimetype"}, - {Height, "height"}, - {Width, "width"}, + {OriginalHeight, "originalHeight"}, + {OriginalWidth, "originalWidth"}, {ProportionalHeight, "proportionalHeight"}, - {Id, "id"}, - {State, "state"}, + {EventId, "eventId"}, + {State, "status"}, {IsEdited, "isEdited"}, {IsEditable, "isEditable"}, {IsEncrypted, "isEncrypted"}, @@ -556,9 +556,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return QVariant(utils::humanReadableFileSize(filesize(event))); case MimeType: return QVariant(QString::fromStdString(mimetype(event))); - case Height: + case OriginalHeight: return QVariant(qulonglong{media_height(event)}); - case Width: + case OriginalWidth: return QVariant(qulonglong{media_width(event)}); case ProportionalHeight: { auto w = media_width(event); @@ -569,7 +569,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return QVariant(prop > 0 ? prop : 1.); } - case Id: { + case EventId: { if (auto replaces = relations(event).replaces()) return QVariant(QString::fromStdString(replaces.value())); else @@ -660,11 +660,11 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r m.insert(names[Filename], data(event, static_cast(Filename))); m.insert(names[Filesize], data(event, static_cast(Filesize))); m.insert(names[MimeType], data(event, static_cast(MimeType))); - m.insert(names[Height], data(event, static_cast(Height))); - m.insert(names[Width], data(event, static_cast(Width))); + m.insert(names[OriginalHeight], data(event, static_cast(OriginalHeight))); + m.insert(names[OriginalWidth], data(event, static_cast(OriginalWidth))); m.insert(names[ProportionalHeight], data(event, static_cast(ProportionalHeight))); - m.insert(names[Id], data(event, static_cast(Id))); + m.insert(names[EventId], data(event, static_cast(EventId))); m.insert(names[State], data(event, static_cast(State))); m.insert(names[IsEdited], data(event, static_cast(IsEdited))); m.insert(names[IsEditable], data(event, static_cast(IsEditable))); diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index f093acb4..46153732 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -192,10 +192,10 @@ public: Filename, Filesize, MimeType, - Height, - Width, + OriginalHeight, + OriginalWidth, ProportionalHeight, - Id, + EventId, State, IsEdited, IsEditable, @@ -245,6 +245,11 @@ public: Q_INVOKABLE void showEvent(QString eventId); Q_INVOKABLE void copyLinkToEvent(QString eventId) const; void cacheMedia(QString eventId, std::function callback); + Q_INVOKABLE void sendReset() + { + beginResetModel(); + endResetModel(); + } std::vector<::Reaction> reactions(const std::string &event_id) {