feat: ponies.im emoji support (custom emojum)
This commit is contained in:
@@ -335,7 +335,7 @@ ToolBar {
|
||||
} else if (completionInfo.type === ChatDocumentHandler.Command) {
|
||||
completionMenu.model = CommandModel.filterModel(completionInfo.keyword);
|
||||
} else {
|
||||
completionMenu.model = EmojiModel.filterModel(completionInfo.keyword);
|
||||
completionMenu.model = Array.from(chatBar.customEmojiModel.filterModel(completionInfo.keyword)).concat(EmojiModel.filterModel(completionInfo.keyword))
|
||||
}
|
||||
|
||||
if (completionMenu.model.length === 0) {
|
||||
@@ -443,6 +443,10 @@ ToolBar {
|
||||
}
|
||||
}
|
||||
|
||||
property CustomEmojiModel customEmojiModel: CustomEmojiModel {
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
|
||||
function pasteImage() {
|
||||
let localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png";
|
||||
if (!Clipboard.saveImage(localPath)) {
|
||||
@@ -457,7 +461,7 @@ ToolBar {
|
||||
if (ChatBoxHelper.hasAttachment) {
|
||||
// send attachment but don't reset the text
|
||||
actionsHandler.postMessage("", ChatBoxHelper.attachmentPath,
|
||||
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, {});
|
||||
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, {}, this.customEmojiModel);
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
messageSent();
|
||||
return;
|
||||
@@ -470,7 +474,7 @@ ToolBar {
|
||||
} else {
|
||||
// send normal message
|
||||
actionsHandler.postMessage(inputField.text.trim(), ChatBoxHelper.attachmentPath,
|
||||
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, userAutocompleted);
|
||||
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, userAutocompleted, this.customEmojiModel);
|
||||
}
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
inputField.clear();
|
||||
|
||||
@@ -10,6 +10,7 @@ import Qt.labs.qmlmodels 1.0
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
Popup {
|
||||
id: control
|
||||
@@ -94,23 +95,40 @@ Popup {
|
||||
Kirigami.BasicListItem {
|
||||
id: emojiItem
|
||||
width: ListView.view.width ?? implicitWidth
|
||||
property string displayName: modelData.unicode
|
||||
text: modelData.unicode + " " + modelData.shortname
|
||||
property string displayName: modelData.isCustom ? modelData.shortname : modelData.unicode
|
||||
text: modelData.shortname
|
||||
reserveSpaceForSubtitle: true
|
||||
|
||||
leading: Label {
|
||||
id: unicodeLabel
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit
|
||||
Layout.preferredWidth: textMetrics.tightBoundingRect.width
|
||||
font.pointSize: Kirigami.Units.gridUnit * 0.75
|
||||
text: modelData.unicode
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
text: modelData.unicode
|
||||
font: unicodeLabel.font
|
||||
leading: Image {
|
||||
source: modelData.isCustom ? modelData.unicode : ""
|
||||
|
||||
width: height
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
radius: height/2
|
||||
gradient: ShimmerGradient { }
|
||||
}
|
||||
|
||||
Label {
|
||||
id: unicodeLabel
|
||||
|
||||
visible: !modelData.isCustom
|
||||
|
||||
font.family: 'emoji'
|
||||
font.pixelSize: height - 2
|
||||
|
||||
text: modelData.unicode
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: completeTriggered();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,16 @@ import org.kde.neochat 1.0 as NeoChat
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: _picker
|
||||
|
||||
property string emojiCategory: "history"
|
||||
property var textArea
|
||||
readonly property var emojiModel: NeoChat.EmojiModel
|
||||
|
||||
property NeoChat.CustomEmojiModel customModel: NeoChat.CustomEmojiModel {
|
||||
connection: NeoChat.Controller.activeConnection
|
||||
}
|
||||
|
||||
signal chosen(string emoji)
|
||||
|
||||
spacing: 0
|
||||
@@ -29,6 +35,7 @@ ColumnLayout {
|
||||
orientation: ListView.Horizontal
|
||||
|
||||
model: ListModel {
|
||||
ListElement { label: "custom"; category: "custom" }
|
||||
ListElement { label: "⌛️"; category: "history" }
|
||||
ListElement { label: "😏"; category: "people" }
|
||||
ListElement { label: "🌲"; category: "nature" }
|
||||
@@ -41,16 +48,23 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: Kirigami.Units.gridUnit * 2
|
||||
id: del
|
||||
|
||||
required property string label
|
||||
required property string category
|
||||
|
||||
width: contentItem.Layout.preferredWidth
|
||||
height: Kirigami.Units.gridUnit * 2
|
||||
|
||||
contentItem: Kirigami.Heading {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
level: 1
|
||||
level: del.label === "custom" ? 4 : 1
|
||||
|
||||
font.family: 'emoji'
|
||||
text: label
|
||||
Layout.preferredWidth: del.label === "custom" ? implicitWidth + Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit * 2
|
||||
|
||||
font.family: del.label === "custom" ? undefined : 'emoji'
|
||||
text: del.label === "custom" ? i18n("Custom") : del.label
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -87,6 +101,8 @@ ColumnLayout {
|
||||
|
||||
model: {
|
||||
switch (emojiCategory) {
|
||||
case "custom":
|
||||
return _picker.customModel
|
||||
case "history":
|
||||
return emojiModel.history
|
||||
case "people":
|
||||
@@ -118,11 +134,32 @@ ColumnLayout {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: 'emoji'
|
||||
text: modelData.unicode
|
||||
text: modelData.isCustom ? "" : modelData.unicode
|
||||
}
|
||||
|
||||
Image {
|
||||
visible: modelData.isCustom
|
||||
source: 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: {
|
||||
chosen(modelData.unicode)
|
||||
if (modelData.isCustom) {
|
||||
chosen(modelData.shortname)
|
||||
} else {
|
||||
chosen(modelData.unicode)
|
||||
}
|
||||
emojiModel.emojiUsed(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
39
imports/NeoChat/Component/ShimmerGradient.qml
Normal file
39
imports/NeoChat/Component/ShimmerGradient.qml
Normal file
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
// Not to be confused with the Shimmer project.
|
||||
// I like their gradiented GTK themes though.
|
||||
|
||||
import QtQuick 2.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
Gradient {
|
||||
id: gradient
|
||||
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
property color color: Kirigami.Theme.textColor
|
||||
property color translucent: Qt.rgba(color.r, color.g, color.b, 0.2)
|
||||
property color bright: Qt.rgba(color.r, color.g, color.b, 0.3)
|
||||
property real pos: 0.5
|
||||
property real offset: 0.6
|
||||
|
||||
property SequentialAnimation ani: SequentialAnimation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: -2.0
|
||||
to: 2.0
|
||||
duration: 700
|
||||
target: gradient
|
||||
properties: "pos"
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
|
||||
GradientStop { position: gradient.pos-gradient.offset; color: gradient.translucent }
|
||||
GradientStop { position: gradient.pos; color: gradient.bright }
|
||||
GradientStop { position: gradient.pos+gradient.offset; color: gradient.translucent }
|
||||
}
|
||||
@@ -4,3 +4,4 @@ ChatTextInput 1.0 ChatTextInput.qml
|
||||
FancyEffectsContainer 1.0 FancyEffectsContainer.qml
|
||||
TypingPane 1.0 TypingPane.qml
|
||||
QuickSwitcher 1.0 QuickSwitcher.qml
|
||||
ShimmerGradient 1.0 ShimmerGradient.qml
|
||||
|
||||
@@ -53,6 +53,11 @@ Kirigami.ScrollablePage {
|
||||
icon.name: "preferences-system-users"
|
||||
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Page/AccountsPage.qml")
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Custom Emoji")
|
||||
icon.name: "preferences-desktop-emoticons"
|
||||
onTriggered: pageSettingStack.push("qrc:/imports/NeoChat/Settings/Emoticons.qml")
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("About NeoChat")
|
||||
icon.name: "help-about"
|
||||
|
||||
104
imports/NeoChat/Settings/Emoticons.qml
Normal file
104
imports/NeoChat/Settings/Emoticons.qml
Normal file
@@ -0,0 +1,104 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: LGPL-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
|
||||
import NeoChat.Settings 1.0
|
||||
|
||||
import NeoChat.Component 1.0 as Components
|
||||
import NeoChat.Dialog 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {
|
||||
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
header: QQC2.ToolBar {
|
||||
width: parent.width
|
||||
contentItem: RowLayout {
|
||||
Item {
|
||||
Layout.fillWidth: Qt.application.layoutDirection == Qt.LeftToRight
|
||||
}
|
||||
QQC2.TextField {
|
||||
id: emojiField
|
||||
placeholderText: i18n("new_emoji_name_here")
|
||||
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /[a-zA-Z_0-9]*/
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18n("Add Emoji...")
|
||||
|
||||
enabled: emojiField.text != ""
|
||||
property var fileDialog: null
|
||||
|
||||
onClicked: {
|
||||
if (this.fileDialog != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay)
|
||||
|
||||
this.fileDialog.chosen.connect((url) => {
|
||||
emojiModel.addEmoji(emojiField.text, url)
|
||||
this.fileDialog = null
|
||||
})
|
||||
this.fileDialog.onRejected.connect(() => {
|
||||
rej()
|
||||
this.fileDialog = null
|
||||
})
|
||||
this.fileDialog.open()
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: Qt.application.layoutDirection == Qt.RightToLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
model: CustomEmojiModel {
|
||||
id: emojiModel
|
||||
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
delegate: Kirigami.BasicListItem {
|
||||
id: del
|
||||
|
||||
required property string name
|
||||
required property url imageURL
|
||||
|
||||
text: name
|
||||
reserveSpaceForSubtitle: true
|
||||
|
||||
leading: Image {
|
||||
width: height
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
source: imageURL
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
radius: height/2
|
||||
gradient: Components.ShimmerGradient { }
|
||||
}
|
||||
}
|
||||
|
||||
trailing: QQC2.ToolButton {
|
||||
width: height
|
||||
icon.name: "delete"
|
||||
onClicked: emojiModel.removeEmoji(del.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
res.qrc
2
res.qrc
@@ -18,6 +18,7 @@
|
||||
<file>imports/NeoChat/Component/FullScreenImage.qml</file>
|
||||
<file>imports/NeoChat/Component/FancyEffectsContainer.qml</file>
|
||||
<file>imports/NeoChat/Component/TypingPane.qml</file>
|
||||
<file>imports/NeoChat/Component/ShimmerGradient.qml</file>
|
||||
<file>imports/NeoChat/Component/QuickSwitcher.qml</file>
|
||||
<file>imports/NeoChat/Component/ChatBox</file>
|
||||
<file>imports/NeoChat/Component/ChatBox/ChatBox.qml</file>
|
||||
@@ -72,6 +73,7 @@
|
||||
<file>imports/NeoChat/Settings/ThemeRadioButton.qml</file>
|
||||
<file>imports/NeoChat/Settings/ColorScheme.qml</file>
|
||||
<file>imports/NeoChat/Settings/GeneralSettingsPage.qml</file>
|
||||
<file>imports/NeoChat/Settings/Emoticons.qml</file>
|
||||
<file>imports/NeoChat/Settings/AppearanceSettingsPage.qml</file>
|
||||
<file>imports/NeoChat/Settings/qmldir</file>
|
||||
</qresource>
|
||||
|
||||
@@ -8,6 +8,8 @@ add_executable(neochat
|
||||
controller.cpp
|
||||
actionshandler.cpp
|
||||
emojimodel.cpp
|
||||
customemojimodel.cpp
|
||||
customemojimodel+network.cpp
|
||||
clipboard.cpp
|
||||
matriximageprovider.cpp
|
||||
messageeventmodel.cpp
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "controller.h"
|
||||
#include "roommanager.h"
|
||||
#include "customemojimodel.h"
|
||||
|
||||
ActionsHandler::ActionsHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -72,7 +73,7 @@ void ActionsHandler::postEdit(const QString &text)
|
||||
if (!match.hasMatch()) {
|
||||
// should not happen but still make sure to send the message normally
|
||||
// just in case.
|
||||
postMessage(text, QString(), QString(), QString(), QVariantMap());
|
||||
postMessage(text, QString(), QString(), QString(), QVariantMap(), nullptr);
|
||||
}
|
||||
const QString regex = match.captured(1);
|
||||
const QString replacement = match.captured(2);
|
||||
@@ -93,11 +94,19 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
const QString &attachementPath,
|
||||
const QString &replyEventId,
|
||||
const QString &editEventId,
|
||||
const QVariantMap &usernames)
|
||||
const QVariantMap &usernames,
|
||||
CustomEmojiModel* cem)
|
||||
{
|
||||
QString rawText = text;
|
||||
QString cleanedText = text;
|
||||
|
||||
auto preprocess = [cem](const QString& it) -> QString {
|
||||
if (cem == nullptr) {
|
||||
return it;
|
||||
}
|
||||
return cem->preprocessText(it);
|
||||
};
|
||||
|
||||
for (auto it = usernames.constBegin(); it != usernames.constEnd(); it++) {
|
||||
cleanedText = cleanedText.replace(it.key(), "[" + it.key() + "](https://matrix.to/#/" + it.value().toString() + ")");
|
||||
}
|
||||
@@ -163,7 +172,7 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
for (int i = 0; i < cleanedText.length(); i++) {
|
||||
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
|
||||
}
|
||||
m_room->postHtmlMessage(cleanedText, rainbowText, RoomMessageEvent::MsgType::Notice, replyEventId, editEventId);
|
||||
m_room->postHtmlMessage(cleanedText, preprocess(rainbowText), RoomMessageEvent::MsgType::Notice, replyEventId, editEventId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -173,7 +182,7 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
for (int i = 0; i < cleanedText.length(); i++) {
|
||||
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
|
||||
}
|
||||
m_room->postHtmlMessage(cleanedText, rainbowText, messageEventType, replyEventId, editEventId);
|
||||
m_room->postHtmlMessage(cleanedText, preprocess(rainbowText), messageEventType, replyEventId, editEventId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -280,5 +289,5 @@ void ActionsHandler::postMessage(const QString &text,
|
||||
cleanedText = cleanedText.remove(0, noticePrefix.length());
|
||||
messageEventType = RoomMessageEvent::MsgType::Notice;
|
||||
}
|
||||
m_room->postMessage(rawText, cleanedText, messageEventType, replyEventId, editEventId);
|
||||
m_room->postMessage(rawText, preprocess(m_room->preprocessText(cleanedText)), messageEventType, replyEventId, editEventId);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class CustomEmojiModel;
|
||||
|
||||
/// \brief Handles user interactions with NeoChat (joining room, creating room,
|
||||
/// sending message). Account management is handled by Controller.
|
||||
class ActionsHandler : public QObject
|
||||
@@ -56,7 +58,7 @@ public Q_SLOTS:
|
||||
///
|
||||
/// This also interprets commands if any.
|
||||
void
|
||||
postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId, const QString &editEventId, const QVariantMap &usernames);
|
||||
postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId, const QString &editEventId, const QVariantMap &usernames, CustomEmojiModel* cem);
|
||||
|
||||
/// \brief Send edit instructions (.e.g s/hallo/hello/)
|
||||
///
|
||||
|
||||
75
src/customemojimodel+network.cpp
Normal file
75
src/customemojimodel+network.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <csapi/account-data.h>
|
||||
#include <csapi/profile.h>
|
||||
#include <csapi/content-repo.h>
|
||||
|
||||
#include "customemojimodel_p.h"
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#define running isJobPending
|
||||
#else
|
||||
#define running isJobRunning
|
||||
#endif
|
||||
|
||||
void CustomEmojiModel::fetchEmojies()
|
||||
{
|
||||
if (d->conn == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& data = d->conn->accountData("im.ponies.user_emotes");
|
||||
if (data == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto emojies = data->contentJson()["emoticons"].toObject();
|
||||
|
||||
beginResetModel();
|
||||
d->emojies.clear();
|
||||
|
||||
for (const auto& emoji : emojies.keys()) {
|
||||
const auto& data = emojies[emoji];
|
||||
|
||||
d->emojies << CustomEmoji {
|
||||
.name = emoji,
|
||||
.url = data["url"].toString(),
|
||||
.regexp = QRegularExpression(QStringLiteral(R"((^|[^\\]))") + emoji)
|
||||
};
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void CustomEmojiModel::addEmoji(const QString& name, const QUrl& location)
|
||||
{
|
||||
using namespace Quotient;
|
||||
|
||||
auto job = d->conn->uploadFile(location.toLocalFile());
|
||||
|
||||
if (running(job)) {
|
||||
connect(job, &BaseJob::success, this, [this, name, job] {
|
||||
const auto& data = d->conn->accountData("im.ponies.user_emotes");
|
||||
auto json = data != nullptr ? data->contentJson() : QJsonObject();
|
||||
auto emojiData = json["emoticons"].toObject();
|
||||
emojiData[QStringLiteral(":%1:").arg(name)] = QJsonObject({
|
||||
{QStringLiteral("url"), job->contentUri()}
|
||||
});
|
||||
json["emoticons"] = emojiData;
|
||||
d->conn->setAccountData("im.ponies.user_emotes", json);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CustomEmojiModel::removeEmoji(const QString& name)
|
||||
{
|
||||
using namespace Quotient;
|
||||
|
||||
const auto& data = d->conn->accountData("im.ponies.user_emotes");
|
||||
Q_ASSERT(data != nullptr); // something's screwed if we get here with a nullptr
|
||||
auto json = data->contentJson();
|
||||
auto emojiData = json["emoticons"].toObject();
|
||||
emojiData.remove(name);
|
||||
json["emoticons"] = emojiData;
|
||||
d->conn->setAccountData("im.ponies.user_emotes", json);
|
||||
}
|
||||
113
src/customemojimodel.cpp
Normal file
113
src/customemojimodel.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "emojimodel.h"
|
||||
#include "customemojimodel_p.h"
|
||||
|
||||
enum Roles {
|
||||
Name,
|
||||
ImageURL,
|
||||
ModelData, // for emulating the regular emoji model's usage, otherwise the UI code would get too complicated
|
||||
};
|
||||
|
||||
CustomEmojiModel::CustomEmojiModel(QObject* parent) : QAbstractListModel(parent), d(new Private)
|
||||
{
|
||||
connect(this, &CustomEmojiModel::connectionChanged, this, &CustomEmojiModel::fetchEmojies);
|
||||
connect(this, &CustomEmojiModel::connectionChanged, this, [this]() {
|
||||
if (!d->conn) return;
|
||||
|
||||
connect(d->conn, &Connection::accountDataChanged, this, [this](const QString& id) {
|
||||
if (id != QStringLiteral("im.ponies.user_emotes")) {
|
||||
return;
|
||||
}
|
||||
fetchEmojies();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
CustomEmojiModel::~CustomEmojiModel()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QVariant CustomEmojiModel::data(const QModelIndex& idx, int role) const
|
||||
{
|
||||
const auto row = idx.row();
|
||||
if (row >= d->emojies.length()) {
|
||||
return QVariant();
|
||||
}
|
||||
const auto& data = d->emojies[row];
|
||||
|
||||
switch (Roles(role)) {
|
||||
case Roles::ModelData:
|
||||
return QVariant::fromValue(Emoji(
|
||||
QStringLiteral("image://mxc/") + data.url.mid(6),
|
||||
data.name,
|
||||
true
|
||||
));
|
||||
case Roles::Name:
|
||||
return data.name;
|
||||
case Roles::ImageURL:
|
||||
return QUrl(QStringLiteral("image://mxc/") + data.url.mid(6));
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
int CustomEmojiModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
|
||||
return d->emojies.length();
|
||||
}
|
||||
|
||||
QHash<int,QByteArray> CustomEmojiModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{ Name, "name" },
|
||||
{ ImageURL, "imageURL" },
|
||||
{ ModelData, "modelData" },
|
||||
};
|
||||
}
|
||||
|
||||
Connection* CustomEmojiModel::connection() const
|
||||
{
|
||||
return d->conn;
|
||||
}
|
||||
|
||||
void CustomEmojiModel::setConnection(Connection* it)
|
||||
{
|
||||
if (d->conn == it) {
|
||||
return;
|
||||
}
|
||||
if (d->conn != nullptr) {
|
||||
disconnect(d->conn, nullptr, this, nullptr);
|
||||
}
|
||||
d->conn = it;
|
||||
Q_EMIT connectionChanged();
|
||||
}
|
||||
|
||||
QString CustomEmojiModel::preprocessText(const QString &it)
|
||||
{
|
||||
auto cp = it;
|
||||
for (const auto& emoji : d->emojies) {
|
||||
cp.replace(emoji.regexp, QStringLiteral(R"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)").arg(emoji.url).arg(emoji.name));
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
QVariantList CustomEmojiModel::filterModel(const QString &filter)
|
||||
{
|
||||
QVariantList results;
|
||||
for (const auto& emoji : d->emojies) {
|
||||
if (results.length() >= 10) break;
|
||||
if (!emoji.name.contains(filter, Qt::CaseInsensitive)) continue;
|
||||
|
||||
results << QVariant::fromValue(Emoji(
|
||||
QStringLiteral("image://mxc/") + emoji.url.mid(6),
|
||||
emoji.name,
|
||||
true
|
||||
));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
55
src/customemojimodel.h
Normal file
55
src/customemojimodel.h
Normal file
@@ -0,0 +1,55 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "connection.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class CustomEmojiModel : public QAbstractListModel
|
||||
{
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
public:
|
||||
|
||||
// constructors
|
||||
|
||||
explicit CustomEmojiModel(QObject* parent = nullptr);
|
||||
~CustomEmojiModel();
|
||||
|
||||
// model
|
||||
|
||||
QVariant data(const QModelIndex& idx, int role = Qt::DisplayRole) const override;
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
|
||||
QHash<int,QByteArray> roleNames() const override;
|
||||
|
||||
// property setters
|
||||
|
||||
Connection* connection() const;
|
||||
void setConnection(Connection* it);
|
||||
Q_SIGNAL void connectionChanged();
|
||||
|
||||
// QML functions
|
||||
|
||||
Q_INVOKABLE QString preprocessText(const QString& it);
|
||||
Q_INVOKABLE QVariantList filterModel(const QString &filter);
|
||||
Q_INVOKABLE void addEmoji(const QString& name, const QUrl& location);
|
||||
Q_INVOKABLE void removeEmoji(const QString& name);
|
||||
|
||||
private:
|
||||
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d;
|
||||
|
||||
void fetchEmojies();
|
||||
|
||||
};
|
||||
19
src/customemojimodel_p.h
Normal file
19
src/customemojimodel_p.h
Normal file
@@ -0,0 +1,19 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "customemojimodel.h"
|
||||
|
||||
struct CustomEmoji
|
||||
{
|
||||
QString name; // with :semicolons:
|
||||
QString url; // mxc://
|
||||
QRegularExpression regexp;
|
||||
};
|
||||
|
||||
struct CustomEmojiModel::Private
|
||||
{
|
||||
Connection* conn = nullptr;
|
||||
QList<CustomEmoji> emojies;
|
||||
};
|
||||
@@ -10,9 +10,10 @@
|
||||
#include <utility>
|
||||
|
||||
struct Emoji {
|
||||
Emoji(QString u, QString s)
|
||||
Emoji(QString u, QString s, bool isCustom = false)
|
||||
: unicode(std::move(std::move(u)))
|
||||
, shortname(std::move(std::move(s)))
|
||||
, isCustom(isCustom)
|
||||
{
|
||||
}
|
||||
Emoji() = default;
|
||||
@@ -28,15 +29,18 @@ struct Emoji {
|
||||
{
|
||||
arch >> object.unicode;
|
||||
arch >> object.shortname;
|
||||
object.isCustom = object.unicode.startsWith("image://");
|
||||
return arch;
|
||||
}
|
||||
|
||||
QString unicode;
|
||||
QString shortname;
|
||||
bool isCustom = false;
|
||||
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString unicode MEMBER unicode)
|
||||
Q_PROPERTY(QString shortname MEMBER shortname)
|
||||
Q_PROPERTY(bool isCustom MEMBER isCustom)
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Emoji)
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
#include "userlistmodel.h"
|
||||
#include "webshortcutmodel.h"
|
||||
#include "spellcheckhighlighter.h"
|
||||
#include "customemojimodel.h"
|
||||
#ifdef HAVE_COLORSCHEME
|
||||
#include "colorschemer.h"
|
||||
#endif
|
||||
@@ -163,6 +164,7 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
|
||||
qmlRegisterType<KWebShortcutModel>("org.kde.neochat", 1, 0, "WebShortcutModel");
|
||||
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
||||
qmlRegisterType<CustomEmojiModel>("org.kde.neochat", 1, 0, "CustomEmojiModel");
|
||||
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
||||
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
||||
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
|
||||
|
||||
@@ -302,7 +302,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
using namespace Quotient;
|
||||
return visit(
|
||||
evt,
|
||||
[prettyPrint, removeReply](const RoomMessageEvent &e) {
|
||||
[this, prettyPrint, removeReply](const RoomMessageEvent &e) {
|
||||
using namespace MessageEventContent;
|
||||
|
||||
// 1. prettyPrint/HTML
|
||||
@@ -314,6 +314,10 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
htmlBody.replace(utils::userPillRegExp, R"(<b class="user-pill">\1</b>)");
|
||||
htmlBody.replace(utils::strikethroughRegExp, "<s>\\1</s>");
|
||||
|
||||
auto url = connection()->homeserver();
|
||||
auto base = url.scheme() + QStringLiteral("://") + url.host() + (url.port() != -1 ? ':'+QString::number(url.port()) : QString());
|
||||
htmlBody.replace(utils::mxcImageRegExp, QStringLiteral(R"(<img \1 src="%1/_matrix/media/r0/download/\2/\3" \4 > )").arg(base));
|
||||
|
||||
return htmlBody;
|
||||
}
|
||||
|
||||
@@ -532,12 +536,14 @@ QString msgTypeToString(MessageEventType msgType)
|
||||
}
|
||||
}
|
||||
|
||||
QString NeoChatRoom::preprocessText(const QString& text)
|
||||
{
|
||||
return markdownToHTML(text);
|
||||
}
|
||||
|
||||
void NeoChatRoom::postMessage(const QString &rawText, const QString &text, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
|
||||
{
|
||||
const auto html = markdownToHTML(text);
|
||||
QString cleanText(text);
|
||||
cleanText.replace(QRegularExpression("\\[(.+)\\]\\(.+\\)"), "\\1");
|
||||
postHtmlMessage(rawText, html, type, replyEventId, relateToEventId);
|
||||
postHtmlMessage(rawText, text, type, replyEventId, relateToEventId);
|
||||
}
|
||||
|
||||
void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
|
||||
|
||||
@@ -138,6 +138,8 @@ public Q_SLOTS:
|
||||
void acceptInvitation();
|
||||
void forget();
|
||||
void sendTypingNotification(bool isTyping);
|
||||
QString preprocessText(const QString &text);
|
||||
|
||||
/// @param rawText The text as it was typed.
|
||||
/// @param cleanedText The text with link to the users.
|
||||
void postMessage(const QString &rawText,
|
||||
|
||||
@@ -64,7 +64,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
||||
replyAction->setPlaceholderText(i18n("Reply..."));
|
||||
QObject::connect(replyAction.get(), &KNotificationReplyAction::replied, [room, replyEventId](const QString &text) {
|
||||
room->postMessage(text, text, RoomMessageEvent::MsgType::Text, replyEventId, QString());
|
||||
room->postMessage(text, room->preprocessText(text), RoomMessageEvent::MsgType::Text, replyEventId, QString());
|
||||
});
|
||||
notification->setReplyAction(std::move(replyAction));
|
||||
#endif
|
||||
|
||||
@@ -22,4 +22,5 @@ static const QRegularExpression removeRichReplyRegex{"<mx-reply>.*?</mx-reply>",
|
||||
static const QRegularExpression codePillRegExp{"<pre><code[^>]*>(.*?)</code></pre>", QRegularExpression::DotMatchesEverythingOption};
|
||||
static const QRegularExpression userPillRegExp{"(<a href=\"https://matrix.to/#/@.*?:.*?\">.*?</a>)", QRegularExpression::DotMatchesEverythingOption};
|
||||
static const QRegularExpression strikethroughRegExp{"<del>(.*?)</del>", QRegularExpression::DotMatchesEverythingOption};
|
||||
static const QRegularExpression mxcImageRegExp{R"AAA(<img(.*?)src="mxc:\/\/(.*?)\/(.*?)"(.*?)>)AAA"};
|
||||
} // namespace utils
|
||||
|
||||
Reference in New Issue
Block a user