From 9060de1d607704bdd50ac58ffe42673b87a3baca Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 5 Dec 2022 16:46:55 +0000 Subject: [PATCH] Improve emojis & reactions --- src/customemojimodel.h | 4 + src/emojimodel.cpp | 58 +++++++-- src/emojimodel.h | 6 + src/qml/Component/ChatBox/ChatBox.qml | 8 +- src/qml/Component/Emoji/EmojiDelegate.qml | 53 ++++++++ src/qml/Component/Emoji/EmojiGrid.qml | 87 ++++++++++++++ src/qml/Component/Emoji/EmojiPicker.qml | 120 ++++++------------- src/qml/Component/Emoji/EmojiTonesPicker.qml | 66 ++++++++++ src/qml/Component/Emoji/ReactionPicker.qml | 86 +++++++++++++ src/qml/Dialog/EmojiDialog.qml | 43 ++++--- src/res.qrc | 4 + 11 files changed, 422 insertions(+), 113 deletions(-) create mode 100644 src/qml/Component/Emoji/EmojiDelegate.qml create mode 100644 src/qml/Component/Emoji/EmojiGrid.qml create mode 100644 src/qml/Component/Emoji/EmojiTonesPicker.qml create mode 100644 src/qml/Component/Emoji/ReactionPicker.qml diff --git a/src/customemojimodel.h b/src/customemojimodel.h index 7b3be8812..5a8cd8dbf 100644 --- a/src/customemojimodel.h +++ b/src/customemojimodel.h @@ -11,6 +11,10 @@ struct CustomEmoji { QString name; // with :semicolons: QString url; // mxc:// QRegularExpression regexp; + + Q_GADGET + Q_PROPERTY(QString unicode MEMBER url) + Q_PROPERTY(QString name MEMBER name) }; class CustomEmojiModel : public QAbstractListModel diff --git a/src/emojimodel.cpp b/src/emojimodel.cpp index b47df81de..ee9d152f6 100644 --- a/src/emojimodel.cpp +++ b/src/emojimodel.cpp @@ -8,8 +8,8 @@ #include +#include "customemojimodel.h" #include -#include EmojiModel::EmojiModel(QObject *parent) : QAbstractListModel(parent) @@ -68,6 +68,13 @@ QVariantList EmojiModel::history() const } QVariantList EmojiModel::filterModel(const QString &filter, bool limit) +{ + auto emojis = CustomEmojiModel::instance().filterModel(filter); + emojis += filterModelNoCustom(filter, limit); + return emojis; +} + +QVariantList EmojiModel::filterModelNoCustom(const QString &filter, bool limit) { QVariantList result; @@ -82,7 +89,6 @@ QVariantList EmojiModel::filterModel(const QString &filter, bool limit) } } } - return result; } @@ -110,11 +116,27 @@ QVariantList EmojiModel::emojis(Category category) const if (category == History) { return history(); } + if (category == HistoryNoCustom) { + QVariantList list; + for (const auto &e : history()) { + auto emoji = qvariant_cast(e); + if (!emoji.isCustom) { + list.append(e); + } + } + return list; + } + if (category == Custom) { + return CustomEmojiModel::instance().filterModel({}); + } return _emojis[category]; } QVariantList EmojiModel::tones(const QString &baseEmoji) const { + if (baseEmoji.endsWith("tone")) { + return _tones.values(baseEmoji.split(":")[0]); + } return _tones.values(baseEmoji); } @@ -123,21 +145,11 @@ QHash EmojiModel::_emojis; QVariantList EmojiModel::categories() const { return QVariantList{ - // {QVariantMap{ - // {"category", EmojiModel::Search}, - // {"name", i18nc("Search for emojis", "Search")}, - // {"emoji", QStringLiteral("🔎")}, - // }}, {QVariantMap{ - {"category", EmojiModel::History}, + {"category", EmojiModel::HistoryNoCustom}, {"name", i18nc("Previously used emojis", "History")}, {"emoji", QStringLiteral("⌛️")}, }}, - {QVariantMap{ - {"category", EmojiModel::Custom}, - {"name", i18nc("'Custom' is a category of emoji", "Custom")}, - {"emoji", QStringLiteral("😏")}, - }}, {QVariantMap{ {"category", EmojiModel::Smileys}, {"name", i18nc("'Smileys' is a category of emoji", "Smileys")}, @@ -185,3 +197,23 @@ QVariantList EmojiModel::categories() const }}, }; } + +QVariantList EmojiModel::categoriesWithCustom() const +{ + auto cats = categories(); + cats.removeAt(0); + cats.insert(0, + QVariantMap{ + {"category", EmojiModel::History}, + {"name", i18nc("Previously used emojis", "History")}, + {"emoji", QStringLiteral("⌛️")}, + }); + cats.insert(1, + QVariantMap{ + {"category", EmojiModel::Custom}, + {"name", i18nc("'Custom' is a category of emoji", "Custom")}, + {"emoji", QStringLiteral("🖼️")}, + }); + ; + return cats; +} diff --git a/src/emojimodel.h b/src/emojimodel.h index 8096378fb..0a47f02d9 100644 --- a/src/emojimodel.h +++ b/src/emojimodel.h @@ -49,6 +49,7 @@ class EmojiModel : public QAbstractListModel Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged) Q_PROPERTY(QVariantList categories READ categories CONSTANT) + Q_PROPERTY(QVariantList categoriesWithCustom READ categoriesWithCustom CONSTANT) public: static EmojiModel &instance() @@ -69,7 +70,9 @@ public: enum Category { Custom, Search, + SearchNoCustom, History, + HistoryNoCustom, Smileys, People, Nature, @@ -89,11 +92,14 @@ public: Q_INVOKABLE QVariantList history() const; Q_INVOKABLE static QVariantList filterModel(const QString &filter, bool limit = true); + Q_INVOKABLE static QVariantList filterModelNoCustom(const QString &filter, bool limit = true); + Q_INVOKABLE QVariantList emojis(Category category) const; Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const; QVariantList categories() const; + QVariantList categoriesWithCustom() const; Q_SIGNALS: void historyChanged(); diff --git a/src/qml/Component/ChatBox/ChatBox.qml b/src/qml/Component/ChatBox/ChatBox.qml index 022d00fad..8aa3f0575 100644 --- a/src/qml/Component/ChatBox/ChatBox.qml +++ b/src/qml/Component/ChatBox/ChatBox.qml @@ -57,15 +57,21 @@ ColumnLayout { id: emojiPickerLoader active: visible visible: chatBar.emojiPaneOpened + onItemChanged: if (visible) { + emojiPickerLoader.item.forceActiveFocus() + } Layout.fillWidth: true sourceComponent: QQC2.Pane { + onActiveFocusChanged: if(activeFocus) { + emojiPicker.forceActiveFocus() + } topPadding: 0 bottomPadding: 0 rightPadding: 0 leftPadding: 0 Kirigami.Theme.colorSet: Kirigami.Theme.View contentItem: EmojiPicker { - textArea: chatBar.textField + id: emojiPicker onChosen: insertText(emoji) } } diff --git a/src/qml/Component/Emoji/EmojiDelegate.qml b/src/qml/Component/Emoji/EmojiDelegate.qml new file mode 100644 index 000000000..a226e97a5 --- /dev/null +++ b/src/qml/Component/Emoji/EmojiDelegate.qml @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.20 as Kirigami + +QQC2.ItemDelegate { + id: emojiDelegate + + property string name + property string emoji + property bool showTones: false + + QQC2.ToolTip.text: emojiDelegate.name + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + + contentItem: Item { + Kirigami.Heading { + anchors.fill: parent + visible: !emojiDelegate.emoji.startsWith("image") + text: emojiDelegate.emoji + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.family: "emoji" + + Kirigami.Icon { + width: Kirigami.Units.gridUnit * 0.5 + height: Kirigami.Units.gridUnit * 0.5 + source: "arrow-down" + anchors.bottom: parent.bottom + anchors.right: parent.right + visible: emojiDelegate.showTones + } + } + Image { + anchors.fill: parent + visible: emojiDelegate.emoji.startsWith("image") + source: visible ? emojiDelegate.emoji : "" + } + } + + background: Rectangle { + color: emojiDelegate.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.backgroundColor + + Rectangle { + anchors.fill: parent + color: Kirigami.Theme.highlightColor + opacity: emojiDelegate.hovered && !emojiDelegate.pressed ? 0.2 : 0 + } + } +} diff --git a/src/qml/Component/Emoji/EmojiGrid.qml b/src/qml/Component/Emoji/EmojiGrid.qml new file mode 100644 index 000000000..9d3da1be3 --- /dev/null +++ b/src/qml/Component/Emoji/EmojiGrid.qml @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.20 as Kirigami +import org.kde.neochat 1.0 + +QQC2.ScrollView { + id: emojiGrid + + property alias model: emojis.model + property alias count: emojis.count + required property int targetIconSize + readonly property int emojisPerRow: emojis.width / targetIconSize + required property bool withCustom + readonly property var searchCategory: withCustom ? EmojiModel.Search : EmojiModel.SearchNoCustom + required property QtObject header + + signal chosen(string unicode) + + onActiveFocusChanged: if (activeFocus) { + emojis.forceActiveFocus() + } + + GridView { + id: emojis + + anchors.fill: parent + anchors.rightMargin: parent.QQC2.ScrollBar.vertical.visible ? parent.QQC2.ScrollBar.vertical.width : 0 + + currentIndex: -1 + keyNavigationEnabled: true + onActiveFocusChanged: if (activeFocus && currentIndex === -1) { + currentIndex = 0 + } else { + currentIndex = -1 + } + onModelChanged: currentIndex = -1 + + cellWidth: emojis.width / emojiGrid.emojisPerRow + cellHeight: emojiGrid.targetIconSize + + KeyNavigation.up: emojiGrid.header + + clip: true + + delegate: EmojiDelegate { + id: emojiDelegate + checked: emojis.currentIndex === model.index + emoji: modelData.unicode + name: modelData.shortName + + width: emojis.cellWidth + height: emojis.cellHeight + + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + onClicked: { + emojiGrid.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode) + EmojiModel.emojiUsed(modelData) + } + Keys.onSpacePressed: pressAndHold() + onPressAndHold: { + if (EmojiModel.tones(modelData.shortName).length === 0) { + return; + } + let tones = tonesPopupComponent.createObject(emojiDelegate, {shortName: modelData.shortName, unicode: modelData.unicode, categoryIconSize: emojiGrid.targetIconSize}) + tones.open() + tones.forceActiveFocus() + } + showTones: EmojiModel.tones(modelData.shortName).length > 0 + } + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + text: i18n("No emojis") + visible: emojis.count === 0 + } + } + Component { + id: tonesPopupComponent + EmojiTonesPicker { + onChosen: emojiGrid.chosen(emoji) + } + } +} diff --git a/src/qml/Component/Emoji/EmojiPicker.qml b/src/qml/Component/Emoji/EmojiPicker.qml index 394752c06..0108006da 100644 --- a/src/qml/Component/Emoji/EmojiPicker.qml +++ b/src/qml/Component/Emoji/EmojiPicker.qml @@ -1,63 +1,54 @@ -// SPDX-FileCopyrightText: 2018-2019 Black Hat -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2022 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later import QtQuick 2.15 import QtQuick.Controls 2.15 as QQC2 import QtQuick.Layouts 1.15 import org.kde.kirigami 2.15 as Kirigami - import org.kde.neochat 1.0 ColumnLayout { - id: _picker + id: emojiPicker - property var emojiCategory: EmojiModel.History - property var textArea - readonly property var emojiModel: EmojiModel + readonly property int categoryIconSize: 45 + readonly property var currentCategory: EmojiModel.categoriesWithCustom[categories.currentIndex].category + readonly property int categoryCount: categories.count signal chosen(string emoji) spacing: 0 + onActiveFocusChanged: if (activeFocus) categories.forceActiveFocus() + QQC2.ScrollView { Layout.fillWidth: true - Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + QQC2.ScrollBar.horizontal.height + 2 // for the focus line QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0 + Layout.preferredHeight: emojiPicker.categoryIconSize + QQC2.ScrollBar.horizontal.height ListView { + id: categories clip: true orientation: ListView.Horizontal - model: EmojiModel.categories - delegate: QQC2.ItemDelegate { - id: del + keyNavigationEnabled: true + keyNavigationWraps: true + Keys.forwardTo: searchField + interactive: width !== contentWidth - width: contentItem.Layout.preferredWidth - height: Kirigami.Units.gridUnit * 2 + model: EmojiModel.categoriesWithCustom + delegate: EmojiDelegate { + id: emojiDelegate - contentItem: Kirigami.Heading { - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - level: modelData.category === EmojiModel.Custom ? 4 : 1 + width: emojiPicker.categoryIconSize + height: width - Layout.preferredWidth: modelData.category === EmojiModel.Custom ? implicitWidth + Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit * 2 + checked: categories.currentIndex === model.index + emoji: modelData.emoji + name: modelData.name - font.family: modelData.category === EmojiModel.Custom ? Kirigami.Theme.defaultFont.family : 'emoji' - text: modelData.category === EmojiModel.Custom ? i18n("Custom") : modelData.emoji + onClicked: { + categories.currentIndex = index } - - Rectangle { - anchors.bottom: parent.bottom - - width: parent.width - height: 2 - - visible: _picker.emojiCategory === modelData.category - - color: Kirigami.Theme.focusColor - } - - onClicked: _picker.emojiCategory = modelData.category } } } @@ -67,57 +58,22 @@ ColumnLayout { Layout.preferredHeight: 1 } - QQC2.ScrollView { + Kirigami.SearchField { + id: searchField + Layout.margins: Kirigami.Units.smallSpacing Layout.fillWidth: true - Layout.preferredHeight: Kirigami.Units.gridUnit * 8 - Layout.fillHeight: true + } - GridView { - cellWidth: Kirigami.Units.gridUnit * 2 - cellHeight: Kirigami.Units.gridUnit * 2 + EmojiGrid { + id: emojiGrid + targetIconSize: emojiPicker.categoryIconSize + model: searchField.text.length === 0 ? EmojiModel.emojis(emojiPicker.currentCategory) : EmojiModel.filterModel(searchField.text, false) + Layout.fillWidth: true + Layout.preferredHeight: 350 + onChosen: emojiPicker.chosen(unicode) + withCustom: true + header: categories - clip: true - - model: _picker.emojiCategory === EmojiModel.Custom ? CustomEmojiModel : EmojiModel.emojis(_picker.emojiCategory) - - delegate: QQC2.ItemDelegate { - width: Kirigami.Units.gridUnit * 2 - height: Kirigami.Units.gridUnit * 2 - - contentItem: Kirigami.Heading { - level: 1 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.family: 'emoji' - text: modelData.isCustom ? "" : modelData.unicode - } - - Image { - visible: modelData.isCustom - source: visible ? modelData.unicode : "" - anchors.fill: parent - anchors.margins: 2 - - sourceSize.width: width - sourceSize.height: height - - Rectangle { - anchors.fill: parent - visible: parent.status === Image.Loading - radius: height/2 - gradient: ShimmerGradient { } - } - } - - onClicked: { - if (modelData.isCustom) { - chosen(modelData.shortName) - } else { - chosen(modelData.unicode) - } - emojiModel.emojiUsed(modelData) - } - } - } + Keys.forwardTo: searchField } } diff --git a/src/qml/Component/Emoji/EmojiTonesPicker.qml b/src/qml/Component/Emoji/EmojiTonesPicker.qml new file mode 100644 index 000000000..eef729edd --- /dev/null +++ b/src/qml/Component/Emoji/EmojiTonesPicker.qml @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.neochat 1.0 + +QQC2.Popup { + id: tones + + signal chosen(string emoji) + + Component.onCompleted: { + tonesList.currentIndex = 0; + tonesList.forceActiveFocus(); + } + + required property string shortName + required property string unicode + required property int categoryIconSize + width: tones.categoryIconSize * tonesList.count + 2 * padding + height: tones.categoryIconSize + 2 * padding + y: -height + padding: 2 + modal: true + dim: true + onOpened: x = Math.min(parent.mapFromGlobal(QQC2.Overlay.overlay.width - tones.width, 0).x, -(width - parent.width) / 2) + background: Kirigami.ShadowedRectangle { + color: Kirigami.Theme.backgroundColor + radius: Kirigami.Units.smallSpacing + shadow.size: Kirigami.Units.smallSpacing + shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10) + border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15) + border.width: 1 + } + + ListView { + id: tonesList + width: parent.width + height: parent.height + orientation: Qt.Horizontal + model: EmojiModel.tones(tones.shortName) + keyNavigationEnabled: true + keyNavigationWraps: true + + delegate: EmojiDelegate { + id: emojiDelegate + checked: tonesList.currentIndex === model.index + emoji: modelData.unicode + name: modelData.shortName + + width: tones.categoryIconSize + height: width + + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + onClicked: { + tones.chosen(modelData.unicode) + EmojiModel.emojiUsed(modelData) + tones.close() + } + } + } +} diff --git a/src/qml/Component/Emoji/ReactionPicker.qml b/src/qml/Component/Emoji/ReactionPicker.qml new file mode 100644 index 000000000..00fa4cd6a --- /dev/null +++ b/src/qml/Component/Emoji/ReactionPicker.qml @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.15 as Kirigami +import org.kde.neochat 1.0 + +ColumnLayout { + id: reactionPicker + height: 400 + + readonly property int categoryIconSize: 45 + readonly property var currentCategory: EmojiModel.categories[categories.currentIndex].category + readonly property alias categoryCount: categories.count + + signal chosen(string emoji) + + spacing: 0 + + QQC2.ScrollView { + Layout.fillWidth: true + Layout.preferredHeight: reactionPicker.categoryIconSize + QQC2.ScrollBar.horizontal.height + QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0 + + ListView { + id: categories + + keyNavigationEnabled: true + focus: true + height: reactionPicker.categoryIconSize + Keys.onReturnPressed: if (emojiGrid.count > 0) emojiGrid.focus = true + Keys.onEnterPressed: if (emojiGrid.count > 0) emojiGrid.focus = true + currentIndex: 2 + keyNavigationWraps: true + Keys.forwardTo: searchField + interactive: width !== contentWidth + + model: EmojiModel.categories + Component.onCompleted: categories.forceActiveFocus() + + delegate: EmojiDelegate { + checked: categories.currentIndex === model.index + emoji: modelData.emoji + name: modelData.name + + height: reactionPicker.categoryIconSize + width: height + + onClicked: { + categories.currentIndex = index + categories.focus = true + } + } + + orientation: Qt.Horizontal + KeyNavigation.down: emojiGrid.count > 0 ? emojiGrid : categories + KeyNavigation.tab: emojiGrid.count > 0 ? emojiGrid : categories + } + } + + Kirigami.Separator { + Layout.fillWidth: true + Layout.preferredHeight: 1 + } + + Kirigami.SearchField { + id: searchField + + Layout.margins: Kirigami.Units.smallSpacing + Layout.fillWidth: true + } + + EmojiGrid { + id: emojiGrid + targetIconSize: reactionPicker.categoryIconSize + model: searchField.text.length === 0 ? EmojiModel.emojis(reactionPicker.currentCategory) : EmojiModel.filterModelNoCustom(searchField.text, false) + Layout.fillWidth: true + Layout.fillHeight: true + withCustom: false + onChosen: reactionPicker.chosen(unicode) + header: categories + Keys.forwardTo: searchField + } +} diff --git a/src/qml/Dialog/EmojiDialog.qml b/src/qml/Dialog/EmojiDialog.qml index a06485d89..eaa9fcc32 100644 --- a/src/qml/Dialog/EmojiDialog.qml +++ b/src/qml/Dialog/EmojiDialog.qml @@ -9,29 +9,38 @@ import org.kde.kirigami 2.15 as Kirigami import org.kde.neochat 1.0 QQC2.Popup { - id: root - + id: emojiPopup signal react(string emoji) + Connections { + target: RoomManager + function onCurrentRoomChanged() { + emojiPopup.close() + } + } + + background: Kirigami.ShadowedRectangle { + Kirigami.Theme.colorSet: Kirigami.Theme.View + color: Kirigami.Theme.backgroundColor + radius: Kirigami.Units.smallSpacing + shadow.size: Kirigami.Units.smallSpacing + shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10) + border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15) + border.width: 2 + } + modal: true focus: true closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutsideParent margins: 0 - padding: 1 - implicitWidth: Kirigami.Units.gridUnit * 16 - implicitHeight: Kirigami.Units.gridUnit * 20 + padding: 2 - background: Rectangle { - Kirigami.Theme.inherit: false - Kirigami.Theme.colorSet: Kirigami.Theme.View - color: Kirigami.Theme.backgroundColor - border.width: 1 - border.color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, - Kirigami.Theme.textColor, - 0.15) - } - - contentItem: EmojiPicker { - onChosen: react(emoji) + implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding + width: Math.min(contentItem.categoryIconSize * contentItem.categoryCount + 2 * padding, QQC2.Overlay.overlay.width) + contentItem: ReactionPicker { + onChosen: { + react(emoji) + emojiPopup.close() + } } } diff --git a/src/res.qrc b/src/res.qrc index c0ce26147..4cad73c0e 100644 --- a/src/res.qrc +++ b/src/res.qrc @@ -93,5 +93,9 @@ qml/Dialog/ConfirmEncryptionDialog.qml qml/Menu/Timeline/RemoveSheet.qml qml/Menu/Timeline/BanSheet.qml + qml/Component/Emoji/ReactionPicker.qml + qml/Component/Emoji/EmojiTonesPicker.qml + qml/Component/Emoji/EmojiDelegate.qml + qml/Component/Emoji/EmojiGrid.qml