summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt3
-rw-r--r--resources/qml/MessageView.qml183
-rw-r--r--resources/qml/TimelineRow.qml3
-rw-r--r--resources/qml/delegates/Reply.qml32
-rw-r--r--src/timeline/EventDelegateChooser.cpp244
-rw-r--r--src/timeline/EventDelegateChooser.h136
-rw-r--r--src/timeline/TimelineModel.cpp20
-rw-r--r--src/timeline/TimelineModel.h2
8 files changed, 432 insertions, 191 deletions
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