Improve emojis & reactions

This commit is contained in:
Tobias Fella
2022-12-05 16:46:55 +00:00
parent 1f83ab4450
commit 9060de1d60
11 changed files with 422 additions and 113 deletions

View File

@@ -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

View File

@@ -8,8 +8,8 @@
#include <algorithm>
#include "customemojimodel.h"
#include <KLocalizedString>
#include <qnamespace.h>
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<Emoji>(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::Category, QVariantList> 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;
}

View File

@@ -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();

View File

@@ -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)
}
}

View File

@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// 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
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -1,63 +1,54 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// 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
}
}

View File

@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// 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()
}
}
}
}

View File

@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// 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
}
}

View File

@@ -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()
}
}
}

View File

@@ -93,5 +93,9 @@
<file alias="ConfirmEncryptionDialog.qml">qml/Dialog/ConfirmEncryptionDialog.qml</file>
<file alias="RemoveSheet.qml">qml/Menu/Timeline/RemoveSheet.qml</file>
<file alias="BanSheet.qml">qml/Menu/Timeline/BanSheet.qml</file>
<file alias="ReactionPicker.qml">qml/Component/Emoji/ReactionPicker.qml</file>
<file alias="EmojiTonesPicker.qml">qml/Component/Emoji/EmojiTonesPicker.qml</file>
<file alias="EmojiDelegate.qml">qml/Component/Emoji/EmojiDelegate.qml</file>
<file alias="EmojiGrid.qml">qml/Component/Emoji/EmojiGrid.qml</file>
</qresource>
</RCC>