From 30965cb503cb02aa1732d9050250c424740347f2 Mon Sep 17 00:00:00 2001 From: Jan Blackquill Date: Sun, 13 Jun 2021 19:52:59 -0400 Subject: [PATCH] feat: ponies.im emoji support (custom emojum) --- imports/NeoChat/Component/ChatBox/ChatBar.qml | 10 +- .../Component/ChatBox/CompletionMenu.qml | 48 +++++--- .../NeoChat/Component/Emoji/EmojiPicker.qml | 49 +++++++- imports/NeoChat/Component/ShimmerGradient.qml | 39 ++++++ imports/NeoChat/Component/qmldir | 1 + imports/NeoChat/Page/SettingsPage.qml | 5 + imports/NeoChat/Settings/Emoticons.qml | 104 ++++++++++++++++ res.qrc | 2 + src/CMakeLists.txt | 2 + src/actionshandler.cpp | 19 ++- src/actionshandler.h | 4 +- src/customemojimodel+network.cpp | 75 ++++++++++++ src/customemojimodel.cpp | 113 ++++++++++++++++++ src/customemojimodel.h | 55 +++++++++ src/customemojimodel_p.h | 19 +++ src/emojimodel.h | 6 +- src/main.cpp | 2 + src/neochatroom.cpp | 16 ++- src/neochatroom.h | 2 + src/notificationsmanager.cpp | 2 +- src/utils.h | 1 + 21 files changed, 537 insertions(+), 37 deletions(-) create mode 100644 imports/NeoChat/Component/ShimmerGradient.qml create mode 100644 imports/NeoChat/Settings/Emoticons.qml create mode 100644 src/customemojimodel+network.cpp create mode 100644 src/customemojimodel.cpp create mode 100644 src/customemojimodel.h create mode 100644 src/customemojimodel_p.h diff --git a/imports/NeoChat/Component/ChatBox/ChatBar.qml b/imports/NeoChat/Component/ChatBox/ChatBar.qml index ce776eaf2..1901cdff7 100644 --- a/imports/NeoChat/Component/ChatBox/ChatBar.qml +++ b/imports/NeoChat/Component/ChatBox/ChatBar.qml @@ -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(); diff --git a/imports/NeoChat/Component/ChatBox/CompletionMenu.qml b/imports/NeoChat/Component/ChatBox/CompletionMenu.qml index 92e17c935..98673f98f 100644 --- a/imports/NeoChat/Component/ChatBox/CompletionMenu.qml +++ b/imports/NeoChat/Component/ChatBox/CompletionMenu.qml @@ -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(); } } diff --git a/imports/NeoChat/Component/Emoji/EmojiPicker.qml b/imports/NeoChat/Component/Emoji/EmojiPicker.qml index dbd185df8..a9b7f3b99 100644 --- a/imports/NeoChat/Component/Emoji/EmojiPicker.qml +++ b/imports/NeoChat/Component/Emoji/EmojiPicker.qml @@ -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) } } diff --git a/imports/NeoChat/Component/ShimmerGradient.qml b/imports/NeoChat/Component/ShimmerGradient.qml new file mode 100644 index 000000000..efb75e9c4 --- /dev/null +++ b/imports/NeoChat/Component/ShimmerGradient.qml @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2021 Carson Black +// 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 } +} \ No newline at end of file diff --git a/imports/NeoChat/Component/qmldir b/imports/NeoChat/Component/qmldir index eca83c0cc..038c8b9b3 100644 --- a/imports/NeoChat/Component/qmldir +++ b/imports/NeoChat/Component/qmldir @@ -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 diff --git a/imports/NeoChat/Page/SettingsPage.qml b/imports/NeoChat/Page/SettingsPage.qml index cc108790f..54b0e8eb6 100644 --- a/imports/NeoChat/Page/SettingsPage.qml +++ b/imports/NeoChat/Page/SettingsPage.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" diff --git a/imports/NeoChat/Settings/Emoticons.qml b/imports/NeoChat/Settings/Emoticons.qml new file mode 100644 index 000000000..3a56fe46b --- /dev/null +++ b/imports/NeoChat/Settings/Emoticons.qml @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2021 Carson Black +// 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) + } + } + } +} diff --git a/res.qrc b/res.qrc index 561f70f0e..b8c3e13fa 100644 --- a/res.qrc +++ b/res.qrc @@ -18,6 +18,7 @@ imports/NeoChat/Component/FullScreenImage.qml imports/NeoChat/Component/FancyEffectsContainer.qml imports/NeoChat/Component/TypingPane.qml + imports/NeoChat/Component/ShimmerGradient.qml imports/NeoChat/Component/QuickSwitcher.qml imports/NeoChat/Component/ChatBox imports/NeoChat/Component/ChatBox/ChatBox.qml @@ -72,6 +73,7 @@ imports/NeoChat/Settings/ThemeRadioButton.qml imports/NeoChat/Settings/ColorScheme.qml imports/NeoChat/Settings/GeneralSettingsPage.qml + imports/NeoChat/Settings/Emoticons.qml imports/NeoChat/Settings/AppearanceSettingsPage.qml imports/NeoChat/Settings/qmldir diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fd884edcf..ad334224e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,8 @@ add_executable(neochat controller.cpp actionshandler.cpp emojimodel.cpp + customemojimodel.cpp + customemojimodel+network.cpp clipboard.cpp matriximageprovider.cpp messageeventmodel.cpp diff --git a/src/actionshandler.cpp b/src/actionshandler.cpp index e675d7651..6c9b74761 100644 --- a/src/actionshandler.cpp +++ b/src/actionshandler.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("" % cleanedText.at(i) % ""; } - 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("" % cleanedText.at(i) % ""; } - 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); } diff --git a/src/actionshandler.h b/src/actionshandler.h index fe2e35ba0..a0ae207d7 100644 --- a/src/actionshandler.h +++ b/src/actionshandler.h @@ -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/) /// diff --git a/src/customemojimodel+network.cpp b/src/customemojimodel+network.cpp new file mode 100644 index 000000000..32d8fbf5a --- /dev/null +++ b/src/customemojimodel+network.cpp @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2021 Carson Black +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#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); +} diff --git a/src/customemojimodel.cpp b/src/customemojimodel.cpp new file mode 100644 index 000000000..84e62f6ed --- /dev/null +++ b/src/customemojimodel.cpp @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2021 Carson Black +// 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 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"(%2)").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; +} diff --git a/src/customemojimodel.h b/src/customemojimodel.h new file mode 100644 index 000000000..b2a610b04 --- /dev/null +++ b/src/customemojimodel.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2021 Carson Black +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +#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 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 d; + + void fetchEmojies(); + +}; diff --git a/src/customemojimodel_p.h b/src/customemojimodel_p.h new file mode 100644 index 000000000..a828d1ffe --- /dev/null +++ b/src/customemojimodel_p.h @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2021 Carson Black +// 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 emojies; +}; diff --git a/src/emojimodel.h b/src/emojimodel.h index ec58882ac..d484b6057 100644 --- a/src/emojimodel.h +++ b/src/emojimodel.h @@ -10,9 +10,10 @@ #include 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) diff --git a/src/main.cpp b/src/main.cpp index 6cd684a3b..853f2a811 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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("org.kde.neochat", 1, 0, "RoomListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "WebShortcutModel"); qmlRegisterType("org.kde.neochat", 1, 0, "UserListModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "CustomEmojiModel"); qmlRegisterType("org.kde.neochat", 1, 0, "MessageEventModel"); qmlRegisterType("org.kde.neochat", 1, 0, "MessageFilterModel"); qmlRegisterType("org.kde.neochat", 1, 0, "PublicRoomListModel"); diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index a3976dd19..e3d98d655 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -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"(\1)"); htmlBody.replace(utils::strikethroughRegExp, "\\1"); + 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"( )").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) diff --git a/src/neochatroom.h b/src/neochatroom.h index 1f8e99021..4feab4dce 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -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, diff --git a/src/notificationsmanager.cpp b/src/notificationsmanager.cpp index ff908aed5..9c48ff99b 100644 --- a/src/notificationsmanager.cpp +++ b/src/notificationsmanager.cpp @@ -64,7 +64,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room, std::unique_ptr 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 diff --git a/src/utils.h b/src/utils.h index 4b8b11e2c..555957c56 100644 --- a/src/utils.h +++ b/src/utils.h @@ -22,4 +22,5 @@ static const QRegularExpression removeRichReplyRegex{".*?", static const QRegularExpression codePillRegExp{"
]*>(.*?)
", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression userPillRegExp{"(.*?)", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression strikethroughRegExp{"(.*?)", QRegularExpression::DotMatchesEverythingOption}; +static const QRegularExpression mxcImageRegExp{R"AAA()AAA"}; } // namespace utils