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 name; // with :semicolons:
QString url; // mxc:// QString url; // mxc://
QRegularExpression regexp; QRegularExpression regexp;
Q_GADGET
Q_PROPERTY(QString unicode MEMBER url)
Q_PROPERTY(QString name MEMBER name)
}; };
class CustomEmojiModel : public QAbstractListModel class CustomEmojiModel : public QAbstractListModel

View File

@@ -8,8 +8,8 @@
#include <algorithm> #include <algorithm>
#include "customemojimodel.h"
#include <KLocalizedString> #include <KLocalizedString>
#include <qnamespace.h>
EmojiModel::EmojiModel(QObject *parent) EmojiModel::EmojiModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
@@ -68,6 +68,13 @@ QVariantList EmojiModel::history() const
} }
QVariantList EmojiModel::filterModel(const QString &filter, bool limit) 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; QVariantList result;
@@ -82,7 +89,6 @@ QVariantList EmojiModel::filterModel(const QString &filter, bool limit)
} }
} }
} }
return result; return result;
} }
@@ -110,11 +116,27 @@ QVariantList EmojiModel::emojis(Category category) const
if (category == History) { if (category == History) {
return 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]; return _emojis[category];
} }
QVariantList EmojiModel::tones(const QString &baseEmoji) const QVariantList EmojiModel::tones(const QString &baseEmoji) const
{ {
if (baseEmoji.endsWith("tone")) {
return _tones.values(baseEmoji.split(":")[0]);
}
return _tones.values(baseEmoji); return _tones.values(baseEmoji);
} }
@@ -123,21 +145,11 @@ QHash<EmojiModel::Category, QVariantList> EmojiModel::_emojis;
QVariantList EmojiModel::categories() const QVariantList EmojiModel::categories() const
{ {
return QVariantList{ return QVariantList{
// {QVariantMap{
// {"category", EmojiModel::Search},
// {"name", i18nc("Search for emojis", "Search")},
// {"emoji", QStringLiteral("🔎")},
// }},
{QVariantMap{ {QVariantMap{
{"category", EmojiModel::History}, {"category", EmojiModel::HistoryNoCustom},
{"name", i18nc("Previously used emojis", "History")}, {"name", i18nc("Previously used emojis", "History")},
{"emoji", QStringLiteral("⌛️")}, {"emoji", QStringLiteral("⌛️")},
}}, }},
{QVariantMap{
{"category", EmojiModel::Custom},
{"name", i18nc("'Custom' is a category of emoji", "Custom")},
{"emoji", QStringLiteral("😏")},
}},
{QVariantMap{ {QVariantMap{
{"category", EmojiModel::Smileys}, {"category", EmojiModel::Smileys},
{"name", i18nc("'Smileys' is a category of emoji", "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 history READ history NOTIFY historyChanged)
Q_PROPERTY(QVariantList categories READ categories CONSTANT) Q_PROPERTY(QVariantList categories READ categories CONSTANT)
Q_PROPERTY(QVariantList categoriesWithCustom READ categoriesWithCustom CONSTANT)
public: public:
static EmojiModel &instance() static EmojiModel &instance()
@@ -69,7 +70,9 @@ public:
enum Category { enum Category {
Custom, Custom,
Search, Search,
SearchNoCustom,
History, History,
HistoryNoCustom,
Smileys, Smileys,
People, People,
Nature, Nature,
@@ -89,11 +92,14 @@ public:
Q_INVOKABLE QVariantList history() const; Q_INVOKABLE QVariantList history() const;
Q_INVOKABLE static QVariantList filterModel(const QString &filter, bool limit = true); 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 emojis(Category category) const;
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const; Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
QVariantList categories() const; QVariantList categories() const;
QVariantList categoriesWithCustom() const;
Q_SIGNALS: Q_SIGNALS:
void historyChanged(); void historyChanged();

View File

@@ -57,15 +57,21 @@ ColumnLayout {
id: emojiPickerLoader id: emojiPickerLoader
active: visible active: visible
visible: chatBar.emojiPaneOpened visible: chatBar.emojiPaneOpened
onItemChanged: if (visible) {
emojiPickerLoader.item.forceActiveFocus()
}
Layout.fillWidth: true Layout.fillWidth: true
sourceComponent: QQC2.Pane { sourceComponent: QQC2.Pane {
onActiveFocusChanged: if(activeFocus) {
emojiPicker.forceActiveFocus()
}
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
rightPadding: 0 rightPadding: 0
leftPadding: 0 leftPadding: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: EmojiPicker { contentItem: EmojiPicker {
textArea: chatBar.textField id: emojiPicker
onChosen: insertText(emoji) 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-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2 import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
ColumnLayout { ColumnLayout {
id: _picker id: emojiPicker
property var emojiCategory: EmojiModel.History readonly property int categoryIconSize: 45
property var textArea readonly property var currentCategory: EmojiModel.categoriesWithCustom[categories.currentIndex].category
readonly property var emojiModel: EmojiModel readonly property int categoryCount: categories.count
signal chosen(string emoji) signal chosen(string emoji)
spacing: 0 spacing: 0
onActiveFocusChanged: if (activeFocus) categories.forceActiveFocus()
QQC2.ScrollView { QQC2.ScrollView {
Layout.fillWidth: true 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 QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0
Layout.preferredHeight: emojiPicker.categoryIconSize + QQC2.ScrollBar.horizontal.height
ListView { ListView {
id: categories
clip: true clip: true
orientation: ListView.Horizontal orientation: ListView.Horizontal
model: EmojiModel.categories keyNavigationEnabled: true
delegate: QQC2.ItemDelegate { keyNavigationWraps: true
id: del Keys.forwardTo: searchField
interactive: width !== contentWidth
width: contentItem.Layout.preferredWidth model: EmojiModel.categoriesWithCustom
height: Kirigami.Units.gridUnit * 2 delegate: EmojiDelegate {
id: emojiDelegate
contentItem: Kirigami.Heading { width: emojiPicker.categoryIconSize
horizontalAlignment: Text.AlignHCenter height: width
verticalAlignment: Text.AlignVCenter
level: modelData.category === EmojiModel.Custom ? 4 : 1
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' onClicked: {
text: modelData.category === EmojiModel.Custom ? i18n("Custom") : modelData.emoji 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 Layout.preferredHeight: 1
} }
QQC2.ScrollView { Kirigami.SearchField {
id: searchField
Layout.margins: Kirigami.Units.smallSpacing
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 8 }
Layout.fillHeight: true
GridView { EmojiGrid {
cellWidth: Kirigami.Units.gridUnit * 2 id: emojiGrid
cellHeight: Kirigami.Units.gridUnit * 2 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 Keys.forwardTo: searchField
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)
}
}
}
} }
} }

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 import org.kde.neochat 1.0
QQC2.Popup { QQC2.Popup {
id: root id: emojiPopup
signal react(string emoji) 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 modal: true
focus: true focus: true
closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutsideParent closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutsideParent
margins: 0 margins: 0
padding: 1 padding: 2
implicitWidth: Kirigami.Units.gridUnit * 16
implicitHeight: Kirigami.Units.gridUnit * 20
background: Rectangle { implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
Kirigami.Theme.inherit: false width: Math.min(contentItem.categoryIconSize * contentItem.categoryCount + 2 * padding, QQC2.Overlay.overlay.width)
Kirigami.Theme.colorSet: Kirigami.Theme.View contentItem: ReactionPicker {
color: Kirigami.Theme.backgroundColor onChosen: {
border.width: 1 react(emoji)
border.color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, emojiPopup.close()
Kirigami.Theme.textColor, }
0.15)
}
contentItem: EmojiPicker {
onChosen: react(emoji)
} }
} }

View File

@@ -93,5 +93,9 @@
<file alias="ConfirmEncryptionDialog.qml">qml/Dialog/ConfirmEncryptionDialog.qml</file> <file alias="ConfirmEncryptionDialog.qml">qml/Dialog/ConfirmEncryptionDialog.qml</file>
<file alias="RemoveSheet.qml">qml/Menu/Timeline/RemoveSheet.qml</file> <file alias="RemoveSheet.qml">qml/Menu/Timeline/RemoveSheet.qml</file>
<file alias="BanSheet.qml">qml/Menu/Timeline/BanSheet.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> </qresource>
</RCC> </RCC>