feat: ponies.im emoji support (custom emojum)

This commit is contained in:
Jan Blackquill
2021-06-13 19:52:59 -04:00
parent 9961483f5c
commit 30965cb503
21 changed files with 537 additions and 37 deletions

View File

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

View File

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

View File

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

View 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 }
}

View File

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

View File

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

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

View File

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

View File

@@ -8,6 +8,8 @@ add_executable(neochat
controller.cpp
actionshandler.cpp
emojimodel.cpp
customemojimodel.cpp
customemojimodel+network.cpp
clipboard.cpp
matriximageprovider.cpp
messageeventmodel.cpp

View File

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

View File

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

View 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
View 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
View 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
View 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;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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