feat: ponies.im emoji support (custom emojum)
This commit is contained in:
@@ -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