From 5d7cb5c28fc655e500cd8463c4c4f84060c903dd Mon Sep 17 00:00:00 2001 From: James Graham Date: Sun, 9 Feb 2025 19:07:53 +0000 Subject: [PATCH] Move the reaction delegate into the bubble Move the reaction delegate into the bubble so it can be instantiated by the Content model. This aims to make sure we only instantiate it when needed rather than for every event. You can now hover the event to show the ReactionComponent with a button to add a reaction if none are currently present Added bonus ReactionModel no longer needs an event pointer, the event ID is enough to get reaction from the room so things are less likely to blow up. --- autotests/reactionmodeltest.cpp | 15 +--- src/enums/messagecomponenttype.h | 1 + src/models/messagecontentmodel.cpp | 38 +++++++++ src/models/messagecontentmodel.h | 7 +- src/models/messagemodel.cpp | 40 --------- src/models/messagemodel.h | 3 - src/models/reactionmodel.cpp | 33 +++++--- src/models/reactionmodel.h | 21 ++++- src/models/threadmodel.h | 3 - src/models/timelinemessagemodel.h | 36 -------- src/timeline/BaseMessageComponentChooser.qml | 8 ++ src/timeline/CMakeLists.txt | 2 +- src/timeline/MessageDelegate.qml | 24 +----- ...tionDelegate.qml => ReactionComponent.qml} | 82 +++++++++++++++++-- 14 files changed, 171 insertions(+), 142 deletions(-) rename src/timeline/{ReactionDelegate.qml => ReactionComponent.qml} (51%) diff --git a/autotests/reactionmodeltest.cpp b/autotests/reactionmodeltest.cpp index 6b157b3cd..d0065a52e 100644 --- a/autotests/reactionmodeltest.cpp +++ b/autotests/reactionmodeltest.cpp @@ -20,11 +20,11 @@ class ReactionModelTest : public QObject private: Connection *connection = nullptr; TestUtils::TestRoom *room = nullptr; + MessageContentModel *parentModel; private Q_SLOTS: void initTestCase(); - void nullModel(); void basicReaction(); void newReaction(); }; @@ -33,20 +33,13 @@ void ReactionModelTest::initTestCase() { connection = Connection::makeMockConnection(u"@bob:kde.org"_s); room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-reactionmodel-sync.json"_s); -} - -void ReactionModelTest::nullModel() -{ - auto model = ReactionModel(nullptr, nullptr); - - QCOMPARE(model.rowCount(), 0); - QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), QVariant()); + parentModel = new MessageContentModel(room, "123456"_L1); } void ReactionModelTest::basicReaction() { auto event = eventCast(room->messageEvents().at(0).get()); - auto model = ReactionModel(event, room); + auto model = ReactionModel(parentModel, event->id(), room); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), u"👍"_s); @@ -58,7 +51,7 @@ void ReactionModelTest::basicReaction() void ReactionModelTest::newReaction() { auto event = eventCast(room->messageEvents().at(0).get()); - auto model = new ReactionModel(event, room); + auto model = new ReactionModel(parentModel, event->id(), room); QCOMPARE(model->rowCount(), 1); QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole), u"Alice Margatroid reacted with 👍"_s); diff --git a/src/enums/messagecomponenttype.h b/src/enums/messagecomponenttype.h index 7f4d82203..138de5f66 100644 --- a/src/enums/messagecomponenttype.h +++ b/src/enums/messagecomponenttype.h @@ -50,6 +50,7 @@ public: LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */ Encrypted, /**< An encrypted message that cannot be decrypted. */ Reply, /**< A component to show a replied-to message. */ + Reaction, /**< A component to show the reactions to this message. */ LinkPreview, /**< A preview of a URL in the message. */ LinkPreviewLoad, /**< A loading dialog for a link preview. */ ChatBar, /**< A text edit for editing a message. */ diff --git a/src/models/messagecontentmodel.cpp b/src/models/messagecontentmodel.cpp index 60eaaa75b..32c265a65 100644 --- a/src/models/messagecontentmodel.cpp +++ b/src/models/messagecontentmodel.cpp @@ -3,6 +3,7 @@ #include "messagecontentmodel.h" #include "eventhandler.h" +#include "messagecomponenttype.h" #include "neochatconfig.h" #include @@ -27,6 +28,7 @@ #include "chatbarcache.h" #include "filetype.h" #include "linkpreviewer.h" +#include "models/reactionmodel.h" #include "neochatconnection.h" #include "neochatroom.h" #include "texthandler.h" @@ -152,12 +154,18 @@ void MessageContentModel::initializeModel() updateReplyModel(); resetModel(); }); + connect(m_room, &Room::updatedEvent, this, [this](const QString &eventId) { + if (eventId == m_eventId) { + updateReactionModel(); + } + }); initializeEvent(); if (m_currentState == Available || m_currentState == Pending) { updateReplyModel(); } resetModel(); + updateReactionModel(); } void MessageContentModel::initializeEvent() @@ -340,6 +348,10 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const if (role == ReplyContentModelRole) { return QVariant::fromValue(m_replyModel); } + if (role == ReactionModelRole) { + return QVariant::fromValue(m_reactionModel); + ; + } if (role == ThreadRootRole) { auto roomMessageEvent = eventCast(event.first); #if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) @@ -400,6 +412,7 @@ QHash MessageContentModel::roleNamesStatic() roles[MessageContentModel::ReplyEventIdRole] = "replyEventId"; roles[MessageContentModel::ReplyAuthorRole] = "replyAuthor"; roles[MessageContentModel::ReplyContentModelRole] = "replyContentModel"; + roles[MessageContentModel::ReactionModelRole] = "reactionModel"; roles[MessageContentModel::ThreadRootRole] = "threadRoot"; roles[MessageContentModel::LinkPreviewerRole] = "linkPreviewer"; roles[MessageContentModel::ChatBarCacheRole] = "chatBarCache"; @@ -480,6 +493,10 @@ QList MessageContentModel::messageContentComponents(bool isEdi newComponents = addLinkPreviews(newComponents); } + if ((m_reactionModel && m_reactionModel->rowCount() > 0)) { + newComponents += MessageComponent{MessageComponentType::Reaction, QString(), {}}; + } + #if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id())) && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) { @@ -731,4 +748,25 @@ void MessageContentModel::updateItineraryModel() } } +void MessageContentModel::updateReactionModel() +{ + if (m_reactionModel != nullptr && m_reactionModel->rowCount() > 0) { + return; + } + + if (m_reactionModel == nullptr) { + m_reactionModel = new ReactionModel(this, m_eventId, m_room); + connect(m_reactionModel, &ReactionModel::reactionsUpdated, this, &MessageContentModel::updateReactionModel); + } + + if (m_reactionModel->rowCount() <= 0) { + m_reactionModel->disconnect(this); + delete m_reactionModel; + m_reactionModel = nullptr; + return; + } + + resetContent(); +} + #include "moc_messagecontentmodel.cpp" diff --git a/src/models/messagecontentmodel.h b/src/models/messagecontentmodel.h index 6eb645126..84508edf9 100644 --- a/src/models/messagecontentmodel.h +++ b/src/models/messagecontentmodel.h @@ -7,11 +7,11 @@ #include #include -#include #include "enums/messagecomponenttype.h" #include "itinerarymodel.h" #include "messagecomponent.h" +#include "models/reactionmodel.h" #include "neochatroommember.h" /** @@ -57,6 +57,8 @@ public: ReplyAuthorRole, /**< The author of the event that was replied to. */ ReplyContentModelRole, /**< The MessageContentModel for the reply event. */ + ReactionModelRole, /**< Reaction model for this event. */ + ThreadRootRole, /**< The thread root event ID for the event. */ LinkPreviewerRole, /**< The link preview details. */ @@ -125,6 +127,7 @@ private: QPointer m_replyModel; void updateReplyModel(); + ReactionModel *m_reactionModel = nullptr; ItineraryModel *m_itineraryModel = nullptr; QList componentsForType(MessageComponentType::Type type); @@ -135,4 +138,6 @@ private: void updateItineraryModel(); bool m_emptyItinerary = false; + + void updateReactionModel(); }; diff --git a/src/models/messagemodel.cpp b/src/models/messagemodel.cpp index 223caaa80..afd968a4e 100644 --- a/src/models/messagemodel.cpp +++ b/src/models/messagemodel.cpp @@ -258,18 +258,6 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const return m_readMarkerModels.contains(event.value().get().id()); } - if (role == ReactionRole) { - if (m_reactionModels.contains(event.value().get().id())) { - return QVariant::fromValue(m_reactionModels[event.value().get().id()].data()); - } else { - return QVariantList(); - } - } - - if (role == ShowReactionsRole) { - return m_reactionModels.contains(event.value().get().id()); - } - if (role == VerifiedRole) { if (event.value().get().originalEvent()) { auto encrypted = dynamic_cast(event.value().get().originalEvent()); @@ -323,8 +311,6 @@ QHash MessageModel::roleNames() const roles[ShowSectionRole] = "showSection"; roles[ReadMarkersRole] = "readMarkers"; roles[ShowReadMarkersRole] = "showReadMarkers"; - roles[ReactionRole] = "reaction"; - roles[ShowReactionsRole] = "showReactions"; roles[VerifiedRole] = "verified"; roles[AuthorDisplayNameRole] = "authorDisplayName"; roles[IsRedactedRole] = "isRedacted"; @@ -454,31 +440,6 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event, bool isP } } } - - if (const auto roomEvent = eventCast(event)) { - // ReactionModel handles updates to add and remove reactions, we only need to - // handle adding and removing whole models here. - if (m_reactionModels.contains(eventId)) { - // If a model already exists but now has no reactions remove it - if (m_reactionModels[eventId]->rowCount() <= 0) { - m_reactionModels.remove(eventId); - if (!resetting) { - refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole}); - } - } - } else { - if (m_room->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) { - // If a model doesn't exist and there are reactions add it. - auto reactionModel = QSharedPointer(new ReactionModel(roomEvent, m_room)); - if (reactionModel->rowCount() > 0) { - m_reactionModels[eventId] = reactionModel; - if (!resetting) { - refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole}); - } - } - } - } - } } void MessageModel::clearModel() @@ -504,7 +465,6 @@ void MessageModel::clearModel() void MessageModel::clearEventObjects() { - m_reactionModels.clear(); m_readMarkerModels.clear(); } diff --git a/src/models/messagemodel.h b/src/models/messagemodel.h index 88d6881ec..411fd9154 100644 --- a/src/models/messagemodel.h +++ b/src/models/messagemodel.h @@ -77,8 +77,6 @@ public: ReadMarkersRole, /**< The first 5 other users at the event for read marker tracking. */ ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */ - ReactionRole, /**< List model for this event. */ - ShowReactionsRole, /**< Whether there are any reactions to be shown. */ VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */ AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */ @@ -155,7 +153,6 @@ private: bool movingEvent = false; QMap> m_readMarkerModels; - QMap> m_reactionModels; void createEventObjects(const Quotient::RoomEvent *event, bool isPending = false); }; diff --git a/src/models/reactionmodel.cpp b/src/models/reactionmodel.cpp index a11033a69..7fee8393d 100644 --- a/src/models/reactionmodel.cpp +++ b/src/models/reactionmodel.cpp @@ -9,22 +9,27 @@ #include +#include "neochatroom.h" + using namespace Qt::StringLiterals; -ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room) - : QAbstractListModel(nullptr) +ReactionModel::ReactionModel(MessageContentModel *parent, const QString &eventId, NeoChatRoom *room) + : QAbstractListModel(parent) , m_room(room) - , m_event(event) + , m_eventId(eventId) { - if (m_event != nullptr && m_room != nullptr) { - connect(m_room, &NeoChatRoom::updatedEvent, this, [this](const QString &eventId) { - if (m_event && m_event->id() == eventId) { - updateReactions(); - } - }); + Q_ASSERT(parent); + Q_ASSERT(parent != nullptr); + Q_ASSERT(!eventId.isEmpty()); + Q_ASSERT(room != nullptr); - updateReactions(); - } + connect(m_room, &NeoChatRoom::updatedEvent, this, [this](const QString &eventId) { + if (m_eventId == eventId) { + updateReactions(); + } + }); + + updateReactions(); } QVariant ReactionModel::data(const QModelIndex &index, int role) const @@ -99,12 +104,16 @@ int ReactionModel::rowCount(const QModelIndex &parent) const void ReactionModel::updateReactions() { + if (m_room == nullptr) { + return; + } + beginResetModel(); m_reactions.clear(); m_shortcodes.clear(); - const auto &annotations = m_room->relatedEvents(*m_event, Quotient::EventRelation::AnnotationType); + const auto &annotations = m_room->relatedEvents(m_eventId, Quotient::EventRelation::AnnotationType); if (annotations.isEmpty()) { endResetModel(); return; diff --git a/src/models/reactionmodel.h b/src/models/reactionmodel.h index df9cb57e1..f65a28b87 100644 --- a/src/models/reactionmodel.h +++ b/src/models/reactionmodel.h @@ -3,11 +3,20 @@ #pragma once -#include "neochatroom.h" #include +#include + #include #include +namespace Quotient +{ +class RoomMessageEvent; +} + +class MessageContentModel; +class NeoChatRoom; + /** * @class ReactionModel * @@ -38,7 +47,7 @@ public: HasLocalMember, /**< Whether the local member is in the list of authors. */ }; - explicit ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room); + explicit ReactionModel(MessageContentModel *parent, const QString &eventId, NeoChatRoom *room); /** * @brief Get the given role value at the given index. @@ -61,9 +70,15 @@ public: */ [[nodiscard]] QHash roleNames() const override; +Q_SIGNALS: + /** + * @brief The reactions in the model have been updated. + */ + void reactionsUpdated(); + private: QPointer m_room; - const Quotient::RoomMessageEvent *m_event; + QString m_eventId; QList m_reactions; QMap m_shortcodes; diff --git a/src/models/threadmodel.h b/src/models/threadmodel.h index 81997ff28..8ea460707 100644 --- a/src/models/threadmodel.h +++ b/src/models/threadmodel.h @@ -19,7 +19,6 @@ #include "messagecontentmodel.h" class NeoChatRoom; -class ReactionModel; /** * @class ThreadFetchModel @@ -172,8 +171,6 @@ private: ThreadFetchModel *m_threadFetchModel; ThreadChatBarModel *m_threadChatBarModel; - QMap> m_reactionModels; - QPointer m_currentJob = nullptr; std::optional m_nextBatch = QString(); bool m_addingPending = false; diff --git a/src/models/timelinemessagemodel.h b/src/models/timelinemessagemodel.h index c62532088..9fa87f593 100644 --- a/src/models/timelinemessagemodel.h +++ b/src/models/timelinemessagemodel.h @@ -32,42 +32,6 @@ class TimelineMessageModel : public MessageModel QML_ELEMENT public: - /** - * @brief Defines the model roles. - */ - enum EventRoles { - DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */ - EventIdRole, /**< The matrix event ID of the event. */ - TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */ - SectionRole, /**< The date of the event as a string. */ - AuthorRole, /**< The author of the event. */ - HighlightRole, /**< Whether the event should be highlighted. */ - SpecialMarksRole, /**< Whether the event is hidden or not. */ - ProgressInfoRole, /**< Progress info when downloading files. */ - GenericDisplayRole, /**< A generic string based upon the message type. */ - MediaInfoRole, /**< The media info for the event. */ - - ContentModelRole, /**< The MessageContentModel for the event. */ - - IsThreadedRole, /**< Whether the message is in a thread. */ - ThreadRootRole, /**< The Matrix ID of the thread root message, if any . */ - - ShowSectionRole, /**< Whether the section header should be shown. */ - - ReadMarkersRole, /**< The first 5 other users at the event for read marker tracking. */ - ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */ - ReactionRole, /**< List model for this event. */ - ShowReactionsRole, /**< Whether there are any reactions to be shown. */ - - VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */ - AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */ - IsRedactedRole, /**< Whether an event has been deleted. */ - IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */ - IsEditableRole, /**< Whether the event can be edited by the user. */ - LastRole, // Keep this last - }; - Q_ENUM(EventRoles) - explicit TimelineMessageModel(QObject *parent = nullptr); /** diff --git a/src/timeline/BaseMessageComponentChooser.qml b/src/timeline/BaseMessageComponentChooser.qml index 195d4c8cc..e7de8b435 100644 --- a/src/timeline/BaseMessageComponentChooser.qml +++ b/src/timeline/BaseMessageComponentChooser.qml @@ -193,6 +193,14 @@ DelegateChooser { } } + DelegateChoice { + roleValue: MessageComponentType.Reaction + delegate: ReactionComponent { + room: root.room + maxContentWidth: root.maxContentWidth + } + } + DelegateChoice { roleValue: MessageComponentType.LinkPreview delegate: LinkPreviewComponent { diff --git a/src/timeline/CMakeLists.txt b/src/timeline/CMakeLists.txt index 1c1fa8077..a395174c9 100644 --- a/src/timeline/CMakeLists.txt +++ b/src/timeline/CMakeLists.txt @@ -17,7 +17,6 @@ ecm_add_qml_module(timeline GENERATE_PLUGIN_SOURCE TimelineEndDelegate.qml Bubble.qml AvatarFlow.qml - ReactionDelegate.qml SectionDelegate.qml BaseMessageComponentChooser.qml MessageComponentChooser.qml @@ -47,6 +46,7 @@ ecm_add_qml_module(timeline GENERATE_PLUGIN_SOURCE PdfPreviewComponent.qml PollComponent.qml QuoteComponent.qml + ReactionComponent.qml ReplyAuthorComponent.qml ReplyButtonComponent.qml ReplyComponent.qml diff --git a/src/timeline/MessageDelegate.qml b/src/timeline/MessageDelegate.qml index 3b2190d35..c573cd6c2 100644 --- a/src/timeline/MessageDelegate.qml +++ b/src/timeline/MessageDelegate.qml @@ -14,7 +14,7 @@ import org.kde.neochat /** * @brief The base delegate for all messages in the timeline. * - * This supports a message bubble plus sender avatar for each message as well as reactions + * This supports a message bubble plus sender avatar for each message * and read markers. A date section can be show for when the message is on a different * day to the previous one. * @@ -72,16 +72,6 @@ TimelineDelegate { */ required property bool showSection - /** - * @brief A model with the reactions to the message in. - */ - required property var reaction - - /** - * @brief Whether the reaction component should be shown. - */ - required property bool showReactions - /** * @brief A model with the first 5 other user read markers for this message. */ @@ -337,18 +327,6 @@ TimelineDelegate { onLongPressed: _private.showMessageMenu() } } - - ReactionDelegate { - Layout.maximumWidth: root.width - Kirigami.Units.largeSpacing * 2 - Layout.alignment: _private.showUserMessageOnRight ? Qt.AlignRight : Qt.AlignLeft - Layout.leftMargin: _private.showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin - Layout.rightMargin: _private.showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0 - - visible: root.showReactions - model: root.reaction - - onReactionClicked: reaction => root.room.toggleReaction(root.eventId, reaction) - } AvatarFlow { Layout.alignment: Qt.AlignRight Layout.rightMargin: Kirigami.Units.largeSpacing diff --git a/src/timeline/ReactionDelegate.qml b/src/timeline/ReactionComponent.qml similarity index 51% rename from src/timeline/ReactionDelegate.qml rename to src/timeline/ReactionComponent.qml index 16901e231..97f8ebfd1 100644 --- a/src/timeline/ReactionDelegate.qml +++ b/src/timeline/ReactionComponent.qml @@ -4,6 +4,7 @@ import QtQuick import QtQuick.Controls as QQC2 +import QtQuick.Layouts import org.kde.kirigami as Kirigami @@ -13,22 +14,36 @@ Flow { id: root /** - * @brief The reaction model to get the reactions from. + * @brief The NeoChatRoom the delegate is being displayed in. */ - property alias model: reactionRepeater.model + required property NeoChatRoom room /** - * @brief The given reaction has been clicked. - * - * Thrown when one of the reaction buttons in the flow is clicked. + * @brief The matrix ID of the message event. */ - signal reactionClicked(string reaction) + required property string eventId + + /** + * @brief The reaction model to get the reactions from. + */ + required property ReactionModel reactionModel + + /** + * @brief The maximum width that the bubble's content can be. + */ + property real maxContentWidth: -1 + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.maximumWidth: root.maxContentWidth spacing: Kirigami.Units.smallSpacing Repeater { id: reactionRepeater + model: root.reactionModel + delegate: QQC2.AbstractButton { id: reactionDelegate @@ -54,9 +69,9 @@ Flow { padding: Kirigami.Units.smallSpacing background: Kirigami.ShadowedRectangle { - color: reactionDelegate.hasLocalMember ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor Kirigami.Theme.inherit: false - Kirigami.Theme.colorSet: NeoChatConfig.compactLayout ? Kirigami.Theme.Window : Kirigami.Theme.View + Kirigami.Theme.colorSet: NeoChatConfig.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window + color: reactionDelegate.hasLocalMember ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor radius: height / 2 shadow { size: Kirigami.Units.smallSpacing @@ -64,7 +79,7 @@ Flow { } } - onClicked: reactionClicked(reactionDelegate.reaction) + onClicked: root.room.toggleReaction(root.eventId, reactionDelegate.reaction) hoverEnabled: true @@ -73,4 +88,53 @@ Flow { QQC2.ToolTip.text: reactionDelegate.toolTip } } + + QQC2.AbstractButton { + id: reactButton + width: Math.round(Kirigami.Units.gridUnit * 1.5) + height: Math.round(Kirigami.Units.gridUnit * 1.5) + + text: i18nc("@button", "React") + + contentItem: Kirigami.Icon { + source: "list-add" + } + + padding: Kirigami.Units.smallSpacing + + background: Rectangle { + color: Kirigami.Theme.backgroundColor + radius: height / 2 + border { + width: reactButton.hovered ? 1 : 0 + color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast) + } + } + + onClicked: { + var dialog = emojiDialog.createObject(reactButton); + dialog.chosen.connect(emoji => { + root.room.toggleReaction(root.eventId, emoji); + if (!Kirigami.Settings.isMobile) { + root.focusChatBar(); + } + }); + dialog.open(); + } + + hoverEnabled: true + + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.text: reactButton.text + } + + Component { + id: emojiDialog + + EmojiDialog { + currentRoom: root.room + showQuickReaction: true + } + } }