Move NeoChatConnection and NeoChatRoom to LibNeoChat

Move `NeoChatConnection` and `NeoChatRoom` to `LibNeoChat` along with any required dependencies.
This commit is contained in:
James Graham
2025-04-07 18:52:15 +00:00
parent 8327b4369e
commit aef4f75c33
54 changed files with 57 additions and 72 deletions

View File

@@ -1,625 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "actionsmodel.h"
#include "chatbarcache.h"
#include "enums/messagetype.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include <Quotient/events/eventcontent.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/user.h>
#include <KLocalizedString>
using Action = ActionsModel::Action;
using namespace Quotient;
using namespace Qt::StringLiterals;
bool ActionsModel::m_allowQuickEdit = false;
QStringList rainbowColors{"#ff2b00"_L1, "#ff5500"_L1, "#ff8000"_L1, "#ffaa00"_L1, "#ffd500"_L1, "#ffff00"_L1, "#d4ff00"_L1, "#aaff00"_L1, "#80ff00"_L1,
"#55ff00"_L1, "#2bff00"_L1, "#00ff00"_L1, "#00ff2b"_L1, "#00ff55"_L1, "#00ff80"_L1, "#00ffaa"_L1, "#00ffd5"_L1, "#00ffff"_L1,
"#00d4ff"_L1, "#00aaff"_L1, "#007fff"_L1, "#0055ff"_L1, "#002bff"_L1, "#0000ff"_L1, "#2a00ff"_L1, "#5500ff"_L1, "#7f00ff"_L1,
"#aa00ff"_L1, "#d400ff"_L1, "#ff00ff"_L1, "#ff00d4"_L1, "#ff00aa"_L1, "#ff0080"_L1, "#ff0055"_L1, "#ff002b"_L1, "#ff0000"_L1};
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(MessageType::Information, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
} else {
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto leaving = room->connection()->room(text);
if (!leaving) {
leaving = room->connection()->roomByAlias(text);
}
if (leaving) {
Q_EMIT room->showMessage(MessageType::Information, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
room->connection()->leaveRoom(leaving);
} else {
Q_EMIT room->showMessage(MessageType::Information, i18nc("Room <roomname> not found", "Room %1 not found.", text));
}
}
return QString();
};
auto roomNickLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(MessageType::Error, i18n("No new nickname provided, no changes will happen."));
} else {
room->connection()->user()->rename(text, room);
}
return QString();
};
QList<ActionsModel::Action> actions{
Action{
u"shrug"_s,
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return u"¯\\\\_(ツ)_/¯ %1"_s.arg(message);
},
Quotient::RoomMessageEvent::MsgType::Text,
kli18n("<message>"),
kli18n("Prepends ¯\\_(ツ)_/¯ to a plain-text message"),
},
Action{
u"lenny"_s,
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return u"( ͡° ͜ʖ ͡°) %1"_s.arg(message);
},
Quotient::RoomMessageEvent::MsgType::Text,
kli18n("<message>"),
kli18n("Prepends ( ͡° ͜ʖ ͡°) to a plain-text message"),
},
Action{
u"tableflip"_s,
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return u"(╯°□°)╯︵ ┻━┻ %1"_s.arg(message);
},
Quotient::RoomMessageEvent::MsgType::Text,
kli18n("<message>"),
kli18n("Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message"),
},
Action{
u"unflip"_s,
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return u"┬──┬ ( ゜-゜ノ) %1"_s.arg(message);
},
Quotient::RoomMessageEvent::MsgType::Text,
kli18n("<message>"),
kli18n("Prepends ┬──┬ ( ゜-゜ノ) to a plain-text message"),
},
Action{
u"rainbow"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
QString rainbowText;
for (int i = 0; i < text.length(); i++) {
rainbowText += u"<font color='%2'>%3</font>"_s.arg(rainbowColors[i % rainbowColors.length()], text.at(i));
}
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
auto content = std::make_unique<Quotient::EventContent::TextContent>(rainbowText, u"text/html"_s);
EventRelation relatesTo =
chatBarCache->isReplying() ? EventRelation::replyTo(chatBarCache->replyId()) : EventRelation::replace(chatBarCache->editId());
room->post<Quotient::RoomMessageEvent>("/rainbow %1"_L1.arg(text), MessageEventType::Text, std::move(content), relatesTo);
return QString();
},
std::nullopt,
kli18n("<message>"),
kli18n("Sends the given message colored as a rainbow"),
},
Action{
u"rainbowme"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
QString rainbowText;
for (int i = 0; i < text.length(); i++) {
rainbowText += u"<font color='%2'>%3</font>"_s.arg(rainbowColors[i % rainbowColors.length()], text.at(i));
}
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
auto content = std::make_unique<Quotient::EventContent::TextContent>(rainbowText, u"text/html"_s);
EventRelation relatesTo =
chatBarCache->isReplying() ? EventRelation::replyTo(chatBarCache->replyId()) : EventRelation::replace(chatBarCache->editId());
room->post<Quotient::RoomMessageEvent>(u"/rainbow %1"_s.arg(text), MessageEventType::Emote, std::move(content), relatesTo);
return QString();
},
std::nullopt,
kli18n("<message>"),
kli18n("Sends the given emote colored as a rainbow"),
},
Action{
u"plain"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
#if Quotient_VERSION_MINOR > 9
room->postText(text.toHtmlEscaped());
#else
room->postPlainText(text.toHtmlEscaped());
#endif
return QString();
},
std::nullopt,
kli18n("<message>"),
kli18n("Sends the given message as plain text"),
},
Action{
u"spoiler"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
auto content = std::make_unique<Quotient::EventContent::TextContent>(u"<span data-mx-spoiler>%1</span>"_s.arg(text), u"text/html"_s);
EventRelation relatesTo =
chatBarCache->isReplying() ? EventRelation::replyTo(chatBarCache->replyId()) : EventRelation::replace(chatBarCache->editId());
room->post<Quotient::RoomMessageEvent>(u"/spoiler %1"_s.arg(text), MessageEventType::Text, std::move(content), relatesTo);
return QString();
},
std::nullopt,
kli18n("<message>"),
kli18n("Sends the given message as a spoiler"),
},
Action{
u"me"_s,
[](const QString &text, NeoChatRoom *, ChatBarCache *) {
return text;
},
RoomMessageEvent::MsgType::Emote,
kli18n("<message>"),
kli18n("Sends the given emote"),
},
Action{
u"notice"_s,
[](const QString &text, NeoChatRoom *, ChatBarCache *) {
return text;
},
RoomMessageEvent::MsgType::Notice,
kli18n("<message>"),
kli18n("Sends the given message as a notice"),
},
Action{
u"invite"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(uR"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"_s);
auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text);
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Invite) {
Q_EMIT room->showMessage(MessageType::Information,
i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
return QString();
}
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Ban) {
Q_EMIT room->showMessage(MessageType::Information, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
return QString();
}
if (room->localMember().id() == text) {
Q_EMIT room->showMessage(MessageType::Positive, i18n("You are already in this room."));
return QString();
}
if (room->joinedMemberIds().contains(text)) {
Q_EMIT room->showMessage(MessageType::Information, i18nc("<user> is already in this room.", "%1 is already in this room.", text));
return QString();
}
room->inviteToRoom(text);
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> was invited into this room.", "%1 was invited into this room.", text));
return QString();
},
std::nullopt,
kli18n("<user id>"),
kli18n("Invites the user to this room"),
},
Action{
u"join"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
if (targetRoom) {
ActionsModel::instance().resolveResource(targetRoom->id());
return QString();
}
Q_EMIT room->showMessage(MessageType::Information, i18nc("Joining room <roomname>.", "Joining room %1.", text));
ActionsModel::instance().resolveResource(text, "join"_L1);
return QString();
},
std::nullopt,
kli18n("<room alias or id>"),
kli18n("Joins the given room"),
},
Action{
u"knock"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(u" "_s);
QString roomName = parts[0];
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
auto regexMatch = roomRegex.match(roomName);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
if (targetRoom) {
ActionsModel::instance().resolveResource(targetRoom->id());
return QString();
}
Q_EMIT room->showMessage(MessageType::Information, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
auto connection = dynamic_cast<NeoChatConnection *>(room->connection());
const auto knownServer = roomName.mid(roomName.indexOf(":"_L1) + 1);
if (parts.length() >= 2) {
ActionsModel::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
} else {
ActionsModel::instance().knockRoom(connection, roomName, QString(), QStringList{knownServer});
}
return QString();
},
std::nullopt,
kli18n("<room alias or id> [<reason>]"),
kli18n("Requests to join the given room"),
},
Action{
u"j"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
QRegularExpression roomRegex(uR"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"_s);
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
if (room->connection()->room(text) || room->connection()->roomByAlias(text)) {
Q_EMIT room->showMessage(MessageType::Information, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
return QString();
}
Q_EMIT room->showMessage(MessageType::Information, i18nc("Joining room <roomname>.", "Joining room %1.", text));
ActionsModel::instance().resolveResource(text, "join"_L1);
return QString();
},
std::nullopt,
kli18n("<room alias or id>"),
kli18n("Joins the given room"),
},
Action{
u"part"_s,
leaveRoomLambda,
std::nullopt,
kli18n("[<room alias or id>]"),
kli18n("Leaves the given room or this room, if there is none given"),
},
Action{
u"leave"_s,
leaveRoomLambda,
std::nullopt,
kli18n("[<room alias or id>]"),
kli18n("Leaves the given room or this room, if there is none given"),
},
Action{
u"nick"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(MessageType::Error, i18n("No new nickname provided, no changes will happen."));
} else {
room->connection()->user()->rename(text);
}
return QString();
},
std::nullopt,
kli18n("<display name>"),
kli18n("Changes your global display name"),
},
Action{
u"roomnick"_s,
roomNickLambda,
std::nullopt,
kli18n("<display name>"),
kli18n("Changes your display name in this room"),
},
Action{
u"myroomnick"_s,
roomNickLambda,
std::nullopt,
kli18n("<display name>"),
kli18n("Changes your display name in this room"),
},
Action{
u"ignore"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(uR"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"_s);
auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
if (room->connection()->ignoredUsers().contains(text)) {
Q_EMIT room->showMessage(MessageType::Information, i18nc("<username> is already ignored.", "%1 is already ignored.", text));
return QString();
}
room->connection()->addToIgnoredUsers(text);
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
return QString();
},
std::nullopt,
kli18n("<user id>"),
kli18n("Ignores the given user"),
},
Action{
u"unignore"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(uR"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"_s);
auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
if (!room->connection()->ignoredUsers().contains(text)) {
Q_EMIT room->showMessage(MessageType::Information, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
return QString();
}
room->connection()->removeFromIgnoredUsers(text);
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
return QString();
},
std::nullopt,
kli18n("<user id>"),
kli18n("Unignores the given user"),
},
Action{
u"react"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
if (chatBarCache->replyId().isEmpty()) {
for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) {
const auto &evt = **it;
if (const auto event = eventCast<const RoomMessageEvent>(&evt)) {
room->toggleReaction(event->id(), text);
return QString();
}
}
}
room->toggleReaction(chatBarCache->replyId(), text);
return QString();
},
std::nullopt,
kli18n("<reaction text>"),
kli18n("React to the message with the given text"),
},
Action{
u"ban"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(u" "_s);
static const QRegularExpression mxidRegex(uR"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"_s);
auto regexMatch = mxidRegex.match(parts[0]);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
auto state = room->currentState().get<RoomMemberEvent>(parts[0]);
if (state && state->membership() == Membership::Ban) {
Q_EMIT room->showMessage(MessageType::Information,
i18nc("<user> is already banned from this room.", "%1 is already banned from this room.", text));
return QString();
}
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return QString();
}
if (plEvent->ban() > plEvent->powerLevelForUser(room->localMember().id())) {
Q_EMIT room->showMessage(MessageType::Error, i18n("You are not allowed to ban users from this room."));
return QString();
}
if (plEvent->powerLevelForUser(room->localMember().id()) <= plEvent->powerLevelForUser(parts[0])) {
Q_EMIT room->showMessage(
MessageType::Error,
i18nc("You are not allowed to ban <username> from this room.", "You are not allowed to ban %1 from this room.", parts[0]));
return QString();
}
room->ban(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> was banned from this room.", "%1 was banned from this room.", parts[0]));
return QString();
},
std::nullopt,
kli18n("<user id> [<reason>]"),
kli18n("Bans the given user"),
},
Action{
u"unban"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(uR"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"_s);
auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return QString();
}
if (plEvent->ban() > plEvent->powerLevelForUser(room->localMember().id())) {
Q_EMIT room->showMessage(MessageType::Error, i18n("You are not allowed to unban users from this room."));
return QString();
}
auto state = room->currentState().get<RoomMemberEvent>(text);
if (state && state->membership() != Membership::Ban) {
Q_EMIT room->showMessage(MessageType::Information, i18nc("<user> is not banned from this room.", "%1 is not banned from this room.", text));
return QString();
}
room->unban(text);
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> was unbanned from this room.", "%1 was unbanned from this room.", text));
return QString();
},
std::nullopt,
kli18n("<user id>"),
kli18n("Removes the ban of the given user"),
},
Action{
u"kick"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(u" "_s);
static const QRegularExpression mxidRegex(uR"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"_s);
auto regexMatch = mxidRegex.match(parts[0]);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", parts[0]));
return QString();
}
if (parts[0] == room->localMember().id()) {
Q_EMIT room->showMessage(MessageType::Error, i18n("You cannot kick yourself from the room."));
return QString();
}
if (!room->isMember(parts[0])) {
Q_EMIT room->showMessage(MessageType::Error, i18nc("<username> is not in this room", "%1 is not in this room.", parts[0]));
return QString();
}
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return QString();
}
auto kick = plEvent->kick();
if (plEvent->powerLevelForUser(room->localMember().id()) < kick) {
Q_EMIT room->showMessage(MessageType::Error, i18n("You are not allowed to kick users from this room."));
return QString();
}
if (plEvent->powerLevelForUser(room->localMember().id()) <= plEvent->powerLevelForUser(parts[0])) {
Q_EMIT room->showMessage(
MessageType::Error,
i18nc("You are not allowed to kick <username> from this room", "You are not allowed to kick %1 from this room.", parts[0]));
return QString();
}
room->kickMember(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> was kicked from this room.", "%1 was kicked from this room.", parts[0]));
return QString();
},
std::nullopt,
kli18n("<user id> [<reason>]"),
kli18n("Removes the user from the room"),
},
};
int ActionsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return actions.size();
}
QVariant ActionsModel::data(const QModelIndex &index, int role) const
{
if (index.row() < 0 || index.row() >= actions.size()) {
return {};
}
if (role == Prefix) {
return actions[index.row()].prefix;
}
if (role == Description) {
return actions[index.row()].description.toString();
}
if (role == CompletionType) {
return u"action"_s;
}
if (role == Parameters) {
return actions[index.row()].parameters.toString();
}
return {};
}
QHash<int, QByteArray> ActionsModel::roleNames() const
{
return {
{Prefix, "prefix"},
{Description, "description"},
{CompletionType, "completionType"},
};
}
QList<Action> &ActionsModel::allActions()
{
return actions;
}
bool ActionsModel::handleQuickEditAction(NeoChatRoom *room, const QString &messageText)
{
if (room == nullptr) {
return false;
}
if (m_allowQuickEdit) {
QRegularExpression sed(u"^s/([^/]*)/([^/]*)(/g)?$"_s);
auto match = sed.match(messageText);
if (match.hasMatch()) {
const QString regex = match.captured(1);
const QString replacement = match.captured(2).toHtmlEscaped();
const QString flags = match.captured(3);
for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) {
if (const auto event = eventCast<const RoomMessageEvent>(&**it)) {
if (event->senderId() == room->localMember().id() && event->has<EventContent::TextContent>()) {
QString originalString;
if (event->content()) {
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content().get())->body;
} else {
originalString = event->plainBody();
}
QString replaceId = event->id();
const auto eventRelation = event->relatesTo();
if (eventRelation && eventRelation->type == "m.replace"_L1) {
replaceId = eventRelation->eventId;
}
std::unique_ptr<EventContent::TextContent> content = nullptr;
if (flags == "/g"_L1) {
content = std::make_unique<Quotient::EventContent::TextContent>(originalString.replace(regex, replacement), u"text/html"_s);
} else {
content = std::make_unique<Quotient::EventContent::TextContent>(originalString.replace(regex, replacement), u"text/html"_s);
}
Quotient::EventRelation relatesTo = Quotient::EventRelation::replace(replaceId);
room->post<Quotient::RoomMessageEvent>(messageText, event->msgtype(), std::move(content), relatesTo);
return true;
}
}
}
}
}
return false;
}
std::pair<std::optional<QString>, std::optional<Quotient::RoomMessageEvent::MsgType>> ActionsModel::handleAction(NeoChatRoom *room, ChatBarCache *chatBarCache)
{
auto sendText = chatBarCache->sendText();
const auto edited = handleQuickEditAction(room, sendText);
if (edited) {
return std::make_pair(std::nullopt, std::nullopt);
}
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = Quotient::RoomMessageEvent::MsgType::Text;
if (sendText.startsWith(QLatin1Char('/'))) {
for (const auto &action : ActionsModel::instance().allActions()) {
if (sendText.indexOf(action.prefix) == 1
&& (sendText.indexOf(" "_L1) == action.prefix.length() + 1 || sendText.length() == action.prefix.length() + 1)) {
sendText = action.handle(sendText.mid(action.prefix.length() + 1).trimmed(), room, chatBarCache);
if (action.messageType.has_value()) {
messageType = action.messageType;
} else {
messageType = std::nullopt;
}
}
}
}
return std::make_pair(messageType.has_value() ? std::make_optional(sendText) : std::nullopt, messageType);
}
void ActionsModel::setAllowQuickEdit(bool allow)
{
m_allowQuickEdit = allow;
}

View File

@@ -1,120 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <KLazyLocalizedString>
#include <QAbstractListModel>
#include <Quotient/events/roommessageevent.h>
class ChatBarCache;
class NeoChatConnection;
class NeoChatRoom;
/**
* @class ActionsModel
*
* This class defines a model for chat actions.
*
* @note A chat action is a message starting with /, resulting in something other
* than a normal message being sent (e.g. /me, /join).
*/
class ActionsModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Definition of an action.
*/
struct Action {
QString prefix; /**< The prefix, without '/' and space after the word. */
/**
* @brief The function to execute when the action is triggered.
*/
std::function<QString(const QString &, NeoChatRoom *, ChatBarCache *)> handle;
/**
* @brief The new message type of a message being sent.
*
* For a non-message action, it's nullopt.
*/
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = std::nullopt;
KLazyLocalizedString parameters; /**< The input parameters expected by the action. */
KLazyLocalizedString description; /**< The description of the action. */
};
static ActionsModel &instance()
{
static ActionsModel _instance;
return _instance;
}
/**
* @brief Defines the model roles.
*/
enum Roles {
Prefix = Qt::DisplayRole, /**< The prefix, without '/' and space after the word. */
Description, /**< The description of the action. */
CompletionType, /**< The completion type (always "action" for this model). */
Parameters, /**< The input parameters expected by the action. */
};
Q_ENUM(Roles)
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Return a vector with all supported actions.
*/
static QList<Action> &allActions();
/**
* @brief Handle special sed style edit action.
*
* @return True if the message has a sed edit which was actioned. False otherwise.
*/
static bool handleQuickEditAction(NeoChatRoom *room, const QString &messageText);
/**
* @brief Handle any action within the message contained in the given ChatBarCache.
*
* @return A modified or unmodified string that needs to be sent or an empty string if
* the handled action replaces sending a normal message.
*/
static std::pair<std::optional<QString>, std::optional<Quotient::RoomMessageEvent::MsgType>> handleAction(NeoChatRoom *room, ChatBarCache *chatBarCache);
static void setAllowQuickEdit(bool allow);
Q_SIGNALS:
/**
* @brief Request a resource is resolved.
*/
void resolveResource(const QString &idOrUri, const QString &action = {});
/**
* @brief Request a room Knock.
*/
void knockRoom(NeoChatConnection *account, const QString &roomAliasOrId, const QString &reason, const QStringList &viaServers);
private:
ActionsModel() = default;
static bool m_allowQuickEdit;
};

View File

@@ -4,10 +4,10 @@
#include "completionmodel.h"
#include <QDebug>
#include "actionsmodel.h"
#include "completionproxymodel.h"
#include "customemojimodel.h"
#include "emojimodel.h"
#include "models/actionsmodel.h"
#include "models/customemojimodel.h"
#include "models/emojimodel.h"
#include "neochatroom.h"
#include "roommanager.h"
#include "userlistmodel.h"

View File

@@ -1,212 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "customemojimodel.h"
#include <QImage>
#include <QMimeDatabase>
#include "emojimodel.h"
#include <Quotient/csapi/account-data.h>
#include <Quotient/csapi/content-repo.h>
using namespace Quotient;
void CustomEmojiModel::setConnection(NeoChatConnection *connection)
{
if (connection == m_connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
fetchEmojis();
}
NeoChatConnection *CustomEmojiModel::connection() const
{
return m_connection;
}
void CustomEmojiModel::fetchEmojis()
{
if (!m_connection) {
return;
}
const auto &data = m_connection->accountData("im.ponies.user_emotes"_L1);
if (data == nullptr) {
return;
}
QJsonObject emojis = data->contentJson()["images"_L1].toObject();
// TODO: Remove with stable migration
const auto legacyEmojis = data->contentJson()["emoticons"_L1].toObject();
for (const auto &emoji : legacyEmojis.keys()) {
if (!emojis.contains(emoji)) {
emojis[emoji] = legacyEmojis[emoji];
}
}
beginResetModel();
m_emojis.clear();
for (const auto &emoji : emojis.keys()) {
const auto &data = emojis[emoji];
const auto e = emoji.startsWith(":"_L1) ? emoji : (u":"_s + emoji + u":"_s);
m_emojis << CustomEmoji{e, data.toObject()["url"_L1].toString(), QRegularExpression(e)};
}
endResetModel();
}
void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
{
using namespace Quotient;
auto job = m_connection->uploadFile(location.toLocalFile());
connect(job, &BaseJob::success, this, [name, location, job, this] {
const auto &data = m_connection->accountData("im.ponies.user_emotes"_L1);
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"_L1].toObject();
QString url;
url = job->contentUri().toString();
QImage image(location.toLocalFile());
QJsonObject imageInfo;
imageInfo["w"_L1] = image.width();
imageInfo["h"_L1] = image.height();
imageInfo["mimetype"_L1] = QMimeDatabase().mimeTypeForFile(location.toLocalFile()).name();
imageInfo["size"_L1] = image.sizeInBytes();
emojiData["%1"_L1.arg(name)] = QJsonObject({
{u"url"_s, url},
{u"info"_s, imageInfo},
{u"body"_s, location.fileName()},
{u"usage"_s, "emoticon"_L1},
});
json["images"_L1] = emojiData;
m_connection->setAccountData("im.ponies.user_emotes"_L1, json);
});
}
void CustomEmojiModel::removeEmoji(const QString &name)
{
using namespace Quotient;
const auto &data = m_connection->accountData("im.ponies.user_emotes"_L1);
Q_ASSERT(data);
auto json = data->contentJson();
const QString _name = name.mid(1).chopped(1);
auto emojiData = json["images"_L1].toObject();
if (emojiData.contains(name)) {
emojiData.remove(name);
json["images"_L1] = emojiData;
}
if (emojiData.contains(_name)) {
emojiData.remove(_name);
json["images"_L1] = emojiData;
}
emojiData = json["emoticons"_L1].toObject();
if (emojiData.contains(name)) {
emojiData.remove(name);
json["emoticons"_L1] = emojiData;
}
if (emojiData.contains(_name)) {
emojiData.remove(_name);
json["emoticons"_L1] = emojiData;
}
m_connection->setAccountData("im.ponies.user_emotes"_L1, json);
}
CustomEmojiModel::CustomEmojiModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(this, &CustomEmojiModel::connectionChanged, this, [this]() {
if (!m_connection) {
return;
}
CustomEmojiModel::fetchEmojis();
connect(m_connection, &Connection::accountDataChanged, this, [this](const QString &id) {
if (id != u"im.ponies.user_emotes"_s) {
return;
}
fetchEmojis();
});
});
CustomEmojiModel::fetchEmojis();
}
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
{
const auto row = idx.row();
if (row >= m_emojis.length()) {
return QVariant();
}
const auto &data = m_emojis[row];
switch (Roles(role)) {
case Roles::ModelData:
return QVariant::fromValue(Emoji(m_connection->makeMediaUrl(QUrl(data.url)).toString(), data.name, true));
case Roles::Name:
case Roles::DisplayRole:
case Roles::ReplacedTextRole:
return data.name;
case Roles::ImageURL:
return m_connection->makeMediaUrl(QUrl(data.url));
case Roles::MxcUrl:
return m_connection->makeMediaUrl(QUrl(data.url));
default:
return {};
}
return QVariant();
}
int CustomEmojiModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_emojis.length();
}
QHash<int, QByteArray> CustomEmojiModel::roleNames() const
{
return {
{Name, "name"},
{ImageURL, "imageURL"},
{ModelData, "modelData"},
{MxcUrl, "mxcUrl"},
};
}
QString CustomEmojiModel::preprocessText(QString text)
{
for (const auto &emoji : std::as_const(m_emojis)) {
text.replace(emoji.regexp,
uR"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)"_s.arg(emoji.url, emoji.name));
}
return text;
}
QVariantList CustomEmojiModel::filterModel(const QString &filter)
{
QVariantList results;
for (const auto &emoji : std::as_const(m_emojis)) {
if (results.length() >= 10)
break;
if (!emoji.name.contains(filter, Qt::CaseInsensitive))
continue;
results << QVariant::fromValue(Emoji(m_connection->makeMediaUrl(QUrl(emoji.url)).toString(), emoji.name, true));
}
return results;
}
#include "moc_customemojimodel.cpp"

View File

@@ -1,116 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <QRegularExpression>
#include "neochatconnection.h"
struct CustomEmoji {
QString name; // with :semicolons:
QString url; // mxc://
QRegularExpression regexp;
Q_GADGET
Q_PROPERTY(QString unicode MEMBER url)
Q_PROPERTY(QString name MEMBER name)
};
/**
* @class CustomEmojiModel
*
* This class defines the model for custom user emojis.
*
* This is based upon the im.ponies.user_emotes spec (MSC2545).
*/
class CustomEmojiModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
Name = Qt::DisplayRole, /**< The name of the emoji. */
ImageURL, /**< The URL for the custom emoji. */
ModelData, /**< for emulating the regular emoji model's usage, otherwise the UI code would get too complicated. */
MxcUrl = 50, /**< The mxc source URL for the custom emoji. */
DisplayRole = 51, /**< The name of the emoji. For compatibility with EmojiModel. */
ReplacedTextRole = 52, /**< The name of the emoji. For compatibility with EmojiModel. */
DescriptionRole = 53, /**< Invalid, reserved. For compatibility with EmojiModel. */
};
Q_ENUM(Roles)
static CustomEmojiModel &instance()
{
static CustomEmojiModel _instance;
return _instance;
}
static CustomEmojiModel *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Substitute any custom emojis for an image in the input text.
*/
Q_INVOKABLE QString preprocessText(QString text);
/**
* @brief Return a list of custom emojis where the name contains the filter text.
*/
Q_INVOKABLE QVariantList filterModel(const QString &filter);
/**
* @brief Add a new emoji to the model.
*/
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
/**
* @brief Remove an emoji from the model.
*/
Q_INVOKABLE void removeEmoji(const QString &name);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
Q_SIGNALS:
void connectionChanged();
private:
explicit CustomEmojiModel(QObject *parent = nullptr);
QList<CustomEmoji> m_emojis;
QPointer<NeoChatConnection> m_connection;
void fetchEmojis();
};

View File

@@ -1,244 +0,0 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QVariant>
#include "emojimodel.h"
#include "emojitones.h"
#include <QDebug>
#include <algorithm>
#include "customemojimodel.h"
#include <KLocalizedString>
using namespace Qt::StringLiterals;
EmojiModel::EmojiModel(QObject *parent)
: QAbstractListModel(parent)
, m_config(KSharedConfig::openStateConfig())
, m_configGroup(KConfigGroup(m_config, u"Editor"_s))
{
if (_emojis.isEmpty()) {
#include "emojis.h"
}
}
int EmojiModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
int total = 0;
for (const auto &category : std::as_const(_emojis)) {
total += category.count();
}
return total;
}
QVariant EmojiModel::data(const QModelIndex &index, int role) const
{
auto row = index.row();
for (const auto &category : std::as_const(_emojis)) {
if (row >= category.count()) {
row -= category.count();
continue;
}
auto emoji = category[row].value<Emoji>();
switch (role) {
case ShortNameRole:
return u":%1:"_s.arg(emoji.shortName);
case UnicodeRole:
case ReplacedTextRole:
return emoji.unicode;
case InvalidRole:
return u"invalid"_s;
case DisplayRole:
return u"%2 :%1:"_s.arg(emoji.shortName, emoji.unicode);
case DescriptionRole:
return emoji.description;
}
}
return {};
}
QHash<int, QByteArray> EmojiModel::roleNames() const
{
return {{ShortNameRole, "shortName"}, {UnicodeRole, "unicode"}};
}
QStringList EmojiModel::lastUsedEmojis() const
{
return m_configGroup.readEntry(u"lastUsedEmojis"_s, QStringList());
}
QVariantList EmojiModel::filterModel(const QString &filter, bool limit)
{
auto emojis = CustomEmojiModel::instance().filterModel(filter);
emojis += filterModelNoCustom(filter, limit);
return emojis;
}
QVariantList EmojiModel::filterModelNoCustom(const QString &filter, bool limit)
{
QVariantList result;
const auto &values = _emojis.values();
for (const auto &e : values) {
for (const auto &variant : e) {
const auto &emoji = qvariant_cast<Emoji>(variant);
if (emoji.shortName.contains(filter, Qt::CaseInsensitive)) {
result.append(variant);
if (result.length() > 10 && limit) {
return result;
}
}
}
}
return result;
}
void EmojiModel::emojiUsed(const QVariant &modelData)
{
auto list = lastUsedEmojis();
const auto emoji = modelData.value<Emoji>();
auto it = list.begin();
while (it != list.end()) {
if (*it == emoji.shortName) {
it = list.erase(it);
} else {
it++;
}
}
list.push_front(emoji.shortName);
m_configGroup.writeEntry(u"lastUsedEmojis"_s, list);
Q_EMIT historyChanged();
}
QVariantList EmojiModel::emojis(Category category) const
{
if (category == History) {
return emojiHistory();
}
if (category == HistoryNoCustom) {
QVariantList list;
const auto &history = emojiHistory();
for (const auto &e : history) {
auto emoji = qvariant_cast<Emoji>(e);
if (!emoji.isCustom) {
list.append(e);
}
}
return list;
}
if (category == Custom) {
return CustomEmojiModel::instance().filterModel({});
}
return _emojis[category];
}
QVariantList EmojiModel::tones(const QString &baseEmoji) const
{
if (baseEmoji.endsWith(u"tone"_s)) {
return EmojiTones::_tones.values(baseEmoji.split(u":"_s)[0]);
}
return EmojiTones::_tones.values(baseEmoji);
}
QHash<EmojiModel::Category, QVariantList> EmojiModel::_emojis;
QVariantList EmojiModel::categories() const
{
return QVariantList{
{QVariantMap{
{u"category"_s, EmojiModel::HistoryNoCustom},
{u"name"_s, i18nc("Previously used emojis", "History")},
{u"emoji"_s, u"⌛️"_s},
}},
{QVariantMap{
{u"category"_s, EmojiModel::Smileys},
{u"name"_s, i18nc("'Smileys' is a category of emoji", "Smileys")},
{u"emoji"_s, u"😏"_s},
}},
{QVariantMap{
{u"category"_s, EmojiModel::People},
{u"name"_s, i18nc("'People' is a category of emoji", "People")},
{u"emoji"_s, u"🙋‍♂️"_s},
}},
{QVariantMap{
{u"category"_s, EmojiModel::Nature},
{u"name"_s, i18nc("'Nature' is a category of emoji", "Nature")},
{u"emoji"_s, u"🌲"_s},
}},
{QVariantMap{
{u"category"_s, EmojiModel::Food},
{u"name"_s, i18nc("'Food' is a category of emoji", "Food")},
{u"emoji"_s, u"🍛"_s},
}},
{QVariantMap{
{u"category"_s, EmojiModel::Activities},
{u"name"_s, i18nc("'Activities' is a category of emoji", "Activities")},
{u"emoji"_s, u"🚁"_s},
}},
{QVariantMap{
{u"category"_s, EmojiModel::Travel},
{u"name"_s, i18nc("'Travel' is a category of emoji", "Travel")},
{u"emoji"_s, u"🚅"_s},
}},
{QVariantMap{
{u"category"_s, EmojiModel::Objects},
{u"name"_s, i18nc("'Objects' is a category of emoji", "Objects")},
{u"emoji"_s, u"💡"_s},
}},
{QVariantMap{
{u"category"_s, EmojiModel::Symbols},
{u"name"_s, i18nc("'Symbols' is a category of emoji", "Symbols")},
{u"emoji"_s, u"🔣"_s},
}},
{QVariantMap{
{u"category"_s, EmojiModel::Flags},
{u"name"_s, i18nc("'Flags' is a category of emoji", "Flags")},
{u"emoji"_s, u"🏁"_s},
}},
};
}
QVariantList EmojiModel::categoriesWithCustom() const
{
auto cats = categories();
cats.removeAt(0);
cats.insert(0,
QVariantMap{
{u"category"_s, EmojiModel::History},
{u"name"_s, i18nc("Previously used emojis", "History")},
{u"emoji"_s, u"⌛️"_s},
});
cats.insert(1,
QVariantMap{
{u"category"_s, EmojiModel::Custom},
{u"name"_s, i18nc("'Custom' is a category of emoji", "Custom")},
{u"emoji"_s, u"🖼️"_s},
});
;
return cats;
}
QVariantList EmojiModel::emojiHistory() const
{
QVariantList list;
const auto &lastUsed = lastUsedEmojis();
for (const auto &historicEmoji : lastUsed) {
for (const auto &emojiCategory : std::as_const(_emojis)) {
for (const auto &emoji : emojiCategory) {
if (qvariant_cast<Emoji>(emoji).shortName == historicEmoji) {
list.append(emoji);
}
}
}
}
return list;
}
#include "moc_emojimodel.cpp"

View File

@@ -1,184 +0,0 @@
// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <KConfigGroup>
#include <KSharedConfig>
#include <QAbstractListModel>
#include <QObject>
#include <QQmlEngine>
struct Emoji {
Emoji(QString unicode, QString shortname, bool isCustom = false)
: unicode(std::move(unicode))
, shortName(std::move(shortname))
, isCustom(isCustom)
{
}
Emoji(QString unicode, QString shortname, QString description)
: unicode(std::move(unicode))
, shortName(std::move(shortname))
, description(std::move(description))
{
}
Emoji() = default;
QString unicode;
QString shortName;
QString description;
bool isCustom = false;
Q_GADGET
Q_PROPERTY(QString unicode MEMBER unicode)
Q_PROPERTY(QString shortName MEMBER shortName)
Q_PROPERTY(QString description MEMBER description)
Q_PROPERTY(bool isCustom MEMBER isCustom)
};
Q_DECLARE_METATYPE(Emoji)
/**
* @class EmojiModel
*
* This class defines the model for visualising a list of emojis.
*/
class EmojiModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief Return a list of emoji categories.
*
* @note No custom emoji categories will be included.
*/
Q_PROPERTY(QVariantList categories READ categories CONSTANT)
/**
* @brief Return a list of emoji categories with custom emojis.
*/
Q_PROPERTY(QVariantList categoriesWithCustom READ categoriesWithCustom CONSTANT)
public:
static EmojiModel &instance()
{
static EmojiModel _instance;
return _instance;
}
static EmojiModel *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
/**
* @brief Defines the model roles.
*/
enum RoleNames {
ShortNameRole = Qt::DisplayRole, /**< The name of the emoji. */
UnicodeRole, /**< The unicode character of the emoji. */
InvalidRole = 50, /**< Invalid, reserved. */
DisplayRole = 51, /**< The display text for an emoji. */
ReplacedTextRole = 52, /**< The text to replace the short name with (i.e. the unicode character). */
DescriptionRole = 53, /**< The long description of an emoji. */
};
Q_ENUM(RoleNames)
/**
* @brief Defines the potential categories an emoji can be placed in.
*/
enum Category {
Custom, /**< A custom user emoji. */
Search, /**< The results of a filter. */
SearchNoCustom, /**< The results of a filter with no custom emojis. */
History, /**< Recently used emojis. */
HistoryNoCustom, /**< Recently used emojis with no custom emojis. */
Smileys, /**< Smileys & emotion emojis. */
People, /**< People & Body emojis. */
Nature, /**< Animals & Nature emojis. */
Food, /**< Food & Drink emojis. */
Activities, /**< Activities emojis. */
Travel, /**< Travel & Places emojis. */
Objects, /**< Objects emojis. */
Symbols, /**< Symbols emojis. */
Flags, /**< Flags emojis. */
Component, /**< ??? */
};
Q_ENUM(Category)
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa RoleNames, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Return a filtered list of emojis.
*
* @note This includes custom emojis, use filterModelNoCustom to return a result
* without custom emojis.
*
* @sa filterModelNoCustom
*/
Q_INVOKABLE static QVariantList filterModel(const QString &filter, bool limit = true);
/**
* @brief Return a filtered list of emojis without custom emojis.
*
* @note Use filterModel to return a result with custom emojis.
*
* @sa filterModel
*/
Q_INVOKABLE static QVariantList filterModelNoCustom(const QString &filter, bool limit = true);
/**
* @brief Return a list of emojis for the given category.
*/
Q_INVOKABLE QVariantList emojis(Category category) const;
/**
* @brief Return a list of emoji tones for the given base emoji.
*/
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
/**
* @brief Return a list of the last used emoji shortnames
*/
QStringList lastUsedEmojis() const;
QVariantList categories() const;
QVariantList categoriesWithCustom() const;
Q_SIGNALS:
void historyChanged();
public Q_SLOTS:
void emojiUsed(const QVariant &modelData);
private:
static QHash<Category, QVariantList> _emojis;
/// Returns QVariants containing the last used Emojis
QVariantList emojiHistory() const;
KSharedConfig::Ptr m_config;
KConfigGroup m_configGroup;
EmojiModel(QObject *parent = nullptr);
};

View File

@@ -3,8 +3,8 @@
#include "messagecontentmodel.h"
#include "contentprovider.h"
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "messagecomponenttype.h"
#include "neochatconfig.h"
#include <QImageReader>

View File

@@ -7,7 +7,7 @@
#include <KLazyLocalizedString>
#include "powerlevel.h"
#include "enums/powerlevel.h"
using namespace Qt::Literals::StringLiterals;

View File

@@ -10,8 +10,8 @@
#include "chatbarcache.h"
#include "contentprovider.h"
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "messagecomponenttype.h"
#include "messagecontentmodel.h"
#include "neochatroom.h"