diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6334565d..01070a82 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -359,6 +359,8 @@ set(SRC_FILES
src/timeline/CommunitiesModel.h
src/timeline/DelegateChooser.cpp
src/timeline/DelegateChooser.h
+ src/timeline/EventDelegateChooser.cpp
+ src/timeline/EventDelegateChooser.h
src/timeline/EventStore.cpp
src/timeline/EventStore.h
src/timeline/InputBar.cpp
@@ -882,6 +884,7 @@ target_link_libraries(nheko PRIVATE
Qt::Gui
Qt::Multimedia
Qt::Qml
+ Qt::QmlPrivate
Qt::QuickControls2
qt6keychain
nlohmann_json::nlohmann_json
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index a0ff0ff1..fde7ee57 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -20,6 +20,7 @@ Item {
property int availableWidth: width
property int padding: Nheko.paddingMedium
property string searchString: ""
+ property Room roommodel: room
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
Connections {
@@ -58,173 +59,33 @@ Item {
spacing: 2
verticalLayoutDirection: ListView.BottomToTop
- delegate: Item {
+ delegate: EventDelegateChooser {
id: wrapper
-
- required property string blurhash
- required property string body
- required property string callType
- required property var day
- required property string duration
- required property int encryptionError
- required property string eventId
- required property string filename
- required property string filesize
- required property string formattedBody
- required property int index
- required property bool isEditable
- required property bool isEdited
- required property bool isEncrypted
- required property bool isOnlyEmoji
- required property bool isSender
- required property bool isStateEvent
- required property int notificationlevel
- required property int originalWidth
- property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day)
- property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent)
- property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId)
- required property double proportionalHeight
- required property var reactions
- required property int relatedEventCacheBuster
- required property string replyTo
- required property string roomName
- required property string roomTopic
- property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
- required property int status
- required property string threadId
- required property string thumbnailUrl
- required property var timestamp
- required property int trustlevel
- required property int type
- required property string typeString
- required property string url
- required property string userId
- required property string userName
- required property int userPowerlevel
-
ListView.delayRemove: true
- anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
- height: (section.item?.height ?? 0) + timelinerow.height
width: chat.delegateMaxWidth
-
- Loader {
- id: section
-
- property var day: wrapper.day
- property bool isSender: wrapper.isSender
- property bool isStateEvent: wrapper.isStateEvent
- property int parentWidth: parent.width
- property var previousMessageDay: wrapper.previousMessageDay
- property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent
- property string previousMessageUserId: wrapper.previousMessageUserId
- property date timestamp: wrapper.timestamp
- property string userId: wrapper.userId
- property string userName: wrapper.userName
- property int userPowerlevel: wrapper.userPowerlevel
-
- active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent
- //asynchronous: true
- sourceComponent: sectionHeader
- visible: status == Loader.Ready
- z: 4
- }
- TimelineRow {
- id: timelinerow
-
- blurhash: wrapper.blurhash
- body: wrapper.body
- callType: wrapper.callType
- duration: wrapper.duration
- encryptionError: wrapper.encryptionError
- eventId: chat.model, wrapper.eventId
- filename: wrapper.filename
- filesize: wrapper.filesize
- formattedBody: wrapper.formattedBody
- index: wrapper.index
- isEditable: wrapper.isEditable
- isEdited: wrapper.isEdited
- isEncrypted: wrapper.isEncrypted
- isOnlyEmoji: wrapper.isOnlyEmoji
- isSender: wrapper.isSender
- isStateEvent: wrapper.isStateEvent
- notificationlevel: wrapper.notificationlevel
- originalWidth: wrapper.originalWidth
- proportionalHeight: wrapper.proportionalHeight
- reactions: wrapper.reactions
- relatedEventCacheBuster: wrapper.relatedEventCacheBuster
- replyTo: wrapper.replyTo
- roomName: wrapper.roomName
- roomTopic: wrapper.roomTopic
- status: wrapper.status
- threadId: wrapper.threadId
- thumbnailUrl: wrapper.thumbnailUrl
- timestamp: wrapper.timestamp
- trustlevel: wrapper.trustlevel
- type: chat.model, wrapper.type
- typeString: wrapper.typeString
- url: wrapper.url
- userId: wrapper.userId
- userName: wrapper.userName
- width: wrapper.width
- y: section.visible && section.active ? section.y + section.height : 0
-
- background: Rectangle {
- id: scrollHighlight
-
- color: palette.highlight
- enabled: false
- opacity: 0
- visible: true
- z: 1
-
- states: State {
- name: "revealed"
- when: wrapper.scrolledToThis
- }
- transitions: Transition {
- from: ""
- to: "revealed"
-
- SequentialAnimation {
- PropertyAnimation {
- duration: 500
- easing.type: Easing.InOutQuad
- from: 0
- properties: "opacity"
- target: scrollHighlight
- to: 1
- }
- PropertyAnimation {
- duration: 500
- easing.type: Easing.InOutQuad
- from: 1
- properties: "opacity"
- target: scrollHighlight
- to: 0
- }
- ScriptAction {
- script: room.eventShown()
- }
- }
- }
- }
-
- onHoveredChanged: {
- if (!Settings.mobileMode && hovered) {
- if (!messageActions.hovered) {
- messageActions.attached = timelinerow;
- messageActions.model = timelinerow;
- }
- }
+ height: main?.height ?? 10
+ room: chatRoot.roommodel
+
+ EventDelegateChoice {
+ roleValues: [
+ MtxEvent.TextMessage,
+ MtxEvent.NoticeMessage,
+ ]
+ TextArea {
+ required property string body
+
+ width: parent.width
+ text: body
}
}
- Connections {
- function onMovementEnded() {
- if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
- chat.model.currentIndex = index;
- }
- target: chat
+ EventDelegateChoice {
+ roleValues: [
+ ]
+ TextArea {
+ width: parent.width
+ text: "Unsupported"
+ }
}
}
footer: Item {
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 16a31a3c..64fa80b1 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -147,7 +147,7 @@ AbstractButton {
columns: Settings.bubbles ? 1 : 2
rowSpacing: 0
rows: Settings.bubbles ? 3 : 2
-
+/*
anchors {
left: parent.left
leftMargin: 4
@@ -230,6 +230,7 @@ AbstractButton {
userId: r.userId
userName: r.userName
}
+ */
Row {
id: metadata
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 4d4983ac..64eb65a3 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -95,37 +95,11 @@ AbstractButton {
onClicked: room.openUserProfile(userId)
}
- MessageDelegate {
+ Rectangle {
Layout.leftMargin: 4
- Layout.preferredHeight: height
- 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
- duration: r.duration
- originalWidth: r.originalWidth
- isOnlyEmoji: r.isOnlyEmoji
- isStateEvent: r.isStateEvent
- userId: r.userId
- userName: r.userName
- roomTopic: r.roomTopic
- roomName: r.roomName
- callType: r.callType
- relatedEventCacheBuster: r.relatedEventCacheBuster
- encryptionError: r.encryptionError
- // This is disabled so that left clicking the reply goes to its location
- enabled: false
+ Layout.preferredHeight: 20
Layout.fillWidth: true
- isReply: true
- keepFullText: r.keepFullText
+ color: "green"
}
}
diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp
new file mode 100644
index 00000000..7618e20b
--- /dev/null
+++ b/src/timeline/EventDelegateChooser.cpp
@@ -0,0 +1,244 @@
+// SPDX-FileCopyrightText: Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "EventDelegateChooser.h"
+#include "TimelineModel.h"
+
+#include "Logging.h"
+
+#include <QQmlEngine>
+#include <QtGlobal>
+
+// privat qt headers to access required properties
+#include <QtQml/private/qqmlincubator_p.h>
+#include <QtQml/private/qqmlobjectcreator_p.h>
+
+QQmlComponent *
+EventDelegateChoice::delegate() const
+{
+ return delegate_;
+}
+
+void
+EventDelegateChoice::setDelegate(QQmlComponent *delegate)
+{
+ if (delegate != delegate_) {
+ delegate_ = delegate;
+ emit delegateChanged();
+ emit changed();
+ }
+}
+
+QList<int>
+EventDelegateChoice::roleValues() const
+{
+ return roleValues_;
+}
+
+void
+EventDelegateChoice::setRoleValues(const QList<int> &value)
+{
+ if (value != roleValues_) {
+ roleValues_ = value;
+ emit roleValuesChanged();
+ emit changed();
+ }
+}
+
+QQmlListProperty<EventDelegateChoice>
+EventDelegateChooser::choices()
+{
+ return QQmlListProperty<EventDelegateChoice>(this,
+ this,
+ &EventDelegateChooser::appendChoice,
+ &EventDelegateChooser::choiceCount,
+ &EventDelegateChooser::choice,
+ &EventDelegateChooser::clearChoices);
+}
+
+void
+EventDelegateChooser::appendChoice(QQmlListProperty<EventDelegateChoice> *p, EventDelegateChoice *c)
+{
+ EventDelegateChooser *dc = static_cast<EventDelegateChooser *>(p->object);
+ dc->choices_.append(c);
+}
+
+qsizetype
+EventDelegateChooser::choiceCount(QQmlListProperty<EventDelegateChoice> *p)
+{
+ return static_cast<EventDelegateChooser *>(p->object)->choices_.count();
+}
+EventDelegateChoice *
+EventDelegateChooser::choice(QQmlListProperty<EventDelegateChoice> *p, qsizetype index)
+{
+ return static_cast<EventDelegateChooser *>(p->object)->choices_.at(index);
+}
+void
+EventDelegateChooser::clearChoices(QQmlListProperty<EventDelegateChoice> *p)
+{
+ static_cast<EventDelegateChooser *>(p->object)->choices_.clear();
+}
+
+void
+EventDelegateChooser::componentComplete()
+{
+ QQuickItem::componentComplete();
+ // eventIncubator.reset(eventIndex);
+}
+
+void
+EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj)
+{
+ auto item = qobject_cast<QQuickItem *>(obj);
+ if (!item)
+ return;
+
+ item->setParentItem(&chooser);
+
+ auto roleNames = chooser.room_->roleNames();
+ QHash<QByteArray, int> nameToRole;
+ for (const auto &[k, v] : roleNames.asKeyValueRange()) {
+ nameToRole.insert(v, k);
+ }
+
+ QHash<int, int> roleToPropIdx;
+ std::vector<QModelRoleData> roles;
+
+ // Workaround for https://bugreports.qt.io/browse/QTBUG-98846
+ QHash<QString, RequiredPropertyKey> requiredProperties;
+ for (const auto &[propKey, prop] :
+ QQmlIncubatorPrivate::get(this)->requiredProperties()->asKeyValueRange()) {
+ requiredProperties.insert(prop.propertyName, propKey);
+ }
+
+ // collect required properties
+ auto mo = obj->metaObject();
+ for (int i = 0; i < mo->propertyCount(); i++) {
+ auto prop = mo->property(i);
+ // nhlog::ui()->critical("Found prop {}", prop.name());
+ // See https://bugreports.qt.io/browse/QTBUG-98846
+ if (!prop.isRequired() && !requiredProperties.contains(prop.name()))
+ continue;
+
+ if (auto role = nameToRole.find(prop.name()); role != nameToRole.end()) {
+ roleToPropIdx.insert(*role, i);
+ roles.emplace_back(*role);
+
+ nhlog::ui()->critical("Found prop {}, idx {}, role {}", prop.name(), i, *role);
+ } else {
+ nhlog::ui()->critical("Required property {} not found in model!", prop.name());
+ }
+ }
+
+ nhlog::ui()->debug("Querying data for id {}", currentId.toStdString());
+ chooser.room_->multiData(currentId, forReply ? chooser.eventId_ : QString(), roles);
+
+ QVariantMap rolesToSet;
+ for (const auto &role : roles) {
+ const auto &roleName = roleNames[role.role()];
+ nhlog::ui()->critical("Setting role {}, {}", role.role(), roleName.toStdString());
+
+ mo->property(roleToPropIdx[role.role()]).write(obj, role.data());
+ rolesToSet.insert(roleName, role.data());
+
+ if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end())
+ QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req);
+ }
+
+ // setInitialProperties(rolesToSet);
+
+ auto update =
+ [this, obj, roleToPropIdx = std::move(roleToPropIdx)](const QList<int> &changedRoles) {
+ std::vector<QModelRoleData> rolesToRequest;
+
+ if (changedRoles.empty()) {
+ for (auto role : roleToPropIdx.keys())
+ rolesToRequest.emplace_back(role);
+ } else {
+ for (auto role : changedRoles) {
+ if (roleToPropIdx.contains(role)) {
+ rolesToRequest.emplace_back(role);
+ }
+ }
+ }
+
+ if (rolesToRequest.empty())
+ return;
+
+ auto mo = obj->metaObject();
+ chooser.room_->multiData(
+ currentId, forReply ? chooser.eventId_ : QString(), rolesToRequest);
+ for (const auto &role : rolesToRequest) {
+ mo->property(roleToPropIdx[role.role()]).write(obj, role.data());
+ }
+ };
+
+ if (!forReply) {
+ auto row = chooser.room_->idToIndex(currentId);
+ connect(chooser.room_,
+ &QAbstractItemModel::dataChanged,
+ obj,
+ [row, update](const QModelIndex &topLeft,
+ const QModelIndex &bottomRight,
+ const QList<int> &changedRoles) {
+ if (row < topLeft.row() || row > bottomRight.row())
+ return;
+
+ update(changedRoles);
+ });
+ }
+}
+
+void
+EventDelegateChooser::DelegateIncubator::reset(QString id)
+{
+ if (!chooser.room_ || id.isEmpty())
+ return;
+
+ nhlog::ui()->debug("Reset with id {}, reply {}", id.toStdString(), forReply);
+
+ this->currentId = id;
+
+ auto role =
+ chooser.room_
+ ->dataById(id, TimelineModel::Roles::Type, forReply ? chooser.eventId_ : QString())
+ .toInt();
+
+ for (const auto choice : qAsConst(chooser.choices_)) {
+ const auto &choiceValue = choice->roleValues();
+ if (choiceValue.contains(role) || choiceValue.empty()) {
+ if (auto child = qobject_cast<QQuickItem *>(object())) {
+ child->setParentItem(nullptr);
+ }
+
+ choice->delegate()->create(*this, QQmlEngine::contextForObject(&chooser));
+ return;
+ }
+ }
+}
+
+void
+EventDelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status)
+{
+ if (status == QQmlIncubator::Ready) {
+ auto child = qobject_cast<QQuickItem *>(object());
+ if (child == nullptr) {
+ nhlog::ui()->error("Delegate has to be derived of Item!");
+ return;
+ }
+
+ child->setParentItem(&chooser);
+ QQmlEngine::setObjectOwnership(child, QQmlEngine::ObjectOwnership::JavaScriptOwnership);
+ if (forReply)
+ emit chooser.replyChanged();
+ else
+ emit chooser.mainChanged();
+
+ } else if (status == QQmlIncubator::Error) {
+ auto errors_ = errors();
+ for (const auto &e : qAsConst(errors_))
+ nhlog::ui()->error("Error instantiating delegate: {}", e.toString().toStdString());
+ }
+}
+
diff --git a/src/timeline/EventDelegateChooser.h b/src/timeline/EventDelegateChooser.h
new file mode 100644
index 00000000..ce22ca3a
--- /dev/null
+++ b/src/timeline/EventDelegateChooser.h
@@ -0,0 +1,136 @@
+// SPDX-FileCopyrightText: Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt
+// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent
+
+#pragma once
+
+#include <QAbstractItemModel>
+#include <QQmlComponent>
+#include <QQmlIncubator>
+#include <QQmlListProperty>
+#include <QQuickItem>
+#include <QtCore/QObject>
+#include <QtCore/QVariant>
+
+#include "TimelineModel.h"
+
+class EventDelegateChoice : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ Q_CLASSINFO("DefaultProperty", "delegate")
+
+public:
+ Q_PROPERTY(QList<int> roleValues READ roleValues WRITE setRoleValues NOTIFY roleValuesChanged
+ REQUIRED FINAL)
+ Q_PROPERTY(
+ QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged REQUIRED FINAL)
+
+ [[nodiscard]] QQmlComponent *delegate() const;
+ void setDelegate(QQmlComponent *delegate);
+
+ [[nodiscard]] QList<int> roleValues() const;
+ void setRoleValues(const QList<int> &value);
+
+signals:
+ void delegateChanged();
+ void roleValuesChanged();
+ void changed();
+
+private:
+ QList<int> roleValues_;
+ QQmlComponent *delegate_ = nullptr;
+};
+
+class EventDelegateChooser : public QQuickItem
+{
+ Q_OBJECT
+ QML_ELEMENT
+ Q_CLASSINFO("DefaultProperty", "choices")
+
+public:
+ Q_PROPERTY(QQmlListProperty<EventDelegateChoice> choices READ choices CONSTANT FINAL)
+ Q_PROPERTY(QQuickItem *main READ main NOTIFY mainChanged FINAL)
+ Q_PROPERTY(TimelineModel *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED FINAL)
+ Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged REQUIRED FINAL)
+ Q_PROPERTY(QString replyTo READ replyTo WRITE setReplyTo NOTIFY replyToChanged REQUIRED FINAL)
+
+ QQmlListProperty<EventDelegateChoice> choices();
+
+ [[nodiscard]] QQuickItem *main() const
+ {
+ return qobject_cast<QQuickItem *>(eventIncubator.object());
+ }
+
+ void setRoom(TimelineModel *m)
+ {
+ if (m != room_) {
+ room_ = m;
+ eventIncubator.reset(eventId_);
+ replyIncubator.reset(replyId);
+ emit roomChanged();
+ }
+ }
+ [[nodiscard]] TimelineModel *room() { return room_; }
+
+ void setEventId(QString idx)
+ {
+ eventId_ = idx;
+ emit eventIdChanged();
+ }
+ [[nodiscard]] QString eventId() const { return eventId_; }
+ void setReplyTo(QString id)
+ {
+ replyId = id;
+ emit replyToChanged();
+ }
+ [[nodiscard]] QString replyTo() const { return replyId; }
+
+ void componentComplete() override;
+
+signals:
+ void mainChanged();
+ void replyChanged();
+ void roomChanged();
+ void eventIdChanged();
+ void replyToChanged();
+
+private:
+ struct DelegateIncubator final : public QQmlIncubator
+ {
+ DelegateIncubator(EventDelegateChooser &parent, bool forReply)
+ : QQmlIncubator(QQmlIncubator::AsynchronousIfNested)
+ , chooser(parent)
+ , forReply(forReply)
+ {
+ }
+ void setInitialState(QObject *object) override;
+ void statusChanged(QQmlIncubator::Status status) override;
+
+ void reset(QString id);
+
+ EventDelegateChooser &chooser;
+ bool forReply;
+ QString currentId;
+
+ QString instantiatedId;
+ int instantiatedRole = -1;
+ QAbstractItemModel *instantiatedModel = nullptr;
+ };
+
+ QVariant roleValue_;
+ QList<EventDelegateChoice *> choices_;
+ DelegateIncubator eventIncubator{*this, false};
+ DelegateIncubator replyIncubator{*this, true};
+ TimelineModel *room_{nullptr};
+ QString eventId_;
+ QString replyId;
+
+ static void appendChoice(QQmlListProperty<EventDelegateChoice> *, EventDelegateChoice *);
+ static qsizetype choiceCount(QQmlListProperty<EventDelegateChoice> *);
+ static EventDelegateChoice *choice(QQmlListProperty<EventDelegateChoice> *, qsizetype index);
+ static void clearChoices(QQmlListProperty<EventDelegateChoice> *);
+};
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index b2a036c5..69ab3f5a 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -926,6 +926,26 @@ TimelineModel::multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSp
}
}
+void
+TimelineModel::multiData(const QString &id,
+ const QString &relatedTo,
+ QModelRoleDataSpan roleDataSpan) const
+{
+ if (id.isEmpty())
+ return;
+
+ auto event = events.get(id.toStdString(), relatedTo.toStdString());
+
+ if (!event)
+ return;
+
+ for (QModelRoleData &roleData : roleDataSpan) {
+ int role = roleData.role();
+
+ roleData.setData(data(*event, role));
+ }
+}
+
QVariant
TimelineModel::dataById(const QString &id, int role, const QString &relatedTo)
{
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index fccc99eb..57caf26d 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -286,6 +286,8 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const override;
+ void
+ multiData(const QString &id, const QString &relatedTo, QModelRoleDataSpan roleDataSpan) const;
QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const;
Q_INVOKABLE QVariant dataById(const QString &id, int role, const QString &relatedTo);
Q_INVOKABLE QVariant dataByIndex(int i, int role = Qt::DisplayRole) const
|